UPDATE
This commit is contained in:
+21
-14
@@ -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": "بارگذاری لیست مکالمات ناموفق بود.",
|
||||
|
||||
@@ -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 && <SensorHubView />}</>
|
||||
{!hasFarmHub && <SensorHubView />}</>
|
||||
}
|
||||
|
||||
@@ -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
@@ -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<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
|
||||
}
|
||||
}
|
||||
export type {
|
||||
FarmHubInfo as SensorHubInfo,
|
||||
UseFarmHubReturn as UseSensorHubReturn,
|
||||
} from "./useFarmHub";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<CropZoningAreaResponse> {
|
||||
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;
|
||||
|
||||
@@ -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<FarmContext>
|
||||
}
|
||||
|
||||
export interface CreateConversationResponse {
|
||||
id: string
|
||||
farm_uuid?: string
|
||||
message_count: number
|
||||
title?: string
|
||||
updated_at?: string
|
||||
@@ -87,33 +95,57 @@ function unwrap<T>(res: ApiResponse<T>): T {
|
||||
}
|
||||
|
||||
export const farmAiAssistantService = {
|
||||
getContext(): Promise<FarmContextResponse> {
|
||||
return apiClient.get<ApiResponse<FarmContextResponse>>(`${PREFIX}/context/`).then(unwrap)
|
||||
getContext(farmUuid: string): Promise<FarmContextResponse> {
|
||||
return apiClient
|
||||
.get<ApiResponse<FarmContextResponse>>(
|
||||
`${PREFIX}/context/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
)
|
||||
.then(unwrap)
|
||||
},
|
||||
|
||||
createChatTask(payload: ChatPayload): Promise<ChatTaskInitResponse> {
|
||||
return apiClient.post<ApiResponse<ChatTaskInitResponse>>(`${PREFIX}/chat/task/`, payload).then(unwrap)
|
||||
},
|
||||
|
||||
getChatTaskStatus(taskId: string): Promise<ChatTaskStatusResponse> {
|
||||
return apiClient.get<ApiResponse<ChatTaskStatusResponse>>(`${PREFIX}/chat/task/${taskId}/status/`).then(unwrap)
|
||||
getChatTaskStatus(taskId: string, farmUuid: string): Promise<ChatTaskStatusResponse> {
|
||||
return apiClient
|
||||
.get<ApiResponse<ChatTaskStatusResponse>>(
|
||||
`${PREFIX}/chat/task/${taskId}/status/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
)
|
||||
.then(unwrap)
|
||||
},
|
||||
|
||||
getConversations(): Promise<ConversationSummary[]> {
|
||||
return apiClient.get<ApiResponse<ConversationSummary[]>>(`${PREFIX}/chats/`).then(unwrap)
|
||||
getConversations(farmUuid: string): Promise<ConversationSummary[]> {
|
||||
return apiClient
|
||||
.get<ApiResponse<ConversationSummary[]>>(
|
||||
`${PREFIX}/chats/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
)
|
||||
.then(unwrap)
|
||||
},
|
||||
|
||||
createConversation(payload?: CreateConversationPayload): Promise<CreateConversationResponse> {
|
||||
return apiClient.post<ApiResponse<CreateConversationResponse>>(`${PREFIX}/chats/`, payload).then(unwrap)
|
||||
},
|
||||
|
||||
deleteConversation(conversationId: string): Promise<{ conversation_id: string }> {
|
||||
return apiClient.delete<ApiResponse<{ conversation_id: string }>>(`${PREFIX}/chats/${conversationId}/`).then(unwrap)
|
||||
deleteConversation(
|
||||
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
|
||||
.get<ApiResponse<ConversationMessagesResponse>>(`${PREFIX}/chats/${conversationId}/messages/`)
|
||||
.get<ApiResponse<ConversationMessagesResponse>>(
|
||||
`${PREFIX}/chats/${conversationId}/messages/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
)
|
||||
.then(unwrap)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ApiResponse<T> {
|
||||
}
|
||||
|
||||
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<FarmDashboardConfig> {
|
||||
async getConfig(farmUuid: string): Promise<FarmDashboardConfig> {
|
||||
try {
|
||||
const response = await apiClient.get<
|
||||
ApiResponse<FarmDashboardConfigResponse> | 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<FarmDashboardConfig>,
|
||||
): Promise<FarmDashboardConfig> {
|
||||
try {
|
||||
const response = await apiClient.patch<
|
||||
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;
|
||||
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<Record<CardId, Record<string, unknown>>>
|
||||
> {
|
||||
try {
|
||||
@@ -243,7 +258,7 @@ export const farmDashboardService = {
|
||||
| ApiResponse<FarmDashboardCardsTaskData>
|
||||
| FarmDashboardCardsResponse
|
||||
| FarmDashboardCardsTaskData
|
||||
>("/api/farm-dashboard/");
|
||||
>(`/api/farm-dashboard/?${buildFarmQuery(farmUuid)}`);
|
||||
return extractCardsPayload(response);
|
||||
} catch {
|
||||
return {};
|
||||
|
||||
@@ -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 {
|
||||
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<FarmData>;
|
||||
@@ -92,10 +94,10 @@ function normalizeRecommendationResult(
|
||||
}
|
||||
|
||||
export const fertilizationRecommendationService = {
|
||||
getConfig(): Promise<FertilizationConfigResponse> {
|
||||
getConfig(farmUuid: string): Promise<FertilizationConfigResponse> {
|
||||
return unwrap(
|
||||
apiClient.get<ApiResponse<FertilizationConfigResponse>>(
|
||||
`${PREFIX}/config/`,
|
||||
`${PREFIX}/config/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -117,6 +119,7 @@ export const fertilizationRecommendationService = {
|
||||
|
||||
getRecommendStatus(
|
||||
taskId: string,
|
||||
farmUuid: string,
|
||||
): Promise<
|
||||
RecommendationTaskStatusResponse<FertilizationRecommendationResult>
|
||||
> {
|
||||
@@ -125,7 +128,9 @@ export const fertilizationRecommendationService = {
|
||||
ApiResponse<
|
||||
RecommendationTaskStatusResponse<FertilizationRecommendationResult>
|
||||
>
|
||||
>(`${PREFIX}/recommend/status/${taskId}/`),
|
||||
>(
|
||||
`${PREFIX}/recommend/status/${taskId}/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
),
|
||||
).then((response) => ({
|
||||
...response,
|
||||
status: normalizeRecommendationTaskStatus(response.status),
|
||||
|
||||
@@ -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<FarmInfo>;
|
||||
soilType?: string;
|
||||
@@ -108,9 +110,11 @@ function normalizeRecommendationResult(
|
||||
}
|
||||
|
||||
export const irrigationRecommendationService = {
|
||||
getConfig(): Promise<IrrigationConfigResponse> {
|
||||
getConfig(farmUuid: string): Promise<IrrigationConfigResponse> {
|
||||
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(
|
||||
taskId: string,
|
||||
farmUuid: string,
|
||||
): Promise<RecommendationTaskStatusResponse<IrrigationRecommendationResult>> {
|
||||
return unwrap(
|
||||
apiClient.get<
|
||||
ApiResponse<
|
||||
RecommendationTaskStatusResponse<IrrigationRecommendationResult>
|
||||
>
|
||||
>(`${PREFIX}/recommend/status/${taskId}/`),
|
||||
>(
|
||||
`${PREFIX}/recommend/status/${taskId}/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
),
|
||||
).then((response) => ({
|
||||
...response,
|
||||
status: normalizeRecommendationTaskStatus(response.status),
|
||||
|
||||
@@ -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<T> {
|
||||
task_id: string;
|
||||
status: RecommendationTaskStatus;
|
||||
farm_uuid?: string;
|
||||
progress?: RecommendationTaskProgress;
|
||||
result?: T;
|
||||
error?: string;
|
||||
|
||||
@@ -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<string, unknown>
|
||||
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<Sensor> {
|
||||
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<Sensor> {
|
||||
return farmHubService.createFarm(payload);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of sensors
|
||||
*/
|
||||
async listSensors(): Promise<Sensor[]> {
|
||||
const response = await apiClient.get<ListSensorsResponse>('/api/sensor-hub/')
|
||||
const data = response?.data
|
||||
listSensors(): Promise<Sensor[]> {
|
||||
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";
|
||||
|
||||
@@ -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<FarmDashboardConfig>(
|
||||
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],
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -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<Record<CardId, React.ComponentType<{ data?: Recor
|
||||
}
|
||||
|
||||
const SoilDataDashboardWrapper = () => {
|
||||
const { farmHub } = useFarmHub()
|
||||
const farmUuid = farmHub?.farm_uuid
|
||||
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
|
||||
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 (
|
||||
|
||||
@@ -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<Record<CardId, React.ComponentType<{ data?: Recor
|
||||
}
|
||||
|
||||
const WaterDataDashboardWrapper = () => {
|
||||
const { farmHub } = useFarmHub()
|
||||
const farmUuid = farmHub?.farm_uuid
|
||||
const [cardsData, setCardsData] = useState<Partial<Record<CardId, Record<string, unknown>>>>({})
|
||||
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 (
|
||||
|
||||
@@ -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<Record<string, unknown>>(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: {
|
||||
|
||||
@@ -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<MapDrawGeoJSON>(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) {
|
||||
|
||||
@@ -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<FarmAIMessage[]>([])
|
||||
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 {
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface FarmAIMessage {
|
||||
|
||||
export interface ConversationSummary {
|
||||
id: string
|
||||
farm_uuid?: string
|
||||
message_count: number
|
||||
title?: string
|
||||
updated_at?: string
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FarmInfo>(DEFAULT_FARM_INFO);
|
||||
const [cropOptions, setCropOptions] = useState<CropOption[]>([]);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<string, string[]> = {
|
||||
const FormSensorHub = ({ onBack }: FormSensorHubProps) => {
|
||||
const t = useTranslations('sensorHub')
|
||||
const [name, setName] = useState('')
|
||||
const [uuidSensor, setUuidSensor] = useState('')
|
||||
const [stationName, setStationName] = useState('')
|
||||
const [plantType, setPlantType] = useState<string | null>(null)
|
||||
const [plantName, setPlantName] = useState<string | null>(null)
|
||||
const [areaGeoJson, setAreaGeoJson] = useState<MapDrawGeoJSON>(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) => {
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<CustomTextField
|
||||
fullWidth
|
||||
label={t('sensorUuid')}
|
||||
placeholder={t('placeholderUuid')}
|
||||
value={uuidSensor}
|
||||
onChange={e => setUuidSensor(e.target.value)}
|
||||
label={t('stationName')}
|
||||
placeholder={t('placeholderStationName')}
|
||||
value={stationName}
|
||||
onChange={e => setStationName(e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
|
||||
@@ -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<Sensor[]>([])
|
||||
const [farms, setFarms] = useState<Farm[]>([])
|
||||
const [data, setData] = useState<CustomInputHorizontalData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selectedOption, setSelectedOption] = useState<string>('')
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<Sensor>()
|
||||
const columnHelper = createColumnHelper<Farm>()
|
||||
|
||||
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<Sensor[]>([])
|
||||
const [data, setData] = useState<Farm[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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({
|
||||
|
||||
@@ -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
|
||||
}) => (
|
||||
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
|
||||
<div>
|
||||
@@ -58,10 +58,10 @@ const DialogContentWithTransition = ({
|
||||
<div className='grid grid-cols-[1fr_auto] items-center gap-4'>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<Typography variant='h6' fontWeight={600}>
|
||||
{selectSensor}
|
||||
{selectFarm}
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
|
||||
{selectSensorDescription}
|
||||
{selectFarmDescription}
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
@@ -70,7 +70,7 @@ const DialogContentWithTransition = ({
|
||||
startIcon={<i className='tabler-plus text-xl' />}
|
||||
onClick={onShowAddForm}
|
||||
>
|
||||
{addSensor}
|
||||
{addFarm}
|
||||
</Button>
|
||||
</div>
|
||||
<OptionSensorHub onConfirm={onConfirm} />
|
||||
@@ -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
|
||||
}) => (
|
||||
<Fade key={showAddForm ? 'form' : 'options'} in timeout={transitionTimeout}>
|
||||
<div>
|
||||
@@ -106,10 +106,10 @@ const DrawerContentWithTransition = ({
|
||||
<div className='grid grid-cols-[1fr_auto] items-center gap-4'>
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<Typography variant='h6' fontWeight={600}>
|
||||
{selectSensor}
|
||||
{selectFarm}
|
||||
</Typography>
|
||||
<Typography variant='body2' color='text.secondary' sx={{ lineHeight: 1.5 }}>
|
||||
{selectSensorDescription}
|
||||
{selectFarmDescription}
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
@@ -118,7 +118,7 @@ const DrawerContentWithTransition = ({
|
||||
startIcon={<i className='tabler-plus text-xl' />}
|
||||
onClick={onShowAddForm}
|
||||
>
|
||||
{addSensor}
|
||||
{addFarm}
|
||||
</Button>
|
||||
</div>
|
||||
<OptionSensorHub onConfirm={onConfirm} />
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user