UPDATE
This commit is contained in:
@@ -23,6 +23,7 @@ import StyledVerticalNavExpandIcon from "@menu/styles/vertical/StyledVerticalNav
|
|||||||
// Style Imports
|
// Style Imports
|
||||||
import menuItemStyles from "@core/styles/vertical/menuItemStyles";
|
import menuItemStyles from "@core/styles/vertical/menuItemStyles";
|
||||||
import menuSectionStyles from "@core/styles/vertical/menuSectionStyles";
|
import menuSectionStyles from "@core/styles/vertical/menuSectionStyles";
|
||||||
|
import { navigationLabels } from "@/constants/navigation";
|
||||||
|
|
||||||
// Menu Data Imports
|
// Menu Data Imports
|
||||||
// import menuData from '@/data/navigation/verticalMenuData'
|
// import menuData from '@/data/navigation/verticalMenuData'
|
||||||
@@ -53,6 +54,9 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const verticalNavOptions = useVerticalNav();
|
const verticalNavOptions = useVerticalNav();
|
||||||
|
|
||||||
|
const translateNav = (key: keyof typeof navigationLabels) =>
|
||||||
|
t.has(key) ? t(key) : navigationLabels[key];
|
||||||
|
|
||||||
// Vars
|
// Vars
|
||||||
const { isBreakpointReached, transitionDuration } = verticalNavOptions;
|
const { isBreakpointReached, transitionDuration } = verticalNavOptions;
|
||||||
|
|
||||||
@@ -92,102 +96,102 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
|
|||||||
href={`/dashboard`}
|
href={`/dashboard`}
|
||||||
icon={<i className="tabler-smart-home" />}
|
icon={<i className="tabler-smart-home" />}
|
||||||
>
|
>
|
||||||
{t("dashboards")}
|
{translateNav("dashboards")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
|
||||||
<MenuSection label={t("farmManagement")}>
|
<MenuSection label={translateNav("farmManagement")}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/yield-harvest"
|
href="/yield-harvest"
|
||||||
icon={<i className="tabler-chart-line" />}
|
icon={<i className="tabler-chart-line" />}
|
||||||
>
|
>
|
||||||
{t("yieldHarvest")}
|
{translateNav("yieldHarvest")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/farmer-todos"
|
href="/farmer-todos"
|
||||||
icon={<i className="tabler-checklist" />}
|
icon={<i className="tabler-checklist" />}
|
||||||
>
|
>
|
||||||
{t("farmerTodos")}
|
{translateNav("farmerTodos")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href="/wallet" icon={<i className="tabler-wallet" />}>
|
<MenuItem href="/wallet" icon={<i className="tabler-wallet" />}>
|
||||||
{t("farmWallet")}
|
{translateNav("farmWallet")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/farmer-calendar"
|
href="/farmer-calendar"
|
||||||
icon={<i className="tabler-calendar-event" />}
|
icon={<i className="tabler-calendar-event" />}
|
||||||
>
|
>
|
||||||
{t("farmCalendar")}
|
{translateNav("farmCalendar")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/economy"
|
href="/economy"
|
||||||
icon={<i className="tabler-cash-banknote" />}
|
icon={<i className="tabler-cash-banknote" />}
|
||||||
>
|
>
|
||||||
{t("economicOverview")}
|
{translateNav("economicOverview")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
|
|
||||||
<MenuSection label={t("riskAlerts")}>
|
<MenuSection label={translateNav("riskAlerts")}>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/farm-alerts"
|
href="/farm-alerts"
|
||||||
icon={<i className="tabler-alert-triangle" />}
|
icon={<i className="tabler-alert-triangle" />}
|
||||||
>
|
>
|
||||||
{t("farmAlerts")}
|
{translateNav("farmAlerts")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href="/pest-risk" icon={<i className="tabler-bug" />}>
|
<MenuItem href="/pest-risk" icon={<i className="tabler-bug" />}>
|
||||||
{t("pestDiseaseRisk")}
|
{translateNav("pestDiseaseRisk")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
<MenuSection label={t("monitoring")}>
|
<MenuSection label={translateNav("monitoring")}>
|
||||||
<MenuItem href="/water-data" icon={<i className="tabler-droplet" />}>
|
<MenuItem href="/water-data" icon={<i className="tabler-droplet" />}>
|
||||||
{t("waterData")}
|
{translateNav("waterData")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href="/soil-data" icon={<i className="tabler-seedling" />}>
|
<MenuItem href="/soil-data" icon={<i className="tabler-seedling" />}>
|
||||||
{t("soilData")}
|
{translateNav("soilData")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem href="/crop-zoning" icon={<i className="tabler-map-2" />}>
|
<MenuItem href="/crop-zoning" icon={<i className="tabler-map-2" />}>
|
||||||
{t("cropZoning")}
|
{translateNav("cropZoning")}
|
||||||
</MenuItem>
|
</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" />}>
|
<MenuItem href="/solid-sensor" icon={<i className="tabler-device-analytics" />}>
|
||||||
{t("sensor7In1")}
|
{translateNav("sensor7In1")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
|
|
||||||
<MenuSection label={t("recommendation")}>
|
<MenuSection label={translateNav("recommendation")}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/irrigation-plan"
|
href="/irrigation-plan"
|
||||||
icon={<i className="tabler-droplet-half-2" />}
|
icon={<i className="tabler-droplet-half-2" />}
|
||||||
>
|
>
|
||||||
{t("irrigationPlanParser")}
|
{translateNav("irrigationPlanParser")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/irrigation-recommendation"
|
href="/irrigation-recommendation"
|
||||||
icon={<i className="tabler-droplet-half-2" />}
|
icon={<i className="tabler-droplet-half-2" />}
|
||||||
>
|
>
|
||||||
{t("irrigationRecommendation")}
|
{translateNav("irrigationRecommendation")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/fertilization-recommendation"
|
href="/fertilization-recommendation"
|
||||||
icon={<i className="tabler-atom-2" />}
|
icon={<i className="tabler-atom-2" />}
|
||||||
>
|
>
|
||||||
{t("fertilizationRecommendation")}
|
{translateNav("fertilizationRecommendation")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/fertilization-plan"
|
href="/fertilization-plan"
|
||||||
icon={<i className="tabler-sparkles" />}
|
icon={<i className="tabler-sparkles" />}
|
||||||
>
|
>
|
||||||
{t("fertilizationPlanParser")}
|
{translateNav("fertilizationPlanParser")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
<MenuSection label={t("aiAssistant")}>
|
<MenuSection label={translateNav("aiAssistant")}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
href="/farm-ai-assistant"
|
href="/farm-ai-assistant"
|
||||||
icon={<i className="tabler-robot" />}
|
icon={<i className="tabler-robot" />}
|
||||||
>
|
>
|
||||||
{t("farmAiAssistant")}
|
{translateNav("farmAiAssistant")}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -36,6 +36,5 @@ export * from "./services/rolesPermissionsService";
|
|||||||
export * from "./services/farmHubService";
|
export * from "./services/farmHubService";
|
||||||
export {
|
export {
|
||||||
type FarmDashboardConfigResponse,
|
type FarmDashboardConfigResponse,
|
||||||
type FarmDashboardCardsResponse,
|
|
||||||
farmDashboardService,
|
farmDashboardService,
|
||||||
} from "./services/farmDashboardService";
|
} from "./services/farmDashboardService";
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Farm Dashboard Service
|
* 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
|
* - Config: disabled cards, row order, drag reorder
|
||||||
* - Cards: all 15 card payloads from /api/farm-dashboard/
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiClient } from "../client";
|
import { apiClient } from "../client";
|
||||||
@@ -29,36 +28,6 @@ export interface FarmDashboardConfigResponse {
|
|||||||
enable_drag_reorder?: boolean;
|
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";
|
const STORAGE_KEY_PREFIX = "farm_dashboard_config";
|
||||||
|
|
||||||
function getStorageKey(farmUuid: string): string {
|
function getStorageKey(farmUuid: string): string {
|
||||||
@@ -101,28 +70,6 @@ function normalizeRowOrder(rowOrder: string[] = []): string[] {
|
|||||||
: [...ROW_IDS];
|
: [...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
|
* Transform API response to frontend config format
|
||||||
*/
|
*/
|
||||||
@@ -242,26 +189,4 @@ export const farmDashboardService = {
|
|||||||
throw err;
|
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";
|
"use client";
|
||||||
|
|
||||||
// React Imports
|
// React Imports
|
||||||
import type { RefObject } from "react";
|
import type { ComponentType, RefObject } from "react";
|
||||||
import { useEffect, useMemo, useState, useCallback, useContext } from "react";
|
import { useEffect, useMemo, useState, useCallback, useContext } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useFarmHub } from "@/hooks/useFarmHub";
|
import { useFarmHub } from "@/hooks/useFarmHub";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
// Context Imports
|
// Context Imports
|
||||||
import NavbarSlotContext from "@/contexts/navbarSlotContext";
|
import NavbarSlotContext from "@/contexts/navbarSlotContext";
|
||||||
@@ -47,6 +48,18 @@ import {
|
|||||||
type FarmDashboardConfig,
|
type FarmDashboardConfig,
|
||||||
} from "@views/dashboards/farm/farmDashboardConfig";
|
} from "@views/dashboards/farm/farmDashboardConfig";
|
||||||
import { farmDashboardService } from "@/libs/api/services/farmDashboardService";
|
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";
|
import FarmDashboardSettingsDropdown from "@views/dashboards/farm/FarmDashboardSettingsDropdown";
|
||||||
|
|
||||||
const cardRowSx = {
|
const cardRowSx = {
|
||||||
@@ -57,7 +70,7 @@ const cardRowSx = {
|
|||||||
|
|
||||||
const CARD_COMPONENTS: Record<
|
const CARD_COMPONENTS: Record<
|
||||||
CardId,
|
CardId,
|
||||||
React.ComponentType<{ data?: Record<string, unknown> }>
|
ComponentType<{ data?: Record<string, unknown> }>
|
||||||
> = {
|
> = {
|
||||||
farmOverviewKpis: FarmOverviewKPIs,
|
farmOverviewKpis: FarmOverviewKPIs,
|
||||||
farmWeatherCard: FarmWeatherCard,
|
farmWeatherCard: FarmWeatherCard,
|
||||||
@@ -96,6 +109,264 @@ function areStringArraysEqual(left: string[], right: string[]): boolean {
|
|||||||
return left.every((value, index) => value === right[index]);
|
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 FarmDashboardWrapper = () => {
|
||||||
const t = useTranslations("farmDashboard");
|
const t = useTranslations("farmDashboard");
|
||||||
const { farmHub } = useFarmHub();
|
const { farmHub } = useFarmHub();
|
||||||
@@ -151,9 +422,6 @@ const FarmDashboardWrapper = () => {
|
|||||||
[t],
|
[t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [cardsData, setCardsData] = useState<
|
|
||||||
Partial<Record<CardId, Record<string, unknown>>>
|
|
||||||
>({});
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
@@ -168,7 +436,7 @@ const FarmDashboardWrapper = () => {
|
|||||||
if (!Array.isArray(cards)) return false;
|
if (!Array.isArray(cards)) return false;
|
||||||
return cards.some((cardId) => !disabledSet.has(cardId));
|
return cards.some((cardId) => !disabledSet.has(cardId));
|
||||||
},
|
},
|
||||||
[config.disabledCardIds],
|
[disabledSet],
|
||||||
);
|
);
|
||||||
|
|
||||||
const visibleRowOrder = useMemo(
|
const visibleRowOrder = useMemo(
|
||||||
@@ -193,18 +461,15 @@ const FarmDashboardWrapper = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!farmUuid) {
|
if (!farmUuid) {
|
||||||
setConfig(DEFAULT_FARM_DASHBOARD_CONFIG);
|
setConfig(DEFAULT_FARM_DASHBOARD_CONFIG);
|
||||||
setCardsData({});
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
Promise.all([
|
farmDashboardService
|
||||||
farmDashboardService.getConfig(farmUuid),
|
.getConfig(farmUuid)
|
||||||
farmDashboardService.getAllCards(farmUuid),
|
.then((configData) => {
|
||||||
])
|
|
||||||
.then(([configData, cards]) => {
|
|
||||||
const validRowOrder = (configData.rowOrder ?? []).filter(
|
const validRowOrder = (configData.rowOrder ?? []).filter(
|
||||||
(id): id is RowId => id in ROW_CARDS,
|
(id): id is RowId => id in ROW_CARDS,
|
||||||
);
|
);
|
||||||
@@ -214,7 +479,6 @@ const FarmDashboardWrapper = () => {
|
|||||||
enableDragReorder: configData.enableDragReorder ?? true,
|
enableDragReorder: configData.enableDragReorder ?? true,
|
||||||
};
|
};
|
||||||
setConfig(merged);
|
setConfig(merged);
|
||||||
setCardsData(cards ?? {});
|
|
||||||
})
|
})
|
||||||
.catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG))
|
.catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -356,16 +620,18 @@ const FarmDashboardWrapper = () => {
|
|||||||
)}
|
)}
|
||||||
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
|
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
|
||||||
{isOverviewRow && cards.includes("farmOverviewKpis") && (
|
{isOverviewRow && cards.includes("farmOverviewKpis") && (
|
||||||
<FarmOverviewKPIs data={cardsData?.farmOverviewKpis} />
|
<FarmDashboardCard
|
||||||
|
cardId="farmOverviewKpis"
|
||||||
|
farmUuid={farmUuid}
|
||||||
|
overview
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!isOverviewRow &&
|
{!isOverviewRow &&
|
||||||
cards.map((cardId: CardId) => {
|
cards.map((cardId: CardId) => {
|
||||||
const size = CARD_GRID_SIZE[cardId];
|
const size = CARD_GRID_SIZE[cardId];
|
||||||
const Component = CARD_COMPONENTS[cardId];
|
|
||||||
if (!Component) return null;
|
|
||||||
return (
|
return (
|
||||||
<Grid key={cardId} size={size} sx={cardRowSx}>
|
<Grid key={cardId} size={size} sx={cardRowSx}>
|
||||||
<Component data={cardsData?.[cardId]} />
|
<FarmDashboardCard cardId={cardId} farmUuid={farmUuid} />
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user