From 04da5ff2fc41222dd9074705646702f40bb1991e Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Tue, 5 May 2026 21:28:34 +0330 Subject: [PATCH] UPDATE --- .../layout/vertical/VerticalMenu.tsx | 50 +-- src/libs/api/index.ts | 1 - src/libs/api/services/farmDashboardService.ts | 77 +---- .../dashboards/farm/FarmDashboardWrapper.tsx | 300 +++++++++++++++++- 4 files changed, 311 insertions(+), 117 deletions(-) diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 4c70f86..51096af 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -23,6 +23,7 @@ import StyledVerticalNavExpandIcon from "@menu/styles/vertical/StyledVerticalNav // Style Imports import menuItemStyles from "@core/styles/vertical/menuItemStyles"; import menuSectionStyles from "@core/styles/vertical/menuSectionStyles"; +import { navigationLabels } from "@/constants/navigation"; // Menu Data Imports // import menuData from '@/data/navigation/verticalMenuData' @@ -53,6 +54,9 @@ const VerticalMenu = ({ scrollMenu }: Props) => { const theme = useTheme(); const verticalNavOptions = useVerticalNav(); + const translateNav = (key: keyof typeof navigationLabels) => + t.has(key) ? t(key) : navigationLabels[key]; + // Vars const { isBreakpointReached, transitionDuration } = verticalNavOptions; @@ -92,102 +96,102 @@ const VerticalMenu = ({ scrollMenu }: Props) => { href={`/dashboard`} icon={} > - {t("dashboards")} + {translateNav("dashboards")} - + } > - {t("yieldHarvest")} + {translateNav("yieldHarvest")} } > - {t("farmerTodos")} + {translateNav("farmerTodos")} }> - {t("farmWallet")} + {translateNav("farmWallet")} } > - {t("farmCalendar")} + {translateNav("farmCalendar")} } > - {t("economicOverview")} + {translateNav("economicOverview")} - + } > - {t("farmAlerts")} + {translateNav("farmAlerts")} }> - {t("pestDiseaseRisk")} + {translateNav("pestDiseaseRisk")} - + }> - {t("waterData")} + {translateNav("waterData")} }> - {t("soilData")} + {translateNav("soilData")} }> - {t("cropZoning")} + {translateNav("cropZoning")} - }> + }> }> - {t("sensor7In1")} + {translateNav("sensor7In1")} - + } > - {t("irrigationPlanParser")} + {translateNav("irrigationPlanParser")} } > - {t("irrigationRecommendation")} + {translateNav("irrigationRecommendation")} } > - {t("fertilizationRecommendation")} + {translateNav("fertilizationRecommendation")} } > - {t("fertilizationPlanParser")} + {translateNav("fertilizationPlanParser")} - + } > - {t("farmAiAssistant")} + {translateNav("farmAiAssistant")} diff --git a/src/libs/api/index.ts b/src/libs/api/index.ts index db45f83..b909a98 100644 --- a/src/libs/api/index.ts +++ b/src/libs/api/index.ts @@ -36,6 +36,5 @@ export * from "./services/rolesPermissionsService"; export * from "./services/farmHubService"; export { type FarmDashboardConfigResponse, - type FarmDashboardCardsResponse, farmDashboardService, } from "./services/farmDashboardService"; diff --git a/src/libs/api/services/farmDashboardService.ts b/src/libs/api/services/farmDashboardService.ts index 80322f4..36db351 100644 --- a/src/libs/api/services/farmDashboardService.ts +++ b/src/libs/api/services/farmDashboardService.ts @@ -1,8 +1,7 @@ /** * Farm Dashboard Service - * Handles API calls for dashboard config and card data. + * Handles API calls for dashboard config only. * - Config: disabled cards, row order, drag reorder - * - Cards: all 15 card payloads from /api/farm-dashboard/ */ import { apiClient } from "../client"; @@ -29,36 +28,6 @@ export interface FarmDashboardConfigResponse { enable_drag_reorder?: boolean; } -/** API response shape for /api/farm-dashboard/ - each key matches CardId */ -export interface FarmDashboardCardsResponse { - farmOverviewKpis?: Record; - farmWeatherCard?: Record; - farmAlertsTracker?: Record; - sensorValuesList?: Record; - sensorRadarChart?: Record; - sensorComparisonChart?: Record; - anomalyDetectionCard?: Record; - farmAlertsTimeline?: Record; - waterNeedPrediction?: Record; - harvestPredictionCard?: Record; - yieldPredictionChart?: Record; - soilMoistureHeatmap?: Record; - ndviHealthCard?: Record; - recommendationsList?: Record; - economicOverview?: Record; -} - -interface FarmDashboardCardsTaskResult { - farm_uuid?: string; - all_cards?: FarmDashboardCardsResponse; -} - -interface FarmDashboardCardsTaskData { - task_id?: string; - status?: string; - result?: FarmDashboardCardsTaskResult; -} - const STORAGE_KEY_PREFIX = "farm_dashboard_config"; function getStorageKey(farmUuid: string): string { @@ -101,28 +70,6 @@ function normalizeRowOrder(rowOrder: string[] = []): string[] { : [...ROW_IDS]; } -function extractCardsPayload( - response: - | ApiResponse - | ApiResponse - | FarmDashboardCardsResponse - | FarmDashboardCardsTaskData, -): Partial>> { - const raw = response && "data" in response ? response.data : response; - - if (!raw || typeof raw !== "object") { - return {}; - } - - if ("result" in raw && raw.result && typeof raw.result === "object") { - return (raw.result.all_cards ?? {}) as Partial< - Record> - >; - } - - return raw as Partial>>; -} - /** * Transform API response to frontend config format */ @@ -242,26 +189,4 @@ export const farmDashboardService = { throw err; } }, - - /** - * Get all dashboard card data from API - * Response: { code: 200, msg: "OK", data: { farmOverviewKpis, farmWeatherCard, ... } } - */ - async getAllCards( - farmUuid: string, - ): Promise< - Partial>> - > { - try { - const response = await apiClient.get< - | ApiResponse - | ApiResponse - | FarmDashboardCardsResponse - | FarmDashboardCardsTaskData - >(`/api/farm-dashboard/?${buildFarmQuery(farmUuid)}`); - return extractCardsPayload(response); - } catch { - return {}; - } - }, }; diff --git a/src/views/dashboards/farm/FarmDashboardWrapper.tsx b/src/views/dashboards/farm/FarmDashboardWrapper.tsx index c12bf2b..df31418 100644 --- a/src/views/dashboards/farm/FarmDashboardWrapper.tsx +++ b/src/views/dashboards/farm/FarmDashboardWrapper.tsx @@ -1,10 +1,11 @@ "use client"; // React Imports -import type { RefObject } from "react"; +import type { ComponentType, RefObject } from "react"; import { useEffect, useMemo, useState, useCallback, useContext } from "react"; import { useTranslations } from "next-intl"; import { useFarmHub } from "@/hooks/useFarmHub"; +import { format } from "date-fns"; // Context Imports import NavbarSlotContext from "@/contexts/navbarSlotContext"; @@ -47,6 +48,18 @@ import { type FarmDashboardConfig, } from "@views/dashboards/farm/farmDashboardConfig"; import { farmDashboardService } from "@/libs/api/services/farmDashboardService"; +import { + farmAlertsService, + type FarmAlertsTrackerResponse, + type FarmAlertNotificationItem, + type FarmAlertTrackerItem, +} from "@/libs/api/services/farmAlertsService"; +import { waterService } from "@/libs/api/services/waterService"; +import { sensor7Service } from "@/libs/api/services/sensor7Service"; +import { soilService } from "@/libs/api/services/soilService"; +import { cropHealthService } from "@/libs/api/services/cropHealthService"; +import { economicOverviewService } from "@/libs/api/services/economicOverviewService"; +import { yieldHarvestService } from "@/libs/api/services/yieldHarvestService"; import FarmDashboardSettingsDropdown from "@views/dashboards/farm/FarmDashboardSettingsDropdown"; const cardRowSx = { @@ -57,7 +70,7 @@ const cardRowSx = { const CARD_COMPONENTS: Record< CardId, - React.ComponentType<{ data?: Record }> + ComponentType<{ data?: Record }> > = { farmOverviewKpis: FarmOverviewKPIs, farmWeatherCard: FarmWeatherCard, @@ -96,6 +109,264 @@ function areStringArraysEqual(left: string[], right: string[]): boolean { return left.every((value, index) => value === right[index]); } +function getSeverityColor( + value?: string, +): "primary" | "warning" | "error" | "info" | "success" { + switch (value?.toLowerCase()) { + case "critical": + case "danger": + case "error": + case "high": + return "error"; + case "warning": + case "medium": + return "warning"; + case "success": + return "success"; + case "low": + case "info": + default: + return "info"; + } +} + +function buildTrackerCardData( + result: FarmAlertsTrackerResponse, +): Record { + const tracker = result.tracker ?? {}; + const totalAlerts = tracker.totalAlerts ?? 0; + const alertStats = Array.isArray(tracker.alertStats) ? tracker.alertStats : []; + const safeTotal = Math.max(totalAlerts, 1); + const criticalCount = ( + alertStats as Array> + ).reduce((sum, item) => { + const severity = String(item.severity ?? "").toLowerCase(); + + return severity === "high" || + severity === "critical" || + severity === "danger" + ? sum + Number(item.count ?? 0) + : sum; + }, 0); + + return { + totalAlerts, + alertStats: alertStats.map((item, index) => ({ + title: String(item.title ?? `Alert ${index + 1}`), + count: String(item.count ?? "0"), + avatarIcon: String(item.avatarIcon ?? "tabler-alert-triangle"), + avatarColor: getSeverityColor( + String(item.severity ?? item.avatarColor ?? result.status_level), + ), + })), + radialBarValue: Math.min(Math.round((criticalCount / safeTotal) * 100), 100), + }; +} + +function buildTimelineData( + result: FarmAlertsTrackerResponse, + notifications: FarmAlertNotificationItem[], +): Record { + const trackerAlerts = Array.isArray(result.tracker?.alerts) + ? result.tracker.alerts + : []; + + if (notifications.length > 0) { + return { + alerts: notifications.map((item) => ({ + title: item.title, + description: item.suggested_action || item.message, + time: format(new Date(item.created_at), "yyyy-MM-dd HH:mm"), + color: getSeverityColor(item.level), + })), + }; + } + + return { + alerts: trackerAlerts.map((item: FarmAlertTrackerItem, index: number) => ({ + title: item.title || `Alert ${index + 1}`, + description: + item.explanation || item.summary || item.recommended_action || "", + time: + item.duration || + (item.timestamp + ? format(new Date(item.timestamp), "yyyy-MM-dd HH:mm") + : "-"), + color: getSeverityColor(item.severity || result.status_level), + })), + }; +} + +function buildRecommendationsData( + result: FarmAlertsTrackerResponse, +): Record { + const tracker = result.tracker ?? {}; + const actions = Array.isArray(tracker.recommendedOperationalActions) + ? tracker.recommendedOperationalActions + : []; + const explanations = Array.isArray(tracker.humanReadableExplanations) + ? tracker.humanReadableExplanations + : []; + + return { + recommendations: actions.map((action, index) => ({ + title: `اقدام پیشنهادی ${index + 1}`, + subtitle: explanations[index] || action, + avatarIcon: "tabler-arrow-up-right", + avatarColor: getSeverityColor(result.status_level), + })), + }; +} + +async function loadDashboardCardData( + cardId: CardId, + farmUuid: string, +): Promise> { + switch (cardId) { + case "farmOverviewKpis": { + const summary = await yieldHarvestService.getSummary(farmUuid); + return (summary.yield_prediction as Record) ?? {}; + } + case "farmWeatherCard": + return await waterService.getWeatherFarmCard(farmUuid); + case "farmAlertsTracker": { + const result = await farmAlertsService.analyzeTracker({ farmUuid }); + return buildTrackerCardData(result); + } + case "sensorValuesList": + return (await sensor7Service.getValuesList({ farmUuid })) as unknown as Record< + string, + unknown + >; + case "sensorRadarChart": + return (await sensor7Service.getRadarChart({ farmUuid })) as unknown as Record< + string, + unknown + >; + case "sensorComparisonChart": + return (await sensor7Service.getComparisonChart({ + farmUuid, + })) as unknown as Record; + case "anomalyDetectionCard": + return await soilService.getAnomalies(farmUuid); + case "farmAlertsTimeline": { + const result = await farmAlertsService.analyzeTracker({ farmUuid }); + const notifications = Array.isArray(result.notifications) + ? result.notifications + : []; + return buildTimelineData(result, notifications); + } + case "waterNeedPrediction": + return await waterService.getNeedPrediction(farmUuid); + case "harvestPredictionCard": + return (await yieldHarvestService.getHarvestPrediction( + farmUuid, + )) as unknown as Record; + case "yieldPredictionChart": { + const summary = await yieldHarvestService.getSummary(farmUuid); + return (summary.yieldPredictionChart as Record) ?? {}; + } + case "soilMoistureHeatmap": + return await soilService.getMoistureHeatmap(farmUuid); + case "ndviHealthCard": { + const summary = await cropHealthService.getSummary(farmUuid); + return (summary.ndviHealthCard as Record) ?? {}; + } + case "recommendationsList": { + const result = await farmAlertsService.analyzeTracker({ farmUuid }); + return buildRecommendationsData(result); + } + case "economicOverview": { + const summary = await economicOverviewService.getSummary(farmUuid); + return (summary.economicOverview as Record) ?? {}; + } + default: + return {}; + } +} + +type FarmDashboardCardProps = { + cardId: CardId; + farmUuid?: string; + overview?: boolean; +}; + +const FarmDashboardCard = ({ + cardId, + farmUuid, + overview = false, +}: FarmDashboardCardProps) => { + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(Boolean(farmUuid)); + + useEffect(() => { + let active = true; + + if (!farmUuid) { + setData(null); + setLoading(false); + return; + } + + setLoading(true); + + loadDashboardCardData(cardId, farmUuid) + .then((nextData) => { + if (active) { + setData(nextData); + } + }) + .catch(() => { + if (active) { + setData(null); + } + }) + .finally(() => { + if (active) { + setLoading(false); + } + }); + + return () => { + active = false; + }; + }, [cardId, farmUuid]); + + if (loading) { + if (overview) { + return ( + + + + + + ); + } + + return ( + + + + ); + } + + if (!data) return null; + + const Component = CARD_COMPONENTS[cardId]; + + return Component ? : null; +}; + const FarmDashboardWrapper = () => { const t = useTranslations("farmDashboard"); const { farmHub } = useFarmHub(); @@ -151,9 +422,6 @@ const FarmDashboardWrapper = () => { [t], ); - const [cardsData, setCardsData] = useState< - Partial>> - >({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -168,7 +436,7 @@ const FarmDashboardWrapper = () => { if (!Array.isArray(cards)) return false; return cards.some((cardId) => !disabledSet.has(cardId)); }, - [config.disabledCardIds], + [disabledSet], ); const visibleRowOrder = useMemo( @@ -193,18 +461,15 @@ const FarmDashboardWrapper = () => { useEffect(() => { if (!farmUuid) { setConfig(DEFAULT_FARM_DASHBOARD_CONFIG); - setCardsData({}); setLoading(false); return; } setLoading(true); - Promise.all([ - farmDashboardService.getConfig(farmUuid), - farmDashboardService.getAllCards(farmUuid), - ]) - .then(([configData, cards]) => { + farmDashboardService + .getConfig(farmUuid) + .then((configData) => { const validRowOrder = (configData.rowOrder ?? []).filter( (id): id is RowId => id in ROW_CARDS, ); @@ -214,7 +479,6 @@ const FarmDashboardWrapper = () => { enableDragReorder: configData.enableDragReorder ?? true, }; setConfig(merged); - setCardsData(cards ?? {}); }) .catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG)) .finally(() => setLoading(false)); @@ -356,16 +620,18 @@ const FarmDashboardWrapper = () => { )} {isOverviewRow && cards.includes("farmOverviewKpis") && ( - + )} {!isOverviewRow && cards.map((cardId: CardId) => { const size = CARD_GRID_SIZE[cardId]; - const Component = CARD_COMPONENTS[cardId]; - if (!Component) return null; return ( - + ); })}