This commit is contained in:
2026-05-05 21:28:34 +03:30
parent c463775bd7
commit 04da5ff2fc
4 changed files with 311 additions and 117 deletions
+27 -23
View File
@@ -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={<i className="tabler-smart-home" />}
>
{t("dashboards")}
{translateNav("dashboards")}
</MenuItem>
<MenuSection label={t("farmManagement")}>
<MenuSection label={translateNav("farmManagement")}>
<MenuItem
href="/yield-harvest"
icon={<i className="tabler-chart-line" />}
>
{t("yieldHarvest")}
{translateNav("yieldHarvest")}
</MenuItem>
<MenuItem
href="/farmer-todos"
icon={<i className="tabler-checklist" />}
>
{t("farmerTodos")}
{translateNav("farmerTodos")}
</MenuItem>
<MenuItem href="/wallet" icon={<i className="tabler-wallet" />}>
{t("farmWallet")}
{translateNav("farmWallet")}
</MenuItem>
<MenuItem
href="/farmer-calendar"
icon={<i className="tabler-calendar-event" />}
>
{t("farmCalendar")}
{translateNav("farmCalendar")}
</MenuItem>
<MenuItem
href="/economy"
icon={<i className="tabler-cash-banknote" />}
>
{t("economicOverview")}
{translateNav("economicOverview")}
</MenuItem>
</MenuSection>
<MenuSection label={t("riskAlerts")}>
<MenuSection label={translateNav("riskAlerts")}>
<MenuItem
href="/farm-alerts"
icon={<i className="tabler-alert-triangle" />}
>
{t("farmAlerts")}
{translateNav("farmAlerts")}
</MenuItem>
<MenuItem href="/pest-risk" icon={<i className="tabler-bug" />}>
{t("pestDiseaseRisk")}
{translateNav("pestDiseaseRisk")}
</MenuItem>
</MenuSection>
<MenuSection label={t("monitoring")}>
<MenuSection label={translateNav("monitoring")}>
<MenuItem href="/water-data" icon={<i className="tabler-droplet" />}>
{t("waterData")}
{translateNav("waterData")}
</MenuItem>
<MenuItem href="/soil-data" icon={<i className="tabler-seedling" />}>
{t("soilData")}
{translateNav("soilData")}
</MenuItem>
<MenuItem href="/crop-zoning" icon={<i className="tabler-map-2" />}>
{t("cropZoning")}
{translateNav("cropZoning")}
</MenuItem>
<SubMenu label={t("sensors")} icon={<i className="tabler-device-analytics" />}>
<SubMenu label={translateNav("sensors")} icon={<i className="tabler-device-analytics" />}>
<MenuItem href="/solid-sensor" icon={<i className="tabler-device-analytics" />}>
{t("sensor7In1")}
{translateNav("sensor7In1")}
</MenuItem>
</SubMenu>
</MenuSection>
<MenuSection label={t("recommendation")}>
<MenuSection label={translateNav("recommendation")}>
<MenuItem
href="/irrigation-plan"
icon={<i className="tabler-droplet-half-2" />}
>
{t("irrigationPlanParser")}
{translateNav("irrigationPlanParser")}
</MenuItem>
<MenuItem
href="/irrigation-recommendation"
icon={<i className="tabler-droplet-half-2" />}
>
{t("irrigationRecommendation")}
{translateNav("irrigationRecommendation")}
</MenuItem>
<MenuItem
href="/fertilization-recommendation"
icon={<i className="tabler-atom-2" />}
>
{t("fertilizationRecommendation")}
{translateNav("fertilizationRecommendation")}
</MenuItem>
<MenuItem
href="/fertilization-plan"
icon={<i className="tabler-sparkles" />}
>
{t("fertilizationPlanParser")}
{translateNav("fertilizationPlanParser")}
</MenuItem>
</MenuSection>
<MenuSection label={t("aiAssistant")}>
<MenuSection label={translateNav("aiAssistant")}>
<MenuItem
href="/farm-ai-assistant"
icon={<i className="tabler-robot" />}
>
{t("farmAiAssistant")}
{translateNav("farmAiAssistant")}
</MenuItem>
</MenuSection>
</Menu>
-1
View File
@@ -36,6 +36,5 @@ export * from "./services/rolesPermissionsService";
export * from "./services/farmHubService";
export {
type FarmDashboardConfigResponse,
type FarmDashboardCardsResponse,
farmDashboardService,
} from "./services/farmDashboardService";
+1 -76
View File
@@ -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<string, unknown>;
farmWeatherCard?: Record<string, unknown>;
farmAlertsTracker?: Record<string, unknown>;
sensorValuesList?: Record<string, unknown>;
sensorRadarChart?: Record<string, unknown>;
sensorComparisonChart?: Record<string, unknown>;
anomalyDetectionCard?: Record<string, unknown>;
farmAlertsTimeline?: Record<string, unknown>;
waterNeedPrediction?: Record<string, unknown>;
harvestPredictionCard?: Record<string, unknown>;
yieldPredictionChart?: Record<string, unknown>;
soilMoistureHeatmap?: Record<string, unknown>;
ndviHealthCard?: Record<string, unknown>;
recommendationsList?: Record<string, unknown>;
economicOverview?: Record<string, unknown>;
}
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<FarmDashboardCardsResponse>
| ApiResponse<FarmDashboardCardsTaskData>
| FarmDashboardCardsResponse
| FarmDashboardCardsTaskData,
): Partial<Record<CardId, Record<string, unknown>>> {
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<CardId, Record<string, unknown>>
>;
}
return raw as Partial<Record<CardId, Record<string, unknown>>>;
}
/**
* 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<Record<CardId, Record<string, unknown>>>
> {
try {
const response = await apiClient.get<
| ApiResponse<FarmDashboardCardsResponse>
| ApiResponse<FarmDashboardCardsTaskData>
| FarmDashboardCardsResponse
| FarmDashboardCardsTaskData
>(`/api/farm-dashboard/?${buildFarmQuery(farmUuid)}`);
return extractCardsPayload(response);
} catch {
return {};
}
},
};
@@ -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<string, unknown> }>
ComponentType<{ data?: Record<string, unknown> }>
> = {
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<string, unknown> {
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<Record<string, unknown>>
).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<string, unknown> {
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<string, unknown> {
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<Record<string, unknown>> {
switch (cardId) {
case "farmOverviewKpis": {
const summary = await yieldHarvestService.getSummary(farmUuid);
return (summary.yield_prediction as Record<string, unknown>) ?? {};
}
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<string, unknown>;
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<string, unknown>;
case "yieldPredictionChart": {
const summary = await yieldHarvestService.getSummary(farmUuid);
return (summary.yieldPredictionChart as Record<string, unknown>) ?? {};
}
case "soilMoistureHeatmap":
return await soilService.getMoistureHeatmap(farmUuid);
case "ndviHealthCard": {
const summary = await cropHealthService.getSummary(farmUuid);
return (summary.ndviHealthCard as Record<string, unknown>) ?? {};
}
case "recommendationsList": {
const result = await farmAlertsService.analyzeTracker({ farmUuid });
return buildRecommendationsData(result);
}
case "economicOverview": {
const summary = await economicOverviewService.getSummary(farmUuid);
return (summary.economicOverview as Record<string, unknown>) ?? {};
}
default:
return {};
}
}
type FarmDashboardCardProps = {
cardId: CardId;
farmUuid?: string;
overview?: boolean;
};
const FarmDashboardCard = ({
cardId,
farmUuid,
overview = false,
}: FarmDashboardCardProps) => {
const [data, setData] = useState<Record<string, unknown> | 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 (
<Grid size={12}>
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight={180}
>
<CircularProgress />
</Box>
</Grid>
);
}
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight={200}
>
<CircularProgress size={28} />
</Box>
);
}
if (!data) return null;
const Component = CARD_COMPONENTS[cardId];
return Component ? <Component data={data} /> : null;
};
const FarmDashboardWrapper = () => {
const t = useTranslations("farmDashboard");
const { farmHub } = useFarmHub();
@@ -151,9 +422,6 @@ const FarmDashboardWrapper = () => {
[t],
);
const [cardsData, setCardsData] = useState<
Partial<Record<CardId, Record<string, unknown>>>
>({});
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 = () => {
)}
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
{isOverviewRow && cards.includes("farmOverviewKpis") && (
<FarmOverviewKPIs data={cardsData?.farmOverviewKpis} />
<FarmDashboardCard
cardId="farmOverviewKpis"
farmUuid={farmUuid}
overview
/>
)}
{!isOverviewRow &&
cards.map((cardId: CardId) => {
const size = CARD_GRID_SIZE[cardId];
const Component = CARD_COMPONENTS[cardId];
if (!Component) return null;
return (
<Grid key={cardId} size={size} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
<FarmDashboardCard cardId={cardId} farmUuid={farmUuid} />
</Grid>
);
})}