diff --git a/ENV.md b/ENV.md index 9486e9c..61a5154 100644 --- a/ENV.md +++ b/ENV.md @@ -5,22 +5,28 @@ This document describes all environment variables needed for the frontend applic ## Required Environment Variables ### Server Configuration + - `PORT` - Server port (default: 9031) - `NODE_ENV` - Node environment (production/development) ### Next.js Configuration + - `BASEPATH` - Base path for Next.js application (optional, leave empty for root) - `NEXT_PUBLIC_APP_URL` - Public URL of the application (e.g., http://localhost:9031) - `NEXT_PUBLIC_DOCS_URL` - Documentation URL (optional) ### API Configuration -- `NEXT_PUBLIC_API_URL` or `ENVOY_GATEWAY_URL` - Envoy Gateway URL for backend API calls (e.g., http://85.208.253.135:8000) - - This is used by the frontend to communicate with backend services via Envoy Gateway - - Defaults to `http://85.208.253.135:8000` if not set + +- `NEXT_PUBLIC_API_URL` - Public API URL for browser-side frontend requests (e.g., http://85.208.253.135:8000) + - Use this for anything that runs in the browser +- `ENVOY_GATEWAY_URL` - Server-side fallback API URL + - This is only available in Next.js server runtime and is not exposed to the browser + - Defaults to `http://85.208.253.135:8000` if neither variable is set ## Optional Environment Variables ### Mapbox + - `MAPBOX_ACCESS_TOKEN` - Mapbox access token for map features ## Example .env file @@ -45,8 +51,5 @@ MAPBOX_ACCESS_TOKEN=your-mapbox-access-token ## Docker Configuration -When using Docker, these environment variables are set in `docker-compose.yaml`. +When using Docker, these environment variables are set in `docker-compose.yaml`. For local development, create a `.env` file in the frontend directory with the values above. - - - diff --git a/src/contexts/authContext.tsx b/src/contexts/authContext.tsx index 8450c35..b0d1ca9 100644 --- a/src/contexts/authContext.tsx +++ b/src/contexts/authContext.tsx @@ -8,6 +8,7 @@ import type { ReactNode } from "react"; import { authService, type AuthResponse, + type AuthTokenValue, type AuthUser, type RegisterRequest, } from "@/libs/api/services/authService"; @@ -44,6 +45,16 @@ const STORAGE_KEYS = { authUser: "auth_user", } as const; +const hasAuthToken = (token: AuthTokenValue | null | undefined) => { + if (!token) return false; + + if (typeof token === "string") { + return token.trim().length > 0; + } + + return typeof token.access === "string" && token.access.trim().length > 0; +}; + export const AuthProvider = ({ children }: AuthProviderProps) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -108,7 +119,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { response: AuthResponse, fallbackMessage: string, ): LoginResult => { - if (!response.data || !response.token?.access) { + if (!response.data || !hasAuthToken(response.token)) { throw new Error(response.msg || fallbackMessage); } @@ -127,7 +138,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { ): Promise => { const response = await authService.login({ identifier, password }); - if (response.code !== 200) { + if (response.code >= 400) { throw new Error(response.msg || "Login failed"); } @@ -137,7 +148,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { const register = async (payload: RegisterPayload): Promise => { const response = await authService.register(payload); - if (response.code !== 200 && response.code !== 201) { + if (response.code >= 400) { throw new Error(response.msg || "Registration failed"); } diff --git a/src/libs/api/client.ts b/src/libs/api/client.ts index 03db3ec..2b72d02 100644 --- a/src/libs/api/client.ts +++ b/src/libs/api/client.ts @@ -2,10 +2,13 @@ * API Client for communicating with Backend via Envoy Gateway */ -const API_BASE_URL = - process.env.NEXT_PUBLIC_API_URL || - process.env.ENVOY_GATEWAY_URL || - "http://node.crop-logic.ir"; +const resolveApiBaseUrl = (): string => { + const publicApiUrl = process.env.NEXT_PUBLIC_API_URL; + const serverApiUrl = + typeof window === "undefined" ? process.env.ENVOY_GATEWAY_URL : undefined; + + return publicApiUrl || serverApiUrl || "http://node.crop-logic.ir"; +}; const AUTH_STORAGE_KEYS = { accessToken: "auth_token", @@ -22,7 +25,7 @@ export class ApiClient { private baseURL: string; private defaultHeaders: Record; - constructor(baseURL: string = API_BASE_URL) { + constructor(baseURL: string = resolveApiBaseUrl()) { this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash this.defaultHeaders = { "Content-Type": "application/json", diff --git a/src/libs/api/services/authService.ts b/src/libs/api/services/authService.ts index c813c3d..91ba4fa 100644 --- a/src/libs/api/services/authService.ts +++ b/src/libs/api/services/authService.ts @@ -16,9 +16,11 @@ export interface AuthUser { export interface AuthTokens { access: string; - refresh: string; + refresh?: string; } +export type AuthTokenValue = string | AuthTokens; + export interface LoginRequest { identifier: string; password: string; @@ -37,7 +39,7 @@ export interface AuthResponse { code: number; msg: string; data: AuthUser; - token: AuthTokens; + token: AuthTokenValue; } export interface RefreshTokenResponse { @@ -56,6 +58,24 @@ export interface UpdateProfileResponse { data: AuthUser; } +const storeAuthToken = (token: AuthTokenValue | null | undefined): void => { + if (!token) return; + + if (typeof token === "string") { + apiClient.setAuthToken(token); + + return; + } + + if (token.access) { + apiClient.setAuthToken(token.access); + } + + if (token.refresh) { + apiClient.setRefreshToken(token.refresh); + } +}; + export const authService = { /** * Login with username, email, or phone number @@ -66,9 +86,7 @@ export const authService = { payload, ); - if (response.token?.access) { - apiClient.setAuthTokens(response.token); - } + storeAuthToken(response.token); return response; }, @@ -82,9 +100,7 @@ export const authService = { payload, ); - if (response.token?.access) { - apiClient.setAuthTokens(response.token); - } + storeAuthToken(response.token); return response; }, diff --git a/src/libs/api/services/farmDashboardService.ts b/src/libs/api/services/farmDashboardService.ts index 4949b89..c6b61d0 100644 --- a/src/libs/api/services/farmDashboardService.ts +++ b/src/libs/api/services/farmDashboardService.ts @@ -5,84 +5,166 @@ * - Cards: all 15 card payloads from /api/farm-dashboard/ */ -import { apiClient } from '../client' -import type { FarmDashboardConfig } from '@/views/dashboards/farm/farmDashboardConfig' -import type { CardId } from '@/views/dashboards/farm/farmDashboardConfig' +import { apiClient } from "../client"; +import { + CARD_IDS, + CARD_TO_ROW, + ROW_CARDS, + ROW_IDS, + type FarmDashboardConfig, + type CardId, + type RowId, +} from "@/views/dashboards/farm/farmDashboardConfig"; export interface ApiResponse { - code: number - msg: string - data: T + code: number; + msg: string; + data: T; } export interface FarmDashboardConfigResponse { - disabled_card_ids: string[] - row_order: string[] - enable_drag_reorder?: boolean + disabled_card_ids: string[]; + row_order: string[]; + enable_drag_reorder?: boolean; } /** API response shape for /api/farm-dashboard/ - each key matches CardId */ export interface FarmDashboardCardsResponse { - farmOverviewKpis?: Record - farmWeatherCard?: Record - farmAlertsTracker?: Record - sensorValuesList?: Record - sensorRadarChart?: Record - sensorComparisonChart?: Record - anomalyDetectionCard?: Record - farmAlertsTimeline?: Record - waterNeedPrediction?: Record - harvestPredictionCard?: Record - yieldPredictionChart?: Record - soilMoistureHeatmap?: Record - ndviHealthCard?: Record - recommendationsList?: Record - economicOverview?: Record + farmOverviewKpis?: Record; + farmWeatherCard?: Record; + farmAlertsTracker?: Record; + sensorValuesList?: Record; + sensorRadarChart?: Record; + sensorComparisonChart?: Record; + anomalyDetectionCard?: Record; + farmAlertsTimeline?: Record; + waterNeedPrediction?: Record; + harvestPredictionCard?: Record; + yieldPredictionChart?: Record; + soilMoistureHeatmap?: Record; + ndviHealthCard?: Record; + recommendationsList?: Record; + economicOverview?: Record; } -const STORAGE_KEY = 'farm_dashboard_config' +interface FarmDashboardCardsTaskResult { + sensor_id?: string; + all_cards?: FarmDashboardCardsResponse; +} + +interface FarmDashboardCardsTaskData { + task_id?: string; + status?: string; + result?: FarmDashboardCardsTaskResult; +} + +const STORAGE_KEY = "farm_dashboard_config"; + +function isCardId(value: string): value is CardId { + return (CARD_IDS as readonly string[]).includes(value); +} + +function isRowId(value: string): value is RowId { + return (ROW_IDS as readonly string[]).includes(value); +} + +function normalizeDisabledCardIds(disabledIds: string[] = []): string[] { + return Array.from( + new Set( + disabledIds.flatMap((id) => { + if (isCardId(id)) return [id]; + if (isRowId(id)) return ROW_CARDS[id]; + return []; + }), + ), + ); +} + +function normalizeRowOrder(rowOrder: string[] = []): string[] { + const normalized = rowOrder + .map((id) => { + if (isRowId(id)) return id; + if (isCardId(id)) return CARD_TO_ROW[id]; + return null; + }) + .filter((id): id is RowId => !!id); + + const deduped = Array.from(new Set(normalized)); + + return deduped.length + ? [...deduped, ...ROW_IDS.filter((id) => !deduped.includes(id))] + : [...ROW_IDS]; +} + +function extractCardsPayload( + response: + | ApiResponse + | ApiResponse + | FarmDashboardCardsResponse + | FarmDashboardCardsTaskData, +): Partial>> { + const raw = response && "data" in response ? response.data : response; + + if (!raw || typeof raw !== "object") { + return {}; + } + + if ("result" in raw && raw.result && typeof raw.result === "object") { + return (raw.result.all_cards ?? {}) as Partial< + Record> + >; + } + + return raw as Partial>>; +} /** * Transform API response to frontend config format */ -function fromApiResponse(data: FarmDashboardConfigResponse): FarmDashboardConfig { +function fromApiResponse( + data: FarmDashboardConfigResponse, +): FarmDashboardConfig { return { - disabledCardIds: data.disabled_card_ids ?? [], - rowOrder: data.row_order ?? [], - enableDragReorder: data.enable_drag_reorder ?? true - } + disabledCardIds: normalizeDisabledCardIds(data.disabled_card_ids), + rowOrder: normalizeRowOrder(data.row_order), + enableDragReorder: data.enable_drag_reorder ?? true, + }; } /** * Transform frontend config to API request format */ -function toApiRequest(config: Partial): Partial { - const req: Partial = {} - if (config.disabledCardIds !== undefined) req.disabled_card_ids = config.disabledCardIds - if (config.rowOrder !== undefined) req.row_order = config.rowOrder - if (config.enableDragReorder !== undefined) req.enable_drag_reorder = config.enableDragReorder - return req +function toApiRequest( + config: Partial, +): Partial { + const req: Partial = {}; + if (config.disabledCardIds !== undefined) + req.disabled_card_ids = config.disabledCardIds; + if (config.rowOrder !== undefined) req.row_order = config.rowOrder; + if (config.enableDragReorder !== undefined) + req.enable_drag_reorder = config.enableDragReorder; + return req; } /** * localStorage fallback when backend is not ready */ function getLocalConfig(): FarmDashboardConfig | null { - if (typeof window === 'undefined') return null + if (typeof window === "undefined") return null; try { - const stored = localStorage.getItem(STORAGE_KEY) - return stored ? (JSON.parse(stored) as FarmDashboardConfig) : null + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? (JSON.parse(stored) as FarmDashboardConfig) : null; } catch { - return null + return null; } } function setLocalConfig(config: FarmDashboardConfig): void { - if (typeof window === 'undefined') return + if (typeof window === "undefined") return; try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(config)) + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); } catch (e) { - console.error('Failed to save farm dashboard config to localStorage', e) + console.error("Failed to save farm dashboard config to localStorage", e); } } @@ -94,46 +176,57 @@ export const farmDashboardService = { try { const response = await apiClient.get< ApiResponse | FarmDashboardConfigResponse - >('/api/farm-dashboard-config') - const raw = response && 'data' in response ? response.data : response - if (raw && typeof raw === 'object' && ('disabled_card_ids' in raw || 'row_order' in raw)) { - return fromApiResponse(raw as FarmDashboardConfigResponse) + >("/api/farm-dashboard-config"); + const raw = response && "data" in response ? response.data : response; + if ( + raw && + typeof raw === "object" && + ("disabled_card_ids" in raw || "row_order" in raw) + ) { + return fromApiResponse(raw as FarmDashboardConfigResponse); } - throw new Error('Invalid response') + throw new Error("Invalid response"); } catch { - const local = getLocalConfig() - if (local) return local - return { disabledCardIds: [], rowOrder: [], enableDragReorder: true } + const local = getLocalConfig(); + if (local) return local; + return { disabledCardIds: [], rowOrder: [], enableDragReorder: true }; } }, /** * Update farm dashboard config */ - async updateConfig(data: Partial): Promise { + async updateConfig( + data: Partial, + ): Promise { try { const response = await apiClient.patch< ApiResponse | FarmDashboardConfigResponse - >('/api/farm-dashboard-config', toApiRequest(data)) - const raw = response && 'data' in response ? response.data : response - if (raw && typeof raw === 'object' && ('disabled_card_ids' in raw || 'row_order' in raw)) { - const config = fromApiResponse(raw as FarmDashboardConfigResponse) - setLocalConfig(config) - return config + >("/api/farm-dashboard-config", toApiRequest(data)); + const raw = response && "data" in response ? response.data : response; + if ( + raw && + typeof raw === "object" && + ("disabled_card_ids" in raw || "row_order" in raw) + ) { + const config = fromApiResponse(raw as FarmDashboardConfigResponse); + setLocalConfig(config); + return config; } - throw new Error('Update failed') + throw new Error("Update failed"); } catch (err) { - const local = getLocalConfig() + const local = getLocalConfig(); if (local) { const merged: FarmDashboardConfig = { disabledCardIds: data.disabledCardIds ?? local.disabledCardIds, rowOrder: data.rowOrder ?? local.rowOrder, - enableDragReorder: data.enableDragReorder ?? local.enableDragReorder ?? true - } - setLocalConfig(merged) - return merged + enableDragReorder: + data.enableDragReorder ?? local.enableDragReorder ?? true, + }; + setLocalConfig(merged); + return merged; } - throw err + throw err; } }, @@ -141,16 +234,19 @@ export const farmDashboardService = { * Get all dashboard card data from API * Response: { code: 200, msg: "OK", data: { farmOverviewKpis, farmWeatherCard, ... } } */ - async getAllCards(): Promise>>> { + async getAllCards(): Promise< + Partial>> + > { try { - const response = await apiClient.get>('/api/farm-dashboard/') - const raw = response?.data ?? response - if (raw && typeof raw === 'object') { - return raw as Partial>> - } - return {} + const response = await apiClient.get< + | ApiResponse + | ApiResponse + | FarmDashboardCardsResponse + | FarmDashboardCardsTaskData + >("/api/farm-dashboard/"); + return extractCardsPayload(response); } catch { - return {} + return {}; } - } -} + }, +}; diff --git a/src/views/dashboards/farm/FarmDashboardWrapper.tsx b/src/views/dashboards/farm/FarmDashboardWrapper.tsx index d34611a..4ae1a98 100644 --- a/src/views/dashboards/farm/FarmDashboardWrapper.tsx +++ b/src/views/dashboards/farm/FarmDashboardWrapper.tsx @@ -1,39 +1,39 @@ -'use client' +"use client"; // React Imports -import type { RefObject } from 'react' -import { useEffect, useMemo, useState, useCallback, useContext } from 'react' -import { useTranslations } from 'next-intl' +import type { RefObject } from "react"; +import { useEffect, useMemo, useState, useCallback, useContext } from "react"; +import { useTranslations } from "next-intl"; // Context Imports -import NavbarSlotContext from '@/contexts/navbarSlotContext' +import NavbarSlotContext from "@/contexts/navbarSlotContext"; // MUI Imports -import Grid from '@mui/material/Grid2' -import IconButton from '@mui/material/IconButton' -import Box from '@mui/material/Box' -import CircularProgress from '@mui/material/CircularProgress' +import Grid from "@mui/material/Grid2"; +import IconButton from "@mui/material/IconButton"; +import Box from "@mui/material/Box"; +import CircularProgress from "@mui/material/CircularProgress"; // Third-party imports -import { useDragAndDrop } from '@formkit/drag-and-drop/react' -import { animations } from '@formkit/drag-and-drop' +import { useDragAndDrop } from "@formkit/drag-and-drop/react"; +import { animations } from "@formkit/drag-and-drop"; // Component Imports -import FarmOverviewKPIs from '@views/dashboards/farm/FarmOverviewKPIs' -import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard' -import FarmAlertsTracker from '@views/dashboards/farm/FarmAlertsTracker' -import SensorValuesList from '@views/dashboards/farm/SensorValuesList' -import SensorRadarChart from '@views/dashboards/farm/SensorRadarChart' -import SensorComparisonChart from '@views/dashboards/farm/SensorComparisonChart' -import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline' -import WaterNeedPrediction from '@views/dashboards/farm/WaterNeedPrediction' -import YieldPredictionChart from '@views/dashboards/farm/YieldPredictionChart' -import HarvestPredictionCard from '@views/dashboards/farm/HarvestPredictionCard' -import SoilMoistureHeatmap from '@views/dashboards/farm/SoilMoistureHeatmap' -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 FarmOverviewKPIs from "@views/dashboards/farm/FarmOverviewKPIs"; +import FarmWeatherCard from "@views/dashboards/farm/FarmWeatherCard"; +import FarmAlertsTracker from "@views/dashboards/farm/FarmAlertsTracker"; +import SensorValuesList from "@views/dashboards/farm/SensorValuesList"; +import SensorRadarChart from "@views/dashboards/farm/SensorRadarChart"; +import SensorComparisonChart from "@views/dashboards/farm/SensorComparisonChart"; +import FarmAlertsTimeline from "@views/dashboards/farm/FarmAlertsTimeline"; +import WaterNeedPrediction from "@views/dashboards/farm/WaterNeedPrediction"; +import YieldPredictionChart from "@views/dashboards/farm/YieldPredictionChart"; +import HarvestPredictionCard from "@views/dashboards/farm/HarvestPredictionCard"; +import SoilMoistureHeatmap from "@views/dashboards/farm/SoilMoistureHeatmap"; +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"; // Config & Service import { @@ -43,18 +43,21 @@ import { DEFAULT_FARM_DASHBOARD_CONFIG, type RowId, type CardId, - type FarmDashboardConfig -} from '@views/dashboards/farm/farmDashboardConfig' -import { farmDashboardService } from '@/libs/api/services/farmDashboardService' -import FarmDashboardSettingsDropdown from '@views/dashboards/farm/FarmDashboardSettingsDropdown' + type FarmDashboardConfig, +} from "@views/dashboards/farm/farmDashboardConfig"; +import { farmDashboardService } from "@/libs/api/services/farmDashboardService"; +import FarmDashboardSettingsDropdown from "@views/dashboards/farm/FarmDashboardSettingsDropdown"; const cardRowSx = { - display: 'flex', - flexDirection: 'column', - '& > *': { flex: 1, minHeight: 0 } -} + display: "flex", + flexDirection: "column", + "& > *": { flex: 1, minHeight: 0 }, +}; -const CARD_COMPONENTS: Record }>> = { +const CARD_COMPONENTS: Record< + CardId, + React.ComponentType<{ data?: Record }> +> = { farmOverviewKpis: FarmOverviewKPIs, farmWeatherCard: FarmWeatherCard, farmAlertsTracker: FarmAlertsTracker, @@ -69,158 +72,179 @@ const CARD_COMPONENTS: Record { - const t = useTranslations('farmDashboard') - const { setSlotContent } = useContext(NavbarSlotContext) - const [config, setConfig] = useState(DEFAULT_FARM_DASHBOARD_CONFIG) + const t = useTranslations("farmDashboard"); + const { setSlotContent } = useContext(NavbarSlotContext); + const [config, setConfig] = useState( + DEFAULT_FARM_DASHBOARD_CONFIG, + ); const cardLabels = useMemo( () => Object.fromEntries( ( [ - 'farmOverviewKpis', - 'farmWeatherCard', - 'farmAlertsTracker', - 'sensorValuesList', - 'sensorRadarChart', - 'sensorComparisonChart', - 'anomalyDetectionCard', - 'farmAlertsTimeline', - 'waterNeedPrediction', - 'harvestPredictionCard', - 'yieldPredictionChart', - 'soilMoistureHeatmap', - 'ndviHealthCard', - 'recommendationsList', - 'economicOverview' + "farmOverviewKpis", + "farmWeatherCard", + "farmAlertsTracker", + "sensorValuesList", + "sensorRadarChart", + "sensorComparisonChart", + "anomalyDetectionCard", + "farmAlertsTimeline", + "waterNeedPrediction", + "harvestPredictionCard", + "yieldPredictionChart", + "soilMoistureHeatmap", + "ndviHealthCard", + "recommendationsList", + "economicOverview", ] as CardId[] - ).map((id) => [id, t(`cards.${id}`)]) + ).map((id) => [id, t(`cards.${id}`)]), ) as Record, - [t] - ) + [t], + ); const rowLabels = useMemo( () => Object.fromEntries( ( [ - 'overviewKpis', - 'weatherAlerts', - 'sensorMonitoring', - 'sensorCharts', - 'alertsWater', - 'predictions', - 'soilHeatmap', - 'ndviRecommendations', - 'economic' + "overviewKpis", + "weatherAlerts", + "sensorMonitoring", + "sensorCharts", + "alertsWater", + "predictions", + "soilHeatmap", + "ndviRecommendations", + "economic", ] as RowId[] - ).map((id) => [id, t(`rows.${id}`)]) + ).map((id) => [id, t(`rows.${id}`)]), ) as Record, - [t] - ) - const [cardsData, setCardsData] = useState>>>({}) - const [loading, setLoading] = useState(true) - const [saving, setSaving] = useState(false) + [t], + ); - const disabledSet = new Set(config.disabledCardIds) + const [cardsData, setCardsData] = useState< + Partial>> + >({}); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const disabledSet = new Set(config.disabledCardIds); const hasVisibleCard = useCallback( (rowId: string) => { - const cards = ROW_CARDS[rowId as RowId] - if (!Array.isArray(cards)) return false - return cards.some(cardId => !disabledSet.has(cardId)) + const cards = ROW_CARDS[rowId as RowId]; + if (!Array.isArray(cards)) return false; + return cards.some((cardId) => !disabledSet.has(cardId)); }, - [config.disabledCardIds] - ) + [config.disabledCardIds], + ); - const visibleRowOrder = config.rowOrder.filter(hasVisibleCard) + const visibleRowOrder = config.rowOrder.filter(hasVisibleCard); + + const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop( + visibleRowOrder, + { + plugins: [animations()], + dragHandle: ".row-drag-handle", + }, + ); + + // useEffect(()=>{ + // console.log("ksjf",visibleRowOrder,orderedRows) + + // },[visibleRowOrder,visibleRowOrder]) - const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop(visibleRowOrder, { - plugins: [animations()], - dragHandle: '.row-drag-handle' - }) useEffect(() => { - Promise.all([farmDashboardService.getConfig(), farmDashboardService.getAllCards()]) + Promise.all([ + farmDashboardService.getConfig(), + farmDashboardService.getAllCards(), + ]) .then(([configData, cards]) => { const validRowOrder = (configData.rowOrder ?? []).filter( - (id): id is RowId => id in ROW_CARDS - ) + (id): id is RowId => id in ROW_CARDS, + ); const merged: FarmDashboardConfig = { disabledCardIds: configData.disabledCardIds ?? [], rowOrder: validRowOrder.length ? validRowOrder : [...ROW_IDS], - enableDragReorder: configData.enableDragReorder ?? true - } - setConfig(merged) - setCardsData(cards ?? {}) + enableDragReorder: configData.enableDragReorder ?? true, + }; + setConfig(merged); + setCardsData(cards ?? {}); }) .catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG)) - .finally(() => setLoading(false)) - }, []) + .finally(() => setLoading(false)); + }, []); useEffect(() => { - setOrderedRows(visibleRowOrder) + setOrderedRows(visibleRowOrder); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config.disabledCardIds]) - + }, [visibleRowOrder]); useEffect(() => { - if (loading) return - if (JSON.stringify(orderedRows) === JSON.stringify(visibleRowOrder)) return - const newRowOrder = mergeRowOrderAfterDrag(config.rowOrder, orderedRows, visibleRowOrder) - setConfig(prev => ({ ...prev, rowOrder: newRowOrder })) - setSaving(true) + if (loading) return; + if (JSON.stringify(orderedRows) === JSON.stringify(visibleRowOrder)) return; + const newRowOrder = mergeRowOrderAfterDrag( + config.rowOrder, + orderedRows, + visibleRowOrder, + ); + setConfig((prev) => ({ ...prev, rowOrder: newRowOrder })); + setSaving(true); farmDashboardService .updateConfig({ rowOrder: newRowOrder }) - .then(updated => setConfig(updated)) + .then((updated) => setConfig(updated)) .catch(() => {}) - .finally(() => setSaving(false)) + .finally(() => setSaving(false)); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [orderedRows]) + }, [orderedRows]); const handleToggleDragReorder = useCallback((enabled: boolean) => { - setConfig(prev => ({ ...prev, enableDragReorder: enabled })) - setSaving(true) + setConfig((prev) => ({ ...prev, enableDragReorder: enabled })); + setSaving(true); farmDashboardService .updateConfig({ enableDragReorder: enabled }) - .then(updated => setConfig(updated)) - .finally(() => setSaving(false)) - }, []) + .then((updated) => setConfig(updated)) + .finally(() => setSaving(false)); + }, []); const handleToggleCard = useCallback( (cardId: CardId, disabled: boolean) => { const next = disabled ? [...config.disabledCardIds, cardId] - : config.disabledCardIds.filter(id => id !== cardId) - setConfig(prev => ({ ...prev, disabledCardIds: next })) - setSaving(true) + : config.disabledCardIds.filter((id) => id !== cardId); + setConfig((prev) => ({ ...prev, disabledCardIds: next })); + setSaving(true); farmDashboardService .updateConfig({ disabledCardIds: next }) - .then(updated => setConfig(updated)) - .catch(() => setConfig(prev => ({ ...prev, disabledCardIds: next }))) - .finally(() => setSaving(false)) + .then((updated) => setConfig(updated)) + .catch(() => setConfig((prev) => ({ ...prev, disabledCardIds: next }))) + .finally(() => setSaving(false)); }, - [config.disabledCardIds] - ) + [config.disabledCardIds], + ); + useEffect(() => { setSlotContent( @@ -233,77 +257,99 @@ const FarmDashboardWrapper = () => { rowLabels={rowLabels} rowCards={ROW_CARDS} saving={saving} - /> - ) - return () => setSlotContent(null) - }, [setSlotContent, config.disabledCardIds, config.enableDragReorder, handleToggleCard, handleToggleDragReorder, saving]) + />, + ); + return () => setSlotContent(null); + }, [ + setSlotContent, + config.disabledCardIds, + config.enableDragReorder, + handleToggleCard, + handleToggleDragReorder, + saving, + ]); if (loading) { return ( - + - ) + ); } return ( - - }> + + } + > {orderedRows.map((rowId: string) => { - const cards = ROW_CARDS[rowId as RowId].filter(cardId => !disabledSet.has(cardId)) - if (cards.length === 0) return null + const cards = ROW_CARDS[rowId as RowId].filter( + (cardId) => !disabledSet.has(cardId), + ); + if (cards.length === 0) return null; - const isOverviewRow = rowId === 'overviewKpis' + const isOverviewRow = rowId === "overviewKpis"; return ( {config.enableDragReorder !== false && ( - + )} - {isOverviewRow && cards.includes('farmOverviewKpis') && ( + {isOverviewRow && cards.includes("farmOverviewKpis") && ( )} {!isOverviewRow && cards.map((cardId: CardId) => { - const size = CARD_GRID_SIZE[cardId] - const Component = CARD_COMPONENTS[cardId] - if (!Component) return null + const size = CARD_GRID_SIZE[cardId]; + const Component = CARD_COMPONENTS[cardId]; + if (!Component) return null; return ( - ) + ); })} - ) + ); })} - ) -} + ); +}; -export default FarmDashboardWrapper +export default FarmDashboardWrapper; diff --git a/src/views/dashboards/farm/FarmOverviewKPIs.tsx b/src/views/dashboards/farm/FarmOverviewKPIs.tsx index 7da7c5c..2a16c21 100644 --- a/src/views/dashboards/farm/FarmOverviewKPIs.tsx +++ b/src/views/dashboards/farm/FarmOverviewKPIs.tsx @@ -25,6 +25,7 @@ const FarmOverviewKPIs = ({ data }: FarmOverviewKPIsProps) => { const kpis = (data?.kpis as KpiItem[] | undefined) ?? [] if (kpis.length === 0) return null + return ( <> {kpis.map((kpi) => (