UPDATE
This commit is contained in:
@@ -5,22 +5,28 @@ This document describes all environment variables needed for the frontend applic
|
|||||||
## Required Environment Variables
|
## Required Environment Variables
|
||||||
|
|
||||||
### Server Configuration
|
### Server Configuration
|
||||||
|
|
||||||
- `PORT` - Server port (default: 9031)
|
- `PORT` - Server port (default: 9031)
|
||||||
- `NODE_ENV` - Node environment (production/development)
|
- `NODE_ENV` - Node environment (production/development)
|
||||||
|
|
||||||
### Next.js Configuration
|
### Next.js Configuration
|
||||||
|
|
||||||
- `BASEPATH` - Base path for Next.js application (optional, leave empty for root)
|
- `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_APP_URL` - Public URL of the application (e.g., http://localhost:9031)
|
||||||
- `NEXT_PUBLIC_DOCS_URL` - Documentation URL (optional)
|
- `NEXT_PUBLIC_DOCS_URL` - Documentation URL (optional)
|
||||||
|
|
||||||
### API Configuration
|
### 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
|
- `NEXT_PUBLIC_API_URL` - Public API URL for browser-side frontend requests (e.g., http://85.208.253.135:8000)
|
||||||
- Defaults to `http://85.208.253.135:8000` if not set
|
- 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
|
## Optional Environment Variables
|
||||||
|
|
||||||
### Mapbox
|
### Mapbox
|
||||||
|
|
||||||
- `MAPBOX_ACCESS_TOKEN` - Mapbox access token for map features
|
- `MAPBOX_ACCESS_TOKEN` - Mapbox access token for map features
|
||||||
|
|
||||||
## Example .env file
|
## 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`.
|
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.
|
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 {
|
import {
|
||||||
authService,
|
authService,
|
||||||
type AuthResponse,
|
type AuthResponse,
|
||||||
|
type AuthTokenValue,
|
||||||
type AuthUser,
|
type AuthUser,
|
||||||
type RegisterRequest,
|
type RegisterRequest,
|
||||||
} from "@/libs/api/services/authService";
|
} from "@/libs/api/services/authService";
|
||||||
@@ -44,6 +45,16 @@ const STORAGE_KEYS = {
|
|||||||
authUser: "auth_user",
|
authUser: "auth_user",
|
||||||
} as const;
|
} 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) => {
|
export const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||||
const [user, setUser] = useState<AuthUser | null>(null);
|
const [user, setUser] = useState<AuthUser | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -108,7 +119,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
response: AuthResponse,
|
response: AuthResponse,
|
||||||
fallbackMessage: string,
|
fallbackMessage: string,
|
||||||
): LoginResult => {
|
): LoginResult => {
|
||||||
if (!response.data || !response.token?.access) {
|
if (!response.data || !hasAuthToken(response.token)) {
|
||||||
throw new Error(response.msg || fallbackMessage);
|
throw new Error(response.msg || fallbackMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +138,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
): Promise<LoginResult> => {
|
): Promise<LoginResult> => {
|
||||||
const response = await authService.login({ identifier, password });
|
const response = await authService.login({ identifier, password });
|
||||||
|
|
||||||
if (response.code !== 200) {
|
if (response.code >= 400) {
|
||||||
throw new Error(response.msg || "Login failed");
|
throw new Error(response.msg || "Login failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +148,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
const register = async (payload: RegisterPayload): Promise<LoginResult> => {
|
const register = async (payload: RegisterPayload): Promise<LoginResult> => {
|
||||||
const response = await authService.register(payload);
|
const response = await authService.register(payload);
|
||||||
|
|
||||||
if (response.code !== 200 && response.code !== 201) {
|
if (response.code >= 400) {
|
||||||
throw new Error(response.msg || "Registration failed");
|
throw new Error(response.msg || "Registration failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
* API Client for communicating with Backend via Envoy Gateway
|
* API Client for communicating with Backend via Envoy Gateway
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const API_BASE_URL =
|
const resolveApiBaseUrl = (): string => {
|
||||||
process.env.NEXT_PUBLIC_API_URL ||
|
const publicApiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
process.env.ENVOY_GATEWAY_URL ||
|
const serverApiUrl =
|
||||||
"http://node.crop-logic.ir";
|
typeof window === "undefined" ? process.env.ENVOY_GATEWAY_URL : undefined;
|
||||||
|
|
||||||
|
return publicApiUrl || serverApiUrl || "http://node.crop-logic.ir";
|
||||||
|
};
|
||||||
|
|
||||||
const AUTH_STORAGE_KEYS = {
|
const AUTH_STORAGE_KEYS = {
|
||||||
accessToken: "auth_token",
|
accessToken: "auth_token",
|
||||||
@@ -22,7 +25,7 @@ export class ApiClient {
|
|||||||
private baseURL: string;
|
private baseURL: string;
|
||||||
private defaultHeaders: Record<string, string>;
|
private defaultHeaders: Record<string, string>;
|
||||||
|
|
||||||
constructor(baseURL: string = API_BASE_URL) {
|
constructor(baseURL: string = resolveApiBaseUrl()) {
|
||||||
this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash
|
this.baseURL = baseURL.replace(/\/$/, ""); // Remove trailing slash
|
||||||
this.defaultHeaders = {
|
this.defaultHeaders = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ export interface AuthUser {
|
|||||||
|
|
||||||
export interface AuthTokens {
|
export interface AuthTokens {
|
||||||
access: string;
|
access: string;
|
||||||
refresh: string;
|
refresh?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthTokenValue = string | AuthTokens;
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
identifier: string;
|
identifier: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -37,7 +39,7 @@ export interface AuthResponse {
|
|||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: AuthUser;
|
data: AuthUser;
|
||||||
token: AuthTokens;
|
token: AuthTokenValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshTokenResponse {
|
export interface RefreshTokenResponse {
|
||||||
@@ -56,6 +58,24 @@ export interface UpdateProfileResponse {
|
|||||||
data: AuthUser;
|
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 = {
|
export const authService = {
|
||||||
/**
|
/**
|
||||||
* Login with username, email, or phone number
|
* Login with username, email, or phone number
|
||||||
@@ -66,9 +86,7 @@ export const authService = {
|
|||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.token?.access) {
|
storeAuthToken(response.token);
|
||||||
apiClient.setAuthTokens(response.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
@@ -82,9 +100,7 @@ export const authService = {
|
|||||||
payload,
|
payload,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.token?.access) {
|
storeAuthToken(response.token);
|
||||||
apiClient.setAuthTokens(response.token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,84 +5,166 @@
|
|||||||
* - Cards: all 15 card payloads from /api/farm-dashboard/
|
* - Cards: all 15 card payloads from /api/farm-dashboard/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiClient } from '../client'
|
import { apiClient } from "../client";
|
||||||
import type { FarmDashboardConfig } from '@/views/dashboards/farm/farmDashboardConfig'
|
import {
|
||||||
import type { CardId } from '@/views/dashboards/farm/farmDashboardConfig'
|
CARD_IDS,
|
||||||
|
CARD_TO_ROW,
|
||||||
|
ROW_CARDS,
|
||||||
|
ROW_IDS,
|
||||||
|
type FarmDashboardConfig,
|
||||||
|
type CardId,
|
||||||
|
type RowId,
|
||||||
|
} from "@/views/dashboards/farm/farmDashboardConfig";
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
code: number
|
code: number;
|
||||||
msg: string
|
msg: string;
|
||||||
data: T
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FarmDashboardConfigResponse {
|
export interface FarmDashboardConfigResponse {
|
||||||
disabled_card_ids: string[]
|
disabled_card_ids: string[];
|
||||||
row_order: string[]
|
row_order: string[];
|
||||||
enable_drag_reorder?: boolean
|
enable_drag_reorder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** API response shape for /api/farm-dashboard/ - each key matches CardId */
|
/** API response shape for /api/farm-dashboard/ - each key matches CardId */
|
||||||
export interface FarmDashboardCardsResponse {
|
export interface FarmDashboardCardsResponse {
|
||||||
farmOverviewKpis?: Record<string, unknown>
|
farmOverviewKpis?: Record<string, unknown>;
|
||||||
farmWeatherCard?: Record<string, unknown>
|
farmWeatherCard?: Record<string, unknown>;
|
||||||
farmAlertsTracker?: Record<string, unknown>
|
farmAlertsTracker?: Record<string, unknown>;
|
||||||
sensorValuesList?: Record<string, unknown>
|
sensorValuesList?: Record<string, unknown>;
|
||||||
sensorRadarChart?: Record<string, unknown>
|
sensorRadarChart?: Record<string, unknown>;
|
||||||
sensorComparisonChart?: Record<string, unknown>
|
sensorComparisonChart?: Record<string, unknown>;
|
||||||
anomalyDetectionCard?: Record<string, unknown>
|
anomalyDetectionCard?: Record<string, unknown>;
|
||||||
farmAlertsTimeline?: Record<string, unknown>
|
farmAlertsTimeline?: Record<string, unknown>;
|
||||||
waterNeedPrediction?: Record<string, unknown>
|
waterNeedPrediction?: Record<string, unknown>;
|
||||||
harvestPredictionCard?: Record<string, unknown>
|
harvestPredictionCard?: Record<string, unknown>;
|
||||||
yieldPredictionChart?: Record<string, unknown>
|
yieldPredictionChart?: Record<string, unknown>;
|
||||||
soilMoistureHeatmap?: Record<string, unknown>
|
soilMoistureHeatmap?: Record<string, unknown>;
|
||||||
ndviHealthCard?: Record<string, unknown>
|
ndviHealthCard?: Record<string, unknown>;
|
||||||
recommendationsList?: Record<string, unknown>
|
recommendationsList?: Record<string, unknown>;
|
||||||
economicOverview?: 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
|
* Transform API response to frontend config format
|
||||||
*/
|
*/
|
||||||
function fromApiResponse(data: FarmDashboardConfigResponse): FarmDashboardConfig {
|
function fromApiResponse(
|
||||||
|
data: FarmDashboardConfigResponse,
|
||||||
|
): FarmDashboardConfig {
|
||||||
return {
|
return {
|
||||||
disabledCardIds: data.disabled_card_ids ?? [],
|
disabledCardIds: normalizeDisabledCardIds(data.disabled_card_ids),
|
||||||
rowOrder: data.row_order ?? [],
|
rowOrder: normalizeRowOrder(data.row_order),
|
||||||
enableDragReorder: data.enable_drag_reorder ?? true
|
enableDragReorder: data.enable_drag_reorder ?? true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform frontend config to API request format
|
* Transform frontend config to API request format
|
||||||
*/
|
*/
|
||||||
function toApiRequest(config: Partial<FarmDashboardConfig>): Partial<FarmDashboardConfigResponse> {
|
function toApiRequest(
|
||||||
const req: Partial<FarmDashboardConfigResponse> = {}
|
config: Partial<FarmDashboardConfig>,
|
||||||
if (config.disabledCardIds !== undefined) req.disabled_card_ids = config.disabledCardIds
|
): Partial<FarmDashboardConfigResponse> {
|
||||||
if (config.rowOrder !== undefined) req.row_order = config.rowOrder
|
const req: Partial<FarmDashboardConfigResponse> = {};
|
||||||
if (config.enableDragReorder !== undefined) req.enable_drag_reorder = config.enableDragReorder
|
if (config.disabledCardIds !== undefined)
|
||||||
return req
|
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
|
* localStorage fallback when backend is not ready
|
||||||
*/
|
*/
|
||||||
function getLocalConfig(): FarmDashboardConfig | null {
|
function getLocalConfig(): FarmDashboardConfig | null {
|
||||||
if (typeof window === 'undefined') return null
|
if (typeof window === "undefined") return null;
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
return stored ? (JSON.parse(stored) as FarmDashboardConfig) : null
|
return stored ? (JSON.parse(stored) as FarmDashboardConfig) : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLocalConfig(config: FarmDashboardConfig): void {
|
function setLocalConfig(config: FarmDashboardConfig): void {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === "undefined") return;
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
const response = await apiClient.get<
|
const response = await apiClient.get<
|
||||||
ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse
|
ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse
|
||||||
>('/api/farm-dashboard-config')
|
>("/api/farm-dashboard-config");
|
||||||
const raw = response && 'data' in response ? response.data : response
|
const raw = response && "data" in response ? response.data : response;
|
||||||
if (raw && typeof raw === 'object' && ('disabled_card_ids' in raw || 'row_order' in raw)) {
|
if (
|
||||||
return fromApiResponse(raw as FarmDashboardConfigResponse)
|
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 {
|
} catch {
|
||||||
const local = getLocalConfig()
|
const local = getLocalConfig();
|
||||||
if (local) return local
|
if (local) return local;
|
||||||
return { disabledCardIds: [], rowOrder: [], enableDragReorder: true }
|
return { disabledCardIds: [], rowOrder: [], enableDragReorder: true };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update farm dashboard config
|
* Update farm dashboard config
|
||||||
*/
|
*/
|
||||||
async updateConfig(data: Partial<FarmDashboardConfig>): Promise<FarmDashboardConfig> {
|
async updateConfig(
|
||||||
|
data: Partial<FarmDashboardConfig>,
|
||||||
|
): Promise<FarmDashboardConfig> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.patch<
|
const response = await apiClient.patch<
|
||||||
ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse
|
ApiResponse<FarmDashboardConfigResponse> | FarmDashboardConfigResponse
|
||||||
>('/api/farm-dashboard-config', toApiRequest(data))
|
>("/api/farm-dashboard-config", toApiRequest(data));
|
||||||
const raw = response && 'data' in response ? response.data : response
|
const raw = response && "data" in response ? response.data : response;
|
||||||
if (raw && typeof raw === 'object' && ('disabled_card_ids' in raw || 'row_order' in raw)) {
|
if (
|
||||||
const config = fromApiResponse(raw as FarmDashboardConfigResponse)
|
raw &&
|
||||||
setLocalConfig(config)
|
typeof raw === "object" &&
|
||||||
return config
|
("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) {
|
} catch (err) {
|
||||||
const local = getLocalConfig()
|
const local = getLocalConfig();
|
||||||
if (local) {
|
if (local) {
|
||||||
const merged: FarmDashboardConfig = {
|
const merged: FarmDashboardConfig = {
|
||||||
disabledCardIds: data.disabledCardIds ?? local.disabledCardIds,
|
disabledCardIds: data.disabledCardIds ?? local.disabledCardIds,
|
||||||
rowOrder: data.rowOrder ?? local.rowOrder,
|
rowOrder: data.rowOrder ?? local.rowOrder,
|
||||||
enableDragReorder: data.enableDragReorder ?? local.enableDragReorder ?? true
|
enableDragReorder:
|
||||||
|
data.enableDragReorder ?? local.enableDragReorder ?? true,
|
||||||
|
};
|
||||||
|
setLocalConfig(merged);
|
||||||
|
return merged;
|
||||||
}
|
}
|
||||||
setLocalConfig(merged)
|
throw err;
|
||||||
return merged
|
|
||||||
}
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -141,16 +234,19 @@ export const farmDashboardService = {
|
|||||||
* Get all dashboard card data from API
|
* Get all dashboard card data from API
|
||||||
* Response: { code: 200, msg: "OK", data: { farmOverviewKpis, farmWeatherCard, ... } }
|
* 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 {
|
try {
|
||||||
const response = await apiClient.get<ApiResponse<FarmDashboardCardsResponse>>('/api/farm-dashboard/')
|
const response = await apiClient.get<
|
||||||
const raw = response?.data ?? response
|
| ApiResponse<FarmDashboardCardsResponse>
|
||||||
if (raw && typeof raw === 'object') {
|
| ApiResponse<FarmDashboardCardsTaskData>
|
||||||
return raw as Partial<Record<CardId, Record<string, unknown>>>
|
| FarmDashboardCardsResponse
|
||||||
}
|
| FarmDashboardCardsTaskData
|
||||||
return {}
|
>("/api/farm-dashboard/");
|
||||||
|
return extractCardsPayload(response);
|
||||||
} catch {
|
} catch {
|
||||||
return {}
|
return {};
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
// React Imports
|
// React Imports
|
||||||
import type { RefObject } from 'react'
|
import type { RefObject } from "react";
|
||||||
import { useEffect, useMemo, useState, useCallback, useContext } from 'react'
|
import { useEffect, useMemo, useState, useCallback, useContext } from "react";
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
// Context Imports
|
// Context Imports
|
||||||
import NavbarSlotContext from '@/contexts/navbarSlotContext'
|
import NavbarSlotContext from "@/contexts/navbarSlotContext";
|
||||||
|
|
||||||
// MUI Imports
|
// MUI Imports
|
||||||
import Grid from '@mui/material/Grid2'
|
import Grid from "@mui/material/Grid2";
|
||||||
import IconButton from '@mui/material/IconButton'
|
import IconButton from "@mui/material/IconButton";
|
||||||
import Box from '@mui/material/Box'
|
import Box from "@mui/material/Box";
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
|
||||||
// Third-party imports
|
// Third-party imports
|
||||||
import { useDragAndDrop } from '@formkit/drag-and-drop/react'
|
import { useDragAndDrop } from "@formkit/drag-and-drop/react";
|
||||||
import { animations } from '@formkit/drag-and-drop'
|
import { animations } from "@formkit/drag-and-drop";
|
||||||
|
|
||||||
// Component Imports
|
// Component Imports
|
||||||
import FarmOverviewKPIs from '@views/dashboards/farm/FarmOverviewKPIs'
|
import FarmOverviewKPIs from "@views/dashboards/farm/FarmOverviewKPIs";
|
||||||
import FarmWeatherCard from '@views/dashboards/farm/FarmWeatherCard'
|
import FarmWeatherCard from "@views/dashboards/farm/FarmWeatherCard";
|
||||||
import FarmAlertsTracker from '@views/dashboards/farm/FarmAlertsTracker'
|
import FarmAlertsTracker from "@views/dashboards/farm/FarmAlertsTracker";
|
||||||
import SensorValuesList from '@views/dashboards/farm/SensorValuesList'
|
import SensorValuesList from "@views/dashboards/farm/SensorValuesList";
|
||||||
import SensorRadarChart from '@views/dashboards/farm/SensorRadarChart'
|
import SensorRadarChart from "@views/dashboards/farm/SensorRadarChart";
|
||||||
import SensorComparisonChart from '@views/dashboards/farm/SensorComparisonChart'
|
import SensorComparisonChart from "@views/dashboards/farm/SensorComparisonChart";
|
||||||
import FarmAlertsTimeline from '@views/dashboards/farm/FarmAlertsTimeline'
|
import FarmAlertsTimeline from "@views/dashboards/farm/FarmAlertsTimeline";
|
||||||
import WaterNeedPrediction from '@views/dashboards/farm/WaterNeedPrediction'
|
import WaterNeedPrediction from "@views/dashboards/farm/WaterNeedPrediction";
|
||||||
import YieldPredictionChart from '@views/dashboards/farm/YieldPredictionChart'
|
import YieldPredictionChart from "@views/dashboards/farm/YieldPredictionChart";
|
||||||
import HarvestPredictionCard from '@views/dashboards/farm/HarvestPredictionCard'
|
import HarvestPredictionCard from "@views/dashboards/farm/HarvestPredictionCard";
|
||||||
import SoilMoistureHeatmap from '@views/dashboards/farm/SoilMoistureHeatmap'
|
import SoilMoistureHeatmap from "@views/dashboards/farm/SoilMoistureHeatmap";
|
||||||
import AnomalyDetectionCard from '@views/dashboards/farm/AnomalyDetectionCard'
|
import AnomalyDetectionCard from "@views/dashboards/farm/AnomalyDetectionCard";
|
||||||
import NDVIHealthCard from '@views/dashboards/farm/NDVIHealthCard'
|
import NDVIHealthCard from "@views/dashboards/farm/NDVIHealthCard";
|
||||||
import RecommendationsList from '@views/dashboards/farm/RecommendationsList'
|
import RecommendationsList from "@views/dashboards/farm/RecommendationsList";
|
||||||
import EconomicOverview from '@views/dashboards/farm/EconomicOverview'
|
import EconomicOverview from "@views/dashboards/farm/EconomicOverview";
|
||||||
|
|
||||||
// Config & Service
|
// Config & Service
|
||||||
import {
|
import {
|
||||||
@@ -43,18 +43,21 @@ import {
|
|||||||
DEFAULT_FARM_DASHBOARD_CONFIG,
|
DEFAULT_FARM_DASHBOARD_CONFIG,
|
||||||
type RowId,
|
type RowId,
|
||||||
type CardId,
|
type CardId,
|
||||||
type FarmDashboardConfig
|
type FarmDashboardConfig,
|
||||||
} from '@views/dashboards/farm/farmDashboardConfig'
|
} from "@views/dashboards/farm/farmDashboardConfig";
|
||||||
import { farmDashboardService } from '@/libs/api/services/farmDashboardService'
|
import { farmDashboardService } from "@/libs/api/services/farmDashboardService";
|
||||||
import FarmDashboardSettingsDropdown from '@views/dashboards/farm/FarmDashboardSettingsDropdown'
|
import FarmDashboardSettingsDropdown from "@views/dashboards/farm/FarmDashboardSettingsDropdown";
|
||||||
|
|
||||||
const cardRowSx = {
|
const cardRowSx = {
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
'& > *': { flex: 1, minHeight: 0 }
|
"& > *": { 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,
|
farmOverviewKpis: FarmOverviewKPIs,
|
||||||
farmWeatherCard: FarmWeatherCard,
|
farmWeatherCard: FarmWeatherCard,
|
||||||
farmAlertsTracker: FarmAlertsTracker,
|
farmAlertsTracker: FarmAlertsTracker,
|
||||||
@@ -69,158 +72,179 @@ const CARD_COMPONENTS: Record<CardId, React.ComponentType<{ data?: Record<string
|
|||||||
soilMoistureHeatmap: SoilMoistureHeatmap,
|
soilMoistureHeatmap: SoilMoistureHeatmap,
|
||||||
ndviHealthCard: NDVIHealthCard,
|
ndviHealthCard: NDVIHealthCard,
|
||||||
recommendationsList: RecommendationsList,
|
recommendationsList: RecommendationsList,
|
||||||
economicOverview: EconomicOverview
|
economicOverview: EconomicOverview,
|
||||||
}
|
};
|
||||||
|
|
||||||
function mergeRowOrderAfterDrag(
|
function mergeRowOrderAfterDrag(
|
||||||
currentRowOrder: string[],
|
currentRowOrder: string[],
|
||||||
newVisibleOrder: string[],
|
newVisibleOrder: string[],
|
||||||
visibleRows: string[]
|
visibleRows: string[],
|
||||||
): string[] {
|
): string[] {
|
||||||
const result = [...currentRowOrder]
|
const result = [...currentRowOrder];
|
||||||
let visibleIndex = 0
|
let visibleIndex = 0;
|
||||||
for (let i = 0; i < result.length; i++) {
|
for (let i = 0; i < result.length; i++) {
|
||||||
if (visibleRows.includes(result[i])) {
|
if (visibleRows.includes(result[i])) {
|
||||||
result[i] = newVisibleOrder[visibleIndex++]
|
result[i] = newVisibleOrder[visibleIndex++];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FarmDashboardWrapper = () => {
|
const FarmDashboardWrapper = () => {
|
||||||
const t = useTranslations('farmDashboard')
|
const t = useTranslations("farmDashboard");
|
||||||
const { setSlotContent } = useContext(NavbarSlotContext)
|
const { setSlotContent } = useContext(NavbarSlotContext);
|
||||||
const [config, setConfig] = useState<FarmDashboardConfig>(DEFAULT_FARM_DASHBOARD_CONFIG)
|
const [config, setConfig] = useState<FarmDashboardConfig>(
|
||||||
|
DEFAULT_FARM_DASHBOARD_CONFIG,
|
||||||
|
);
|
||||||
|
|
||||||
const cardLabels = useMemo(
|
const cardLabels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
'farmOverviewKpis',
|
"farmOverviewKpis",
|
||||||
'farmWeatherCard',
|
"farmWeatherCard",
|
||||||
'farmAlertsTracker',
|
"farmAlertsTracker",
|
||||||
'sensorValuesList',
|
"sensorValuesList",
|
||||||
'sensorRadarChart',
|
"sensorRadarChart",
|
||||||
'sensorComparisonChart',
|
"sensorComparisonChart",
|
||||||
'anomalyDetectionCard',
|
"anomalyDetectionCard",
|
||||||
'farmAlertsTimeline',
|
"farmAlertsTimeline",
|
||||||
'waterNeedPrediction',
|
"waterNeedPrediction",
|
||||||
'harvestPredictionCard',
|
"harvestPredictionCard",
|
||||||
'yieldPredictionChart',
|
"yieldPredictionChart",
|
||||||
'soilMoistureHeatmap',
|
"soilMoistureHeatmap",
|
||||||
'ndviHealthCard',
|
"ndviHealthCard",
|
||||||
'recommendationsList',
|
"recommendationsList",
|
||||||
'economicOverview'
|
"economicOverview",
|
||||||
] as CardId[]
|
] as CardId[]
|
||||||
).map((id) => [id, t(`cards.${id}`)])
|
).map((id) => [id, t(`cards.${id}`)]),
|
||||||
) as Record<CardId, string>,
|
) as Record<CardId, string>,
|
||||||
[t]
|
[t],
|
||||||
)
|
);
|
||||||
|
|
||||||
const rowLabels = useMemo(
|
const rowLabels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
(
|
(
|
||||||
[
|
[
|
||||||
'overviewKpis',
|
"overviewKpis",
|
||||||
'weatherAlerts',
|
"weatherAlerts",
|
||||||
'sensorMonitoring',
|
"sensorMonitoring",
|
||||||
'sensorCharts',
|
"sensorCharts",
|
||||||
'alertsWater',
|
"alertsWater",
|
||||||
'predictions',
|
"predictions",
|
||||||
'soilHeatmap',
|
"soilHeatmap",
|
||||||
'ndviRecommendations',
|
"ndviRecommendations",
|
||||||
'economic'
|
"economic",
|
||||||
] as RowId[]
|
] as RowId[]
|
||||||
).map((id) => [id, t(`rows.${id}`)])
|
).map((id) => [id, t(`rows.${id}`)]),
|
||||||
) as Record<RowId, string>,
|
) as Record<RowId, string>,
|
||||||
[t]
|
[t],
|
||||||
)
|
);
|
||||||
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 [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(
|
const hasVisibleCard = useCallback(
|
||||||
(rowId: string) => {
|
(rowId: string) => {
|
||||||
const cards = ROW_CARDS[rowId as RowId]
|
const cards = ROW_CARDS[rowId as RowId];
|
||||||
if (!Array.isArray(cards)) return false
|
if (!Array.isArray(cards)) return false;
|
||||||
return cards.some(cardId => !disabledSet.has(cardId))
|
return cards.some((cardId) => !disabledSet.has(cardId));
|
||||||
},
|
},
|
||||||
[config.disabledCardIds]
|
[config.disabledCardIds],
|
||||||
)
|
);
|
||||||
|
|
||||||
const visibleRowOrder = config.rowOrder.filter(hasVisibleCard)
|
const visibleRowOrder = config.rowOrder.filter(hasVisibleCard);
|
||||||
|
|
||||||
const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop(visibleRowOrder, {
|
const [containerRef, orderedRows, setOrderedRows] = useDragAndDrop(
|
||||||
|
visibleRowOrder,
|
||||||
|
{
|
||||||
plugins: [animations()],
|
plugins: [animations()],
|
||||||
dragHandle: '.row-drag-handle'
|
dragHandle: ".row-drag-handle",
|
||||||
})
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// useEffect(()=>{
|
||||||
|
// console.log("ksjf",visibleRowOrder,orderedRows)
|
||||||
|
|
||||||
|
// },[visibleRowOrder,visibleRowOrder])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([farmDashboardService.getConfig(), farmDashboardService.getAllCards()])
|
Promise.all([
|
||||||
|
farmDashboardService.getConfig(),
|
||||||
|
farmDashboardService.getAllCards(),
|
||||||
|
])
|
||||||
.then(([configData, cards]) => {
|
.then(([configData, cards]) => {
|
||||||
const validRowOrder = (configData.rowOrder ?? []).filter(
|
const validRowOrder = (configData.rowOrder ?? []).filter(
|
||||||
(id): id is RowId => id in ROW_CARDS
|
(id): id is RowId => id in ROW_CARDS,
|
||||||
)
|
);
|
||||||
const merged: FarmDashboardConfig = {
|
const merged: FarmDashboardConfig = {
|
||||||
disabledCardIds: configData.disabledCardIds ?? [],
|
disabledCardIds: configData.disabledCardIds ?? [],
|
||||||
rowOrder: validRowOrder.length ? validRowOrder : [...ROW_IDS],
|
rowOrder: validRowOrder.length ? validRowOrder : [...ROW_IDS],
|
||||||
enableDragReorder: configData.enableDragReorder ?? true
|
enableDragReorder: configData.enableDragReorder ?? true,
|
||||||
}
|
};
|
||||||
setConfig(merged)
|
setConfig(merged);
|
||||||
setCardsData(cards ?? {})
|
setCardsData(cards ?? {});
|
||||||
})
|
})
|
||||||
.catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG))
|
.catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG))
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false));
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOrderedRows(visibleRowOrder)
|
setOrderedRows(visibleRowOrder);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [config.disabledCardIds])
|
}, [visibleRowOrder]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) return
|
if (loading) return;
|
||||||
if (JSON.stringify(orderedRows) === JSON.stringify(visibleRowOrder)) return
|
if (JSON.stringify(orderedRows) === JSON.stringify(visibleRowOrder)) return;
|
||||||
const newRowOrder = mergeRowOrderAfterDrag(config.rowOrder, orderedRows, visibleRowOrder)
|
const newRowOrder = mergeRowOrderAfterDrag(
|
||||||
setConfig(prev => ({ ...prev, rowOrder: newRowOrder }))
|
config.rowOrder,
|
||||||
setSaving(true)
|
orderedRows,
|
||||||
|
visibleRowOrder,
|
||||||
|
);
|
||||||
|
setConfig((prev) => ({ ...prev, rowOrder: newRowOrder }));
|
||||||
|
setSaving(true);
|
||||||
farmDashboardService
|
farmDashboardService
|
||||||
.updateConfig({ rowOrder: newRowOrder })
|
.updateConfig({ rowOrder: newRowOrder })
|
||||||
.then(updated => setConfig(updated))
|
.then((updated) => setConfig(updated))
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setSaving(false))
|
.finally(() => setSaving(false));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [orderedRows])
|
}, [orderedRows]);
|
||||||
|
|
||||||
const handleToggleDragReorder = useCallback((enabled: boolean) => {
|
const handleToggleDragReorder = useCallback((enabled: boolean) => {
|
||||||
setConfig(prev => ({ ...prev, enableDragReorder: enabled }))
|
setConfig((prev) => ({ ...prev, enableDragReorder: enabled }));
|
||||||
setSaving(true)
|
setSaving(true);
|
||||||
farmDashboardService
|
farmDashboardService
|
||||||
.updateConfig({ enableDragReorder: enabled })
|
.updateConfig({ enableDragReorder: enabled })
|
||||||
.then(updated => setConfig(updated))
|
.then((updated) => setConfig(updated))
|
||||||
.finally(() => setSaving(false))
|
.finally(() => setSaving(false));
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleToggleCard = useCallback(
|
const handleToggleCard = useCallback(
|
||||||
(cardId: CardId, disabled: boolean) => {
|
(cardId: CardId, disabled: boolean) => {
|
||||||
const next = disabled
|
const next = disabled
|
||||||
? [...config.disabledCardIds, cardId]
|
? [...config.disabledCardIds, cardId]
|
||||||
: config.disabledCardIds.filter(id => id !== cardId)
|
: config.disabledCardIds.filter((id) => id !== cardId);
|
||||||
setConfig(prev => ({ ...prev, disabledCardIds: next }))
|
setConfig((prev) => ({ ...prev, disabledCardIds: next }));
|
||||||
setSaving(true)
|
setSaving(true);
|
||||||
farmDashboardService
|
farmDashboardService
|
||||||
.updateConfig({ disabledCardIds: next })
|
.updateConfig({ disabledCardIds: next })
|
||||||
.then(updated => setConfig(updated))
|
.then((updated) => setConfig(updated))
|
||||||
.catch(() => setConfig(prev => ({ ...prev, disabledCardIds: next })))
|
.catch(() => setConfig((prev) => ({ ...prev, disabledCardIds: next })))
|
||||||
.finally(() => setSaving(false))
|
.finally(() => setSaving(false));
|
||||||
},
|
},
|
||||||
[config.disabledCardIds]
|
[config.disabledCardIds],
|
||||||
)
|
);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSlotContent(
|
setSlotContent(
|
||||||
@@ -233,77 +257,99 @@ const FarmDashboardWrapper = () => {
|
|||||||
rowLabels={rowLabels}
|
rowLabels={rowLabels}
|
||||||
rowCards={ROW_CARDS}
|
rowCards={ROW_CARDS}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
/>
|
/>,
|
||||||
)
|
);
|
||||||
return () => setSlotContent(null)
|
return () => setSlotContent(null);
|
||||||
}, [setSlotContent, config.disabledCardIds, config.enableDragReorder, handleToggleCard, handleToggleDragReorder, saving])
|
}, [
|
||||||
|
setSlotContent,
|
||||||
|
config.disabledCardIds,
|
||||||
|
config.enableDragReorder,
|
||||||
|
handleToggleCard,
|
||||||
|
handleToggleDragReorder,
|
||||||
|
saving,
|
||||||
|
]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
minHeight={200}
|
||||||
|
>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position='relative'>
|
<Box position="relative">
|
||||||
<Grid container spacing={6} ref={containerRef as RefObject<HTMLDivElement>}>
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={6}
|
||||||
|
ref={containerRef as RefObject<HTMLDivElement>}
|
||||||
|
>
|
||||||
{orderedRows.map((rowId: string) => {
|
{orderedRows.map((rowId: string) => {
|
||||||
const cards = ROW_CARDS[rowId as RowId].filter(cardId => !disabledSet.has(cardId))
|
const cards = ROW_CARDS[rowId as RowId].filter(
|
||||||
if (cards.length === 0) return null
|
(cardId) => !disabledSet.has(cardId),
|
||||||
|
);
|
||||||
|
if (cards.length === 0) return null;
|
||||||
|
|
||||||
const isOverviewRow = rowId === 'overviewKpis'
|
const isOverviewRow = rowId === "overviewKpis";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
key={rowId}
|
key={rowId}
|
||||||
size={12}
|
size={12}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'flex-start',
|
alignItems: "flex-start",
|
||||||
gap: 2,
|
gap: 2,
|
||||||
...(config.enableDragReorder !== false && { '&:hover .row-drag-handle': { opacity: 1 } })
|
...(config.enableDragReorder !== false && {
|
||||||
|
"&:hover .row-drag-handle": { opacity: 1 },
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config.enableDragReorder !== false && (
|
{config.enableDragReorder !== false && (
|
||||||
<IconButton
|
<IconButton
|
||||||
className='row-drag-handle'
|
className="row-drag-handle"
|
||||||
size='small'
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
cursor: 'grab',
|
cursor: "grab",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
mt: 1,
|
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>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
|
<Grid container spacing={6} sx={{ flex: 1, minWidth: 0 }}>
|
||||||
{isOverviewRow && cards.includes('farmOverviewKpis') && (
|
{isOverviewRow && cards.includes("farmOverviewKpis") && (
|
||||||
<FarmOverviewKPIs data={cardsData?.farmOverviewKpis} />
|
<FarmOverviewKPIs data={cardsData?.farmOverviewKpis} />
|
||||||
)}
|
)}
|
||||||
{!isOverviewRow &&
|
{!isOverviewRow &&
|
||||||
cards.map((cardId: CardId) => {
|
cards.map((cardId: CardId) => {
|
||||||
const size = CARD_GRID_SIZE[cardId]
|
const size = CARD_GRID_SIZE[cardId];
|
||||||
const Component = CARD_COMPONENTS[cardId]
|
const Component = CARD_COMPONENTS[cardId];
|
||||||
if (!Component) return null
|
if (!Component) return null;
|
||||||
return (
|
return (
|
||||||
<Grid key={cardId} size={size} sx={cardRowSx}>
|
<Grid key={cardId} size={size} sx={cardRowSx}>
|
||||||
<Component data={cardsData?.[cardId]} />
|
<Component data={cardsData?.[cardId]} />
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FarmDashboardWrapper
|
export default FarmDashboardWrapper;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const FarmOverviewKPIs = ({ data }: FarmOverviewKPIsProps) => {
|
|||||||
const kpis = (data?.kpis as KpiItem[] | undefined) ?? []
|
const kpis = (data?.kpis as KpiItem[] | undefined) ?? []
|
||||||
if (kpis.length === 0) return null
|
if (kpis.length === 0) return null
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{kpis.map((kpi) => (
|
{kpis.map((kpi) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user