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 (
-
+
);
})}