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