UPDATE
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const resolveBackendBaseUrl = (): string => {
|
||||
const publicApiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
const serverApiUrl = process.env.ENVOY_GATEWAY_URL;
|
||||
|
||||
return (serverApiUrl || publicApiUrl || "http://node.crop-logic.ir").replace(/\/$/, "");
|
||||
};
|
||||
|
||||
const resolveSensorExternalApiKey = (request: NextRequest): string | null => {
|
||||
const requestApiKey = request.headers.get("x-api-key");
|
||||
|
||||
if (requestApiKey) {
|
||||
return requestApiKey;
|
||||
}
|
||||
|
||||
return (
|
||||
process.env.SENSOR_EXTERNAL_API_KEY ||
|
||||
process.env.SENSOR_EXTERNAL_API_LOGS_KEY ||
|
||||
process.env.NEXT_PUBLIC_SENSOR_EXTERNAL_API_KEY ||
|
||||
"12345"
|
||||
);
|
||||
};
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const apiKey = resolveSensorExternalApiKey(request);
|
||||
const authorization = request.headers.get("authorization");
|
||||
|
||||
if (!apiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail:
|
||||
"Sensor external API key is not configured on the frontend server.",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const query = request.nextUrl.searchParams.toString();
|
||||
const endpoint = `${resolveBackendBaseUrl()}/api/sensor-external-api/logs/${
|
||||
query ? `?${query}` : ""
|
||||
}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(authorization ? { Authorization: authorization } : {}),
|
||||
"X-API-Key": apiKey,
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
return new NextResponse(body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"content-type": response.headers.get("content-type") || "application/json",
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{
|
||||
detail: "Failed to fetch sensor external logs from backend.",
|
||||
},
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { apiClient } from "../client";
|
||||
|
||||
const SUMMARY_PREFIX = "/api/sensor-7-in-1";
|
||||
const SENSORS_PREFIX = "/api/sensors";
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface SensorSummaryMeta {
|
||||
name: string;
|
||||
physicalDeviceUuid: string | null;
|
||||
sensorCatalogCode: string;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface SensorValuesListItem {
|
||||
id?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
trendNumber: number;
|
||||
trend?: "positive" | "negative";
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface SensorValuesListResponse {
|
||||
sensor?: SensorSummaryMeta | null;
|
||||
sensors: SensorValuesListItem[];
|
||||
}
|
||||
|
||||
export interface SensorChartSeries {
|
||||
name: string;
|
||||
data: number[];
|
||||
}
|
||||
|
||||
export interface SensorComparisonChartResponse {
|
||||
currentValue: number;
|
||||
vsLastWeek: string;
|
||||
vsLastWeekValue?: number;
|
||||
categories: string[];
|
||||
series: SensorChartSeries[];
|
||||
}
|
||||
|
||||
export interface SensorRadarChartResponse {
|
||||
labels: string[];
|
||||
series: SensorChartSeries[];
|
||||
}
|
||||
|
||||
export interface Sensor7SummaryData {
|
||||
sensor?: SensorSummaryMeta | null;
|
||||
sensorValuesList?: SensorValuesListResponse | null;
|
||||
avgSoilMoisture?: Record<string, unknown> | null;
|
||||
sensorRadarChart?: SensorRadarChartResponse | null;
|
||||
sensorComparisonChart?: SensorComparisonChartResponse | null;
|
||||
anomalyDetectionCard?: Record<string, unknown> | null;
|
||||
soilMoistureHeatmap?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
const EMPTY_COMPARISON_CHART: SensorComparisonChartResponse = {
|
||||
currentValue: 0,
|
||||
vsLastWeek: "+0.0%",
|
||||
categories: [],
|
||||
series: [],
|
||||
};
|
||||
|
||||
const EMPTY_RADAR_CHART: SensorRadarChartResponse = {
|
||||
labels: [],
|
||||
series: [],
|
||||
};
|
||||
|
||||
const EMPTY_VALUES_LIST: SensorValuesListResponse = {
|
||||
sensors: [],
|
||||
};
|
||||
|
||||
function extract<T>(response: ApiResponse<T> | T): T {
|
||||
return response && typeof response === "object" && "data" in response
|
||||
? (response as ApiResponse<T>).data
|
||||
: (response as T);
|
||||
}
|
||||
|
||||
function normalizeSeries(series: unknown): SensorChartSeries[] {
|
||||
if (!Array.isArray(series)) return [];
|
||||
|
||||
return series
|
||||
.filter((item): item is { name?: unknown; data?: unknown } => !!item && typeof item === "object")
|
||||
.map((item) => ({
|
||||
name: typeof item.name === "string" ? item.name : "",
|
||||
data: Array.isArray(item.data)
|
||||
? item.data.filter((value): value is number => typeof value === "number" && Number.isFinite(value))
|
||||
: [],
|
||||
}))
|
||||
.filter((item) => item.name || item.data.length > 0);
|
||||
}
|
||||
|
||||
export function normalizeComparisonChartResponse(
|
||||
data?: Partial<SensorComparisonChartResponse> | null,
|
||||
): SensorComparisonChartResponse {
|
||||
if (!data || typeof data !== "object") {
|
||||
return { ...EMPTY_COMPARISON_CHART };
|
||||
}
|
||||
|
||||
return {
|
||||
currentValue:
|
||||
typeof data.currentValue === "number" && Number.isFinite(data.currentValue)
|
||||
? data.currentValue
|
||||
: 0,
|
||||
vsLastWeek: typeof data.vsLastWeek === "string" ? data.vsLastWeek : "+0.0%",
|
||||
vsLastWeekValue:
|
||||
typeof data.vsLastWeekValue === "number" && Number.isFinite(data.vsLastWeekValue)
|
||||
? data.vsLastWeekValue
|
||||
: undefined,
|
||||
categories: Array.isArray(data.categories)
|
||||
? data.categories.filter((value): value is string => typeof value === "string")
|
||||
: [],
|
||||
series: normalizeSeries(data.series),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeRadarChartResponse(
|
||||
data?: Partial<SensorRadarChartResponse> | null,
|
||||
): SensorRadarChartResponse {
|
||||
if (!data || typeof data !== "object") {
|
||||
return { ...EMPTY_RADAR_CHART };
|
||||
}
|
||||
|
||||
return {
|
||||
labels: Array.isArray(data.labels)
|
||||
? data.labels.filter((value): value is string => typeof value === "string")
|
||||
: [],
|
||||
series: normalizeSeries(data.series),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeValuesListResponse(
|
||||
data?: Partial<SensorValuesListResponse> | null,
|
||||
): SensorValuesListResponse {
|
||||
if (!data || typeof data !== "object") {
|
||||
return { ...EMPTY_VALUES_LIST };
|
||||
}
|
||||
|
||||
const sensors = Array.isArray(data.sensors)
|
||||
? data.sensors
|
||||
.filter((item): item is SensorValuesListItem => !!item && typeof item === "object")
|
||||
.map((item) => {
|
||||
const trend: SensorValuesListItem["trend"] =
|
||||
item.trend === "negative"
|
||||
? "negative"
|
||||
: item.trend === "positive"
|
||||
? "positive"
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: typeof item.id === "string" ? item.id : undefined,
|
||||
title: typeof item.title === "string" ? item.title : "-",
|
||||
subtitle: typeof item.subtitle === "string" ? item.subtitle : "-",
|
||||
trendNumber:
|
||||
typeof item.trendNumber === "number" && Number.isFinite(item.trendNumber)
|
||||
? item.trendNumber
|
||||
: 0,
|
||||
trend,
|
||||
unit: typeof item.unit === "string" ? item.unit : "",
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
sensor: data.sensor ?? undefined,
|
||||
sensors,
|
||||
};
|
||||
}
|
||||
|
||||
export const sensor7Service = {
|
||||
async getSummary(farmUuid: string): Promise<Sensor7SummaryData> {
|
||||
const response = await apiClient.get<ApiResponse<Sensor7SummaryData> | Sensor7SummaryData>(
|
||||
`${SUMMARY_PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`,
|
||||
);
|
||||
|
||||
const data = extract(response) ?? {};
|
||||
|
||||
return {
|
||||
...data,
|
||||
sensorValuesList: normalizeValuesListResponse(data.sensorValuesList),
|
||||
sensorComparisonChart: normalizeComparisonChartResponse(data.sensorComparisonChart),
|
||||
sensorRadarChart: normalizeRadarChartResponse(data.sensorRadarChart),
|
||||
};
|
||||
},
|
||||
|
||||
async getComparisonChart(params: {
|
||||
farmUuid: string;
|
||||
range?: "7d" | "30d";
|
||||
}): Promise<SensorComparisonChartResponse> {
|
||||
const searchParams = new URLSearchParams({
|
||||
farm_uuid: params.farmUuid,
|
||||
range: params.range ?? "7d",
|
||||
});
|
||||
|
||||
const response = await apiClient.get<Partial<SensorComparisonChartResponse>>(
|
||||
`${SENSORS_PREFIX}/comparison-chart/?${searchParams.toString()}`,
|
||||
);
|
||||
|
||||
return normalizeComparisonChartResponse(response);
|
||||
},
|
||||
|
||||
async getRadarChart(params: {
|
||||
farmUuid: string;
|
||||
range?: "today" | "7d" | "30d";
|
||||
}): Promise<SensorRadarChartResponse> {
|
||||
const searchParams = new URLSearchParams({
|
||||
farm_uuid: params.farmUuid,
|
||||
range: params.range ?? "7d",
|
||||
});
|
||||
|
||||
const response = await apiClient.get<Partial<SensorRadarChartResponse>>(
|
||||
`${SENSORS_PREFIX}/radar-chart/?${searchParams.toString()}`,
|
||||
);
|
||||
|
||||
return normalizeRadarChartResponse(response);
|
||||
},
|
||||
|
||||
async getValuesList(params: {
|
||||
farmUuid: string;
|
||||
range?: "1h" | "24h" | "7d";
|
||||
}): Promise<SensorValuesListResponse> {
|
||||
const searchParams = new URLSearchParams({
|
||||
farm_uuid: params.farmUuid,
|
||||
range: params.range ?? "7d",
|
||||
});
|
||||
|
||||
const response = await apiClient.get<Partial<SensorValuesListResponse>>(
|
||||
`${SENSORS_PREFIX}/values-list/?${searchParams.toString()}`,
|
||||
);
|
||||
|
||||
return normalizeValuesListResponse(response);
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,3 @@
|
||||
import { apiClient } from "../client";
|
||||
|
||||
const PREFIX = "/api/sensor-external-api";
|
||||
|
||||
export interface SensorExternalFarmSensor {
|
||||
uuid: string;
|
||||
sensor_catalog_uuid: string | null;
|
||||
@@ -49,13 +45,66 @@ export interface SensorExternalRequestLogsResponse {
|
||||
data: SensorExternalRequestLog[];
|
||||
}
|
||||
|
||||
interface SensorExternalLogsParams {
|
||||
farmUuid: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
physicalDeviceUuid?: string;
|
||||
sensorType?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}
|
||||
|
||||
const getAuthHeaders = (): Record<string, string> => {
|
||||
if (typeof window === "undefined") {
|
||||
return {};
|
||||
}
|
||||
|
||||
const token = localStorage.getItem("auth_token");
|
||||
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
};
|
||||
|
||||
async function fetchLocalJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
errorData = (await response.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
errorData = { message: response.statusText };
|
||||
}
|
||||
|
||||
const message =
|
||||
(typeof errorData.msg === "string" && errorData.msg) ||
|
||||
(typeof errorData.detail === "string" && errorData.detail) ||
|
||||
(typeof errorData.message === "string" && errorData.message) ||
|
||||
"An error occurred";
|
||||
|
||||
throw {
|
||||
message,
|
||||
code: response.status,
|
||||
details: errorData,
|
||||
};
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const sensorExternalApiService = {
|
||||
listRequestLogs(params: {
|
||||
farmUuid: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}): Promise<SensorExternalRequestLogsResponse> {
|
||||
const searchParams = new URLSearchParams({ farm_uuid: params.farmUuid });
|
||||
listRequestLogs(params: SensorExternalLogsParams): Promise<SensorExternalRequestLogsResponse> {
|
||||
const searchParams = new URLSearchParams({
|
||||
farm_uuid: params.farmUuid,
|
||||
});
|
||||
|
||||
if (typeof params.page === "number") {
|
||||
searchParams.set("page", String(params.page));
|
||||
@@ -65,8 +114,24 @@ export const sensorExternalApiService = {
|
||||
searchParams.set("page_size", String(params.pageSize));
|
||||
}
|
||||
|
||||
return apiClient.get<SensorExternalRequestLogsResponse>(
|
||||
`${PREFIX}/logs/?${searchParams.toString()}`,
|
||||
if (params.physicalDeviceUuid) {
|
||||
searchParams.set("physical_device_uuid", params.physicalDeviceUuid);
|
||||
}
|
||||
|
||||
if (params.sensorType) {
|
||||
searchParams.set("sensor_type", params.sensorType);
|
||||
}
|
||||
|
||||
if (params.dateFrom) {
|
||||
searchParams.set("date_from", params.dateFrom);
|
||||
}
|
||||
|
||||
if (params.dateTo) {
|
||||
searchParams.set("date_to", params.dateTo);
|
||||
}
|
||||
|
||||
return fetchLocalJson<SensorExternalRequestLogsResponse>(
|
||||
`/api/sensor-external-api/logs/?${searchParams.toString()}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -28,9 +28,19 @@ import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
|
||||
import { useFarmHub } from "@/hooks/useFarmHub";
|
||||
import {
|
||||
type SensorExternalCatalog,
|
||||
type SensorExternalFarmSensor,
|
||||
normalizeComparisonChartResponse,
|
||||
normalizeRadarChartResponse,
|
||||
normalizeValuesListResponse,
|
||||
sensor7Service,
|
||||
type Sensor7SummaryData,
|
||||
type SensorComparisonChartResponse,
|
||||
type SensorRadarChartResponse,
|
||||
type SensorValuesListResponse,
|
||||
} from "@/libs/api/services/sensor7Service";
|
||||
import {
|
||||
sensorExternalApiService,
|
||||
type SensorExternalRequestLog,
|
||||
} from "@/libs/api/services/sensorExternalApiService";
|
||||
import SensorComparisonChart from "@/views/dashboards/farm/SensorComparisonChart";
|
||||
@@ -40,13 +50,19 @@ import SensorHealthPanel from "@/views/dashboards/farm/shared-sensors/SensorHeal
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
const MOCK_FARM_UUID = "11111111-1111-1111-1111-111111111111";
|
||||
const DEFAULT_COMPARISON_RANGE = "7d" as const;
|
||||
const DEFAULT_RADAR_RANGE = "7d" as const;
|
||||
const DEFAULT_VALUES_RANGE = "24h" as const;
|
||||
|
||||
const EMPTY_COMPARISON_CHART: SensorComparisonChartResponse =
|
||||
normalizeComparisonChartResponse();
|
||||
const EMPTY_RADAR_CHART: SensorRadarChartResponse = normalizeRadarChartResponse();
|
||||
const EMPTY_VALUES_LIST: SensorValuesListResponse = normalizeValuesListResponse();
|
||||
|
||||
type StatusColor = "success" | "warning" | "error" | "info";
|
||||
type AccentTone = "primary" | "success" | "warning" | "info";
|
||||
|
||||
const formatPersianNumber = (value: number): string =>
|
||||
value.toLocaleString("fa-IR");
|
||||
const formatPersianNumber = (value: number): string => value.toLocaleString("fa-IR");
|
||||
|
||||
const formatDateTime = (value?: string | null): string => {
|
||||
if (!value) return "-";
|
||||
@@ -64,6 +80,7 @@ const formatDateTime = (value?: string | null): string => {
|
||||
const shortenUuid = (value?: string | null): string => {
|
||||
if (!value) return "-";
|
||||
if (value.length <= 16) return value;
|
||||
|
||||
return `${value.slice(0, 8)}...${value.slice(-6)}`;
|
||||
};
|
||||
|
||||
@@ -72,12 +89,8 @@ const formatScalar = (value: unknown): string => {
|
||||
if (value === undefined) return "undefined";
|
||||
if (Array.isArray(value)) return `[${value.length} item]`;
|
||||
if (typeof value === "object") return "{...}";
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const getPayloadFieldCount = (payload?: Record<string, unknown> | null): number => {
|
||||
if (!payload || typeof payload !== "object") return 0;
|
||||
return Object.keys(payload).length;
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatPayloadPreview = (payload?: Record<string, unknown> | null): string => {
|
||||
@@ -95,181 +108,58 @@ const formatPayloadPreview = (payload?: Record<string, unknown> | null): string
|
||||
return entries.length > 3 ? `${preview} | +${entries.length - 3}` : preview;
|
||||
};
|
||||
|
||||
const normalizeMetricLabel = (key: string): string =>
|
||||
key
|
||||
.replace(/_/g, " ")
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.trim();
|
||||
const getErrorMessage = (error: unknown, fallback: string): string => {
|
||||
if (error && typeof error === "object") {
|
||||
const candidate = error as {
|
||||
message?: unknown;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const extractNumericPayloadEntries = (
|
||||
payload?: Record<string, unknown> | null,
|
||||
): Array<[string, number]> => {
|
||||
if (!payload || typeof payload !== "object") return [];
|
||||
if (typeof candidate.message === "string" && candidate.message) {
|
||||
return candidate.message;
|
||||
}
|
||||
|
||||
return Object.entries(payload).filter(
|
||||
(entry): entry is [string, number] => typeof entry[1] === "number" && Number.isFinite(entry[1]),
|
||||
);
|
||||
if (typeof candidate.details?.detail === "string" && candidate.details.detail) {
|
||||
return candidate.details.detail;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const buildMockSeries = (baseValue: number): number[] =>
|
||||
[0.92, 0.98, 0.94, 1.03, 0.97, 1.05, 1]
|
||||
.map((multiplier) => Number((baseValue * multiplier).toFixed(1)));
|
||||
const getStringRecordField = (
|
||||
record: Record<string, unknown> | null | undefined,
|
||||
key: string,
|
||||
): string | null => {
|
||||
if (!record) return null;
|
||||
|
||||
const createMockCatalog = (
|
||||
uuid: string,
|
||||
code: string,
|
||||
name: string,
|
||||
returnedFields: string[],
|
||||
): SensorExternalCatalog => ({
|
||||
uuid,
|
||||
code,
|
||||
name,
|
||||
description: `کاتالوگ نمونه برای ${name}`,
|
||||
customizable_fields: [],
|
||||
supported_power_sources: ["solar", "battery"],
|
||||
returned_data_fields: returnedFields,
|
||||
sample_payload: null,
|
||||
is_active: true,
|
||||
created_at: "2025-01-01T08:00:00Z",
|
||||
updated_at: "2025-01-10T08:00:00Z",
|
||||
});
|
||||
const value = record[key];
|
||||
|
||||
const createMockSensor = (
|
||||
uuid: string,
|
||||
physicalDeviceUuid: string,
|
||||
name: string,
|
||||
sensorType: string,
|
||||
catalogUuid: string,
|
||||
): SensorExternalFarmSensor => ({
|
||||
uuid,
|
||||
sensor_catalog_uuid: catalogUuid,
|
||||
physical_device_uuid: physicalDeviceUuid,
|
||||
name,
|
||||
sensor_type: sensorType,
|
||||
is_active: true,
|
||||
specifications: { protection: "IP68", install_depth_cm: 30 },
|
||||
power_source: { primary: "solar", backup: "battery" },
|
||||
created_at: "2025-01-01T08:00:00Z",
|
||||
updated_at: "2025-01-10T08:00:00Z",
|
||||
});
|
||||
return typeof value === "string" && value ? value : null;
|
||||
};
|
||||
|
||||
const sensor7Catalog = createMockCatalog(
|
||||
"catalog-7in1-01",
|
||||
"SOIL-7IN1",
|
||||
"کاتالوگ سنسور خاک 7 در 1",
|
||||
["moisture", "temperature", "humidity", "ph", "ec", "nitrogen", "potassium"],
|
||||
);
|
||||
const mapSummaryValuesList = (
|
||||
valuesList?: Sensor7SummaryData["sensorValuesList"] | null,
|
||||
): SensorValuesListResponse => {
|
||||
if (!valuesList) {
|
||||
return EMPTY_VALUES_LIST;
|
||||
}
|
||||
|
||||
const weatherCatalog = createMockCatalog(
|
||||
"catalog-weather-01",
|
||||
"WEATHER-PRO",
|
||||
"کاتالوگ سنسور اقلیم",
|
||||
["air_temperature", "air_humidity", "wind_speed", "light"],
|
||||
);
|
||||
|
||||
const sensor7FarmSensor = createMockSensor(
|
||||
"farm-sensor-7in1-01",
|
||||
"device-7in1-0001",
|
||||
"سنسور خاک 7 در 1 - بلوک شمالی",
|
||||
"Soil 7 in 1",
|
||||
sensor7Catalog.uuid,
|
||||
);
|
||||
|
||||
const weatherFarmSensor = createMockSensor(
|
||||
"farm-sensor-weather-01",
|
||||
"device-weather-0002",
|
||||
"سنسور اقلیم - گلخانه 1",
|
||||
"Climate Station",
|
||||
weatherCatalog.uuid,
|
||||
);
|
||||
|
||||
const MOCK_LOGS: SensorExternalRequestLog[] = [
|
||||
{
|
||||
id: 108,
|
||||
farm_uuid: MOCK_FARM_UUID,
|
||||
sensor_catalog_uuid: sensor7Catalog.uuid,
|
||||
physical_device_uuid: sensor7FarmSensor.physical_device_uuid,
|
||||
farm_sensor: sensor7FarmSensor,
|
||||
sensor_catalog: sensor7Catalog,
|
||||
payload: {
|
||||
moisture: 61,
|
||||
temperature: 27.4,
|
||||
humidity: 58,
|
||||
ph: 6.7,
|
||||
ec: 1.45,
|
||||
nitrogen: 44,
|
||||
potassium: 39,
|
||||
},
|
||||
created_at: "2025-05-02T11:20:00Z",
|
||||
},
|
||||
{
|
||||
id: 107,
|
||||
farm_uuid: MOCK_FARM_UUID,
|
||||
sensor_catalog_uuid: sensor7Catalog.uuid,
|
||||
physical_device_uuid: sensor7FarmSensor.physical_device_uuid,
|
||||
farm_sensor: sensor7FarmSensor,
|
||||
sensor_catalog: sensor7Catalog,
|
||||
payload: {
|
||||
moisture: 58,
|
||||
temperature: 26.9,
|
||||
humidity: 56,
|
||||
ph: 6.6,
|
||||
ec: 1.38,
|
||||
nitrogen: 42,
|
||||
potassium: 37,
|
||||
},
|
||||
created_at: "2025-05-02T08:15:00Z",
|
||||
},
|
||||
{
|
||||
id: 106,
|
||||
farm_uuid: MOCK_FARM_UUID,
|
||||
sensor_catalog_uuid: sensor7Catalog.uuid,
|
||||
physical_device_uuid: sensor7FarmSensor.physical_device_uuid,
|
||||
farm_sensor: sensor7FarmSensor,
|
||||
sensor_catalog: sensor7Catalog,
|
||||
payload: {
|
||||
moisture: 54,
|
||||
temperature: 28.1,
|
||||
humidity: 54,
|
||||
ph: 6.8,
|
||||
ec: 1.41,
|
||||
nitrogen: 40,
|
||||
potassium: 35,
|
||||
},
|
||||
created_at: "2025-05-01T17:40:00Z",
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
farm_uuid: MOCK_FARM_UUID,
|
||||
sensor_catalog_uuid: weatherCatalog.uuid,
|
||||
physical_device_uuid: weatherFarmSensor.physical_device_uuid,
|
||||
farm_sensor: weatherFarmSensor,
|
||||
sensor_catalog: weatherCatalog,
|
||||
payload: {
|
||||
air_temperature: 31.2,
|
||||
air_humidity: 47,
|
||||
wind_speed: 12.4,
|
||||
light: 840,
|
||||
},
|
||||
created_at: "2025-05-02T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
farm_uuid: MOCK_FARM_UUID,
|
||||
sensor_catalog_uuid: weatherCatalog.uuid,
|
||||
physical_device_uuid: weatherFarmSensor.physical_device_uuid,
|
||||
farm_sensor: weatherFarmSensor,
|
||||
sensor_catalog: weatherCatalog,
|
||||
payload: {
|
||||
air_temperature: 29.8,
|
||||
air_humidity: 50,
|
||||
wind_speed: 10.2,
|
||||
light: 790,
|
||||
},
|
||||
created_at: "2025-05-02T07:30:00Z",
|
||||
},
|
||||
];
|
||||
const sensors = Array.isArray(valuesList.sensors)
|
||||
? valuesList.sensors.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.subtitle || item.id || "-",
|
||||
subtitle: item.title || "-",
|
||||
trendNumber: item.trendNumber,
|
||||
trend: item.trend,
|
||||
unit: item.unit,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
sensors,
|
||||
};
|
||||
};
|
||||
|
||||
const getLogStatus = (
|
||||
log: SensorExternalRequestLog,
|
||||
@@ -388,23 +278,188 @@ const LoadingState = () => (
|
||||
|
||||
const Sensor7Page = () => {
|
||||
const theme = useTheme();
|
||||
const { farmHub } = useFarmHub();
|
||||
const farmUuid = farmHub?.farm_uuid ?? null;
|
||||
|
||||
const [summaryData, setSummaryData] = useState<Sensor7SummaryData | null>(null);
|
||||
const [comparisonChartData, setComparisonChartData] =
|
||||
useState<SensorComparisonChartResponse>(EMPTY_COMPARISON_CHART);
|
||||
const [radarChartData, setRadarChartData] =
|
||||
useState<SensorRadarChartResponse>(EMPTY_RADAR_CHART);
|
||||
const [sensorValuesListData, setSensorValuesListData] =
|
||||
useState<SensorValuesListResponse>(EMPTY_VALUES_LIST);
|
||||
const [dashboardLoading, setDashboardLoading] = useState(false);
|
||||
const [dashboardErrorMessage, setDashboardErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [logs, setLogs] = useState<SensorExternalRequestLog[]>([]);
|
||||
const [count, setCount] = useState(MOCK_LOGS.length);
|
||||
const [count, setCount] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasLoadedOnce, setHasLoadedOnce] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [hasLoadedLogs, setHasLoadedLogs] = useState(false);
|
||||
const [logsErrorMessage, setLogsErrorMessage] = useState<string | null>(null);
|
||||
const [selectedLogId, setSelectedLogId] = useState<number | null>(null);
|
||||
const [selectedDeviceId, setSelectedDeviceId] = useState<string>("");
|
||||
|
||||
const paginatedMockLogs = useMemo(() => {
|
||||
const start = (page - 1) * pageSize;
|
||||
return MOCK_LOGS.slice(start, start + pageSize);
|
||||
}, [page, pageSize]);
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
setSelectedLogId(null);
|
||||
setSelectedDeviceId("");
|
||||
}, [farmUuid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!farmUuid) {
|
||||
setSummaryData(null);
|
||||
setComparisonChartData(EMPTY_COMPARISON_CHART);
|
||||
setRadarChartData(EMPTY_RADAR_CHART);
|
||||
setSensorValuesListData(EMPTY_VALUES_LIST);
|
||||
setDashboardLoading(false);
|
||||
setDashboardErrorMessage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
setDashboardLoading(true);
|
||||
setDashboardErrorMessage(null);
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
sensor7Service.getSummary(farmUuid),
|
||||
sensor7Service.getComparisonChart({
|
||||
farmUuid,
|
||||
range: DEFAULT_COMPARISON_RANGE,
|
||||
}),
|
||||
sensor7Service.getRadarChart({
|
||||
farmUuid,
|
||||
range: DEFAULT_RADAR_RANGE,
|
||||
}),
|
||||
sensor7Service.getValuesList({
|
||||
farmUuid,
|
||||
range: DEFAULT_VALUES_RANGE,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (isCancelled) return;
|
||||
|
||||
const [summaryResult, comparisonResult, radarResult, valuesListResult] = results;
|
||||
|
||||
const nextSummaryData = summaryResult.status === "fulfilled" ? summaryResult.value : null;
|
||||
const nextComparisonChartData =
|
||||
comparisonResult.status === "fulfilled"
|
||||
? comparisonResult.value
|
||||
: normalizeComparisonChartResponse(nextSummaryData?.sensorComparisonChart);
|
||||
const nextRadarChartData =
|
||||
radarResult.status === "fulfilled"
|
||||
? radarResult.value
|
||||
: normalizeRadarChartResponse(nextSummaryData?.sensorRadarChart);
|
||||
const nextValuesListData =
|
||||
valuesListResult.status === "fulfilled"
|
||||
? valuesListResult.value
|
||||
: mapSummaryValuesList(nextSummaryData?.sensorValuesList);
|
||||
|
||||
setSummaryData(nextSummaryData);
|
||||
setComparisonChartData(nextComparisonChartData);
|
||||
setRadarChartData(nextRadarChartData);
|
||||
setSensorValuesListData(nextValuesListData);
|
||||
|
||||
const failedResults = results.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === "rejected",
|
||||
);
|
||||
|
||||
if (failedResults.length === results.length) {
|
||||
setDashboardErrorMessage(
|
||||
getErrorMessage(failedResults[0]?.reason, "بارگذاری داده های سنسور انجام نشد."),
|
||||
);
|
||||
} else if (failedResults.length > 0) {
|
||||
setDashboardErrorMessage(
|
||||
"بخشی از داده های سنسور بارگذاری نشد و داده های در دسترس نمایش داده شدند.",
|
||||
);
|
||||
} else {
|
||||
setDashboardErrorMessage(null);
|
||||
}
|
||||
|
||||
setDashboardLoading(false);
|
||||
};
|
||||
|
||||
void loadDashboardData();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [farmUuid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!farmUuid) {
|
||||
setLogs([]);
|
||||
setCount(0);
|
||||
setLogsLoading(false);
|
||||
setLogsErrorMessage(null);
|
||||
setHasLoadedLogs(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
|
||||
const loadLogs = async () => {
|
||||
setLogsLoading(true);
|
||||
setLogsErrorMessage(null);
|
||||
|
||||
try {
|
||||
const response = await sensorExternalApiService.listRequestLogs({
|
||||
farmUuid,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
if (isCancelled) return;
|
||||
|
||||
setLogs(Array.isArray(response.data) ? response.data : []);
|
||||
setCount(typeof response.count === "number" ? response.count : 0);
|
||||
} catch (error) {
|
||||
if (isCancelled) return;
|
||||
|
||||
setLogs([]);
|
||||
setCount(0);
|
||||
setLogsErrorMessage(getErrorMessage(error, "بارگذاری لاگ های سنسور ممکن نشد."));
|
||||
} finally {
|
||||
if (!isCancelled) {
|
||||
setLogsLoading(false);
|
||||
setHasLoadedLogs(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadLogs();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [farmUuid, page, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logs.length) {
|
||||
setSelectedDeviceId("");
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedDeviceId((current) => {
|
||||
if (current && logs.some((item) => item.physical_device_uuid === current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const summaryDeviceId = summaryData?.sensor?.physicalDeviceUuid;
|
||||
|
||||
if (summaryDeviceId && logs.some((item) => item.physical_device_uuid === summaryDeviceId)) {
|
||||
return summaryDeviceId;
|
||||
}
|
||||
|
||||
return logs[0]?.physical_device_uuid ?? "";
|
||||
});
|
||||
}, [logs, summaryData?.sensor?.physicalDeviceUuid]);
|
||||
|
||||
const totalPages = useMemo(
|
||||
() => Math.max(1, Math.ceil(count / pageSize)),
|
||||
() => Math.max(1, Math.ceil((count || 0) / pageSize)),
|
||||
[count, pageSize],
|
||||
);
|
||||
|
||||
@@ -423,65 +478,34 @@ const Sensor7Page = () => {
|
||||
|
||||
options.set(log.physical_device_uuid, {
|
||||
value: log.physical_device_uuid,
|
||||
sensorName: log.farm_sensor?.name || "سنسور بدون نام",
|
||||
sensorType: log.farm_sensor?.sensor_type || "نوع نامشخص",
|
||||
sensorName: log.farm_sensor?.name || summaryData?.sensor?.name || "سنسور بدون نام",
|
||||
sensorType:
|
||||
log.farm_sensor?.sensor_type || summaryData?.sensor?.sensorCatalogCode || "نوع نامشخص",
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(options.values());
|
||||
}, [logs]);
|
||||
}, [logs, summaryData?.sensor?.name, summaryData?.sensor?.sensorCatalogCode]);
|
||||
|
||||
const activeDeviceId = useMemo(() => {
|
||||
if (selectedDeviceId && sensorOptions.some((option) => option.value === selectedDeviceId)) {
|
||||
return selectedDeviceId;
|
||||
}
|
||||
|
||||
const summaryDeviceId = summaryData?.sensor?.physicalDeviceUuid;
|
||||
|
||||
if (summaryDeviceId && sensorOptions.some((option) => option.value === summaryDeviceId)) {
|
||||
return summaryDeviceId;
|
||||
}
|
||||
|
||||
return sensorOptions[0]?.value ?? "";
|
||||
}, [selectedDeviceId, sensorOptions]);
|
||||
}, [selectedDeviceId, sensorOptions, summaryData?.sensor?.physicalDeviceUuid]);
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() => logs.filter((item) => item.physical_device_uuid === activeDeviceId),
|
||||
[activeDeviceId, logs],
|
||||
);
|
||||
const filteredLogs = useMemo(() => {
|
||||
if (!activeDeviceId) return logs;
|
||||
|
||||
const selectedLog = useMemo(
|
||||
() => filteredLogs.find((item) => item.id === selectedLogId) ?? filteredLogs[0] ?? null,
|
||||
[filteredLogs, selectedLogId],
|
||||
);
|
||||
|
||||
const mappedLogsCount = useMemo(
|
||||
() => filteredLogs.filter((item) => Boolean(item.farm_sensor)).length,
|
||||
[filteredLogs],
|
||||
);
|
||||
|
||||
const catalogedLogsCount = useMemo(
|
||||
() => filteredLogs.filter((item) => Boolean(item.sensor_catalog)).length,
|
||||
[filteredLogs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
setLogs(paginatedMockLogs);
|
||||
setCount(MOCK_LOGS.length);
|
||||
setSelectedDeviceId((current) =>
|
||||
paginatedMockLogs.some((item) => item.physical_device_uuid === current)
|
||||
? current
|
||||
: (paginatedMockLogs[0]?.physical_device_uuid ?? ""),
|
||||
);
|
||||
setSelectedLogId((current) =>
|
||||
paginatedMockLogs.some((item) => item.id === current)
|
||||
? current
|
||||
: (paginatedMockLogs[0]?.id ?? null),
|
||||
);
|
||||
setHasLoadedOnce(true);
|
||||
setLoading(false);
|
||||
}, 250);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [paginatedMockLogs]);
|
||||
return logs.filter((item) => item.physical_device_uuid === activeDeviceId);
|
||||
}, [activeDeviceId, logs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filteredLogs.length) {
|
||||
@@ -494,22 +518,35 @@ const Sensor7Page = () => {
|
||||
);
|
||||
}, [filteredLogs]);
|
||||
|
||||
const handlePageSizeChange = (event: SelectChangeEvent<number>) => {
|
||||
const nextPageSize = Number(event.target.value);
|
||||
setPageSize(nextPageSize);
|
||||
setPage(1);
|
||||
};
|
||||
const selectedLog = useMemo(
|
||||
() => filteredLogs.find((item) => item.id === selectedLogId) ?? filteredLogs[0] ?? null,
|
||||
[filteredLogs, selectedLogId],
|
||||
);
|
||||
|
||||
const selectedSensorOption =
|
||||
sensorOptions.find((option) => option.value === activeDeviceId) ?? null;
|
||||
|
||||
const selectedSensorName =
|
||||
selectedLog?.farm_sensor?.name || selectedSensorOption?.sensorName || summaryData?.sensor?.name || "-";
|
||||
const selectedSensorType =
|
||||
selectedLog?.farm_sensor?.sensor_type ||
|
||||
selectedSensorOption?.sensorType ||
|
||||
summaryData?.sensor?.sensorCatalogCode ||
|
||||
"پس از دریافت داده نمایش داده می شود";
|
||||
const lastReceivedAt = selectedLog?.created_at || summaryData?.sensor?.updatedAt || null;
|
||||
const avgSoilMoistureStats =
|
||||
getStringRecordField(summaryData?.avgSoilMoisture ?? null, "stats") || "-";
|
||||
const avgSoilMoistureHelper =
|
||||
getStringRecordField(summaryData?.avgSoilMoisture ?? null, "chipText") ||
|
||||
getStringRecordField(summaryData?.avgSoilMoisture ?? null, "subtitle") ||
|
||||
"از summary سنسور 7 در 1";
|
||||
|
||||
const metrics = [
|
||||
{
|
||||
icon: "tabler-sensor",
|
||||
label: "سنسور انتخاب شده",
|
||||
value: selectedSensorOption?.sensorName || "-",
|
||||
helper:
|
||||
selectedSensorOption?.sensorType || "پس از دریافت داده نمایش داده می شود",
|
||||
value: selectedSensorName,
|
||||
helper: selectedSensorType,
|
||||
tone: "primary" as AccentTone,
|
||||
},
|
||||
{
|
||||
@@ -522,123 +559,46 @@ const Sensor7Page = () => {
|
||||
{
|
||||
icon: "tabler-device-analytics",
|
||||
label: "آخرین دریافت",
|
||||
value: selectedLog ? formatDateTime(selectedLog.created_at) : "-",
|
||||
helper: "جدیدترین لاگ همین سنسور در صفحه جاری",
|
||||
value: formatDateTime(lastReceivedAt),
|
||||
helper: "بر اساس summary یا جدیدترین لاگ موجود",
|
||||
tone: "warning" as AccentTone,
|
||||
},
|
||||
{
|
||||
icon: "tabler-link",
|
||||
label: "پوشش اطلاعات سنسور",
|
||||
value: `${formatPersianNumber(mappedLogsCount)} / ${formatPersianNumber(filteredLogs.length)}`,
|
||||
helper: `${formatPersianNumber(mappedLogsCount)} مورد با سنسور مزرعه، ${formatPersianNumber(catalogedLogsCount)} مورد با کاتالوگ`,
|
||||
icon: "tabler-droplet",
|
||||
label: "میانگین رطوبت خاک",
|
||||
value: avgSoilMoistureStats,
|
||||
helper: avgSoilMoistureHelper,
|
||||
tone: "success" as AccentTone,
|
||||
},
|
||||
];
|
||||
|
||||
const selectedFarmSensor = selectedLog?.farm_sensor ?? null;
|
||||
const handlePageSizeChange = (event: SelectChangeEvent<number>) => {
|
||||
const nextPageSize = Number(event.target.value);
|
||||
setPageSize(nextPageSize);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const comparisonChartData = useMemo(() => {
|
||||
const numericEntries = extractNumericPayloadEntries(selectedLog?.payload);
|
||||
const primaryEntry = numericEntries[0] ?? ["moisture", 48];
|
||||
const secondaryEntry = numericEntries[1] ?? ["temperature", 32];
|
||||
const currentValue = Number(primaryEntry[1].toFixed(0));
|
||||
const previousAverage =
|
||||
filteredLogs.length > 1
|
||||
? filteredLogs
|
||||
.slice(1, 4)
|
||||
.map((log) => {
|
||||
const value = log.payload?.[primaryEntry[0]];
|
||||
return typeof value === "number" ? value : null;
|
||||
})
|
||||
.filter((value): value is number => value !== null)
|
||||
: [];
|
||||
const baseline =
|
||||
previousAverage.length > 0
|
||||
? previousAverage.reduce((sum, value) => sum + value, 0) / previousAverage.length
|
||||
: currentValue * 0.93;
|
||||
const deltaPercent = baseline ? (((currentValue - baseline) / baseline) * 100).toFixed(1) : "0.0";
|
||||
|
||||
return {
|
||||
currentValue,
|
||||
vsLastWeek: `${Number(deltaPercent) >= 0 ? "+" : ""}${deltaPercent}%`,
|
||||
categories: ["شنبه", "یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنج شنبه", "جمعه"],
|
||||
series: [
|
||||
{
|
||||
name: normalizeMetricLabel(primaryEntry[0]),
|
||||
data: buildMockSeries(primaryEntry[1]),
|
||||
},
|
||||
{
|
||||
name: normalizeMetricLabel(secondaryEntry[0]),
|
||||
data: buildMockSeries(secondaryEntry[1]),
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [filteredLogs, selectedLog?.payload]);
|
||||
|
||||
const sensorValuesListData = useMemo(() => {
|
||||
const numericEntries = extractNumericPayloadEntries(selectedLog?.payload);
|
||||
const fallbackEntries: Array<[string, number]> = [
|
||||
["moisture", 61],
|
||||
["temperature", 27],
|
||||
["ph", 6.8],
|
||||
["ec", 1.4],
|
||||
];
|
||||
const entries = (numericEntries.length ? numericEntries : fallbackEntries).slice(0, 5);
|
||||
|
||||
return {
|
||||
sensors: entries.map(([key, value], index) => {
|
||||
const previousValue =
|
||||
filteredLogs[index + 1] && typeof filteredLogs[index + 1].payload?.[key] === "number"
|
||||
? Number(filteredLogs[index + 1].payload?.[key])
|
||||
: value * (0.9 + index * 0.03);
|
||||
const trendNumber = Number((((value - previousValue) / (previousValue || 1)) * 100).toFixed(1));
|
||||
|
||||
return {
|
||||
title: normalizeMetricLabel(key),
|
||||
subtitle: `مقدار فعلی: ${value.toLocaleString("fa-IR", { maximumFractionDigits: 2 })}`,
|
||||
trendNumber,
|
||||
trend: trendNumber < 0 ? "negative" : "positive",
|
||||
unit: "",
|
||||
};
|
||||
}),
|
||||
};
|
||||
}, [filteredLogs, selectedLog?.payload]);
|
||||
|
||||
const radarChartData = useMemo(() => {
|
||||
const numericEntries = extractNumericPayloadEntries(selectedLog?.payload);
|
||||
const entries = (numericEntries.length
|
||||
? numericEntries
|
||||
: [
|
||||
["moisture", 61],
|
||||
["temperature", 27],
|
||||
["humidity", 58],
|
||||
["ph", 68],
|
||||
["ec", 42],
|
||||
]).slice(0, 6);
|
||||
|
||||
return {
|
||||
labels: entries.map(([key]) => normalizeMetricLabel(key)),
|
||||
series: [
|
||||
{
|
||||
name: "وضعیت فعلی",
|
||||
data: entries.map(([, value]) => Number(value.toFixed(1))),
|
||||
},
|
||||
{
|
||||
name: "بازه ایده آل",
|
||||
data: entries.map(([, value], index) =>
|
||||
Number((value * (index % 2 === 0 ? 0.92 : 1.06)).toFixed(1)),
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [selectedLog?.payload]);
|
||||
if (!farmUuid) {
|
||||
return (
|
||||
<Alert severity="info">
|
||||
برای مشاهده داده های سنسور 7 در 1 ابتدا یک مزرعه را در Sensor Hub انتخاب کنید.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-6">
|
||||
<Alert severity="info">
|
||||
این صفحه فعلاً با داده های mock نمایش داده می شود تا طراحی و توسعه رابط کاربری سریع تر
|
||||
انجام شود.
|
||||
</Alert>
|
||||
{dashboardErrorMessage ? (
|
||||
<Alert
|
||||
severity={
|
||||
comparisonChartData.series.length || radarChartData.series.length || sensorValuesListData.sensors.length
|
||||
? "warning"
|
||||
: "error"
|
||||
}
|
||||
>
|
||||
{dashboardErrorMessage}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{metrics.map((metric) => (
|
||||
@@ -648,20 +608,17 @@ const Sensor7Page = () => {
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<SensorHealthPanel
|
||||
payload={selectedLog?.payload}
|
||||
sensorName={selectedFarmSensor?.name || selectedSensorOption?.sensorName}
|
||||
/>
|
||||
<SensorHealthPanel payload={selectedLog?.payload} sensorName={selectedSensorName} />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, lg: 7 }}>
|
||||
<SensorComparisonChart data={comparisonChartData} />
|
||||
<SensorComparisonChart data={comparisonChartData as unknown as Record<string, unknown>} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, lg: 5 }}>
|
||||
<SensorValuesList data={sensorValuesListData} />
|
||||
<SensorValuesList data={sensorValuesListData as unknown as Record<string, unknown>} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<SensorRadarChart data={radarChartData} />
|
||||
<SensorRadarChart data={radarChartData as unknown as Record<string, unknown>} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -676,7 +633,7 @@ const Sensor7Page = () => {
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{loading && hasLoadedOnce ? <LinearProgress /> : null}
|
||||
{logsLoading || dashboardLoading ? <LinearProgress /> : null}
|
||||
|
||||
<CardContent className="p-5 sm:p-6 flex flex-col gap-5">
|
||||
<Stack
|
||||
@@ -690,8 +647,8 @@ const Sensor7Page = () => {
|
||||
محتوای سنسور انتخاب شده
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" className="mt-1">
|
||||
این صفحه فقط داده های مربوط به یک سنسور را نشان می دهد؛ سنسور را از
|
||||
لیست انتخاب کنید و روی هر ردیف بزنید تا جزئیات همان لاگ باز شود.
|
||||
داده های این بخش از endpoint لاگ سنسور خارجی خوانده می شوند؛ سنسور را از
|
||||
لیست انتخاب کنید و روی هر ردیف بزنید تا جزئیات همان لاگ نمایش داده شود.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -707,6 +664,7 @@ const Sensor7Page = () => {
|
||||
value={activeDeviceId}
|
||||
label="سنسور"
|
||||
onChange={(event) => setSelectedDeviceId(event.target.value)}
|
||||
disabled={!sensorOptions.length}
|
||||
>
|
||||
{sensorOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
@@ -754,18 +712,16 @@ const Sensor7Page = () => {
|
||||
|
||||
<Divider />
|
||||
|
||||
{!hasLoadedOnce && loading ? (
|
||||
{!hasLoadedLogs && logsLoading ? (
|
||||
<LoadingState />
|
||||
) : errorMessage ? (
|
||||
<Alert severity="error">{errorMessage}</Alert>
|
||||
) : logsErrorMessage ? (
|
||||
<Alert severity="error">{logsErrorMessage}</Alert>
|
||||
) : logs.length === 0 ? (
|
||||
<Alert severity="info">
|
||||
هنوز لاگی برای این مزرعه ثبت نشده است یا در این صفحه داده ای وجود ندارد.
|
||||
</Alert>
|
||||
) : filteredLogs.length === 0 ? (
|
||||
<Alert severity="info">
|
||||
برای سنسور انتخاب شده در این صفحه لاگی پیدا نشد.
|
||||
</Alert>
|
||||
<Alert severity="info">برای سنسور انتخاب شده در این صفحه لاگی پیدا نشد.</Alert>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ borderRadius: 4 }}>
|
||||
@@ -854,6 +810,7 @@ const Sensor7Page = () => {
|
||||
textOverflow: "ellipsis",
|
||||
fontFamily: "var(--font-geist-mono), monospace",
|
||||
}}
|
||||
title={status.description}
|
||||
>
|
||||
{formatPayloadPreview(log.payload)}
|
||||
</Typography>
|
||||
|
||||
Reference in New Issue
Block a user