This commit is contained in:
2026-03-26 15:25:17 +03:30
parent 4b291155d0
commit e89c3a1b16
7 changed files with 425 additions and 249 deletions
+9 -6
View File
@@ -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
@@ -47,6 +53,3 @@ MAPBOX_ACCESS_TOKEN=your-mapbox-access-token
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.
+14 -3
View File
@@ -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<AuthUser | null>(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<LoginResult> => {
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<LoginResult> => {
const response = await authService.register(payload);
if (response.code !== 200 && response.code !== 201) {
if (response.code >= 400) {
throw new Error(response.msg || "Registration failed");
}
+8 -5
View File
@@ -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<string, string>;
constructor(baseURL: string = API_BASE_URL) {
constructor(baseURL: string = resolveApiBaseUrl()) {
this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash
this.defaultHeaders = {
"Content-Type": "application/json",
+24 -8
View File
@@ -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;
},
+171 -75
View File
@@ -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<T> {
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<string, unknown>
farmWeatherCard?: Record<string, unknown>
farmAlertsTracker?: Record<string, unknown>
sensorValuesList?: Record<string, unknown>
sensorRadarChart?: Record<string, unknown>
sensorComparisonChart?: Record<string, unknown>
anomalyDetectionCard?: Record<string, unknown>
farmAlertsTimeline?: Record<string, unknown>
waterNeedPrediction?: Record<string, unknown>
harvestPredictionCard?: Record<string, unknown>
yieldPredictionChart?: Record<string, unknown>
soilMoistureHeatmap?: Record<string, unknown>
ndviHealthCard?: Record<string, unknown>
recommendationsList?: Record<string, unknown>
economicOverview?: Record<string, unknown>
farmOverviewKpis?: Record<string, unknown>;
farmWeatherCard?: Record<string, unknown>;
farmAlertsTracker?: Record<string, unknown>;
sensorValuesList?: Record<string, unknown>;
sensorRadarChart?: Record<string, unknown>;
sensorComparisonChart?: Record<string, unknown>;
anomalyDetectionCard?: Record<string, unknown>;
farmAlertsTimeline?: Record<string, unknown>;
waterNeedPrediction?: Record<string, unknown>;
harvestPredictionCard?: Record<string, unknown>;
yieldPredictionChart?: Record<string, unknown>;
soilMoistureHeatmap?: Record<string, unknown>;
ndviHealthCard?: Record<string, unknown>;
recommendationsList?: Record<string, unknown>;
economicOverview?: Record<string, unknown>;
}
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<FarmDashboardCardsResponse>
| ApiResponse<FarmDashboardCardsTaskData>
| FarmDashboardCardsResponse
| FarmDashboardCardsTaskData,
): Partial<Record<CardId, Record<string, unknown>>> {
const raw = response && "data" in response ? response.data : response;
if (!raw || typeof raw !== "object") {
return {};
}
if ("result" in raw && raw.result && typeof raw.result === "object") {
return (raw.result.all_cards ?? {}) as Partial<
Record<CardId, Record<string, unknown>>
>;
}
return raw as Partial<Record<CardId, Record<string, unknown>>>;
}
/**
* Transform API response to frontend config format
*/
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<FarmDashboardConfig>): Partial<FarmDashboardConfigResponse> {
const req: Partial<FarmDashboardConfigResponse> = {}
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<FarmDashboardConfig>,
): Partial<FarmDashboardConfigResponse> {
const req: Partial<FarmDashboardConfigResponse> = {};
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> | 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<FarmDashboardConfig>): Promise<FarmDashboardConfig> {
async updateConfig(
data: Partial<FarmDashboardConfig>,
): Promise<FarmDashboardConfig> {
try {
const response = await apiClient.patch<
ApiResponse<FarmDashboardConfigResponse> | 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<Partial<Record<CardId, Record<string, unknown>>>> {
async getAllCards(): Promise<
Partial<Record<CardId, Record<string, unknown>>>
> {
try {
const response = await apiClient.get<ApiResponse<FarmDashboardCardsResponse>>('/api/farm-dashboard/')
const raw = response?.data ?? response
if (raw && typeof raw === 'object') {
return raw as Partial<Record<CardId, Record<string, unknown>>>
}
return {}
const response = await apiClient.get<
| ApiResponse<FarmDashboardCardsResponse>
| ApiResponse<FarmDashboardCardsTaskData>
| FarmDashboardCardsResponse
| FarmDashboardCardsTaskData
>("/api/farm-dashboard/");
return extractCardsPayload(response);
} catch {
return {}
return {};
}
}
}
},
};
+197 -151
View File
@@ -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<CardId, React.ComponentType<{ data?: Record<string, unknown> }>> = {
const CARD_COMPONENTS: Record<
CardId,
React.ComponentType<{ data?: Record<string, unknown> }>
> = {
farmOverviewKpis: FarmOverviewKPIs,
farmWeatherCard: FarmWeatherCard,
farmAlertsTracker: FarmAlertsTracker,
@@ -69,158 +72,179 @@ const CARD_COMPONENTS: Record<CardId, React.ComponentType<{ data?: Record<string
soilMoistureHeatmap: SoilMoistureHeatmap,
ndviHealthCard: NDVIHealthCard,
recommendationsList: RecommendationsList,
economicOverview: EconomicOverview
}
economicOverview: EconomicOverview,
};
function mergeRowOrderAfterDrag(
currentRowOrder: string[],
newVisibleOrder: string[],
visibleRows: string[]
visibleRows: string[],
): string[] {
const result = [...currentRowOrder]
let visibleIndex = 0
const result = [...currentRowOrder];
let visibleIndex = 0;
for (let i = 0; i < result.length; i++) {
if (visibleRows.includes(result[i])) {
result[i] = newVisibleOrder[visibleIndex++]
result[i] = newVisibleOrder[visibleIndex++];
}
}
return result
return result;
}
const FarmDashboardWrapper = () => {
const t = useTranslations('farmDashboard')
const { setSlotContent } = useContext(NavbarSlotContext)
const [config, setConfig] = useState<FarmDashboardConfig>(DEFAULT_FARM_DASHBOARD_CONFIG)
const t = useTranslations("farmDashboard");
const { setSlotContent } = useContext(NavbarSlotContext);
const [config, setConfig] = useState<FarmDashboardConfig>(
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<CardId, string>,
[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<RowId, string>,
[t]
)
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
[t],
);
const disabledSet = new Set(config.disabledCardIds)
const [cardsData, setCardsData] = useState<
Partial<Record<CardId, Record<string, unknown>>>
>({});
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 (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight={200}
>
<CircularProgress />
</Box>
)
);
}
return (
<Box position='relative'>
<Grid container spacing={6} ref={containerRef as RefObject<HTMLDivElement>}>
<Box position="relative">
<Grid
container
spacing={6}
ref={containerRef as RefObject<HTMLDivElement>}
>
{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 (
<Grid
key={rowId}
size={12}
sx={{
display: 'flex',
alignItems: 'flex-start',
display: "flex",
alignItems: "flex-start",
gap: 2,
...(config.enableDragReorder !== false && { '&:hover .row-drag-handle': { opacity: 1 } })
...(config.enableDragReorder !== false && {
"&:hover .row-drag-handle": { opacity: 1 },
}),
}}
>
{config.enableDragReorder !== false && (
<IconButton
className='row-drag-handle'
size='small'
className="row-drag-handle"
size="small"
sx={{
opacity: 0.5,
cursor: 'grab',
cursor: "grab",
flexShrink: 0,
mt: 1,
'&:active': { cursor: 'grabbing' }
"&:active": { cursor: "grabbing" },
}}
aria-label={t('settings.dragRow', { row: rowLabels[rowId as RowId] })}
aria-label={t("settings.dragRow", {
row: rowLabels[rowId as RowId],
})}
>
<i className='tabler-arrows-move text-textSecondary' />
<i className="tabler-arrows-move text-textSecondary" />
</IconButton>
)}
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
{isOverviewRow && cards.includes('farmOverviewKpis') && (
{isOverviewRow && cards.includes("farmOverviewKpis") && (
<FarmOverviewKPIs data={cardsData?.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 (
<Grid key={cardId} size={size} sx={cardRowSx}>
<Component data={cardsData?.[cardId]} />
</Grid>
)
);
})}
</Grid>
</Grid>
)
);
})}
</Grid>
</Box>
)
}
);
};
export default FarmDashboardWrapper
export default FarmDashboardWrapper;
@@ -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) => (