This commit is contained in:
2026-04-02 23:44:24 +03:30
parent 9af6f7f36f
commit 76fd2a29f8
27 changed files with 632 additions and 294 deletions
+21 -14
View File
@@ -265,33 +265,36 @@
} }
}, },
"sensorHub": { "sensorHub": {
"title": "مرکز سنسور", "title": "مرکز مزرعه",
"cancel": "انصراف", "cancel": "انصراف",
"selectSensor": "انتخاب سنسور", "selectSensor": "انتخاب مزرعه",
"selectSensorDescription": "سنسور مورد نظر را انتخاب کنید یا سنسور جدید اضافه کنید", "selectSensorDescription": "مزرعه مورد نظر را انتخاب کنید یا مزرعه جدید اضافه کنید",
"addSensor": "اضافه کردن سنسور", "addSensor": "افزودن مزرعه",
"sensorData": "داده سنسور", "sensorData": "داده مزرعه",
"back": "بازگشت", "back": "بازگشت",
"sensorName": "نام سنسور", "sensorName": "نام مزرعه",
"sensorUuid": "شناسه سنسور (UUID)", "sensorUuid": "شناسه مزرعه (UUID)",
"placeholderName": "نام سنسور را وارد کنید", "stationName": "نام ایستگاه یا سنسور اصلی",
"placeholderUuid": "شناسه سنسور را وارد کنید", "placeholderName": "نام مزرعه را وارد کنید",
"placeholderUuid": "شناسه مزرعه را وارد کنید",
"placeholderStationName": "نام ایستگاه اصلی را وارد کنید",
"plantType": "نوع گیاه", "plantType": "نوع گیاه",
"plantName": "اسم گیاه", "plantName": "اسم گیاه",
"saveSensor": "ذخیره سنسور", "mapAreaDescription": "محدوده مزرعه را روی نقشه مشخص کنید تا در صورت نیاز zoning اولیه هم ساخته شود.",
"saveSensor": "ذخیره مزرعه",
"saving": "در حال ذخیره...", "saving": "در حال ذخیره...",
"errorSave": "خطا در ذخیره سنسور", "errorSave": "خطا در ذخیره مزرعه",
"columns": { "columns": {
"name": "نام", "name": "نام",
"lastUpdate": "آخرین بروزرسانی", "lastUpdate": "آخرین بروزرسانی",
"uuid": "شناسه یکتا" "uuid": "شناسه مزرعه"
}, },
"ariaClose": "بستن", "ariaClose": "بستن",
"errorLoad": "خطا در بارگذاری سنسورها" "errorLoad": "خطا در بارگذاری مزارع"
}, },
"accountSettings": { "accountSettings": {
"account": "حساب کاربری", "account": "حساب کاربری",
"sensorHub": "مرکز سنسور", "sensorHub": "مرکز مزرعه",
"firstName": "نام", "firstName": "نام",
"lastName": "نام خانوادگی", "lastName": "نام خانوادگی",
"email": "ایمیل", "email": "ایمیل",
@@ -540,6 +543,7 @@
"failed": "پردازش محدوده زمین ناموفق بود." "failed": "پردازش محدوده زمین ناموفق بود."
}, },
"errors": { "errors": {
"noFarm": "ابتدا یک مزرعه انتخاب کنید.",
"areaLoadFailed": "بارگذاری محدوده زمین با خطا مواجه شد.", "areaLoadFailed": "بارگذاری محدوده زمین با خطا مواجه شد.",
"timeout": "دریافت نتیجه محدوده زمین بیش از حد طول کشید. دوباره تلاش کنید." "timeout": "دریافت نتیجه محدوده زمین بیش از حد طول کشید. دوباره تلاش کنید."
}, },
@@ -636,6 +640,7 @@
"generateCta": "تولید برنامه آبیاری", "generateCta": "تولید برنامه آبیاری",
"generating": "در حال تحلیل و تولید برنامه آبیاری...", "generating": "در حال تحلیل و تولید برنامه آبیاری...",
"errors": { "errors": {
"noFarm": "ابتدا یک مزرعه انتخاب کنید.",
"generateFailed": "دریافت برنامه آبیاری با خطا مواجه شد.", "generateFailed": "دریافت برنامه آبیاری با خطا مواجه شد.",
"timeout": "دریافت نتیجه بیش از حد طول کشید. دوباره تلاش کنید." "timeout": "دریافت نتیجه بیش از حد طول کشید. دوباره تلاش کنید."
}, },
@@ -686,6 +691,7 @@
"generateCta": "تولید برنامه کوددهی", "generateCta": "تولید برنامه کوددهی",
"generating": "در حال تحلیل و تولید نسخه تغذیه‌ای...", "generating": "در حال تحلیل و تولید نسخه تغذیه‌ای...",
"errors": { "errors": {
"noFarm": "ابتدا یک مزرعه انتخاب کنید.",
"generateFailed": "دریافت برنامه کوددهی با خطا مواجه شد.", "generateFailed": "دریافت برنامه کوددهی با خطا مواجه شد.",
"timeout": "دریافت نتیجه بیش از حد طول کشید. دوباره تلاش کنید." "timeout": "دریافت نتیجه بیش از حد طول کشید. دوباره تلاش کنید."
}, },
@@ -759,6 +765,7 @@
"whyThis": "چرا این توصیه؟" "whyThis": "چرا این توصیه؟"
}, },
"errors": { "errors": {
"noFarm": "ابتدا یک مزرعه انتخاب کنید.",
"contextLoad": "بارگذاری زمینه مزرعه ناموفق بود.", "contextLoad": "بارگذاری زمینه مزرعه ناموفق بود.",
"chatSend": "ارسال پیام ناموفق بود. دوباره تلاش کنید.", "chatSend": "ارسال پیام ناموفق بود. دوباره تلاش کنید.",
"conversationLoad": "بارگذاری لیست مکالمات ناموفق بود.", "conversationLoad": "بارگذاری لیست مکالمات ناموفق بود.",
+4 -5
View File
@@ -1,20 +1,19 @@
'use client' 'use client'
// React Imports // React Imports
import { useEffect, type ReactNode } from 'react' import type { ReactNode } from 'react'
// Hook Imports // Hook Imports
import { useSensorHub } from '@/hooks/useSensorHub' import { useFarmHub } from '@/hooks/useFarmHub'
import { Box } from '@mui/material'
import SensorHubView from '@/views/sensorHub' import SensorHubView from '@/views/sensorHub'
interface SensorHubProps { interface SensorHubProps {
children: ReactNode children: ReactNode
} }
export default function SensorHub({ children }: SensorHubProps) { export default function SensorHub({ children }: SensorHubProps) {
const { hasSensorHub } = useSensorHub() const { hasFarmHub } = useFarmHub()
return <> {children} return <> {children}
{!hasSensorHub && <SensorHubView />}</> {!hasFarmHub && <SensorHubView />}</>
} }
+105
View File
@@ -0,0 +1,105 @@
"use client";
import { useCallback, useEffect, useState } from "react";
export const FARM_HUB_STORAGE_KEY = "farm_hub";
const LEGACY_SENSOR_HUB_STORAGE_KEY = "sensor_hub";
export interface FarmHubInfo {
farm_uuid: string;
id?: string;
name?: string;
is_active?: boolean;
[key: string]: unknown;
}
export interface UseFarmHubReturn {
farmHub: FarmHubInfo | null;
hasFarmHub: boolean;
setFarmHub: (data: FarmHubInfo | null) => void;
getFarmUuid: () => string | null;
}
const normalizeFarmHub = (raw: unknown): FarmHubInfo | null => {
if (!raw || typeof raw !== "object") {
return null;
}
const candidate = raw as Record<string, unknown>;
const farmUuid =
typeof candidate.farm_uuid === "string"
? candidate.farm_uuid
: typeof candidate.id === "string"
? candidate.id
: null;
if (!farmUuid) {
return null;
}
return {
...candidate,
farm_uuid: farmUuid,
id: typeof candidate.id === "string" ? candidate.id : farmUuid,
} as FarmHubInfo;
};
const parseFarmHub = (raw: string | null): FarmHubInfo | null => {
if (!raw) return null;
try {
return normalizeFarmHub(JSON.parse(raw));
} catch {
return null;
}
};
export const getStoredFarmHub = (): FarmHubInfo | null => {
if (typeof window === "undefined") return null;
return (
parseFarmHub(localStorage.getItem(FARM_HUB_STORAGE_KEY)) ??
parseFarmHub(localStorage.getItem(LEGACY_SENSOR_HUB_STORAGE_KEY))
);
};
export const getStoredFarmUuid = (): string | null =>
getStoredFarmHub()?.farm_uuid ?? null;
export const useFarmHub = (): UseFarmHubReturn => {
const [farmHub, setFarmHubState] = useState<FarmHubInfo | null>(null);
useEffect(() => {
setFarmHubState(getStoredFarmHub());
}, []);
const setFarmHub = useCallback((data: FarmHubInfo | null) => {
if (typeof window === "undefined") return;
if (data === null) {
localStorage.removeItem(FARM_HUB_STORAGE_KEY);
setFarmHubState(null);
return;
}
const normalized = normalizeFarmHub(data);
if (!normalized) {
return;
}
localStorage.setItem(FARM_HUB_STORAGE_KEY, JSON.stringify(normalized));
setFarmHubState(normalized);
}, []);
const getFarmUuid = useCallback(() => {
return farmHub?.farm_uuid ?? getStoredFarmUuid();
}, [farmHub]);
return {
farmHub,
hasFarmHub: farmHub !== null,
setFarmHub,
getFarmUuid,
};
};
+11 -69
View File
@@ -1,71 +1,13 @@
'use client' "use client";
// React Imports export {
import { useState, useEffect, useCallback } from 'react' FARM_HUB_STORAGE_KEY as SENSOR_HUB_STORAGE_KEY,
getStoredFarmHub as getStoredSensorHub,
getStoredFarmUuid as getStoredSensorHubId,
useFarmHub as useSensorHub,
} from "./useFarmHub";
const SENSOR_HUB_STORAGE_KEY = 'sensor_hub' export type {
FarmHubInfo as SensorHubInfo,
export interface SensorHubInfo { UseFarmHubReturn as UseSensorHubReturn,
id: string } from "./useFarmHub";
[key: string]: unknown
}
export interface UseSensorHubReturn {
/** Sensor hub data from localStorage */
sensorHub: SensorHubInfo | null
/** Whether sensor_hub exists in localStorage */
hasSensorHub: boolean
/** Save sensor hub to localStorage */
setSensorHub: (data: SensorHubInfo | null) => void
/** Get headers to attach to API requests (e.g. X-Sensor-Hub-Id) */
getSensorHubHeaders: () => Record<string, string>
}
const parseSensorHub = (raw: string | null): SensorHubInfo | null => {
if (!raw) return null
try {
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === 'object' && typeof parsed.id === 'string') {
return parsed as SensorHubInfo
}
} catch {
// ignore invalid JSON
}
return null
}
export const useSensorHub = (): UseSensorHubReturn => {
const [sensorHub, setSensorHubState] = useState<SensorHubInfo | null>(null)
useEffect(() => {
if (typeof window === 'undefined') return
const stored = localStorage.getItem(SENSOR_HUB_STORAGE_KEY)
setSensorHubState(parseSensorHub(stored))
}, [])
const setSensorHub = useCallback((data: SensorHubInfo | null) => {
if (typeof window === 'undefined') return
if (data === null) {
localStorage.removeItem(SENSOR_HUB_STORAGE_KEY)
setSensorHubState(null)
} else {
localStorage.setItem(SENSOR_HUB_STORAGE_KEY, JSON.stringify(data))
setSensorHubState(data)
}
}, [])
const getSensorHubHeaders = useCallback((): Record<string, string> => {
const hub = sensorHub ?? (typeof window !== 'undefined' ? parseSensorHub(localStorage.getItem(SENSOR_HUB_STORAGE_KEY)) : null)
if (!hub?.id) return {}
return {
'X-Sensor-Hub-Id': hub.id
}
}, [sensorHub])
return {
sensorHub,
hasSensorHub: sensorHub !== null,
setSensorHub,
getSensorHubHeaders
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ export {
userManagementService, userManagementService,
} from "./services/userManagementService"; } from "./services/userManagementService";
export * from "./services/rolesPermissionsService"; export * from "./services/rolesPermissionsService";
export * from "./services/sensorHubService"; export * from "./services/farmHubService";
export { export {
type FarmDashboardConfigResponse, type FarmDashboardConfigResponse,
type FarmDashboardCardsResponse, type FarmDashboardCardsResponse,
+15 -15
View File
@@ -217,7 +217,7 @@ function getAreaCacheUserKey(): string {
} }
function getAreaCacheKey( function getAreaCacheKey(
sensorUuid: string, farmUuid: string,
page: number, page: number,
pageSize: number, pageSize: number,
): string { ): string {
@@ -225,21 +225,21 @@ function getAreaCacheKey(
AREA_CACHE_KEY_PREFIX, AREA_CACHE_KEY_PREFIX,
AREA_CACHE_VERSION, AREA_CACHE_VERSION,
getAreaCacheUserKey(), getAreaCacheUserKey(),
sensorUuid, farmUuid,
page, page,
pageSize, pageSize,
].join(":"); ].join(":");
} }
function readCachedArea( function readCachedArea(
sensorUuid: string, farmUuid: string,
page: number, page: number,
pageSize: number, pageSize: number,
): CropZoningAreaResult | null { ): CropZoningAreaResult | null {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
try { try {
const raw = localStorage.getItem(getAreaCacheKey(sensorUuid, page, pageSize)); const raw = localStorage.getItem(getAreaCacheKey(farmUuid, page, pageSize));
if (!raw) { if (!raw) {
return null; return null;
@@ -248,7 +248,7 @@ function readCachedArea(
const parsed = JSON.parse(raw) as CachedAreaEntry; const parsed = JSON.parse(raw) as CachedAreaEntry;
if (!parsed?.expiresAt || parsed.expiresAt < Date.now()) { if (!parsed?.expiresAt || parsed.expiresAt < Date.now()) {
localStorage.removeItem(getAreaCacheKey(sensorUuid, page, pageSize)); localStorage.removeItem(getAreaCacheKey(farmUuid, page, pageSize));
return null; return null;
} }
@@ -259,7 +259,7 @@ function readCachedArea(
} }
function writeCachedArea( function writeCachedArea(
sensorUuid: string, farmUuid: string,
page: number, page: number,
pageSize: number, pageSize: number,
value: CropZoningAreaResult, value: CropZoningAreaResult,
@@ -273,7 +273,7 @@ function writeCachedArea(
}; };
localStorage.setItem( localStorage.setItem(
getAreaCacheKey(sensorUuid, page, pageSize), getAreaCacheKey(farmUuid, page, pageSize),
JSON.stringify(payload), JSON.stringify(payload),
); );
} catch { } catch {
@@ -320,7 +320,7 @@ export const cropZoningService = {
}, },
getArea( getArea(
sensorUuid: string, farmUuid: string,
options?: { page?: number; pageSize?: number; useCache?: boolean }, options?: { page?: number; pageSize?: number; useCache?: boolean },
): Promise<CropZoningAreaResponse> { ): Promise<CropZoningAreaResponse> {
const page = options?.page ?? 1; const page = options?.page ?? 1;
@@ -328,11 +328,11 @@ export const cropZoningService = {
const useCache = options?.useCache ?? true; const useCache = options?.useCache ?? true;
if (useCache) { if (useCache) {
const cached = readCachedArea(sensorUuid, page, pageSize); const cached = readCachedArea(farmUuid, page, pageSize);
if (cached) { if (cached) {
logAreaRequest("cache-hit", { logAreaRequest("cache-hit", {
sensorUuid, farmUuid,
page, page,
pageSize, pageSize,
pagination: cached.pagination ?? null, pagination: cached.pagination ?? null,
@@ -343,7 +343,7 @@ export const cropZoningService = {
} }
} }
const params = new URLSearchParams({ sensor_uuid: sensorUuid }); const params = new URLSearchParams({ farm_uuid: farmUuid });
params.set("page", String(page)); params.set("page", String(page));
params.set("page_size", String(pageSize)); params.set("page_size", String(pageSize));
@@ -351,7 +351,7 @@ export const cropZoningService = {
const endpoint = `${PREFIX}/area/?${params.toString()}`; const endpoint = `${PREFIX}/area/?${params.toString()}`;
logAreaRequest("request", { logAreaRequest("request", {
sensorUuid, farmUuid,
page, page,
pageSize, pageSize,
endpoint, endpoint,
@@ -362,7 +362,7 @@ export const cropZoningService = {
).then((response) => { ).then((response) => {
if ("task_id" in response) { if ("task_id" in response) {
logAreaRequest("response", { logAreaRequest("response", {
sensorUuid, farmUuid,
page, page,
pageSize, pageSize,
taskId: response.task_id, taskId: response.task_id,
@@ -375,7 +375,7 @@ export const cropZoningService = {
const taskStatus = normalized.task?.status?.toLowerCase(); const taskStatus = normalized.task?.status?.toLowerCase();
logAreaRequest("response", { logAreaRequest("response", {
sensorUuid, farmUuid,
page, page,
pageSize, pageSize,
taskStatus: normalized.task?.status ?? null, taskStatus: normalized.task?.status ?? null,
@@ -391,7 +391,7 @@ export const cropZoningService = {
taskStatus !== "failure" && taskStatus !== "failure" &&
taskStatus !== "failed" taskStatus !== "failed"
) { ) {
writeCachedArea(sensorUuid, page, pageSize, normalized); writeCachedArea(farmUuid, page, pageSize, normalized);
} }
return normalized; return normalized;
+42 -10
View File
@@ -4,6 +4,7 @@ import type { ConversationSummary, FarmContext } from '@views/dashboards/farm/fa
const PREFIX = '/api/farm-ai-assistant' const PREFIX = '/api/farm-ai-assistant'
export interface FarmContextResponse { export interface FarmContextResponse {
farm_uuid?: string
soilType: string soilType: string
waterEC: string waterEC: string
selectedCrop: string selectedCrop: string
@@ -24,6 +25,7 @@ export interface ChatSection {
} }
export interface ChatPayload { export interface ChatPayload {
farm_uuid: string
content?: string content?: string
images?: string[] images?: string[]
conversation_id?: string conversation_id?: string
@@ -37,12 +39,14 @@ export interface ChatTaskInitResponse {
status_url?: string status_url?: string
conversation_id: string conversation_id: string
message_id: string message_id: string
farm_uuid?: string
} }
export interface ChatTaskStatusResponse { export interface ChatTaskStatusResponse {
task_id: string task_id: string
status: 'PENDING' | 'STARTED' | 'SUCCESS' | 'FAILURE' status: 'PENDING' | 'STARTED' | 'SUCCESS' | 'FAILURE'
conversation_id: string conversation_id: string
farm_uuid?: string
progress?: { progress?: {
message?: string message?: string
} }
@@ -53,6 +57,7 @@ export interface ChatTaskStatusResponse {
export interface ChatMessageResponse { export interface ChatMessageResponse {
message_id: string message_id: string
conversation_id: string conversation_id: string
farm_uuid?: string
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: string content: string
sections: ChatSection[] sections: ChatSection[]
@@ -62,16 +67,19 @@ export interface ChatMessageResponse {
export interface ConversationMessagesResponse { export interface ConversationMessagesResponse {
conversation_id: string conversation_id: string
farm_uuid?: string
messages: ChatMessageResponse[] messages: ChatMessageResponse[]
} }
export interface CreateConversationPayload { export interface CreateConversationPayload {
farm_uuid: string
title?: string title?: string
farm_context?: Partial<FarmContext> farm_context?: Partial<FarmContext>
} }
export interface CreateConversationResponse { export interface CreateConversationResponse {
id: string id: string
farm_uuid?: string
message_count: number message_count: number
title?: string title?: string
updated_at?: string updated_at?: string
@@ -87,33 +95,57 @@ function unwrap<T>(res: ApiResponse<T>): T {
} }
export const farmAiAssistantService = { export const farmAiAssistantService = {
getContext(): Promise<FarmContextResponse> { getContext(farmUuid: string): Promise<FarmContextResponse> {
return apiClient.get<ApiResponse<FarmContextResponse>>(`${PREFIX}/context/`).then(unwrap) return apiClient
.get<ApiResponse<FarmContextResponse>>(
`${PREFIX}/context/?farm_uuid=${encodeURIComponent(farmUuid)}`,
)
.then(unwrap)
}, },
createChatTask(payload: ChatPayload): Promise<ChatTaskInitResponse> { createChatTask(payload: ChatPayload): Promise<ChatTaskInitResponse> {
return apiClient.post<ApiResponse<ChatTaskInitResponse>>(`${PREFIX}/chat/task/`, payload).then(unwrap) return apiClient.post<ApiResponse<ChatTaskInitResponse>>(`${PREFIX}/chat/task/`, payload).then(unwrap)
}, },
getChatTaskStatus(taskId: string): Promise<ChatTaskStatusResponse> { getChatTaskStatus(taskId: string, farmUuid: string): Promise<ChatTaskStatusResponse> {
return apiClient.get<ApiResponse<ChatTaskStatusResponse>>(`${PREFIX}/chat/task/${taskId}/status/`).then(unwrap) return apiClient
.get<ApiResponse<ChatTaskStatusResponse>>(
`${PREFIX}/chat/task/${taskId}/status/?farm_uuid=${encodeURIComponent(farmUuid)}`,
)
.then(unwrap)
}, },
getConversations(): Promise<ConversationSummary[]> { getConversations(farmUuid: string): Promise<ConversationSummary[]> {
return apiClient.get<ApiResponse<ConversationSummary[]>>(`${PREFIX}/chats/`).then(unwrap) return apiClient
.get<ApiResponse<ConversationSummary[]>>(
`${PREFIX}/chats/?farm_uuid=${encodeURIComponent(farmUuid)}`,
)
.then(unwrap)
}, },
createConversation(payload?: CreateConversationPayload): Promise<CreateConversationResponse> { createConversation(payload?: CreateConversationPayload): Promise<CreateConversationResponse> {
return apiClient.post<ApiResponse<CreateConversationResponse>>(`${PREFIX}/chats/`, payload).then(unwrap) return apiClient.post<ApiResponse<CreateConversationResponse>>(`${PREFIX}/chats/`, payload).then(unwrap)
}, },
deleteConversation(conversationId: string): Promise<{ conversation_id: string }> { deleteConversation(
return apiClient.delete<ApiResponse<{ conversation_id: string }>>(`${PREFIX}/chats/${conversationId}/`).then(unwrap) conversationId: string,
farmUuid: string,
): Promise<{ conversation_id: string; farm_uuid?: string }> {
return apiClient
.delete<ApiResponse<{ conversation_id: string; farm_uuid?: string }>>(
`${PREFIX}/chats/${conversationId}/?farm_uuid=${encodeURIComponent(farmUuid)}`,
)
.then(unwrap)
}, },
getConversationMessages(conversationId: string): Promise<ConversationMessagesResponse> { getConversationMessages(
conversationId: string,
farmUuid: string,
): Promise<ConversationMessagesResponse> {
return apiClient return apiClient
.get<ApiResponse<ConversationMessagesResponse>>(`${PREFIX}/chats/${conversationId}/messages/`) .get<ApiResponse<ConversationMessagesResponse>>(
`${PREFIX}/chats/${conversationId}/messages/?farm_uuid=${encodeURIComponent(farmUuid)}`,
)
.then(unwrap) .then(unwrap)
} }
} }
+31 -16
View File
@@ -23,6 +23,7 @@ export interface ApiResponse<T> {
} }
export interface FarmDashboardConfigResponse { export interface FarmDashboardConfigResponse {
farm_uuid?: string;
disabled_card_ids: string[]; disabled_card_ids: string[];
row_order: string[]; row_order: string[];
enable_drag_reorder?: boolean; enable_drag_reorder?: boolean;
@@ -48,7 +49,7 @@ export interface FarmDashboardCardsResponse {
} }
interface FarmDashboardCardsTaskResult { interface FarmDashboardCardsTaskResult {
sensor_id?: string; farm_uuid?: string;
all_cards?: FarmDashboardCardsResponse; all_cards?: FarmDashboardCardsResponse;
} }
@@ -58,7 +59,11 @@ interface FarmDashboardCardsTaskData {
result?: FarmDashboardCardsTaskResult; result?: FarmDashboardCardsTaskResult;
} }
const STORAGE_KEY = "farm_dashboard_config"; const STORAGE_KEY_PREFIX = "farm_dashboard_config";
function getStorageKey(farmUuid: string): string {
return `${STORAGE_KEY_PREFIX}:${farmUuid}`;
}
function isCardId(value: string): value is CardId { function isCardId(value: string): value is CardId {
return (CARD_IDS as readonly string[]).includes(value); return (CARD_IDS as readonly string[]).includes(value);
@@ -149,34 +154,38 @@ function toApiRequest(
/** /**
* localStorage fallback when backend is not ready * localStorage fallback when backend is not ready
*/ */
function getLocalConfig(): FarmDashboardConfig | null { function getLocalConfig(farmUuid: string): 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(getStorageKey(farmUuid));
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(farmUuid: string, config: FarmDashboardConfig): void {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
try { try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); localStorage.setItem(getStorageKey(farmUuid), 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);
} }
} }
function buildFarmQuery(farmUuid: string): string {
return `farm_uuid=${encodeURIComponent(farmUuid)}`;
}
export const farmDashboardService = { export const farmDashboardService = {
/** /**
* Get farm dashboard config for current user * Get farm dashboard config for the selected farm
*/ */
async getConfig(): Promise<FarmDashboardConfig> { async getConfig(farmUuid: string): Promise<FarmDashboardConfig> {
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/?${buildFarmQuery(farmUuid)}`);
const raw = response && "data" in response ? response.data : response; const raw = response && "data" in response ? response.data : response;
if ( if (
raw && raw &&
@@ -187,7 +196,7 @@ export const farmDashboardService = {
} }
throw new Error("Invalid response"); throw new Error("Invalid response");
} catch { } catch {
const local = getLocalConfig(); const local = getLocalConfig(farmUuid);
if (local) return local; if (local) return local;
return { disabledCardIds: [], rowOrder: [], enableDragReorder: true }; return { disabledCardIds: [], rowOrder: [], enableDragReorder: true };
} }
@@ -197,12 +206,16 @@ export const farmDashboardService = {
* Update farm dashboard config * Update farm dashboard config
*/ */
async updateConfig( async updateConfig(
farmUuid: string,
data: Partial<FarmDashboardConfig>, data: Partial<FarmDashboardConfig>,
): Promise<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/", {
farm_uuid: farmUuid,
...toApiRequest(data),
});
const raw = response && "data" in response ? response.data : response; const raw = response && "data" in response ? response.data : response;
if ( if (
raw && raw &&
@@ -210,12 +223,12 @@ export const farmDashboardService = {
("disabled_card_ids" in raw || "row_order" in raw) ("disabled_card_ids" in raw || "row_order" in raw)
) { ) {
const config = fromApiResponse(raw as FarmDashboardConfigResponse); const config = fromApiResponse(raw as FarmDashboardConfigResponse);
setLocalConfig(config); setLocalConfig(farmUuid, config);
return config; return config;
} }
throw new Error("Update failed"); throw new Error("Update failed");
} catch (err) { } catch (err) {
const local = getLocalConfig(); const local = getLocalConfig(farmUuid);
if (local) { if (local) {
const merged: FarmDashboardConfig = { const merged: FarmDashboardConfig = {
disabledCardIds: data.disabledCardIds ?? local.disabledCardIds, disabledCardIds: data.disabledCardIds ?? local.disabledCardIds,
@@ -223,7 +236,7 @@ export const farmDashboardService = {
enableDragReorder: enableDragReorder:
data.enableDragReorder ?? local.enableDragReorder ?? true, data.enableDragReorder ?? local.enableDragReorder ?? true,
}; };
setLocalConfig(merged); setLocalConfig(farmUuid, merged);
return merged; return merged;
} }
throw err; throw err;
@@ -234,7 +247,9 @@ 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< async getAllCards(
farmUuid: string,
): Promise<
Partial<Record<CardId, Record<string, unknown>>> Partial<Record<CardId, Record<string, unknown>>>
> { > {
try { try {
@@ -243,7 +258,7 @@ export const farmDashboardService = {
| ApiResponse<FarmDashboardCardsTaskData> | ApiResponse<FarmDashboardCardsTaskData>
| FarmDashboardCardsResponse | FarmDashboardCardsResponse
| FarmDashboardCardsTaskData | FarmDashboardCardsTaskData
>("/api/farm-dashboard/"); >(`/api/farm-dashboard/?${buildFarmQuery(farmUuid)}`);
return extractCardsPayload(response); return extractCardsPayload(response);
} catch { } catch {
return {}; return {};
+115
View File
@@ -0,0 +1,115 @@
import { apiClient } from "../client";
export interface FarmTypeInfo {
uuid: string;
name: string;
description?: string;
metadata?: Record<string, unknown>;
}
export interface FarmProductInfo {
uuid: string;
name: string;
description?: string;
metadata?: Record<string, unknown>;
}
export interface FarmSensorInfo {
uuid?: string;
name: string;
sensor_type?: string;
is_active?: boolean;
specifications?: Record<string, unknown>;
power_source?: Record<string, unknown>;
customization?: Record<string, unknown>;
last_updated?: string;
}
export interface Farm {
farm_uuid: string;
name: string;
is_active?: boolean;
customization?: Record<string, unknown>;
farm_type?: FarmTypeInfo | null;
products?: FarmProductInfo[];
sensors?: FarmSensorInfo[];
zoning?: unknown;
last_updated?: string;
[key: string]: unknown;
}
export interface ListFarmsResponse {
status?: string;
data?: Farm | Farm[];
}
export interface CreateFarmPayload {
name: string;
farm_type_uuid?: string;
product_uuids?: string[];
customization?: Record<string, unknown>;
sensors?: FarmSensorInfo[];
area_geojson?: Record<string, unknown>;
area_m2?: number;
}
type ApiEnvelope<T> = {
data?: T;
};
const PREFIX = "/api/farm-hub";
function unwrapFarmCollection(
response: Farm[] | ListFarmsResponse | Farm | ApiEnvelope<Farm | Farm[]>,
): Farm[] {
const payload =
response && typeof response === "object" && "data" in response
? response.data
: response;
if (Array.isArray(payload)) {
return payload;
}
if (payload && typeof payload === "object") {
return [payload as Farm];
}
return [];
}
function unwrapSingle<T>(response: T | ApiEnvelope<T>): T {
const payload =
response && typeof response === "object" && "data" in response
? response.data
: response;
return payload as T;
}
export const farmHubService = {
async createFarm(payload: CreateFarmPayload): Promise<Farm> {
const response = await apiClient.post<Farm | ApiEnvelope<Farm>>(
`${PREFIX}/`,
payload,
);
return unwrapSingle(response);
},
async listFarms(): Promise<Farm[]> {
const response = await apiClient.get<
Farm[] | ListFarmsResponse | Farm | ApiEnvelope<Farm | Farm[]>
>(`${PREFIX}/`);
return unwrapFarmCollection(response);
},
async activateFarm(farmUuid: string): Promise<void> {
await apiClient.post(`${PREFIX}/active/`, { farm_uuid: farmUuid });
},
async deactivateFarm(farmUuid: string): Promise<void> {
await apiClient.post(`${PREFIX}/deactive/`, { farm_uuid: farmUuid });
},
};
@@ -30,6 +30,7 @@ export interface CropOption {
} }
export interface FertilizationConfigResponse { export interface FertilizationConfigResponse {
farm_uuid?: string;
farmData: FarmData; farmData: FarmData;
growthStages: GrowthStage[]; growthStages: GrowthStage[];
cropOptions: CropOption[]; cropOptions: CropOption[];
@@ -44,6 +45,7 @@ export interface FertilizationPlan {
} }
export interface FertilizationRecommendPayload { export interface FertilizationRecommendPayload {
farm_uuid: string;
crop_id?: string; crop_id?: string;
growth_stage?: string; growth_stage?: string;
farm_data?: Partial<FarmData>; farm_data?: Partial<FarmData>;
@@ -92,10 +94,10 @@ function normalizeRecommendationResult(
} }
export const fertilizationRecommendationService = { export const fertilizationRecommendationService = {
getConfig(): Promise<FertilizationConfigResponse> { getConfig(farmUuid: string): Promise<FertilizationConfigResponse> {
return unwrap( return unwrap(
apiClient.get<ApiResponse<FertilizationConfigResponse>>( apiClient.get<ApiResponse<FertilizationConfigResponse>>(
`${PREFIX}/config/`, `${PREFIX}/config/?farm_uuid=${encodeURIComponent(farmUuid)}`,
), ),
); );
}, },
@@ -117,6 +119,7 @@ export const fertilizationRecommendationService = {
getRecommendStatus( getRecommendStatus(
taskId: string, taskId: string,
farmUuid: string,
): Promise< ): Promise<
RecommendationTaskStatusResponse<FertilizationRecommendationResult> RecommendationTaskStatusResponse<FertilizationRecommendationResult>
> { > {
@@ -125,7 +128,9 @@ export const fertilizationRecommendationService = {
ApiResponse< ApiResponse<
RecommendationTaskStatusResponse<FertilizationRecommendationResult> RecommendationTaskStatusResponse<FertilizationRecommendationResult>
> >
>(`${PREFIX}/recommend/status/${taskId}/`), >(
`${PREFIX}/recommend/status/${taskId}/?farm_uuid=${encodeURIComponent(farmUuid)}`,
),
).then((response) => ({ ).then((response) => ({
...response, ...response,
status: normalizeRecommendationTaskStatus(response.status), status: normalizeRecommendationTaskStatus(response.status),
@@ -25,6 +25,7 @@ export interface CropOption {
} }
export interface IrrigationConfigResponse { export interface IrrigationConfigResponse {
farm_uuid?: string;
farmInfo: FarmInfo; farmInfo: FarmInfo;
cropOptions: CropOption[]; cropOptions: CropOption[];
} }
@@ -38,6 +39,7 @@ export interface IrrigationPlan {
} }
export interface IrrigationRecommendPayload { export interface IrrigationRecommendPayload {
farm_uuid: string;
crop_id?: string; crop_id?: string;
farm_data?: Partial<FarmInfo>; farm_data?: Partial<FarmInfo>;
soilType?: string; soilType?: string;
@@ -108,9 +110,11 @@ function normalizeRecommendationResult(
} }
export const irrigationRecommendationService = { export const irrigationRecommendationService = {
getConfig(): Promise<IrrigationConfigResponse> { getConfig(farmUuid: string): Promise<IrrigationConfigResponse> {
return unwrap( return unwrap(
apiClient.get<ApiResponse<IrrigationConfigResponse>>(`${PREFIX}/config/`), apiClient.get<ApiResponse<IrrigationConfigResponse>>(
`${PREFIX}/config/?farm_uuid=${encodeURIComponent(farmUuid)}`,
),
); );
}, },
@@ -131,13 +135,16 @@ export const irrigationRecommendationService = {
getRecommendStatus( getRecommendStatus(
taskId: string, taskId: string,
farmUuid: string,
): Promise<RecommendationTaskStatusResponse<IrrigationRecommendationResult>> { ): Promise<RecommendationTaskStatusResponse<IrrigationRecommendationResult>> {
return unwrap( return unwrap(
apiClient.get< apiClient.get<
ApiResponse< ApiResponse<
RecommendationTaskStatusResponse<IrrigationRecommendationResult> RecommendationTaskStatusResponse<IrrigationRecommendationResult>
> >
>(`${PREFIX}/recommend/status/${taskId}/`), >(
`${PREFIX}/recommend/status/${taskId}/?farm_uuid=${encodeURIComponent(farmUuid)}`,
),
).then((response) => ({ ).then((response) => ({
...response, ...response,
status: normalizeRecommendationTaskStatus(response.status), status: normalizeRecommendationTaskStatus(response.status),
@@ -7,6 +7,7 @@ export type RecommendationTaskStatus =
export interface RecommendationTaskInitResponse { export interface RecommendationTaskInitResponse {
task_id: string; task_id: string;
status: RecommendationTaskStatus; status: RecommendationTaskStatus;
farm_uuid?: string;
} }
export interface RecommendationTaskProgress { export interface RecommendationTaskProgress {
@@ -16,6 +17,7 @@ export interface RecommendationTaskProgress {
export interface RecommendationTaskStatusResponse<T> { export interface RecommendationTaskStatusResponse<T> {
task_id: string; task_id: string;
status: RecommendationTaskStatus; status: RecommendationTaskStatus;
farm_uuid?: string;
progress?: RecommendationTaskProgress; progress?: RecommendationTaskProgress;
result?: T; result?: T;
error?: string; error?: string;
+19 -48
View File
@@ -1,54 +1,25 @@
/** import {
* Sensor Hub Service type CreateFarmPayload,
* Handles sensor hub API calls type Farm,
*/ farmHubService,
} from "./farmHubService";
import { apiClient } from '../client' export type Sensor = Farm;
export type ListSensorsResponse = Farm[];
export interface Sensor { export type AddSensorPayload = CreateFarmPayload;
name: string
uuid_sensor: string
last_updated: string
[key: string]: unknown
}
export interface ListSensorsResponse {
status?: string
data: Sensor | Sensor[]
}
export interface AddSensorPayload {
name: string
uuid_sensor: string
area_geojson?: Record<string, unknown>
area_m2?: number
}
export const sensorHubService = { export const sensorHubService = {
/** addSensor(payload: AddSensorPayload): Promise<Sensor> {
* Add a new sensor return farmHubService.createFarm(payload);
*/
async addSensor(payload: AddSensorPayload): Promise<Sensor> {
const response = await apiClient.post<{ data?: Sensor }>('/api/sensor-hub/', payload)
return (response?.data as Sensor) ?? (payload as unknown as Sensor)
}, },
/** listSensors(): Promise<Sensor[]> {
* Get list of sensors return farmHubService.listFarms();
*/ },
async listSensors(): Promise<Sensor[]> { };
const response = await apiClient.get<ListSensorsResponse>('/api/sensor-hub/')
const data = response?.data
if (Array.isArray(data)) { export {
return data type CreateFarmPayload,
} type Farm,
farmHubService,
if (data && typeof data === 'object') { } from "./farmHubService";
return [data as Sensor]
}
return []
}
}
@@ -4,6 +4,7 @@
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";
import { useFarmHub } from "@/hooks/useFarmHub";
// Context Imports // Context Imports
import NavbarSlotContext from "@/contexts/navbarSlotContext"; import NavbarSlotContext from "@/contexts/navbarSlotContext";
@@ -97,6 +98,8 @@ function areStringArraysEqual(left: string[], right: string[]): boolean {
const FarmDashboardWrapper = () => { const FarmDashboardWrapper = () => {
const t = useTranslations("farmDashboard"); const t = useTranslations("farmDashboard");
const { farmHub } = useFarmHub();
const farmUuid = farmHub?.farm_uuid;
const { setSlotContent } = useContext(NavbarSlotContext); const { setSlotContent } = useContext(NavbarSlotContext);
const [config, setConfig] = useState<FarmDashboardConfig>( const [config, setConfig] = useState<FarmDashboardConfig>(
DEFAULT_FARM_DASHBOARD_CONFIG, DEFAULT_FARM_DASHBOARD_CONFIG,
@@ -188,9 +191,18 @@ const FarmDashboardWrapper = () => {
useEffect(() => { useEffect(() => {
if (!farmUuid) {
setConfig(DEFAULT_FARM_DASHBOARD_CONFIG);
setCardsData({});
setLoading(false);
return;
}
setLoading(true);
Promise.all([ Promise.all([
farmDashboardService.getConfig(), farmDashboardService.getConfig(farmUuid),
farmDashboardService.getAllCards(), farmDashboardService.getAllCards(farmUuid),
]) ])
.then(([configData, cards]) => { .then(([configData, cards]) => {
const validRowOrder = (configData.rowOrder ?? []).filter( const validRowOrder = (configData.rowOrder ?? []).filter(
@@ -206,7 +218,7 @@ const FarmDashboardWrapper = () => {
}) })
.catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG)) .catch(() => setConfig(DEFAULT_FARM_DASHBOARD_CONFIG))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, [farmUuid]);
useEffect(() => { useEffect(() => {
if (areStringArraysEqual(orderedRows, visibleRowOrder)) return; if (areStringArraysEqual(orderedRows, visibleRowOrder)) return;
@@ -214,6 +226,7 @@ const FarmDashboardWrapper = () => {
}, [orderedRows, setOrderedRows, visibleRowOrder]); }, [orderedRows, setOrderedRows, visibleRowOrder]);
useEffect(() => { useEffect(() => {
if (!farmUuid) return;
if (loading) return; if (loading) return;
if (areStringArraysEqual(orderedRows, visibleRowOrder)) return; if (areStringArraysEqual(orderedRows, visibleRowOrder)) return;
const newRowOrder = mergeRowOrderAfterDrag( const newRowOrder = mergeRowOrderAfterDrag(
@@ -225,35 +238,37 @@ const FarmDashboardWrapper = () => {
setConfig((prev) => ({ ...prev, rowOrder: newRowOrder })); setConfig((prev) => ({ ...prev, rowOrder: newRowOrder }));
setSaving(true); setSaving(true);
farmDashboardService farmDashboardService
.updateConfig({ rowOrder: newRowOrder }) .updateConfig(farmUuid, { rowOrder: newRowOrder })
.then((updated) => setConfig(updated)) .then((updated) => setConfig(updated))
.catch(() => {}) .catch(() => {})
.finally(() => setSaving(false)); .finally(() => setSaving(false));
}, [config.rowOrder, loading, orderedRows, visibleRowOrder]); }, [config.rowOrder, farmUuid, loading, orderedRows, visibleRowOrder]);
const handleToggleDragReorder = useCallback((enabled: boolean) => { const handleToggleDragReorder = useCallback((enabled: boolean) => {
if (!farmUuid) return;
setConfig((prev) => ({ ...prev, enableDragReorder: enabled })); setConfig((prev) => ({ ...prev, enableDragReorder: enabled }));
setSaving(true); setSaving(true);
farmDashboardService farmDashboardService
.updateConfig({ enableDragReorder: enabled }) .updateConfig(farmUuid, { enableDragReorder: enabled })
.then((updated) => setConfig(updated)) .then((updated) => setConfig(updated))
.finally(() => setSaving(false)); .finally(() => setSaving(false));
}, []); }, [farmUuid]);
const handleToggleCard = useCallback( const handleToggleCard = useCallback(
(cardId: CardId, disabled: boolean) => { (cardId: CardId, disabled: boolean) => {
if (!farmUuid) return;
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(farmUuid, { 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, farmUuid],
); );
@@ -2,6 +2,7 @@
// React Imports // React Imports
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
// MUI Imports // MUI Imports
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
@@ -42,16 +43,25 @@ const CARD_COMPONENTS: Partial<Record<CardId, React.ComponentType<{ data?: Recor
} }
const SoilDataDashboardWrapper = () => { const SoilDataDashboardWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({}) const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
if (!farmUuid) {
setCardsData({})
setLoading(false)
return
}
setLoading(true)
farmDashboardService farmDashboardService
.getAllCards() .getAllCards(farmUuid)
.then(cards => setCardsData(cards ?? {})) .then(cards => setCardsData(cards ?? {}))
.catch(() => setCardsData({})) .catch(() => setCardsData({}))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [farmUuid])
if (loading) { if (loading) {
return ( return (
@@ -2,6 +2,7 @@
// React Imports // React Imports
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useFarmHub } from '@/hooks/useFarmHub'
// MUI Imports // MUI Imports
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
@@ -39,16 +40,25 @@ const CARD_COMPONENTS: Partial<Record<CardId, React.ComponentType<{ data?: Recor
} }
const WaterDataDashboardWrapper = () => { const WaterDataDashboardWrapper = () => {
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({}) const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
if (!farmUuid) {
setCardsData({})
setLoading(false)
return
}
setLoading(true)
farmDashboardService farmDashboardService
.getAllCards() .getAllCards(farmUuid)
.then(cards => setCardsData(cards ?? {})) .then(cards => setCardsData(cards ?? {}))
.catch(() => setCardsData({})) .catch(() => setCardsData({}))
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, []) }, [farmUuid])
if (loading) { if (loading) {
return ( return (
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { useFarmHub } from '@/hooks/useFarmHub'
// MUI Imports // MUI Imports
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
@@ -41,12 +42,20 @@ const FORECAST_CATEGORIES = ['امروز', 'فردا', 'شنبه', 'یکشنبه
export default function CropZoningWeatherSection() { export default function CropZoningWeatherSection() {
const t = useTranslations('cropZoning.weather') const t = useTranslations('cropZoning.weather')
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [weatherData, setWeatherData] = useState<Record<string, unknown>>(DEFAULT_WEATHER) const [weatherData, setWeatherData] = useState<Record<string, unknown>>(DEFAULT_WEATHER)
const [forecastSeries, setForecastSeries] = useState(DEFAULT_FORECAST_SERIES) const [forecastSeries, setForecastSeries] = useState(DEFAULT_FORECAST_SERIES)
useEffect(() => { useEffect(() => {
if (!farmUuid) {
setWeatherData(DEFAULT_WEATHER)
setForecastSeries(DEFAULT_FORECAST_SERIES)
return
}
farmDashboardService farmDashboardService
.getAllCards() .getAllCards(farmUuid)
.then(cards => { .then(cards => {
const w = cards?.farmWeatherCard const w = cards?.farmWeatherCard
if (w && typeof w === 'object') { if (w && typeof w === 'object') {
@@ -58,7 +67,7 @@ export default function CropZoningWeatherSection() {
} }
}) })
.catch(() => {}) .catch(() => {})
}, []) }, [farmUuid])
const forecastOptions: ApexOptions = { const forecastOptions: ApexOptions = {
chart: { chart: {
@@ -3,7 +3,7 @@
import { useState, useCallback, useEffect, useMemo } from "react"; import { useState, useCallback, useEffect, useMemo } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useSensorHub } from "@/hooks/useSensorHub"; import { useFarmHub } from "@/hooks/useFarmHub";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import LinearProgress from "@mui/material/LinearProgress"; import LinearProgress from "@mui/material/LinearProgress";
@@ -43,7 +43,8 @@ const mergeZones = (pages: ZoneInitialData[][]) =>
export default function CropZoningWrapper() { export default function CropZoningWrapper() {
const t = useTranslations("cropZoning"); const t = useTranslations("cropZoning");
const { sensorHub } = useSensorHub(); const { farmHub } = useFarmHub();
const farmUuid = farmHub?.farm_uuid;
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null); const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null);
const [isClientReady, setIsClientReady] = useState(false); const [isClientReady, setIsClientReady] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -71,8 +72,10 @@ export default function CropZoningWrapper() {
const loadArea = async () => { const loadArea = async () => {
if (!isClientReady) return; if (!isClientReady) return;
if (!sensorHub?.id) { if (!farmUuid) {
setError(t("errors.noSensor")); setAreaGeoJson(null);
setZonesData(null);
setError(t("errors.noFarm"));
setLoading(false); setLoading(false);
return; return;
} }
@@ -92,7 +95,7 @@ export default function CropZoningWrapper() {
completedTaskMessage?: string, completedTaskMessage?: string,
) => { ) => {
console.log("[crop-zoning][wrapper][first-page]", { console.log("[crop-zoning][wrapper][first-page]", {
sensorUuid: sensorHub.id, farmUuid,
pagination: firstResponse.pagination ?? null, pagination: firstResponse.pagination ?? null,
zonesCount: firstResponse.zones?.length ?? 0, zonesCount: firstResponse.zones?.length ?? 0,
taskStatus: firstResponse.task?.status ?? null, taskStatus: firstResponse.task?.status ?? null,
@@ -115,13 +118,13 @@ export default function CropZoningWrapper() {
if (cancelled) break; if (cancelled) break;
console.log("[crop-zoning][wrapper][fetch-page]", { console.log("[crop-zoning][wrapper][fetch-page]", {
sensorUuid: sensorHub.id, farmUuid,
page, page,
pageSize, pageSize,
totalPages, totalPages,
}); });
const pageRes = await cropZoningService.getArea(sensorHub.id, { const pageRes = await cropZoningService.getArea(farmUuid, {
page, page,
pageSize, pageSize,
}); });
@@ -131,7 +134,7 @@ export default function CropZoningWrapper() {
} }
console.log("[crop-zoning][wrapper][page-response]", { console.log("[crop-zoning][wrapper][page-response]", {
sensorUuid: sensorHub.id, farmUuid,
page, page,
pagination: pageRes.pagination ?? null, pagination: pageRes.pagination ?? null,
zonesCount: pageRes.zones?.length ?? 0, zonesCount: pageRes.zones?.length ?? 0,
@@ -163,12 +166,12 @@ export default function CropZoningWrapper() {
while (!cancelled && polls < MAX_POLLS) { while (!cancelled && polls < MAX_POLLS) {
console.log("[crop-zoning][wrapper][poll]", { console.log("[crop-zoning][wrapper][poll]", {
sensorUuid: sensorHub.id, farmUuid,
pollAttempt: polls + 1, pollAttempt: polls + 1,
pageSize: ZONES_PAGE_SIZE, pageSize: ZONES_PAGE_SIZE,
}); });
const res = await cropZoningService.getArea(sensorHub.id, { const res = await cropZoningService.getArea(farmUuid, {
page: 1, page: 1,
pageSize: ZONES_PAGE_SIZE, pageSize: ZONES_PAGE_SIZE,
}); });
@@ -179,7 +182,7 @@ export default function CropZoningWrapper() {
const taskStatus = getNormalizedTaskStatus(task?.status); const taskStatus = getNormalizedTaskStatus(task?.status);
console.log("[crop-zoning][wrapper][poll-response]", { console.log("[crop-zoning][wrapper][poll-response]", {
sensorUuid: sensorHub.id, farmUuid,
pollAttempt: polls + 1, pollAttempt: polls + 1,
taskStatus: task?.status ?? null, taskStatus: task?.status ?? null,
pagination: res.pagination ?? null, pagination: res.pagination ?? null,
@@ -234,7 +237,7 @@ export default function CropZoningWrapper() {
loadArea(); loadArea();
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [isClientReady, sensorHub, t]); }, [farmUuid, isClientReady, t]);
const mapZonesData = useMemo(() => { const mapZonesData = useMemo(() => {
if (activeLayer === "crops" && zonesData) { if (activeLayer === "crops" && zonesData) {
@@ -17,6 +17,7 @@ import classnames from 'classnames'
// Util Imports // Util Imports
import { commonLayoutClasses } from '@layouts/utils/layoutClasses' import { commonLayoutClasses } from '@layouts/utils/layoutClasses'
import { farmAiAssistantService } from '@/libs/api/services/farmAiAssistantService' import { farmAiAssistantService } from '@/libs/api/services/farmAiAssistantService'
import { useFarmHub } from '@/hooks/useFarmHub'
import type { ConversationSummary, FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes' import type { ConversationSummary, FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes'
import ChatSidebar from './ChatSidebar' import ChatSidebar from './ChatSidebar'
@@ -45,6 +46,8 @@ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
export default function FarmAiAssistantChat() { export default function FarmAiAssistantChat() {
const t = useTranslations('farmAiAssistant') const t = useTranslations('farmAiAssistant')
const theme = useTheme() const theme = useTheme()
const { farmHub } = useFarmHub()
const farmUuid = farmHub?.farm_uuid
const [messages, setMessages] = useState<FarmAIMessage[]>([]) const [messages, setMessages] = useState<FarmAIMessage[]>([])
const [inputValue, setInputValue] = useState('') const [inputValue, setInputValue] = useState('')
const [isContextExpanded, setIsContextExpanded] = useState(true) const [isContextExpanded, setIsContextExpanded] = useState(true)
@@ -78,9 +81,14 @@ export default function FarmAiAssistantChat() {
}) })
const loadConversations = async () => { const loadConversations = async () => {
if (!farmUuid) {
setConversations([])
return
}
setConversationLoading(true) setConversationLoading(true)
try { try {
const data = await farmAiAssistantService.getConversations() const data = await farmAiAssistantService.getConversations(farmUuid)
setConversations(data) setConversations(data)
} catch { } catch {
toast.error(t('errors.conversationLoad')) toast.error(t('errors.conversationLoad'))
@@ -97,8 +105,21 @@ export default function FarmAiAssistantChat() {
// Fetch farm context on mount // Fetch farm context on mount
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
setConversationId(null)
setMessages([])
if (!farmUuid) {
setContextLoading(false)
setConversations([])
return () => {
cancelled = true
}
}
setContextLoading(true)
farmAiAssistantService farmAiAssistantService
.getContext() .getContext(farmUuid)
.then(data => { .then(data => {
if (!cancelled) { if (!cancelled) {
setFarmContext({ setFarmContext({
@@ -121,11 +142,11 @@ export default function FarmAiAssistantChat() {
return () => { return () => {
cancelled = true cancelled = true
} }
}, [t]) }, [farmUuid, t])
useEffect(() => { useEffect(() => {
loadConversations() loadConversations()
}, []) }, [farmUuid])
useEffect(() => { useEffect(() => {
if (sidebarOpen) { if (sidebarOpen) {
@@ -150,6 +171,10 @@ export default function FarmAiAssistantChat() {
const handleSend = async (text?: string) => { const handleSend = async (text?: string) => {
const content = (text || inputValue).trim() const content = (text || inputValue).trim()
if (!content) return if (!content) return
if (!farmUuid) {
toast.error(t('errors.noFarm'))
return
}
setInputValue('') setInputValue('')
setSelectedChip(null) setSelectedChip(null)
@@ -165,6 +190,7 @@ export default function FarmAiAssistantChat() {
try { try {
const task = await farmAiAssistantService.createChatTask({ const task = await farmAiAssistantService.createChatTask({
farm_uuid: farmUuid,
content, content,
title: !conversationId ? content.slice(0, 60) : undefined, title: !conversationId ? content.slice(0, 60) : undefined,
farm_context: farmContext, farm_context: farmContext,
@@ -176,7 +202,7 @@ export default function FarmAiAssistantChat() {
} }
let attempts = 0 let attempts = 0
let taskStatus = await farmAiAssistantService.getChatTaskStatus(task.task_id) let taskStatus = await farmAiAssistantService.getChatTaskStatus(task.task_id, farmUuid)
while (taskStatus.status === 'PENDING' || taskStatus.status === 'STARTED') { while (taskStatus.status === 'PENDING' || taskStatus.status === 'STARTED') {
attempts += 1 attempts += 1
@@ -186,7 +212,7 @@ export default function FarmAiAssistantChat() {
} }
await sleep(1500) await sleep(1500)
taskStatus = await farmAiAssistantService.getChatTaskStatus(task.task_id) taskStatus = await farmAiAssistantService.getChatTaskStatus(task.task_id, farmUuid)
} }
if (taskStatus.status === 'FAILURE' || !taskStatus.result) { if (taskStatus.status === 'FAILURE' || !taskStatus.result) {
@@ -205,6 +231,11 @@ export default function FarmAiAssistantChat() {
} }
const handleNewChat = async () => { const handleNewChat = async () => {
if (!farmUuid) {
toast.error(t('errors.noFarm'))
return
}
setConversationId(null) setConversationId(null)
setMessages([]) setMessages([])
setSelectedChip(null) setSelectedChip(null)
@@ -214,6 +245,7 @@ export default function FarmAiAssistantChat() {
try { try {
const conversation = await farmAiAssistantService.createConversation({ const conversation = await farmAiAssistantService.createConversation({
farm_uuid: farmUuid,
title: t('sidebar.newChat'), title: t('sidebar.newChat'),
farm_context: farmContext farm_context: farmContext
}) })
@@ -227,13 +259,18 @@ export default function FarmAiAssistantChat() {
} }
const handleSelectConversation = async (id: string) => { const handleSelectConversation = async (id: string) => {
if (!farmUuid) {
toast.error(t('errors.noFarm'))
return
}
setConversationId(id) setConversationId(id)
setMessages([]) setMessages([])
setIsTyping(true) setIsTyping(true)
setSidebarOpen(false) setSidebarOpen(false)
try { try {
const data = await farmAiAssistantService.getConversationMessages(id) const data = await farmAiAssistantService.getConversationMessages(id, farmUuid)
setMessages(data.messages.map(mapApiMessageToUi)) setMessages(data.messages.map(mapApiMessageToUi))
} catch { } catch {
@@ -37,6 +37,7 @@ export interface FarmAIMessage {
export interface ConversationSummary { export interface ConversationSummary {
id: string id: string
farm_uuid?: string
message_count: number message_count: number
title?: string title?: string
updated_at?: string updated_at?: string
@@ -10,6 +10,7 @@ import Button from "@mui/material/Button";
import Collapse from "@mui/material/Collapse"; import Collapse from "@mui/material/Collapse";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import { useTheme, alpha } from "@mui/material/styles"; import { useTheme, alpha } from "@mui/material/styles";
import { useFarmHub } from "@/hooks/useFarmHub";
import type { import type {
FarmData, FarmData,
GrowthStage, GrowthStage,
@@ -54,6 +55,8 @@ const getErrorMessage = (error: unknown, fallback: string) =>
export default function SmartFertilizationRecommendation() { export default function SmartFertilizationRecommendation() {
const t = useTranslations("fertilization"); const t = useTranslations("fertilization");
const theme = useTheme(); const theme = useTheme();
const { farmHub } = useFarmHub();
const farmUuid = farmHub?.farm_uuid;
const primaryMain = theme.palette.primary.main; const primaryMain = theme.palette.primary.main;
const primaryLight = theme.palette.primary.light; const primaryLight = theme.palette.primary.light;
const primaryDark = theme.palette.primary.dark; const primaryDark = theme.palette.primary.dark;
@@ -77,8 +80,19 @@ export default function SmartFertilizationRecommendation() {
const [reasoningExpanded, setReasoningExpanded] = useState(false); const [reasoningExpanded, setReasoningExpanded] = useState(false);
useEffect(() => { useEffect(() => {
setPlan(null);
setRequestError(null);
if (!farmUuid) {
setConfigError(t("errors.noFarm"));
setConfigLoading(false);
return;
}
setConfigLoading(true);
setConfigError(null);
fertilizationRecommendationService fertilizationRecommendationService
.getConfig() .getConfig(farmUuid)
.then(({ farmData: farm, growthStages: stages, cropOptions: crops }) => { .then(({ farmData: farm, growthStages: stages, cropOptions: crops }) => {
if (farm) setFarmData(farm); if (farm) setFarmData(farm);
if (stages?.length) { if (stages?.length) {
@@ -91,10 +105,10 @@ export default function SmartFertilizationRecommendation() {
setConfigError(err?.message ?? "Failed to load config"); setConfigError(err?.message ?? "Failed to load config");
}) })
.finally(() => setConfigLoading(false)); .finally(() => setConfigLoading(false));
}, []); }, [farmUuid, t]);
const handleGenerate = async () => { const handleGenerate = async () => {
if (!selectedCrop) return; if (!selectedCrop || !farmUuid) return;
setLoading(true); setLoading(true);
setPlan(null); setPlan(null);
setRequestError(null); setRequestError(null);
@@ -103,6 +117,7 @@ export default function SmartFertilizationRecommendation() {
try { try {
const recommendation = await fertilizationRecommendationService.recommend( const recommendation = await fertilizationRecommendationService.recommend(
{ {
farm_uuid: farmUuid,
crop_id: selectedCrop, crop_id: selectedCrop,
growth_stage: growthStage, growth_stage: growthStage,
farm_data: { farm_data: {
@@ -121,6 +136,7 @@ export default function SmartFertilizationRecommendation() {
let taskStatus = let taskStatus =
await fertilizationRecommendationService.getRecommendStatus( await fertilizationRecommendationService.getRecommendStatus(
recommendation.task_id, recommendation.task_id,
farmUuid,
); );
while (isRecommendationTaskRunning(taskStatus.status)) { while (isRecommendationTaskRunning(taskStatus.status)) {
@@ -135,6 +151,7 @@ export default function SmartFertilizationRecommendation() {
taskStatus = taskStatus =
await fertilizationRecommendationService.getRecommendStatus( await fertilizationRecommendationService.getRecommendStatus(
recommendation.task_id, recommendation.task_id,
farmUuid,
); );
} }
@@ -10,6 +10,7 @@ import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import { useTheme, alpha } from "@mui/material/styles"; import { useTheme, alpha } from "@mui/material/styles";
import { useFarmHub } from "@/hooks/useFarmHub";
import type { import type {
FarmInfo, FarmInfo,
CropOption, CropOption,
@@ -37,6 +38,8 @@ const getErrorMessage = (error: unknown, fallback: string) =>
export default function SmartIrrigationRecommendation() { export default function SmartIrrigationRecommendation() {
const t = useTranslations("irrigation"); const t = useTranslations("irrigation");
const theme = useTheme(); const theme = useTheme();
const { farmHub } = useFarmHub();
const farmUuid = farmHub?.farm_uuid;
const [farmInfo, setFarmInfo] = useState<FarmInfo>(DEFAULT_FARM_INFO); const [farmInfo, setFarmInfo] = useState<FarmInfo>(DEFAULT_FARM_INFO);
const [cropOptions, setCropOptions] = useState<CropOption[]>([]); const [cropOptions, setCropOptions] = useState<CropOption[]>([]);
const [configLoading, setConfigLoading] = useState(true); const [configLoading, setConfigLoading] = useState(true);
@@ -53,8 +56,20 @@ export default function SmartIrrigationRecommendation() {
const paperBg = theme.palette.background.paper; const paperBg = theme.palette.background.paper;
useEffect(() => { useEffect(() => {
setPlan(null);
setWaterBalance(null);
setRequestError(null);
if (!farmUuid) {
setConfigError(t("errors.noFarm"));
setConfigLoading(false);
return;
}
setConfigLoading(true);
setConfigError(null);
irrigationRecommendationService irrigationRecommendationService
.getConfig() .getConfig(farmUuid)
.then(({ farmInfo: info, cropOptions: crops }) => { .then(({ farmInfo: info, cropOptions: crops }) => {
setFarmInfo(info); setFarmInfo(info);
setCropOptions(crops.length > 0 ? crops : []); setCropOptions(crops.length > 0 ? crops : []);
@@ -63,10 +78,10 @@ export default function SmartIrrigationRecommendation() {
setConfigError(err?.message ?? "Failed to load config"); setConfigError(err?.message ?? "Failed to load config");
}) })
.finally(() => setConfigLoading(false)); .finally(() => setConfigLoading(false));
}, []); }, [farmUuid, t]);
const handleGenerate = async () => { const handleGenerate = async () => {
if (!selectedCrop) return; if (!selectedCrop || !farmUuid) return;
setLoading(true); setLoading(true);
setPlan(null); setPlan(null);
setWaterBalance(null); setWaterBalance(null);
@@ -74,6 +89,7 @@ export default function SmartIrrigationRecommendation() {
setStatusMessage(t("generating")); setStatusMessage(t("generating"));
try { try {
const recommendation = await irrigationRecommendationService.recommend({ const recommendation = await irrigationRecommendationService.recommend({
farm_uuid: farmUuid,
crop_id: selectedCrop, crop_id: selectedCrop,
farm_data: { farm_data: {
soilType: farmInfo.soilType, soilType: farmInfo.soilType,
@@ -90,6 +106,7 @@ export default function SmartIrrigationRecommendation() {
let taskStatus = let taskStatus =
await irrigationRecommendationService.getRecommendStatus( await irrigationRecommendationService.getRecommendStatus(
recommendation.task_id, recommendation.task_id,
farmUuid,
); );
while (isRecommendationTaskRunning(taskStatus.status)) { while (isRecommendationTaskRunning(taskStatus.status)) {
@@ -103,6 +120,7 @@ export default function SmartIrrigationRecommendation() {
await sleep(1500); await sleep(1500);
taskStatus = await irrigationRecommendationService.getRecommendStatus( taskStatus = await irrigationRecommendationService.getRecommendStatus(
recommendation.task_id, recommendation.task_id,
farmUuid,
); );
} }
@@ -13,13 +13,13 @@ import CardContent from '@mui/material/CardContent'
import Fade from '@mui/material/Fade' import Fade from '@mui/material/Fade'
// Hook Imports // Hook Imports
import { useSensorHub } from '@/hooks/useSensorHub' import { useFarmHub } from '@/hooks/useFarmHub'
// API Imports // API Imports
import type { Sensor } from '@/libs/api/services/sensorHubService' import { farmHubService } from '@/libs/api/services/farmHubService'
import type { Farm } from '@/libs/api/services/farmHubService'
// Component Imports // Component Imports
import SensorHubTable from '@views/sensorHub/SensorHubTable'
import OptionSensorHub from '@views/sensorHub/OptionSensorHub' import OptionSensorHub from '@views/sensorHub/OptionSensorHub'
import FormSensorHub from '@views/sensorHub/FormSensorHub' import FormSensorHub from '@views/sensorHub/FormSensorHub'
@@ -28,12 +28,13 @@ const transitionTimeout = { enter: 300, exit: 200 }
const SensorHubTabContent = () => { const SensorHubTabContent = () => {
const t = useTranslations('sensorHub') const t = useTranslations('sensorHub')
const [showAddForm, setShowAddForm] = useState(false) const [showAddForm, setShowAddForm] = useState(false)
const { setSensorHub } = useSensorHub() const { setFarmHub } = useFarmHub()
const handleBack = () => setShowAddForm(false) const handleBack = () => setShowAddForm(false)
const handleConfirm = (sensor: Sensor) => { const handleConfirm = (farm: Farm) => {
setSensorHub({ id: sensor.uuid_sensor, ...sensor }) setFarmHub({ id: farm.farm_uuid, ...farm })
farmHubService.activateFarm(farm.farm_uuid).catch(() => {})
} }
return ( return (
+18 -9
View File
@@ -13,7 +13,7 @@ import Alert from '@mui/material/Alert'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
// API Imports // API Imports
import { sensorHubService } from '@/libs/api' import { farmHubService } from '@/libs/api'
// Component Imports // Component Imports
import CustomTextField from '@core/components/mui/TextField' import CustomTextField from '@core/components/mui/TextField'
@@ -51,7 +51,7 @@ const PLANT_NAMES_BY_TYPE: Record<string, string[]> = {
const FormSensorHub = ({ onBack }: FormSensorHubProps) => { const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
const t = useTranslations('sensorHub') const t = useTranslations('sensorHub')
const [name, setName] = useState('') const [name, setName] = useState('')
const [uuidSensor, setUuidSensor] = useState('') const [stationName, setStationName] = useState('')
const [plantType, setPlantType] = useState<string | null>(null) const [plantType, setPlantType] = useState<string | null>(null)
const [plantName, setPlantName] = useState<string | null>(null) const [plantName, setPlantName] = useState<string | null>(null)
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null) const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(null)
@@ -68,11 +68,20 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
e.preventDefault() e.preventDefault()
setError(null) setError(null)
setLoading(true) setLoading(true)
console.log(areaGeoJson)
try { try {
await sensorHubService.addSensor({ await farmHubService.createFarm({
name, name,
uuid_sensor: uuidSensor, ...(stationName
? {
sensors: [
{
name: stationName,
sensor_type: 'weather_station',
is_active: true
}
]
}
: {}),
...(areaGeoJson && { area_geojson: areaGeoJson }), ...(areaGeoJson && { area_geojson: areaGeoJson }),
...(areaM2 !== undefined && { area_m2: areaM2 }) ...(areaM2 !== undefined && { area_m2: areaM2 })
}) })
@@ -120,10 +129,10 @@ const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<CustomTextField <CustomTextField
fullWidth fullWidth
label={t('sensorUuid')} label={t('stationName')}
placeholder={t('placeholderUuid')} placeholder={t('placeholderStationName')}
value={uuidSensor} value={stationName}
onChange={e => setUuidSensor(e.target.value)} onChange={e => setStationName(e.target.value)}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
+24 -17
View File
@@ -16,14 +16,15 @@ import DateObject from 'react-date-object'
import type { CustomInputHorizontalData } from '@core/components/custom-inputs/types' import type { CustomInputHorizontalData } from '@core/components/custom-inputs/types'
// API Imports // API Imports
import { sensorHubService } from '@/libs/api' import { farmHubService } from '@/libs/api'
import type { Sensor } from '@/libs/api/services/sensorHubService' import { getStoredFarmHub } from '@/hooks/useFarmHub'
import type { Farm } from '@/libs/api/services/farmHubService'
// Component Imports // Component Imports
import CustomInputHorizontal from '@core/components/custom-inputs/Horizontal' import CustomInputHorizontal from '@core/components/custom-inputs/Horizontal'
type OptionSensorHubProps = { type OptionSensorHubProps = {
onConfirm?: (sensor: Sensor) => void onConfirm?: (farm: Farm) => void
} }
const formatLastUpdated = (dateStr: string | null | undefined): string => { const formatLastUpdated = (dateStr: string | null | undefined): string => {
@@ -37,27 +38,33 @@ const formatLastUpdated = (dateStr: string | null | undefined): string => {
} }
} }
const sensorToOption = (sensor: Sensor, isFirst: boolean): CustomInputHorizontalData => ({ const farmToOption = (farm: Farm, isSelected: boolean): CustomInputHorizontalData => ({
title: sensor.name, title: farm.name,
meta: formatLastUpdated(sensor.last_updated), meta: formatLastUpdated(farm.last_updated),
content: sensor.uuid_sensor, content: farm.farm_uuid,
value: sensor.uuid_sensor, value: farm.farm_uuid,
isSelected: isFirst isSelected
}) })
const OptionSensorHub = ({ onConfirm }: OptionSensorHubProps) => { const OptionSensorHub = ({ onConfirm }: OptionSensorHubProps) => {
const [sensors, setSensors] = useState<Sensor[]>([]) const [farms, setFarms] = useState<Farm[]>([])
const [data, setData] = useState<CustomInputHorizontalData[]>([]) const [data, setData] = useState<CustomInputHorizontalData[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [selectedOption, setSelectedOption] = useState<string>('') const [selectedOption, setSelectedOption] = useState<string>('')
useEffect(() => { useEffect(() => {
const fetchSensors = async () => { const fetchFarms = async () => {
try { try {
setLoading(true) setLoading(true)
const sensorsList = await sensorHubService.listSensors() const farmsList = await farmHubService.listFarms()
setSensors(sensorsList) const storedFarmUuid = getStoredFarmHub()?.farm_uuid
const options = sensorsList.map((s, i) => sensorToOption(s, i === 0)) const selectedFarmUuid =
farmsList.find(farm => farm.farm_uuid === storedFarmUuid)?.farm_uuid ??
farmsList.find(farm => farm.is_active)?.farm_uuid ??
farmsList[0]?.farm_uuid
setFarms(farmsList)
const options = farmsList.map(farm => farmToOption(farm, farm.farm_uuid === selectedFarmUuid))
setData(options) setData(options)
if (options.length > 0) { if (options.length > 0) {
const selected = options.find(o => o.isSelected) ?? options[0] const selected = options.find(o => o.isSelected) ?? options[0]
@@ -65,14 +72,14 @@ const OptionSensorHub = ({ onConfirm }: OptionSensorHubProps) => {
setSelectedOption(selected.value) setSelectedOption(selected.value)
} }
} catch { } catch {
setSensors([]) setFarms([])
setData([]) setData([])
} finally { } finally {
setLoading(false) setLoading(false)
} }
} }
fetchSensors() fetchFarms()
}, []) }, [])
const handleOptionChange = (prop: string | ChangeEvent<HTMLInputElement>) => { const handleOptionChange = (prop: string | ChangeEvent<HTMLInputElement>) => {
@@ -96,7 +103,7 @@ const OptionSensorHub = ({ onConfirm }: OptionSensorHubProps) => {
} }
const handleConfirm = () => { const handleConfirm = () => {
const selected = sensors.find(s => s.uuid_sensor === selectedOption) const selected = farms.find(farm => farm.farm_uuid === selectedOption)
if (selected && onConfirm) { if (selected && onConfirm) {
onConfirm(selected) onConfirm(selected)
} }
+9 -9
View File
@@ -14,8 +14,8 @@ import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '
import DateObject from 'react-date-object' import DateObject from 'react-date-object'
// API Imports // API Imports
import { sensorHubService } from '@/libs/api' import { farmHubService } from '@/libs/api'
import type { Sensor } from '@/libs/api/services/sensorHubService' import type { Farm } from '@/libs/api/services/farmHubService'
// Style Imports // Style Imports
import styles from '@core/styles/table.module.css' import styles from '@core/styles/table.module.css'
@@ -32,7 +32,7 @@ const formatToShamsi = (dateStr: string | null | undefined): string => {
} }
// Column Definitions // Column Definitions
const columnHelper = createColumnHelper<Sensor>() const columnHelper = createColumnHelper<Farm>()
const SensorHubTable = () => { const SensorHubTable = () => {
const t = useTranslations('sensorHub') const t = useTranslations('sensorHub')
@@ -47,24 +47,24 @@ const SensorHubTable = () => {
cell: info => formatToShamsi(info.getValue()), cell: info => formatToShamsi(info.getValue()),
header: t('columns.lastUpdate') header: t('columns.lastUpdate')
}), }),
columnHelper.accessor('uuid_sensor', { columnHelper.accessor('farm_uuid', {
cell: info => info.getValue(), cell: info => info.getValue(),
header: t('columns.uuid') header: t('columns.uuid')
}) })
], ],
[t] [t]
) )
const [data, setData] = useState<Sensor[]>([]) const [data, setData] = useState<Farm[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
const fetchSensors = async () => { const fetchFarms = async () => {
try { try {
setLoading(true) setLoading(true)
setError(null) setError(null)
const sensors = await sensorHubService.listSensors() const farms = await farmHubService.listFarms()
setData(sensors) setData(farms)
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : t('errorLoad')) setError(err instanceof Error ? err.message : t('errorLoad'))
setData([]) setData([])
@@ -73,7 +73,7 @@ const SensorHubTable = () => {
} }
} }
fetchSensors() fetchFarms()
}, []) }, [])
const table = useReactTable({ const table = useReactTable({
+30 -29
View File
@@ -6,13 +6,13 @@ import type { Theme } from '@mui/material/styles'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
// Hook Imports // Hook Imports
import { useSensorHub } from '@/hooks/useSensorHub' import { useFarmHub } from '@/hooks/useFarmHub'
// API Imports // API Imports
import type { Sensor } from '@/libs/api/services/sensorHubService' import { farmHubService } from '@/libs/api/services/farmHubService'
import type { Farm } from '@/libs/api/services/farmHubService'
import Dialog from '@mui/material/Dialog' import Dialog from '@mui/material/Dialog'
import DialogContent from '@mui/material/DialogContent' import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle'
import Drawer from '@mui/material/Drawer' import Drawer from '@mui/material/Drawer'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import IconButton from '@mui/material/IconButton' import IconButton from '@mui/material/IconButton'
@@ -37,17 +37,17 @@ const DialogContentWithTransition = ({
onShowAddForm, onShowAddForm,
onBack, onBack,
onConfirm, onConfirm,
selectSensor, selectFarm,
selectSensorDescription, selectFarmDescription,
addSensor addFarm
}: { }: {
showAddForm: boolean showAddForm: boolean
onShowAddForm: () => void onShowAddForm: () => void
onBack: () => void onBack: () => void
onConfirm: (sensor: Sensor) => void onConfirm: (farm: Farm) => void
selectSensor: string selectFarm: string
selectSensorDescription: string selectFarmDescription: string
addSensor: string addFarm: string
}) => ( }) => (
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}> <Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
<div> <div>
@@ -58,10 +58,10 @@ const DialogContentWithTransition = ({
<div className='grid grid-cols-[1fr_auto] items-center gap-4'> <div className='grid grid-cols-[1fr_auto] items-center gap-4'>
<div className='flex flex-col gap-0.5'> <div className='flex flex-col gap-0.5'>
<Typography variant='h6' fontWeight={600}> <Typography variant='h6' fontWeight={600}>
{selectSensor} {selectFarm}
</Typography> </Typography>
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}> <Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
{selectSensorDescription} {selectFarmDescription}
</Typography> </Typography>
</div> </div>
<Button <Button
@@ -70,7 +70,7 @@ const DialogContentWithTransition = ({
startIcon={<i className='tabler-plus text-xl' />} startIcon={<i className='tabler-plus text-xl' />}
onClick={onShowAddForm} onClick={onShowAddForm}
> >
{addSensor} {addFarm}
</Button> </Button>
</div> </div>
<OptionSensorHub onConfirm={onConfirm} /> <OptionSensorHub onConfirm={onConfirm} />
@@ -85,17 +85,17 @@ const DrawerContentWithTransition = ({
onShowAddForm, onShowAddForm,
onBack, onBack,
onConfirm, onConfirm,
selectSensor, selectFarm,
selectSensorDescription, selectFarmDescription,
addSensor addFarm
}: { }: {
showAddForm: boolean showAddForm: boolean
onShowAddForm: () => void onShowAddForm: () => void
onBack: () => void onBack: () => void
onConfirm: (sensor: Sensor) => void onConfirm: (farm: Farm) => void
selectSensor: string selectFarm: string
selectSensorDescription: string selectFarmDescription: string
addSensor: string addFarm: string
}) => ( }) => (
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}> <Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
<div> <div>
@@ -106,10 +106,10 @@ const DrawerContentWithTransition = ({
<div className='grid grid-cols-[1fr_auto] items-center gap-4'> <div className='grid grid-cols-[1fr_auto] items-center gap-4'>
<div className='flex flex-col gap-0.5'> <div className='flex flex-col gap-0.5'>
<Typography variant='h6' fontWeight={600}> <Typography variant='h6' fontWeight={600}>
{selectSensor} {selectFarm}
</Typography> </Typography>
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}> <Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
{selectSensorDescription} {selectFarmDescription}
</Typography> </Typography>
</div> </div>
<Button <Button
@@ -118,7 +118,7 @@ const DrawerContentWithTransition = ({
startIcon={<i className='tabler-plus text-xl' />} startIcon={<i className='tabler-plus text-xl' />}
onClick={onShowAddForm} onClick={onShowAddForm}
> >
{addSensor} {addFarm}
</Button> </Button>
</div> </div>
<OptionSensorHub onConfirm={onConfirm} /> <OptionSensorHub onConfirm={onConfirm} />
@@ -132,18 +132,19 @@ const TableModalSheet = ({ open, onClose }: TableModalSheetProps) => {
const t = useTranslations('sensorHub') const t = useTranslations('sensorHub')
const [showAddForm, setShowAddForm] = useState(false) const [showAddForm, setShowAddForm] = useState(false)
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')) const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
const { setSensorHub } = useSensorHub() const { setFarmHub } = useFarmHub()
const contentProps = { const contentProps = {
selectSensor: t('selectSensor'), selectFarm: t('selectSensor'),
selectSensorDescription: t('selectSensorDescription'), selectFarmDescription: t('selectSensorDescription'),
addSensor: t('addSensor') addFarm: t('addSensor')
} }
const handleBack = () => setShowAddForm(false) const handleBack = () => setShowAddForm(false)
const handleConfirm = (sensor: Sensor) => { const handleConfirm = (farm: Farm) => {
setSensorHub({ id: sensor.uuid_sensor, ...sensor }) setFarmHub({ id: farm.farm_uuid, ...farm })
farmHubService.activateFarm(farm.farm_uuid).catch(() => {})
onClose() onClose()
} }