From 4b0f5601fc41d96cb3223c99f8aecf89f00480c4 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Tue, 5 May 2026 23:54:24 +0330 Subject: [PATCH] UPDATE --- DASHBOARD_EMPTY_CARD_REPORT.md | 82 ++++ .../services/pestDetectionDomainService.ts | 35 +- src/libs/api/services/sensor7Service.ts | 266 ++++++++-- .../dashboards/farm/AnomalyDetectionCard.tsx | 4 - .../dashboards/farm/EconomicOverview.tsx | 2 - .../dashboards/farm/FarmAlertsTimeline.tsx | 4 - .../dashboards/farm/FarmAlertsTracker.tsx | 2 - .../dashboards/farm/FarmDashboardWrapper.tsx | 464 ++++++++++++++---- .../dashboards/farm/FarmOverviewKPIs.tsx | 172 +++++-- .../dashboards/farm/FarmWalletSummaryCard.tsx | 73 +++ src/views/dashboards/farm/FarmWeatherCard.tsx | 4 - .../farm/FarmerCalendarSummaryCard.tsx | 98 ++++ .../dashboards/farm/FarmerTodoSummaryCard.tsx | 100 ++++ .../dashboards/farm/HarvestPredictionCard.tsx | 2 - .../dashboards/farm/RecommendationsList.tsx | 2 - .../dashboards/farm/SensorComparisonChart.tsx | 28 +- .../dashboards/farm/SensorRadarChart.tsx | 21 +- .../dashboards/farm/SensorValuesList.tsx | 62 +-- .../dashboards/farm/WaterNeedPrediction.tsx | 4 - .../dashboards/farm/YieldPredictionChart.tsx | 2 - .../dashboards/farm/farmDashboardConfig.ts | 28 +- .../dashboards/farm/sensor7/Sensor7Page.tsx | 287 ++++++++--- .../farm/shared-sensors/DeviceCodeSelect.tsx | 60 +++ 23 files changed, 1486 insertions(+), 316 deletions(-) create mode 100644 DASHBOARD_EMPTY_CARD_REPORT.md create mode 100644 src/views/dashboards/farm/FarmWalletSummaryCard.tsx create mode 100644 src/views/dashboards/farm/FarmerCalendarSummaryCard.tsx create mode 100644 src/views/dashboards/farm/FarmerTodoSummaryCard.tsx create mode 100644 src/views/dashboards/farm/shared-sensors/DeviceCodeSelect.tsx diff --git a/DASHBOARD_EMPTY_CARD_REPORT.md b/DASHBOARD_EMPTY_CARD_REPORT.md new file mode 100644 index 0000000..c885b11 --- /dev/null +++ b/DASHBOARD_EMPTY_CARD_REPORT.md @@ -0,0 +1,82 @@ +# گزارش جای خالی کارت‌های داشبورد + +## خلاصه + +در داشبورد فعلی بعضی اسلات‌ها `Quick Access` دارند اما خود کارت رندر نمی‌شود. +علت اصلی این است که wrapper اسلات را می‌سازد، ولی بعضی کامپوننت‌ها وقتی داده‌ی کافی ندارند `return null` می‌کنند. + +## کارت‌های خالی فعلی + +### 1) `sensorValuesList` +- **ردیف:** `sensorMonitoring` +- **جای کارت:** ستون چپ ردیف سنسور +- **Route:** `/solid-sensor` +- **وضعیت:** فقط دکمه `دسترسی سریع` دیده می‌شود، خود کارت خالی است +- **علت فنی:** اگر `data.sensors.length === 0` باشد، کامپوننت رندر نمی‌شود +- **مرجع:** `src/views/dashboards/farm/SensorValuesList.tsx:30` + +### 2) `sensorRadarChart` +- **ردیف:** `sensorMonitoring` +- **جای کارت:** ستون راست ردیف سنسور +- **Route:** `/solid-sensor` +- **وضعیت:** فقط دکمه `دسترسی سریع` دیده می‌شود، خود کارت خالی است +- **علت فنی:** اگر `data.series.length === 0` باشد، کامپوننت رندر نمی‌شود +- **مرجع:** `src/views/dashboards/farm/SensorRadarChart.tsx:30` + +### 3) `sensorComparisonChart` +- **ردیف:** `sensorCharts` +- **جای کارت:** ستون بزرگ سمت چپ +- **Route:** `/solid-sensor` +- **وضعیت:** فقط دکمه `دسترسی سریع` دیده می‌شود، خود کارت خالی است +- **علت فنی:** اگر `data.series.length === 0` باشد، کامپوننت رندر نمی‌شود +- **مرجع:** `src/views/dashboards/farm/SensorComparisonChart.tsx:33` + +## کارت‌هایی که ممکن است در شرایط خاص خالی شوند + +این‌ها در HTML ارسالی شما فعلاً دیده می‌شوند، ولی در صورت خالی بودن داده می‌توانند ناپدید شوند: + +### `waterNeedPrediction` +- **شرط خالی شدن:** `series.length === 0` +- **مرجع:** `src/views/dashboards/farm/WaterNeedPrediction.tsx:37` + +### `soilMoistureHeatmap` +- **شرط خالی شدن:** `series.length === 0` +- **مرجع:** `src/views/dashboards/farm/SoilMoistureHeatmap.tsx:37` + +### `yieldPredictionChart` +- **شرط خالی شدن:** `series.length === 0` +- **مرجع:** `src/views/dashboards/farm/YieldPredictionChart.tsx:57` + +### `recommendationsList` +- **شرط خالی شدن:** `recommendations.length === 0` +- **مرجع:** `src/views/dashboards/farm/RecommendationsList.tsx:32` + +## دلیل اینکه فقط آیکن دیده می‌شود + +Wrapper همیشه این دکمه را رندر می‌کند: +- `src/views/dashboards/farm/FarmDashboardWrapper.tsx:921` + +اما خود کارت از اینجا `null` برمی‌گرداند: +- `src/views/dashboards/farm/FarmDashboardWrapper.tsx:478` + +یعنی: +- اسلات گرید وجود دارد +- دکمه quick access وجود دارد +- ولی محتوای کارت به خاطر داده‌ی خالی رندر نمی‌شود + +## نتیجه نهایی + +بر اساس HTML فعلی، **سه جای خالی قطعی** در داشبورد وجود دارد: + +1. `sensorValuesList` +2. `sensorRadarChart` +3. `sensorComparisonChart` + +## پیشنهاد اصلاح + +بهترین اصلاح این است که به‌جای `return null`: + +- یک empty state واقعی داخل کارت نمایش داده شود +- یا در `FarmDashboardWrapper` وقتی داده‌ی کارت خالی است، اصلاً `Quick Access` هم رندر نشود + +اگر خواستی، قدم بعدی را هم انجام می‌دهم و همین سه کارت خالی را با `empty state card` درست می‌کنم. diff --git a/src/libs/api/services/pestDetectionDomainService.ts b/src/libs/api/services/pestDetectionDomainService.ts index 4357cf3..eadf9fa 100644 --- a/src/libs/api/services/pestDetectionDomainService.ts +++ b/src/libs/api/services/pestDetectionDomainService.ts @@ -1,7 +1,5 @@ import { apiClient } from '../client' -import type { ApiError } from '../client' -const DETECTION_PREFIX = '/api/pest-detection' const DISEASE_PREFIX = '/api/pest-disease' export interface RiskCard { @@ -29,12 +27,6 @@ interface ApiResponse { result?: T } -function isRouteMismatchError(error: unknown): boolean { - const statusCode = (error as ApiError | undefined)?.code - - return statusCode === 404 || statusCode === 405 -} - function extract(res: ApiResponse | T): T { if (res && typeof res === 'object') { if ('data' in res) return (res as ApiResponse).data @@ -92,29 +84,10 @@ function toKpiCard( export const pestDetectionDomainService = { async getRiskSummary(farmUuid: string): Promise { - let res: ApiResponse> | Record - - try { - res = await apiClient.post> | Record>( - `${DETECTION_PREFIX}/risk-summary/`, - { farm_uuid: farmUuid } - ) - } catch (error) { - if (!isRouteMismatchError(error)) throw error - - try { - res = await apiClient.get> | Record>( - `${DETECTION_PREFIX}/risk-summary/?farm_uuid=${encodeURIComponent(farmUuid)}` - ) - } catch (fallbackError) { - if (!isRouteMismatchError(fallbackError)) throw fallbackError - - res = await apiClient.post> | Record>( - `${DISEASE_PREFIX}/risk-summary/`, - { farm_uuid: farmUuid } - ) - } - } + const res = await apiClient.post> | Record>( + `${DISEASE_PREFIX}/risk-summary/`, + { farm_uuid: farmUuid } + ) const data = extract(res) const diseaseRisk = (data?.diseaseRisk as Record | undefined) ?? (data?.disease_risk as Record | undefined) diff --git a/src/libs/api/services/sensor7Service.ts b/src/libs/api/services/sensor7Service.ts index f26f21e..4869085 100644 --- a/src/libs/api/services/sensor7Service.ts +++ b/src/libs/api/services/sensor7Service.ts @@ -1,7 +1,7 @@ import { apiClient } from "../client"; +import { sensorExternalApiService } from "./sensorExternalApiService"; -const SUMMARY_PREFIX = "/api/sensor-7-in-1"; -const SENSORS_PREFIX = "/api/sensors"; +const DEVICE_HUB_PREFIX = "/api/device-hub"; export interface ApiResponse { code: number; @@ -58,6 +58,35 @@ export interface Sensor7SummaryData { soilMoistureHeatmap?: Record | null; } +export interface DeviceCatalogMeta { + uuid: string; + code: string; + name: string; + device_communication_type?: string | null; +} + +export interface DeviceDetailResponse { + uuid: string; + physical_device_uuid: string; + name: string; + device_catalog?: DeviceCatalogMeta | null; + specifications?: Record | null; + power_source?: Record | null; + last_payload_at?: string | null; +} + +export interface DeviceLatestPayloadResponse { + raw_payload?: Record | null; + normalized_payload?: Record | null; + readings?: unknown[]; + last_payload_at?: string | null; +} + +export interface DeviceCodesResponse { + physical_device_uuid: string; + device_codes: string[]; +} + const EMPTY_COMPARISON_CHART: SensorComparisonChartResponse = { currentValue: 0, vsLastWeek: "+0.0%", @@ -74,12 +103,45 @@ const EMPTY_VALUES_LIST: SensorValuesListResponse = { sensors: [], }; +const primaryDevicePromiseCache = new Map>(); +const deviceCodesPromiseCache = new Map>(); + function extract(response: ApiResponse | T): T { return response && typeof response === "object" && "data" in response ? (response as ApiResponse).data : (response as T); } +function buildDeviceHubUrl(physicalDeviceUuid: string, suffix = ""): string { + const base = `${DEVICE_HUB_PREFIX}/devices/${encodeURIComponent(physicalDeviceUuid)}/`; + + return suffix ? `${base}${suffix}` : base; +} + +function buildDeviceHubRequestUrl( + physicalDeviceUuid: string, + suffix = "", + queryParams?: Record, +): string { + const baseUrl = buildDeviceHubUrl(physicalDeviceUuid, suffix); + + if (!queryParams) { + return baseUrl; + } + + const searchParams = new URLSearchParams(); + + Object.entries(queryParams).forEach(([key, value]) => { + if (value) { + searchParams.set(key, value); + } + }); + + const queryString = searchParams.toString(); + + return queryString ? `${baseUrl}?${queryString}` : baseUrl; +} + function normalizeSeries(series: unknown): SensorChartSeries[] { if (!Array.isArray(series)) return []; @@ -171,10 +233,134 @@ export function normalizeValuesListResponse( }; } +function normalizeDeviceCodesResponse( + data?: Partial | null, +): DeviceCodesResponse { + if (!data || typeof data !== "object") { + return { + physical_device_uuid: "", + device_codes: [], + }; + } + + return { + physical_device_uuid: + typeof data.physical_device_uuid === "string" ? data.physical_device_uuid : "", + device_codes: Array.isArray(data.device_codes) + ? data.device_codes.filter( + (value): value is string => typeof value === "string" && value.length > 0, + ) + : [], + }; +} + +async function getCachedDeviceCodes( + physicalDeviceUuid: string, +): Promise { + const cached = deviceCodesPromiseCache.get(physicalDeviceUuid); + + if (cached) { + return cached; + } + + const request = apiClient + .get | DeviceCodesResponse>( + buildDeviceHubUrl(physicalDeviceUuid, "device-codes/"), + ) + .then((response) => normalizeDeviceCodesResponse(extract(response))) + .catch((error) => { + deviceCodesPromiseCache.delete(physicalDeviceUuid); + throw error; + }); + + deviceCodesPromiseCache.set(physicalDeviceUuid, request); + + return request; +} + +async function resolveDeviceCode( + physicalDeviceUuid: string, + deviceCode?: string, +): Promise { + if (deviceCode) { + return deviceCode; + } + + try { + const response = await getCachedDeviceCodes(physicalDeviceUuid); + + return response.device_codes[0]; + } catch { + return undefined; + } +} + export const sensor7Service = { - async getSummary(farmUuid: string): Promise { + async resolvePrimaryPhysicalDeviceUuid(farmUuid: string): Promise { + const cached = primaryDevicePromiseCache.get(farmUuid); + + if (cached) { + return cached; + } + + const request = sensorExternalApiService + .listRequestLogs({ + farmUuid, + page: 1, + pageSize: 50, + }) + .then((response) => { + const firstLog = Array.isArray(response.data) + ? response.data.find((item) => typeof item.physical_device_uuid === "string") + : null; + + return firstLog?.physical_device_uuid ?? null; + }) + .catch(() => null); + + primaryDevicePromiseCache.set(farmUuid, request); + + return request; + }, + + async getDeviceDetail(physicalDeviceUuid: string): Promise { + const deviceCode = await resolveDeviceCode(physicalDeviceUuid); + const response = await apiClient.get | DeviceDetailResponse>( + buildDeviceHubRequestUrl(physicalDeviceUuid, "", { device_code: deviceCode }), + ); + + return extract(response); + }, + + async getDeviceCodes(physicalDeviceUuid: string): Promise { + return getCachedDeviceCodes(physicalDeviceUuid); + }, + + async getLatestPayload( + physicalDeviceUuid: string, + deviceCode?: string, + ): Promise { + const resolvedDeviceCode = await resolveDeviceCode(physicalDeviceUuid, deviceCode); + const response = await apiClient.get< + ApiResponse | DeviceLatestPayloadResponse + >( + buildDeviceHubRequestUrl(physicalDeviceUuid, "latest/", { + device_code: resolvedDeviceCode, + }), + ); + + return extract(response); + }, + + async getSummary( + physicalDeviceUuid: string, + deviceCode?: string, + ): Promise { + const resolvedDeviceCode = await resolveDeviceCode(physicalDeviceUuid, deviceCode); const response = await apiClient.get | Sensor7SummaryData>( - `${SUMMARY_PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`, + buildDeviceHubRequestUrl(physicalDeviceUuid, "summary/", { + device_code: resolvedDeviceCode, + }), ); const data = extract(response) ?? {}; @@ -188,50 +374,68 @@ export const sensor7Service = { }, async getComparisonChart(params: { - farmUuid: string; + physicalDeviceUuid: string; + deviceCode?: string; range?: "7d" | "30d"; }): Promise { - const searchParams = new URLSearchParams({ - farm_uuid: params.farmUuid, - range: params.range ?? "7d", - }); - - const response = await apiClient.get>( - `${SENSORS_PREFIX}/comparison-chart/?${searchParams.toString()}`, + const resolvedDeviceCode = await resolveDeviceCode( + params.physicalDeviceUuid, + params.deviceCode, ); - return normalizeComparisonChartResponse(response); + const response = await apiClient.get< + ApiResponse> | Partial + >( + buildDeviceHubRequestUrl(params.physicalDeviceUuid, "comparison-chart/", { + range: params.range ?? "7d", + device_code: resolvedDeviceCode, + }), + ); + + return normalizeComparisonChartResponse(extract(response)); }, async getRadarChart(params: { - farmUuid: string; + physicalDeviceUuid: string; + deviceCode?: string; range?: "today" | "7d" | "30d"; }): Promise { - const searchParams = new URLSearchParams({ - farm_uuid: params.farmUuid, - range: params.range ?? "7d", - }); - - const response = await apiClient.get>( - `${SENSORS_PREFIX}/radar-chart/?${searchParams.toString()}`, + const resolvedDeviceCode = await resolveDeviceCode( + params.physicalDeviceUuid, + params.deviceCode, ); - return normalizeRadarChartResponse(response); + const response = await apiClient.get< + ApiResponse> | Partial + >( + buildDeviceHubRequestUrl(params.physicalDeviceUuid, "radar-chart/", { + range: params.range ?? "7d", + device_code: resolvedDeviceCode, + }), + ); + + return normalizeRadarChartResponse(extract(response)); }, async getValuesList(params: { - farmUuid: string; + physicalDeviceUuid: string; + deviceCode?: string; range?: "1h" | "24h" | "7d"; }): Promise { - const searchParams = new URLSearchParams({ - farm_uuid: params.farmUuid, - range: params.range ?? "7d", - }); - - const response = await apiClient.get>( - `${SENSORS_PREFIX}/values-list/?${searchParams.toString()}`, + const resolvedDeviceCode = await resolveDeviceCode( + params.physicalDeviceUuid, + params.deviceCode, ); - return normalizeValuesListResponse(response); + const response = await apiClient.get< + ApiResponse> | Partial + >( + buildDeviceHubRequestUrl(params.physicalDeviceUuid, "values-list/", { + range: params.range ?? "7d", + device_code: resolvedDeviceCode, + }), + ); + + return normalizeValuesListResponse(extract(response)); }, }; diff --git a/src/views/dashboards/farm/AnomalyDetectionCard.tsx b/src/views/dashboards/farm/AnomalyDetectionCard.tsx index 8beaedd..c943c51 100644 --- a/src/views/dashboards/farm/AnomalyDetectionCard.tsx +++ b/src/views/dashboards/farm/AnomalyDetectionCard.tsx @@ -10,9 +10,6 @@ import CardContent from '@mui/material/CardContent' import Typography from '@mui/material/Typography' import Chip from '@mui/material/Chip' -// Component Imports -import OptionMenu from '@core/components/option-menu' - type AnomalyItem = { sensor: string value: string @@ -35,7 +32,6 @@ const AnomalyDetectionCard = ({ data }: AnomalyDetectionCardProps) => { avatar={} title={t('cards.anomalyDetectionCard')} subheader={t('subheaders.outOfRangeValues')} - action={} sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }} /> diff --git a/src/views/dashboards/farm/EconomicOverview.tsx b/src/views/dashboards/farm/EconomicOverview.tsx index 82106bc..bec1321 100644 --- a/src/views/dashboards/farm/EconomicOverview.tsx +++ b/src/views/dashboards/farm/EconomicOverview.tsx @@ -14,7 +14,6 @@ import { useTheme } from '@mui/material/styles' import type { ApexOptions } from 'apexcharts' // Component Imports -import OptionMenu from '@core/components/option-menu' import CardStatsVertical from '@/components/card-statistics/Vertical' // Styled Component Imports @@ -77,7 +76,6 @@ const EconomicOverview = ({ data }: EconomicOverviewProps) => { } /> {economicData.length > 0 && ( diff --git a/src/views/dashboards/farm/FarmAlertsTimeline.tsx b/src/views/dashboards/farm/FarmAlertsTimeline.tsx index 25ccab1..bdbbb13 100644 --- a/src/views/dashboards/farm/FarmAlertsTimeline.tsx +++ b/src/views/dashboards/farm/FarmAlertsTimeline.tsx @@ -17,9 +17,6 @@ import TimelineConnector from '@mui/lab/TimelineConnector' import MuiTimeline from '@mui/lab/Timeline' import type { TimelineProps } from '@mui/lab/Timeline' -// Component Imports -import OptionMenu from '@core/components/option-menu' - // Styled Timeline const Timeline = styled(MuiTimeline)({ paddingLeft: 0, @@ -52,7 +49,6 @@ const FarmAlertsTimeline = ({ data }: FarmAlertsTimelineProps) => { title={t('cards.farmAlertsTimeline')} titleTypographyProps={{ variant: 'h5' }} subheader={t('subheaders.explainableRecommendations')} - action={} sx={{ '& .MuiCardHeader-avatar': { mr: 3 } }} /> diff --git a/src/views/dashboards/farm/FarmAlertsTracker.tsx b/src/views/dashboards/farm/FarmAlertsTracker.tsx index 5195203..a8cdc07 100644 --- a/src/views/dashboards/farm/FarmAlertsTracker.tsx +++ b/src/views/dashboards/farm/FarmAlertsTracker.tsx @@ -19,7 +19,6 @@ import type { ApexOptions } from 'apexcharts' import type { ThemeColor } from '@core/types' // Components Imports -import OptionMenu from '@core/components/option-menu' import CustomAvatar from '@core/components/mui/Avatar' // Styled Component Imports @@ -98,7 +97,6 @@ const FarmAlertsTracker = ({ data }: FarmAlertsTrackerProps) => { } />
diff --git a/src/views/dashboards/farm/FarmDashboardWrapper.tsx b/src/views/dashboards/farm/FarmDashboardWrapper.tsx index df31418..f0e20bc 100644 --- a/src/views/dashboards/farm/FarmDashboardWrapper.tsx +++ b/src/views/dashboards/farm/FarmDashboardWrapper.tsx @@ -2,10 +2,11 @@ // React Imports import type { ComponentType, RefObject } from "react"; -import { useEffect, useMemo, useState, useCallback, useContext } from "react"; +import { useEffect, useMemo, useState, useCallback, useContext, useRef } from "react"; import { useTranslations } from "next-intl"; import { useFarmHub } from "@/hooks/useFarmHub"; import { format } from "date-fns"; +import Link from "next/link"; // Context Imports import NavbarSlotContext from "@/contexts/navbarSlotContext"; @@ -36,6 +37,9 @@ import AnomalyDetectionCard from "@views/dashboards/farm/AnomalyDetectionCard"; import NDVIHealthCard from "@views/dashboards/farm/NDVIHealthCard"; import RecommendationsList from "@views/dashboards/farm/RecommendationsList"; import EconomicOverview from "@views/dashboards/farm/EconomicOverview"; +import FarmerTodoSummaryCard from "@views/dashboards/farm/FarmerTodoSummaryCard"; +import FarmerCalendarSummaryCard from "@views/dashboards/farm/FarmerCalendarSummaryCard"; +import FarmWalletSummaryCard from "@views/dashboards/farm/FarmWalletSummaryCard"; // Config & Service import { @@ -60,6 +64,9 @@ 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 { farmerTodoService } from "@/libs/api/services/farmerTodoService"; +import { eventService } from "@/libs/api/services/eventService"; +import { pestDetectionDomainService } from "@/libs/api/services/pestDetectionDomainService"; import FarmDashboardSettingsDropdown from "@views/dashboards/farm/FarmDashboardSettingsDropdown"; const cardRowSx = { @@ -86,9 +93,67 @@ const CARD_COMPONENTS: Record< soilMoistureHeatmap: SoilMoistureHeatmap, ndviHealthCard: NDVIHealthCard, recommendationsList: RecommendationsList, + diseaseRiskCard: FarmOverviewKPIs, + pestRiskCard: FarmOverviewKPIs, + farmerTodoSummary: FarmerTodoSummaryCard, + farmerCalendarSummary: FarmerCalendarSummaryCard, + farmWalletSummary: FarmWalletSummaryCard, economicOverview: EconomicOverview, }; +const CARD_ROUTES: Record = { + farmOverviewKpis: "/yield-harvest", + farmWeatherCard: "/water-data", + farmAlertsTracker: "/farm-alerts", + sensorValuesList: "/solid-sensor", + sensorRadarChart: "/solid-sensor", + sensorComparisonChart: "/solid-sensor", + anomalyDetectionCard: "/soil-data", + farmAlertsTimeline: "/farm-alerts", + waterNeedPrediction: "/water-data", + harvestPredictionCard: "/yield-harvest", + yieldPredictionChart: "/yield-harvest", + soilMoistureHeatmap: "/soil-data", + ndviHealthCard: "/crop-zoning", + recommendationsList: "/farm-alerts", + diseaseRiskCard: "/pest-risk", + pestRiskCard: "/pest-risk", + farmerTodoSummary: "/farmer-todos", + farmerCalendarSummary: "/farmer-calendar", + farmWalletSummary: "/wallet", + economicOverview: "/economic-overview", +}; + +const QuickAccessButton = ({ href }: { href: string }) => { + return ( + `1px solid ${theme.palette.divider}`, + backgroundColor: "background.paper", + backdropFilter: "blur(8px)", + boxShadow: 1, + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + + ); +}; + function mergeRowOrderAfterDrag( currentRowOrder: string[], newVisibleOrder: string[], @@ -221,6 +286,7 @@ function buildRecommendationsData( async function loadDashboardCardData( cardId: CardId, farmUuid: string, + sensorPhysicalDeviceUuid?: string | null, ): Promise> { switch (cardId) { case "farmOverviewKpis": { @@ -234,18 +300,25 @@ async function loadDashboardCardData( return buildTrackerCardData(result); } case "sensorValuesList": - return (await sensor7Service.getValuesList({ farmUuid })) as unknown as Record< + if (!sensorPhysicalDeviceUuid) return {}; + return (await sensor7Service.getValuesList({ + physicalDeviceUuid: sensorPhysicalDeviceUuid, + })) as unknown as Record< string, unknown >; case "sensorRadarChart": - return (await sensor7Service.getRadarChart({ farmUuid })) as unknown as Record< + if (!sensorPhysicalDeviceUuid) return {}; + return (await sensor7Service.getRadarChart({ + physicalDeviceUuid: sensorPhysicalDeviceUuid, + })) as unknown as Record< string, unknown >; case "sensorComparisonChart": + if (!sensorPhysicalDeviceUuid) return {}; return (await sensor7Service.getComparisonChart({ - farmUuid, + physicalDeviceUuid: sensorPhysicalDeviceUuid, })) as unknown as Record; case "anomalyDetectionCard": return await soilService.getAnomalies(farmUuid); @@ -276,92 +349,145 @@ async function loadDashboardCardData( const result = await farmAlertsService.analyzeTracker({ farmUuid }); return buildRecommendationsData(result); } + case "diseaseRiskCard": { + const summary = await pestDetectionDomainService.getRiskSummary(farmUuid); + return (summary.diseaseRisk as Record) ?? {}; + } + case "pestRiskCard": { + const summary = await pestDetectionDomainService.getRiskSummary(farmUuid); + return (summary.pestRisk as Record) ?? {}; + } case "economicOverview": { const summary = await economicOverviewService.getSummary(farmUuid); return (summary.economicOverview as Record) ?? {}; } + case "farmerTodoSummary": { + const summary = await farmerTodoService.getSummary(farmUuid); + return summary as unknown as Record; + } + case "farmerCalendarSummary": { + const now = new Date(); + const start = new Date(now.getFullYear(), now.getMonth(), 1).toISOString(); + const end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59).toISOString(); + const events = await eventService.listEvents({ farm_uuid: farmUuid, start, end }); + + const normalized = events + .map((event) => { + const startDate = new Date(event.start); + if (Number.isNaN(startDate.getTime())) return null; + + return { + id: event.id, + title: event.title, + start: startDate, + allDay: Boolean(event.allDay), + categoryLabel: String(event.extendedProps?.calendar ?? "ETC"), + }; + }) + .filter((item): item is NonNullable => Boolean(item)) + .sort((left, right) => left.start.getTime() - right.start.getTime()); + + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + const todayEnd = new Date(); + todayEnd.setHours(23, 59, 59, 999); + const weekEnd = new Date(todayStart); + weekEnd.setDate(weekEnd.getDate() + 7); + + const upcomingEvents = normalized.filter( + (event) => event.start.getTime() >= Date.now(), + ); + const nextEvent = upcomingEvents[0]; + + return { + todayCount: normalized.filter( + (event) => + event.start.getTime() >= todayStart.getTime() && + event.start.getTime() <= todayEnd.getTime(), + ).length, + weekCount: normalized.filter( + (event) => + event.start.getTime() >= todayStart.getTime() && + event.start.getTime() <= weekEnd.getTime(), + ).length, + nextEventTitle: nextEvent?.title ?? "رویداد زمان بندی شده ای وجود ندارد", + nextEventMeta: nextEvent + ? `${nextEvent.allDay ? "تمام روز" : nextEvent.start.toLocaleString("fa-IR")}` + : "تقویم مزرعه در این بازه خالی است.", + upcomingEvents: upcomingEvents.slice(0, 2).map((event) => ({ + id: event.id, + title: event.title, + dateLabel: event.allDay + ? event.start.toLocaleDateString("fa-IR") + : event.start.toLocaleString("fa-IR"), + categoryLabel: event.categoryLabel, + })), + }; + } + case "farmWalletSummary": + return { + balance: "۲۴۸,۵۰۰,۰۰۰ تومان", + pendingSettlement: "۱۲,۴۰۰,۰۰۰ تومان", + monthlyInflow: "۹۴,۸۰۰,۰۰۰ تومان", + monthlyOutflow: "۵۱,۲۰۰,۰۰۰ تومان", + healthLabel: "نقدینگی پایدار", + }; default: return {}; } } -type FarmDashboardCardProps = { +type DashboardCardRendererProps = { cardId: CardId; - farmUuid?: string; - overview?: boolean; + data?: Record; }; -const FarmDashboardCard = ({ +const OVERVIEW_KPI_ROUTES: Record = { + "predicted-yield": "/yield-harvest", + "quality-score": "/yield-harvest", + "days-to-harvest": "/yield-harvest", + "harvest-readiness": "/yield-harvest", + "loss-risk": "/yield-harvest", + "water-stress-index": "/water-data", + "disease-risk": "/pest-risk", + "pest-risk": "/pest-risk", +}; + +function getOverviewData( + cardsData: Partial>>, +): Record { + const baseKpis = Array.isArray(cardsData?.farmOverviewKpis?.kpis) + ? (cardsData.farmOverviewKpis?.kpis as Record[]) + : []; + const waterStressKpis = + cardsData?.waterNeedPrediction != null && + typeof cardsData.waterNeedPrediction === "object" && + cardsData.waterNeedPrediction.waterStressIndex != null + ? [cardsData.waterNeedPrediction.waterStressIndex as Record] + : []; + const diseaseKpis = Array.isArray(cardsData?.diseaseRiskCard?.kpis) + ? (cardsData.diseaseRiskCard?.kpis as Record[]) + : []; + const pestKpis = Array.isArray(cardsData?.pestRiskCard?.kpis) + ? (cardsData.pestRiskCard?.kpis as Record[]) + : []; + + return { + ...cardsData?.farmOverviewKpis, + kpis: [...baseKpis, ...waterStressKpis, ...diseaseKpis, ...pestKpis], + }; +} + +const DashboardCardRenderer = ({ 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 ( - - - - ); - } - + data, +}: DashboardCardRendererProps) => { if (!data) return null; + if (cardId === "diseaseRiskCard" || cardId === "pestRiskCard") { + return ; + } + const Component = CARD_COMPONENTS[cardId]; return Component ? : null; @@ -371,6 +497,7 @@ const FarmDashboardWrapper = () => { const t = useTranslations("farmDashboard"); const { farmHub } = useFarmHub(); const farmUuid = farmHub?.farm_uuid; + const [sensorPhysicalDeviceUuid, setSensorPhysicalDeviceUuid] = useState(null); const { setSlotContent } = useContext(NavbarSlotContext); const [config, setConfig] = useState( DEFAULT_FARM_DASHBOARD_CONFIG, @@ -395,6 +522,11 @@ const FarmDashboardWrapper = () => { "soilMoistureHeatmap", "ndviHealthCard", "recommendationsList", + "diseaseRiskCard", + "pestRiskCard", + "farmerTodoSummary", + "farmerCalendarSummary", + "farmWalletSummary", "economicOverview", ] as CardId[] ).map((id) => [id, t(`cards.${id}`)]), @@ -415,6 +547,8 @@ const FarmDashboardWrapper = () => { "predictions", "soilHeatmap", "ndviRecommendations", + "pestRisk", + "dailyOperations", "economic", ] as RowId[] ).map((id) => [id, t(`rows.${id}`)]), @@ -423,7 +557,17 @@ const FarmDashboardWrapper = () => { ); const [loading, setLoading] = useState(true); + const [cardsLoading, setCardsLoading] = useState(false); const [saving, setSaving] = useState(false); + const [cardsData, setCardsData] = useState< + Partial>> + >({}); + const [failedCardIds, setFailedCardIds] = useState([]); + const previousVisibleRowOrderRef = useRef([]); + const visibleRowOrderChangedRef = useRef(false); + const lastCardsRequestKeyRef = useRef(null); + const activeCardsRequestKeyRef = useRef(null); + const completedCardsRequestKeyRef = useRef(null); const disabledSet = useMemo( () => new Set(config.disabledCardIds), @@ -444,6 +588,14 @@ const FarmDashboardWrapper = () => { [config.rowOrder, hasVisibleCard], ); + const visibleCardIds = useMemo( + () => + config.rowOrder.flatMap((rowId) => + ROW_CARDS[rowId as RowId].filter((cardId) => !disabledSet.has(cardId)), + ), + [config.rowOrder, disabledSet], + ); + const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop( visibleRowOrder, { @@ -458,9 +610,40 @@ const FarmDashboardWrapper = () => { // },[visibleRowOrder,visibleRowOrder]) + useEffect(() => { + if (!farmUuid) { + setSensorPhysicalDeviceUuid(null); + return; + } + + let active = true; + + sensor7Service + .resolvePrimaryPhysicalDeviceUuid(farmUuid) + .then((deviceUuid) => { + if (active) { + setSensorPhysicalDeviceUuid(deviceUuid); + } + }) + .catch(() => { + if (active) { + setSensorPhysicalDeviceUuid(null); + } + }); + + return () => { + active = false; + }; + }, [farmUuid]); + useEffect(() => { if (!farmUuid) { setConfig(DEFAULT_FARM_DASHBOARD_CONFIG); + setCardsData({}); + setFailedCardIds([]); + lastCardsRequestKeyRef.current = null; + activeCardsRequestKeyRef.current = null; + completedCardsRequestKeyRef.current = null; setLoading(false); return; } @@ -485,13 +668,116 @@ const FarmDashboardWrapper = () => { }, [farmUuid]); useEffect(() => { + let active = true; + + if (!farmUuid) { + setCardsData({}); + setFailedCardIds([]); + lastCardsRequestKeyRef.current = null; + activeCardsRequestKeyRef.current = null; + completedCardsRequestKeyRef.current = null; + setCardsLoading(false); + return; + } + + if (loading) return; + + if (visibleCardIds.length === 0) { + setCardsData({}); + setFailedCardIds([]); + lastCardsRequestKeyRef.current = null; + activeCardsRequestKeyRef.current = null; + completedCardsRequestKeyRef.current = null; + setCardsLoading(false); + return; + } + + const requestKey = `${farmUuid}:${sensorPhysicalDeviceUuid ?? "none"}:${visibleCardIds.join(",")}`; + + if (lastCardsRequestKeyRef.current !== requestKey) { + lastCardsRequestKeyRef.current = requestKey; + setFailedCardIds([]); + completedCardsRequestKeyRef.current = null; + } + + if ( + activeCardsRequestKeyRef.current === requestKey || + completedCardsRequestKeyRef.current === requestKey + ) { + return; + } + + const cardsToFetch = visibleCardIds.filter( + (cardId) => !failedCardIds.includes(cardId), + ); + + if (cardsToFetch.length === 0) { + setCardsLoading(false); + return; + } + + setCardsLoading(true); + activeCardsRequestKeyRef.current = requestKey; + + Promise.allSettled( + cardsToFetch.map(async (cardId) => ({ + cardId, + data: await loadDashboardCardData(cardId, farmUuid, sensorPhysicalDeviceUuid), + })), + ) + .then((results) => { + if (!active) return; + + const nextData: Partial>> = {}; + const nextFailedCardIds: CardId[] = []; + + results.forEach((result, index) => { + const cardId = cardsToFetch[index]; + + if (result.status === "fulfilled") { + nextData[cardId] = result.value.data; + } else { + nextFailedCardIds.push(cardId); + } + }); + + setCardsData((prev) => ({ ...prev, ...nextData })); + setFailedCardIds((prev) => + Array.from(new Set([...prev, ...nextFailedCardIds])), + ); + }) + .finally(() => { + if (active) { + activeCardsRequestKeyRef.current = null; + completedCardsRequestKeyRef.current = requestKey; + setCardsLoading(false); + } + }); + + return () => { + active = false; + }; + }, [farmUuid, loading, sensorPhysicalDeviceUuid, visibleCardIds]); + + useEffect(() => { + const visibleRowOrderChanged = !areStringArraysEqual( + previousVisibleRowOrderRef.current, + visibleRowOrder, + ); + + previousVisibleRowOrderRef.current = visibleRowOrder; + visibleRowOrderChangedRef.current = visibleRowOrderChanged; + + if (!visibleRowOrderChanged) return; if (areStringArraysEqual(orderedRows, visibleRowOrder)) return; + setOrderedRows(visibleRowOrder); }, [orderedRows, setOrderedRows, visibleRowOrder]); useEffect(() => { if (!farmUuid) return; if (loading) return; + if (visibleRowOrderChangedRef.current) return; if (areStringArraysEqual(orderedRows, visibleRowOrder)) return; const newRowOrder = mergeRowOrderAfterDrag( config.rowOrder, @@ -559,7 +845,7 @@ const FarmDashboardWrapper = () => { saving, ]); - if (loading) { + if (loading || cardsLoading) { return ( { )} {isOverviewRow && cards.includes("farmOverviewKpis") && ( - + + + OVERVIEW_KPI_ROUTES[kpi.id] ?? CARD_ROUTES.farmOverviewKpis + } + /> + )} {!isOverviewRow && cards.map((cardId: CardId) => { const size = CARD_GRID_SIZE[cardId]; return ( - - + + + ); })} diff --git a/src/views/dashboards/farm/FarmOverviewKPIs.tsx b/src/views/dashboards/farm/FarmOverviewKPIs.tsx index dd991be..5ab223c 100644 --- a/src/views/dashboards/farm/FarmOverviewKPIs.tsx +++ b/src/views/dashboards/farm/FarmOverviewKPIs.tsx @@ -2,6 +2,9 @@ // MUI Imports import Grid from "@mui/material/Grid2"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import Link from "next/link"; // Component Imports import CardStatsVertical from "@components/card-statistics/Vertical"; @@ -19,10 +22,75 @@ type KpiItem = { interface FarmOverviewKPIsProps { data?: Record; + showQuickAccess?: boolean; + getQuickAccessHref?: (kpi: KpiItem) => string | null; } -const FarmOverviewKPIs = ({ data }: FarmOverviewKPIsProps) => { - const kpis = (data?.kpis as KpiItem[] | undefined) ?? []; +const WATER_STRESS_FALLBACK_KPI: KpiItem = { + id: "water-stress-index", + title: "شاخص تنش آبی", + subtitle: "فعلی", + stats: "", + avatarColor: "secondary", + avatarIcon: "tabler-droplet", + chipText: "بدون داده", + chipColor: "secondary", +}; + +const normalizeKpi = ( + item: Record | undefined, + index: number, +): KpiItem => { + const hasReadableContent = Boolean( + item && + (typeof item.title === "string" || + typeof item.subtitle === "string" || + typeof item.stats === "string" || + typeof item.chipText === "string"), + ); + + if (!hasReadableContent) { + return { + ...WATER_STRESS_FALLBACK_KPI, + id: `${WATER_STRESS_FALLBACK_KPI.id}-${index}`, + }; + } + + return { + id: + typeof item?.id === "string" && item.id + ? item.id + : `farm-overview-kpi-${index}`, + title: typeof item?.title === "string" ? item.title : "-", + subtitle: typeof item?.subtitle === "string" ? item.subtitle : "", + stats: typeof item?.stats === "string" ? item.stats : "", + avatarColor: + typeof item?.avatarColor === "string" + ? item.avatarColor + : WATER_STRESS_FALLBACK_KPI.avatarColor, + avatarIcon: + typeof item?.avatarIcon === "string" + ? item.avatarIcon + : WATER_STRESS_FALLBACK_KPI.avatarIcon, + chipText: + typeof item?.chipText === "string" + ? item.chipText + : WATER_STRESS_FALLBACK_KPI.chipText, + chipColor: + typeof item?.chipColor === "string" + ? item.chipColor + : WATER_STRESS_FALLBACK_KPI.chipColor, + }; +}; + +const FarmOverviewKPIs = ({ + data, + showQuickAccess = false, + getQuickAccessHref, +}: FarmOverviewKPIsProps) => { + const rawKpis = (data?.kpis as Record[] | undefined) ?? []; + const kpis = rawKpis.map((item, index) => normalizeKpi(item, index)); + if (kpis.length === 0) return null; const getGridSize = (count: number) => { @@ -35,35 +103,77 @@ const FarmOverviewKPIs = ({ data }: FarmOverviewKPIsProps) => { }; return ( - <> - {kpis.map((kpi) => ( - - - - ))} - + + {kpis.map((kpi) => { + const quickAccessHref = getQuickAccessHref?.(kpi) ?? null; + + return ( + + + {showQuickAccess && quickAccessHref && ( + `1px solid ${theme.palette.divider}`, + backgroundColor: "background.paper", + backdropFilter: "blur(8px)", + boxShadow: 1, + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + + )} + + + + ); + })} + ); }; diff --git a/src/views/dashboards/farm/FarmWalletSummaryCard.tsx b/src/views/dashboards/farm/FarmWalletSummaryCard.tsx new file mode 100644 index 0000000..77e6568 --- /dev/null +++ b/src/views/dashboards/farm/FarmWalletSummaryCard.tsx @@ -0,0 +1,73 @@ +'use client' + +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import CardHeader from '@mui/material/CardHeader' +import Chip from '@mui/material/Chip' +import Typography from '@mui/material/Typography' +import Box from '@mui/material/Box' +import { alpha, useTheme } from '@mui/material/styles' + +import CustomAvatar from '@core/components/mui/Avatar' + +interface FarmWalletSummaryCardProps { + data?: Record +} + +const FarmWalletSummaryCard = ({ data }: FarmWalletSummaryCardProps) => { + const theme = useTheme() + const balance = String(data?.balance ?? '۲۴۸,۵۰۰,۰۰۰ تومان') + const pendingSettlement = String(data?.pendingSettlement ?? '۱۲,۴۰۰,۰۰۰ تومان') + const monthlyInflow = String(data?.monthlyInflow ?? '۹۴,۸۰۰,۰۰۰ تومان') + const monthlyOutflow = String(data?.monthlyOutflow ?? '۵۱,۲۰۰,۰۰۰ تومان') + const healthLabel = String(data?.healthLabel ?? 'جریان نقدی پایدار') + + return ( + + + + + } + action={} + /> + + + + موجودی کل + + {balance} + + +
+ + + در انتظار تسویه + + {pendingSettlement} + + + + ورودی ماه + + {monthlyInflow} + +
+ + + + خروجی ۳۰ روز اخیر + + + {monthlyOutflow} + + +
+
+ ) +} + +export default FarmWalletSummaryCard diff --git a/src/views/dashboards/farm/FarmWeatherCard.tsx b/src/views/dashboards/farm/FarmWeatherCard.tsx index 763f7c8..7a55e4b 100644 --- a/src/views/dashboards/farm/FarmWeatherCard.tsx +++ b/src/views/dashboards/farm/FarmWeatherCard.tsx @@ -14,9 +14,6 @@ import { useTheme } from '@mui/material/styles' // Third-party Imports import type { ApexOptions } from 'apexcharts' -// Component Imports -import OptionMenu from '@core/components/option-menu' - // Styled Component Imports const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) @@ -91,7 +88,6 @@ const FarmWeatherCard = ({ data }: FarmWeatherCardProps) => { title={t('cards.farmWeatherCard')} subheader={condition ? `${condition}, ${temperature}${unit}` : `${temperature}${unit}`} className='pbe-3' - action={} />
diff --git a/src/views/dashboards/farm/FarmerCalendarSummaryCard.tsx b/src/views/dashboards/farm/FarmerCalendarSummaryCard.tsx new file mode 100644 index 0000000..2e11958 --- /dev/null +++ b/src/views/dashboards/farm/FarmerCalendarSummaryCard.tsx @@ -0,0 +1,98 @@ +'use client' + +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import CardHeader from '@mui/material/CardHeader' +import Chip from '@mui/material/Chip' +import Typography from '@mui/material/Typography' +import Box from '@mui/material/Box' +import { alpha, useTheme } from '@mui/material/styles' + +import CustomAvatar from '@core/components/mui/Avatar' + +type UpcomingEvent = { + id: string + title: string + dateLabel: string + categoryLabel: string +} + +interface FarmerCalendarSummaryCardProps { + data?: Record +} + +const FarmerCalendarSummaryCard = ({ data }: FarmerCalendarSummaryCardProps) => { + const theme = useTheme() + const todayCount = Number(data?.todayCount ?? 0) + const weekCount = Number(data?.weekCount ?? 0) + const nextEventTitle = String(data?.nextEventTitle ?? 'رویداد زمان بندی شده ای وجود ندارد') + const nextEventMeta = String(data?.nextEventMeta ?? 'تقویم روزانه را از صفحه تقویم دنبال کن.') + const upcomingEvents = (data?.upcomingEvents as UpcomingEvent[] | undefined) ?? [] + + return ( + + + + + } + /> + +
+ + + برنامه امروز + + {todayCount} رویداد + + + + این هفته + + {weekCount} رویداد + +
+ + + + {nextEventTitle} + + + {nextEventMeta} + + + +
+ {upcomingEvents.slice(0, 2).map(event => ( + +
+
+ + {event.title} + + + {event.dateLabel} + +
+ +
+
+ ))} +
+
+
+ ) +} + +export default FarmerCalendarSummaryCard diff --git a/src/views/dashboards/farm/FarmerTodoSummaryCard.tsx b/src/views/dashboards/farm/FarmerTodoSummaryCard.tsx new file mode 100644 index 0000000..e5ee5c8 --- /dev/null +++ b/src/views/dashboards/farm/FarmerTodoSummaryCard.tsx @@ -0,0 +1,100 @@ +'use client' + +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import CardHeader from '@mui/material/CardHeader' +import Chip from '@mui/material/Chip' +import LinearProgress from '@mui/material/LinearProgress' +import Typography from '@mui/material/Typography' +import Box from '@mui/material/Box' +import { alpha, useTheme } from '@mui/material/styles' + +import CustomAvatar from '@core/components/mui/Avatar' + +interface FarmerTodoSummaryCardProps { + data?: Record +} + +const FarmerTodoSummaryCard = ({ data }: FarmerTodoSummaryCardProps) => { + const theme = useTheme() + const todayTasksCount = Number(data?.todayTasksCount ?? 0) + const completedCount = Number(data?.completedCount ?? 0) + const urgentCount = Number(data?.urgentCount ?? 0) + const progressValue = Number(data?.progressValue ?? 0) + const nextTask = (data?.nextTask as Record | undefined) ?? null + + return ( + + + + + } + /> + +
+ + {todayTasksCount} + کار باز امروز + + 0 ? 'error' : 'success'} + variant='tonal' + label={urgentCount > 0 ? `${urgentCount} فوری` : 'بدون مورد فوری'} + /> +
+ +
+
+ + پیشرفت انجام کارها + + {progressValue}% +
+ +
+ +
+ + + انجام شده + + {completedCount} + + + + کار بعدی + + {String(nextTask?.time ?? '--:--')} + +
+ + + + {String(nextTask?.title ?? 'برای امروز هنوز کاری ثبت نشده')} + + + {nextTask ? `${String(nextTask.zone ?? '-')}` : 'از صفحه کارهای روزانه می‌توانی تسک جدید ثبت کنی.'} + + +
+
+ ) +} + +export default FarmerTodoSummaryCard diff --git a/src/views/dashboards/farm/HarvestPredictionCard.tsx b/src/views/dashboards/farm/HarvestPredictionCard.tsx index fa444c3..ba2d68e 100644 --- a/src/views/dashboards/farm/HarvestPredictionCard.tsx +++ b/src/views/dashboards/farm/HarvestPredictionCard.tsx @@ -12,7 +12,6 @@ import Chip from '@mui/material/Chip' // Component Imports import CustomAvatar from '@core/components/mui/Avatar' -import OptionMenu from '@core/components/option-menu' interface HarvestPredictionCardProps { data?: Record @@ -35,7 +34,6 @@ const HarvestPredictionCard = ({ data }: HarvestPredictionCardProps) => { } title={t('cards.harvestPredictionCard')} subheader={t('subheaders.aiEstimatedDate')} - action={} />
diff --git a/src/views/dashboards/farm/RecommendationsList.tsx b/src/views/dashboards/farm/RecommendationsList.tsx index dbcebb7..a52c101 100644 --- a/src/views/dashboards/farm/RecommendationsList.tsx +++ b/src/views/dashboards/farm/RecommendationsList.tsx @@ -13,7 +13,6 @@ import Typography from '@mui/material/Typography' import classnames from 'classnames' // Component Imports -import OptionMenu from '@core/components/option-menu' import CustomAvatar from '@core/components/mui/Avatar' type RecommendationType = { @@ -37,7 +36,6 @@ const RecommendationsList = ({ data }: RecommendationsListProps) => { } /> {recommendations.map((item, index) => ( diff --git a/src/views/dashboards/farm/SensorComparisonChart.tsx b/src/views/dashboards/farm/SensorComparisonChart.tsx index 1ab2116..4c90f50 100644 --- a/src/views/dashboards/farm/SensorComparisonChart.tsx +++ b/src/views/dashboards/farm/SensorComparisonChart.tsx @@ -13,15 +13,19 @@ import { useTheme } from '@mui/material/styles' // Third-party Imports import type { ApexOptions } from 'apexcharts' +import DeviceCodeSelect, { + type DeviceCodeSelectProps +} from '@/views/dashboards/farm/shared-sensors/DeviceCodeSelect' // Styled Component Imports const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) interface SensorComparisonChartProps { data?: Record + deviceCodeSelectProps?: DeviceCodeSelectProps } -const SensorComparisonChart = ({ data }: SensorComparisonChartProps) => { +const SensorComparisonChart = ({ data, deviceCodeSelectProps }: SensorComparisonChartProps) => { const t = useTranslations('farmDashboard') const series = (data?.series as Array<{ name: string; data: number[] }>) ?? [] const categories = @@ -30,7 +34,6 @@ const SensorComparisonChart = ({ data }: SensorComparisonChartProps) => { const currentValue = (data?.currentValue as number | undefined) ?? 48 const vsLastWeek = (data?.vsLastWeek as string) ?? t('fallback.plusPercentVsLastWeek', { val: '5' }) const theme = useTheme() - if (series.length === 0) return null const options: ApexOptions = { chart: { @@ -81,15 +84,24 @@ const SensorComparisonChart = ({ data }: SensorComparisonChartProps) => { : null} /> -
- {currentValue}% - - {vsLastWeek} + {series.length > 0 ? ( + <> +
+ {currentValue}% + + {vsLastWeek} + +
+ + + ) : ( + + برای `device_code` انتخاب‌شده داده‌ای برای نمودار مقایسه‌ای موجود نیست. -
- + )}
) diff --git a/src/views/dashboards/farm/SensorRadarChart.tsx b/src/views/dashboards/farm/SensorRadarChart.tsx index f3545c2..77afe80 100644 --- a/src/views/dashboards/farm/SensorRadarChart.tsx +++ b/src/views/dashboards/farm/SensorRadarChart.tsx @@ -8,29 +8,30 @@ import { useTranslations } from 'next-intl' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' +import Typography from '@mui/material/Typography' import { useTheme } from '@mui/material/styles' // Third Party Imports import type { ApexOptions } from 'apexcharts' - -// Component Imports -import OptionMenu from '@core/components/option-menu' +import DeviceCodeSelect, { + type DeviceCodeSelectProps +} from '@/views/dashboards/farm/shared-sensors/DeviceCodeSelect' // Styled Component Imports const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) interface SensorRadarChartProps { data?: Record + deviceCodeSelectProps?: DeviceCodeSelectProps } -const SensorRadarChart = ({ data }: SensorRadarChartProps) => { +const SensorRadarChart = ({ data, deviceCodeSelectProps }: SensorRadarChartProps) => { const t = useTranslations('farmDashboard') const series = (data?.series as Array<{ name: string; data: number[] }>) ?? [] const labels = (data?.labels as string[]) ?? [] const theme = useTheme() const textDisabled = 'var(--mui-palette-text-disabled)' const divider = 'var(--mui-palette-divider)' - if (series.length === 0) return null const options: ApexOptions = { chart: { @@ -74,10 +75,16 @@ const SensorRadarChart = ({ data }: SensorRadarChartProps) => { } + action={deviceCodeSelectProps ? : null} /> - + {series.length > 0 ? ( + + ) : ( + + برای `device_code` انتخاب‌شده داده‌ای برای نمودار رادار موجود نیست. + + )} ) diff --git a/src/views/dashboards/farm/SensorValuesList.tsx b/src/views/dashboards/farm/SensorValuesList.tsx index d708932..53bc0e6 100644 --- a/src/views/dashboards/farm/SensorValuesList.tsx +++ b/src/views/dashboards/farm/SensorValuesList.tsx @@ -11,9 +11,9 @@ import Typography from '@mui/material/Typography' // Third-party Imports import classnames from 'classnames' - -// Component Imports -import OptionMenu from '@core/components/option-menu' +import DeviceCodeSelect, { + type DeviceCodeSelectProps +} from '@/views/dashboards/farm/shared-sensors/DeviceCodeSelect' type SensorDataType = { title: string @@ -25,45 +25,51 @@ type SensorDataType = { interface SensorValuesListProps { data?: Record + deviceCodeSelectProps?: DeviceCodeSelectProps } -const SensorValuesList = ({ data }: SensorValuesListProps) => { +const SensorValuesList = ({ data, deviceCodeSelectProps }: SensorValuesListProps) => { const t = useTranslations('farmDashboard') const sensors = (data?.sensors as SensorDataType[] | undefined) ?? [] - if (sensors.length === 0) return null return ( } + action={deviceCodeSelectProps ? : null} /> - {sensors.map((item, index) => ( -
-
-
- - {item.title} - - {item.subtitle} -
-
- - {`${item.trendNumber > 0 ? '+' : ''}${item.trendNumber}%`} + {sensors.length > 0 ? ( + sensors.map((item, index) => ( +
+
+
+ + {item.title} + + {item.subtitle} +
+
+ + {`${item.trendNumber > 0 ? '+' : ''}${item.trendNumber}%`} +
-
- ))} + )) + ) : ( + + برای `device_code` انتخاب‌شده داده‌ای برای لیست مقادیر موجود نیست. + + )} ) diff --git a/src/views/dashboards/farm/WaterNeedPrediction.tsx b/src/views/dashboards/farm/WaterNeedPrediction.tsx index 8ec718a..31197df 100644 --- a/src/views/dashboards/farm/WaterNeedPrediction.tsx +++ b/src/views/dashboards/farm/WaterNeedPrediction.tsx @@ -16,9 +16,6 @@ import { useTheme } from '@mui/material/styles' // Third-party Imports import type { ApexOptions } from 'apexcharts' -// Component Imports -import OptionMenu from '@core/components/option-menu' - // Styled Component Imports const AppReactApexCharts = dynamic(() => import('@/libs/styles/AppReactApexCharts')) @@ -82,7 +79,6 @@ const WaterNeedPrediction = ({ data }: WaterNeedPredictionProps) => { } />
diff --git a/src/views/dashboards/farm/YieldPredictionChart.tsx b/src/views/dashboards/farm/YieldPredictionChart.tsx index 54c0bce..317bbb6 100644 --- a/src/views/dashboards/farm/YieldPredictionChart.tsx +++ b/src/views/dashboards/farm/YieldPredictionChart.tsx @@ -16,7 +16,6 @@ import classnames from 'classnames' import type { ApexOptions } from 'apexcharts' // Component Imports -import OptionMenu from '@core/components/option-menu' import CustomAvatar from '@core/components/mui/Avatar' // Styled Component Imports @@ -94,7 +93,6 @@ const YieldPredictionChart = ({ data }: YieldPredictionChartProps) => { } /> diff --git a/src/views/dashboards/farm/farmDashboardConfig.ts b/src/views/dashboards/farm/farmDashboardConfig.ts index f12edbd..a29b8c4 100644 --- a/src/views/dashboards/farm/farmDashboardConfig.ts +++ b/src/views/dashboards/farm/farmDashboardConfig.ts @@ -12,6 +12,8 @@ export const ROW_IDS = [ 'predictions', 'soilHeatmap', 'ndviRecommendations', + 'pestRisk', + 'dailyOperations', 'economic' ] as const @@ -32,6 +34,11 @@ export const CARD_IDS = [ 'soilMoistureHeatmap', 'ndviHealthCard', 'recommendationsList', + 'diseaseRiskCard', + 'pestRiskCard', + 'farmerTodoSummary', + 'farmerCalendarSummary', + 'farmWalletSummary', 'economicOverview' ] as const @@ -53,6 +60,11 @@ export const CARD_TO_ROW: Record = { soilMoistureHeatmap: 'soilHeatmap', ndviHealthCard: 'ndviRecommendations', recommendationsList: 'ndviRecommendations', + diseaseRiskCard: 'overviewKpis', + pestRiskCard: 'overviewKpis', + farmerTodoSummary: 'dailyOperations', + farmerCalendarSummary: 'dailyOperations', + farmWalletSummary: 'dailyOperations', economicOverview: 'economic' } @@ -72,6 +84,11 @@ export const CARD_GRID_SIZE: Record = { soilMoistureHeatmap: 'Soil Moisture Heatmap', ndviHealthCard: 'NDVI Health', recommendationsList: 'Recommendations', + diseaseRiskCard: 'Disease Risk', + pestRiskCard: 'Pest Risk', + farmerTodoSummary: 'Todo Summary', + farmerCalendarSummary: 'Calendar Summary', + farmWalletSummary: 'Wallet Summary', economicOverview: 'Economic Overview' } @@ -104,12 +126,14 @@ export const ROW_LABELS: Record = { predictions: 'Predictions', soilHeatmap: 'Soil Moisture Heatmap', ndviRecommendations: 'NDVI & Recommendations', + pestRisk: 'Pest Risk', + dailyOperations: 'Daily Operations', economic: 'Economic Overview' } /** Cards that belong to each row (for rendering) */ export const ROW_CARDS: Record = { - overviewKpis: ['farmOverviewKpis'], + overviewKpis: ['farmOverviewKpis', 'diseaseRiskCard', 'pestRiskCard'], weatherAlerts: ['farmWeatherCard', 'farmAlertsTracker'], sensorMonitoring: ['sensorValuesList', 'sensorRadarChart'], sensorCharts: ['sensorComparisonChart', 'anomalyDetectionCard'], @@ -117,6 +141,8 @@ export const ROW_CARDS: Record = { predictions: ['harvestPredictionCard', 'yieldPredictionChart'], soilHeatmap: ['soilMoistureHeatmap'], ndviRecommendations: ['ndviHealthCard', 'recommendationsList'], + pestRisk: [], + dailyOperations: ['farmerTodoSummary', 'farmerCalendarSummary', 'farmWalletSummary'], economic: ['economicOverview'] } diff --git a/src/views/dashboards/farm/sensor7/Sensor7Page.tsx b/src/views/dashboards/farm/sensor7/Sensor7Page.tsx index 9e4aeac..bcba880 100644 --- a/src/views/dashboards/farm/sensor7/Sensor7Page.tsx +++ b/src/views/dashboards/farm/sensor7/Sensor7Page.tsx @@ -31,6 +31,7 @@ import { alpha, useTheme } from "@mui/material/styles"; import { useFarmHub } from "@/hooks/useFarmHub"; import { normalizeComparisonChartResponse, + type DeviceCodesResponse, normalizeRadarChartResponse, normalizeValuesListResponse, sensor7Service, @@ -290,6 +291,11 @@ const Sensor7Page = () => { useState(EMPTY_VALUES_LIST); const [dashboardLoading, setDashboardLoading] = useState(false); const [dashboardErrorMessage, setDashboardErrorMessage] = useState(null); + const [deviceCodes, setDeviceCodes] = useState([]); + const [selectedDeviceCode, setSelectedDeviceCode] = useState(""); + const [deviceCodesLoading, setDeviceCodesLoading] = useState(false); + const [deviceCodesResolved, setDeviceCodesResolved] = useState(false); + const [deviceCodesErrorMessage, setDeviceCodesErrorMessage] = useState(null); const [logs, setLogs] = useState([]); const [count, setCount] = useState(0); @@ -300,89 +306,37 @@ const Sensor7Page = () => { const [logsErrorMessage, setLogsErrorMessage] = useState(null); const [selectedLogId, setSelectedLogId] = useState(null); const [selectedDeviceId, setSelectedDeviceId] = useState(""); + const [resolvedPrimaryDeviceId, setResolvedPrimaryDeviceId] = useState(""); useEffect(() => { setPage(1); setSelectedLogId(null); setSelectedDeviceId(""); + setResolvedPrimaryDeviceId(""); + setDeviceCodes([]); + setSelectedDeviceCode(""); + setDeviceCodesLoading(false); + setDeviceCodesResolved(false); + setDeviceCodesErrorMessage(null); }, [farmUuid]); useEffect(() => { if (!farmUuid) { - setSummaryData(null); - setComparisonChartData(EMPTY_COMPARISON_CHART); - setRadarChartData(EMPTY_RADAR_CHART); - setSensorValuesListData(EMPTY_VALUES_LIST); - setDashboardLoading(false); - setDashboardErrorMessage(null); + setResolvedPrimaryDeviceId(""); return; } let isCancelled = false; - const loadDashboardData = async () => { - setDashboardLoading(true); - setDashboardErrorMessage(null); + const resolveDevice = async () => { + const deviceId = await sensor7Service.resolvePrimaryPhysicalDeviceUuid(farmUuid); - const results = await Promise.allSettled([ - sensor7Service.getSummary(farmUuid), - sensor7Service.getComparisonChart({ - farmUuid, - range: DEFAULT_COMPARISON_RANGE, - }), - sensor7Service.getRadarChart({ - farmUuid, - range: DEFAULT_RADAR_RANGE, - }), - sensor7Service.getValuesList({ - farmUuid, - range: DEFAULT_VALUES_RANGE, - }), - ]); - - if (isCancelled) return; - - const [summaryResult, comparisonResult, radarResult, valuesListResult] = results; - - const nextSummaryData = summaryResult.status === "fulfilled" ? summaryResult.value : null; - const nextComparisonChartData = - comparisonResult.status === "fulfilled" - ? comparisonResult.value - : normalizeComparisonChartResponse(nextSummaryData?.sensorComparisonChart); - const nextRadarChartData = - radarResult.status === "fulfilled" - ? radarResult.value - : normalizeRadarChartResponse(nextSummaryData?.sensorRadarChart); - const nextValuesListData = - valuesListResult.status === "fulfilled" - ? valuesListResult.value - : mapSummaryValuesList(nextSummaryData?.sensorValuesList); - - setSummaryData(nextSummaryData); - setComparisonChartData(nextComparisonChartData); - setRadarChartData(nextRadarChartData); - setSensorValuesListData(nextValuesListData); - - const failedResults = results.filter( - (result): result is PromiseRejectedResult => result.status === "rejected", - ); - - if (failedResults.length === results.length) { - setDashboardErrorMessage( - getErrorMessage(failedResults[0]?.reason, "بارگذاری داده های سنسور انجام نشد."), - ); - } else if (failedResults.length > 0) { - setDashboardErrorMessage( - "بخشی از داده های سنسور بارگذاری نشد و داده های در دسترس نمایش داده شدند.", - ); - } else { - setDashboardErrorMessage(null); + if (!isCancelled) { + setResolvedPrimaryDeviceId(deviceId ?? ""); } - - setDashboardLoading(false); }; - void loadDashboardData(); + void resolveDevice(); return () => { isCancelled = true; @@ -501,6 +455,184 @@ const Sensor7Page = () => { return sensorOptions[0]?.value ?? ""; }, [selectedDeviceId, sensorOptions, summaryData?.sensor?.physicalDeviceUuid]); + const dashboardDeviceId = activeDeviceId || resolvedPrimaryDeviceId; + + useEffect(() => { + if (!dashboardDeviceId) { + setDeviceCodes([]); + setSelectedDeviceCode(""); + setDeviceCodesLoading(false); + setDeviceCodesResolved(false); + setDeviceCodesErrorMessage(null); + return; + } + + let isCancelled = false; + + const loadDeviceCodes = async () => { + setDeviceCodesLoading(true); + setDeviceCodesResolved(false); + setDeviceCodesErrorMessage(null); + + try { + const response = await sensor7Service.getDeviceCodes(dashboardDeviceId); + + if (isCancelled) return; + + const nextDeviceCodes = Array.isArray(response.device_codes) ? response.device_codes : []; + + setDeviceCodes(nextDeviceCodes); + setSelectedDeviceCode((current) => + current && nextDeviceCodes.includes(current) ? current : (nextDeviceCodes[0] ?? ""), + ); + + if (nextDeviceCodes.length === 0) { + setDeviceCodesErrorMessage("برای این دستگاه هیچ device_code ای ثبت نشده است."); + } + } catch (error) { + if (isCancelled) return; + + setDeviceCodes([]); + setSelectedDeviceCode(""); + setDeviceCodesErrorMessage( + getErrorMessage(error, "بارگذاری device_code های این دستگاه انجام نشد."), + ); + } finally { + if (!isCancelled) { + setDeviceCodesLoading(false); + setDeviceCodesResolved(true); + } + } + }; + + void loadDeviceCodes(); + + return () => { + isCancelled = true; + }; + }, [dashboardDeviceId]); + + useEffect(() => { + if (!farmUuid || !dashboardDeviceId) { + setSummaryData(null); + setComparisonChartData(EMPTY_COMPARISON_CHART); + setRadarChartData(EMPTY_RADAR_CHART); + setSensorValuesListData(EMPTY_VALUES_LIST); + setDashboardLoading(false); + setDashboardErrorMessage(null); + return; + } + + if (!deviceCodesResolved) { + return; + } + + if (!selectedDeviceCode) { + setSummaryData(null); + setComparisonChartData(EMPTY_COMPARISON_CHART); + setRadarChartData(EMPTY_RADAR_CHART); + setSensorValuesListData(EMPTY_VALUES_LIST); + setDashboardLoading(false); + + if (deviceCodesErrorMessage) { + setDashboardErrorMessage(deviceCodesErrorMessage); + } + + return; + } + + let isCancelled = false; + + const loadDashboardData = async () => { + setDashboardLoading(true); + setDashboardErrorMessage(null); + + const results = await Promise.allSettled([ + sensor7Service.getSummary(dashboardDeviceId, selectedDeviceCode), + sensor7Service.getComparisonChart({ + physicalDeviceUuid: dashboardDeviceId, + deviceCode: selectedDeviceCode, + range: DEFAULT_COMPARISON_RANGE, + }), + sensor7Service.getRadarChart({ + physicalDeviceUuid: dashboardDeviceId, + deviceCode: selectedDeviceCode, + range: DEFAULT_RADAR_RANGE, + }), + sensor7Service.getValuesList({ + physicalDeviceUuid: dashboardDeviceId, + deviceCode: selectedDeviceCode, + range: DEFAULT_VALUES_RANGE, + }), + ]); + + if (isCancelled) return; + + const [summaryResult, comparisonResult, radarResult, valuesListResult] = results; + + const nextSummaryData = summaryResult.status === "fulfilled" ? summaryResult.value : null; + const nextComparisonChartData = + comparisonResult.status === "fulfilled" + ? comparisonResult.value + : normalizeComparisonChartResponse(nextSummaryData?.sensorComparisonChart); + const nextRadarChartData = + radarResult.status === "fulfilled" + ? radarResult.value + : normalizeRadarChartResponse(nextSummaryData?.sensorRadarChart); + const nextValuesListData = + valuesListResult.status === "fulfilled" + ? valuesListResult.value + : mapSummaryValuesList(nextSummaryData?.sensorValuesList); + + setSummaryData(nextSummaryData); + setComparisonChartData(nextComparisonChartData); + setRadarChartData(nextRadarChartData); + setSensorValuesListData(nextValuesListData); + + const failedResults = results.filter( + (result): result is PromiseRejectedResult => result.status === "rejected", + ); + + if (failedResults.length === results.length) { + setDashboardErrorMessage( + getErrorMessage(failedResults[0]?.reason, "بارگذاری داده های سنسور انجام نشد."), + ); + } else if (failedResults.length > 0) { + setDashboardErrorMessage( + "بخشی از داده های سنسور بارگذاری نشد و داده های در دسترس نمایش داده شدند.", + ); + } else { + setDashboardErrorMessage(null); + } + + setDashboardLoading(false); + }; + + void loadDashboardData(); + + return () => { + isCancelled = true; + }; + }, [ + dashboardDeviceId, + deviceCodesErrorMessage, + deviceCodesResolved, + farmUuid, + selectedDeviceCode, + ]); + + const deviceCodeSelectProps = useMemo( + () => ({ + options: deviceCodes, + value: selectedDeviceCode, + onChange: setSelectedDeviceCode, + loading: deviceCodesLoading, + disabled: !dashboardDeviceId, + label: "device_code", + }), + [dashboardDeviceId, deviceCodes, deviceCodesLoading, selectedDeviceCode], + ); + const filteredLogs = useMemo(() => { if (!activeDeviceId) return logs; @@ -600,6 +732,12 @@ const Sensor7Page = () => { ) : null} + {deviceCodesErrorMessage && !dashboardErrorMessage ? ( + 0 ? "warning" : "error"}> + {deviceCodesErrorMessage} + + ) : null} + {metrics.map((metric) => ( @@ -612,13 +750,22 @@ const Sensor7Page = () => { - } /> + } + deviceCodeSelectProps={deviceCodeSelectProps} + /> - } /> + } + deviceCodeSelectProps={deviceCodeSelectProps} + /> - } /> + } + deviceCodeSelectProps={deviceCodeSelectProps} + /> @@ -647,8 +794,8 @@ const Sensor7Page = () => { محتوای سنسور انتخاب شده - داده های این بخش از endpoint لاگ سنسور خارجی خوانده می شوند؛ سنسور را از - لیست انتخاب کنید و روی هر ردیف بزنید تا جزئیات همان لاگ نمایش داده شود. + داده های این بخش از لاگ سنسور و API داینامیک device-hub خوانده می شوند؛ + سنسور را از لیست انتخاب کنید و روی هر ردیف بزنید تا جزئیات همان لاگ نمایش داده شود. diff --git a/src/views/dashboards/farm/shared-sensors/DeviceCodeSelect.tsx b/src/views/dashboards/farm/shared-sensors/DeviceCodeSelect.tsx new file mode 100644 index 0000000..aad4c69 --- /dev/null +++ b/src/views/dashboards/farm/shared-sensors/DeviceCodeSelect.tsx @@ -0,0 +1,60 @@ +'use client' + +import FormControl from '@mui/material/FormControl' +import InputLabel from '@mui/material/InputLabel' +import MenuItem from '@mui/material/MenuItem' +import Select from '@mui/material/Select' +import type { SelectChangeEvent } from '@mui/material/Select' + +export interface DeviceCodeSelectProps { + options: string[] + value: string + onChange: (value: string) => void + loading?: boolean + disabled?: boolean + label?: string + minWidth?: number +} + +const DeviceCodeSelect = ({ + options, + value, + onChange, + loading = false, + disabled = false, + label = 'device_code', + minWidth = 180 +}: DeviceCodeSelectProps) => { + const labelId = `device-code-select-${label}` + + const handleChange = (event: SelectChangeEvent) => { + onChange(event.target.value) + } + + return ( + + {label} + + labelId={labelId} + value={value} + label={label} + onChange={handleChange} + disabled={disabled || (loading && options.length === 0)} + > + {options.length > 0 ? ( + options.map(option => ( + + {option} + + )) + ) : ( + + {loading ? 'در حال بارگذاری...' : 'device_code موجود نیست'} + + )} + + + ) +} + +export default DeviceCodeSelect