UPDATE
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user