(
+ `${PREFIX}/logs/?${searchParams.toString()}`,
+ );
+ },
+};
diff --git a/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx
index 198f273..f4844a8 100644
--- a/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx
+++ b/src/views/dashboards/farm/cropZoning/CropZoningWrapper.tsx
@@ -80,9 +80,25 @@ export default function CropZoningWrapper() {
return;
}
+ const layerLabel = t(`layers.${activeLayer}`);
+ const loadingMessage = `${t("loadingArea")} - ${layerLabel}`;
+ const getLayerArea = (() => {
+ switch (activeLayer) {
+ case "waterNeed":
+ return cropZoningService.getWaterNeedArea;
+ case "soilQuality":
+ return cropZoningService.getSoilQualityArea;
+ case "cultivationRisk":
+ return cropZoningService.getCultivationRiskArea;
+ case "crops":
+ default:
+ return cropZoningService.getArea;
+ }
+ })();
+
setLoading(true);
setError(null);
- setProgress({ message: t("loadingArea"), percent: 0 });
+ setProgress({ message: loadingMessage, percent: 0 });
try {
let polls = 0;
@@ -110,7 +126,10 @@ export default function CropZoningWrapper() {
setZonesData(firstPageZones as ZoneInitialData[]);
setProgress({
- message: totalPages > 1 ? `${t("loadingArea")} (1/${totalPages})` : completedTaskMessage || t("loadingArea"),
+ message:
+ totalPages > 1
+ ? `${loadingMessage} (1/${totalPages})`
+ : completedTaskMessage || loadingMessage,
percent: totalPages > 1 ? 80 : 100,
});
@@ -124,7 +143,7 @@ export default function CropZoningWrapper() {
totalPages,
});
- const pageRes = await cropZoningService.getArea(farmUuid, {
+ const pageRes = await getLayerArea(farmUuid, {
page,
pageSize,
});
@@ -150,7 +169,7 @@ export default function CropZoningWrapper() {
setZonesData(mergedZones);
setProgress({
- message: `${t("loadingArea")} (${pagesLoaded}/${totalPages})`,
+ message: `${loadingMessage} (${pagesLoaded}/${totalPages})`,
percent: Math.min(80 + pagesProgress, 100),
});
}
@@ -158,7 +177,7 @@ export default function CropZoningWrapper() {
if (!cancelled) {
setZonesData(mergeZones(zonePages));
setProgress({
- message: completedTaskMessage || t("loadingArea"),
+ message: completedTaskMessage || loadingMessage,
percent: 100,
});
}
@@ -171,7 +190,7 @@ export default function CropZoningWrapper() {
pageSize: ZONES_PAGE_SIZE,
});
- const res = await cropZoningService.getArea(farmUuid, {
+ const res = await getLayerArea(farmUuid, {
page: 1,
pageSize: ZONES_PAGE_SIZE,
});
@@ -192,7 +211,7 @@ export default function CropZoningWrapper() {
if (task) {
setProgress({
- message: task.message || task.stage_label || t("loadingArea"),
+ message: task.message || task.stage_label || loadingMessage,
percent: Math.min(Math.round((task.progress_percent || 0) * 0.8), 80),
});
}
@@ -200,13 +219,13 @@ export default function CropZoningWrapper() {
if (taskStatus === "completed" || taskStatus === "success") {
await loadAllZonePages(
res,
- task?.message || task?.stage_label || t("loadingArea"),
+ task?.message || task?.stage_label || loadingMessage,
);
break;
}
if ((!task && res.area) || (res.area && taskStatus !== "pending" && taskStatus !== "processing")) {
- await loadAllZonePages(res, task?.message || task?.stage_label || t("loadingArea"));
+ await loadAllZonePages(res, task?.message || task?.stage_label || loadingMessage);
break;
}
@@ -237,20 +256,54 @@ export default function CropZoningWrapper() {
loadArea();
return () => { cancelled = true; };
- }, [farmUuid, isClientReady, t]);
+ }, [activeLayer, farmUuid, isClientReady, t]);
const mapZonesData = useMemo(() => {
- if (activeLayer === "crops" && zonesData) {
- return zonesData.map(z => ({
+ if (!zonesData) return null;
+
+ return zonesData.map(z => {
+ if (activeLayer === "waterNeed") {
+ return {
+ zoneId: z.zoneId,
+ geometry: z.geometry,
+ color: z.waterNeedLayer?.color || "#94a3b8",
+ tooltipContent: `${z.waterNeedLayer?.value || z.waterNeedLayer?.level || "نامشخص"}
`,
+ cultivable: false,
+ zoneInitialData: z,
+ };
+ }
+
+ if (activeLayer === "soilQuality") {
+ return {
+ zoneId: z.zoneId,
+ geometry: z.geometry,
+ color: z.soilQualityLayer?.color || "#94a3b8",
+ tooltipContent: `${z.soilQualityLayer?.score ?? z.soilQualityLayer?.level ?? "نامشخص"}
`,
+ cultivable: false,
+ zoneInitialData: z,
+ };
+ }
+
+ if (activeLayer === "cultivationRisk") {
+ return {
+ zoneId: z.zoneId,
+ geometry: z.geometry,
+ color: z.cultivationRiskLayer?.color || "#94a3b8",
+ tooltipContent: `${z.cultivationRiskLayer?.level || "نامشخص"}
`,
+ cultivable: false,
+ zoneInitialData: z,
+ };
+ }
+
+ return {
zoneId: z.zoneId,
geometry: z.geometry,
color: z.crop ? CROP_COLORS[z.crop as CropType] || "#94a3b8" : "#94a3b8",
tooltipContent: `${z.crop || "نامشخص"}
`,
cultivable: !!z.crop,
zoneInitialData: z,
- }));
- }
- return null;
+ };
+ });
}, [activeLayer, zonesData]);
const handleZoneClick = useCallback((zone: ZoneInitialData) => {
diff --git a/src/views/dashboards/farm/sensor7/Sensor7Page.tsx b/src/views/dashboards/farm/sensor7/Sensor7Page.tsx
index 5156f82..90a59d0 100644
--- a/src/views/dashboards/farm/sensor7/Sensor7Page.tsx
+++ b/src/views/dashboards/farm/sensor7/Sensor7Page.tsx
@@ -1,38 +1,466 @@
"use client";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+
+import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
+import Chip from "@mui/material/Chip";
import CircularProgress from "@mui/material/CircularProgress";
-import Typography from "@mui/material/Typography";
+import Divider from "@mui/material/Divider";
+import FormControl from "@mui/material/FormControl";
+import Grid from "@mui/material/Grid2";
+import InputLabel from "@mui/material/InputLabel";
+import LinearProgress from "@mui/material/LinearProgress";
+import MenuItem from "@mui/material/MenuItem";
+import Pagination from "@mui/material/Pagination";
+import Paper from "@mui/material/Paper";
+import Select from "@mui/material/Select";
+import type { SelectChangeEvent } from "@mui/material/Select";
+import Skeleton from "@mui/material/Skeleton";
+import Stack from "@mui/material/Stack";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
-import Paper from "@mui/material/Paper";
+import Typography from "@mui/material/Typography";
+import { alpha, useTheme } from "@mui/material/styles";
+
import { useFarmHub } from "@/hooks/useFarmHub";
import { useFarmAccessProfile } from "@/hooks/useFarmAccessProfile";
+import type { ApiError } from "@/libs/api/client";
import { hasAccessByRule } from "@/libs/api/services/accessControlService";
+import {
+ sensorExternalApiService,
+ type SensorExternalCatalog,
+ type SensorExternalFarmSensor,
+ type SensorExternalRequestLog,
+} from "@/libs/api/services/sensorExternalApiService";
const SENSOR_7_ACCESS_RULE = "sensor-7-page-access";
+const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
+const DEFAULT_PAGE_SIZE = 20;
-const MOCK_SENSOR_ROWS = [
- { id: "S7-001", name: "Sensor 7-A", status: "online", lastReading: "24.1" },
- { id: "S7-002", name: "Sensor 7-B", status: "offline", lastReading: "-" },
- { id: "S7-003", name: "Sensor 7-C", status: "online", lastReading: "23.7" },
-];
+type StatusColor = "success" | "warning" | "error" | "info";
+type AccentTone = "primary" | "success" | "warning" | "info";
+
+const formatPersianNumber = (value: number): string =>
+ value.toLocaleString("fa-IR");
+
+const formatDateTime = (value?: string | null): string => {
+ if (!value) return "-";
+
+ try {
+ return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(new Date(value));
+ } catch {
+ return value;
+ }
+};
+
+const shortenUuid = (value?: string | null): string => {
+ if (!value) return "-";
+ if (value.length <= 16) return value;
+ return `${value.slice(0, 8)}...${value.slice(-6)}`;
+};
+
+const formatScalar = (value: unknown): string => {
+ if (value === null) return "null";
+ 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 | null): number => {
+ if (!payload || typeof payload !== "object") return 0;
+ return Object.keys(payload).length;
+};
+
+const formatPayloadPreview = (payload?: Record | null): string => {
+ if (!payload || typeof payload !== "object") return "payload خالی است";
+
+ const entries = Object.entries(payload);
+
+ if (!entries.length) return "payload خالی است";
+
+ const preview = entries
+ .slice(0, 3)
+ .map(([key, value]) => `${key}: ${formatScalar(value)}`)
+ .join(" | ");
+
+ return entries.length > 3 ? `${preview} | +${entries.length - 3}` : preview;
+};
+
+const formatJson = (value: unknown): string => {
+ try {
+ return JSON.stringify(value ?? {}, null, 2);
+ } catch {
+ return "{}";
+ }
+};
+
+const resolveErrorMessage = (
+ error: unknown,
+ fallback: string,
+): string => {
+ const apiError = error as
+ | (ApiError & { details?: { detail?: unknown; msg?: unknown } })
+ | undefined;
+
+ if (typeof apiError?.details?.detail === "string") {
+ return apiError.details.detail;
+ }
+
+ if (typeof apiError?.details?.msg === "string") {
+ return apiError.details.msg;
+ }
+
+ if (typeof apiError?.message === "string" && apiError.message.trim()) {
+ return apiError.message;
+ }
+
+ return fallback;
+};
+
+const getLogStatus = (
+ log: SensorExternalRequestLog,
+): {
+ label: string;
+ description: string;
+ color: StatusColor;
+ icon: string;
+} => {
+ if (log.farm_sensor && log.sensor_catalog) {
+ return {
+ label: "همگام",
+ description: "لاگ با سنسور مزرعه و کاتالوگ متناظر تکمیل شده است.",
+ color: "success",
+ icon: "tabler-circle-check",
+ };
+ }
+
+ if (log.farm_sensor && !log.sensor_catalog) {
+ return {
+ label: "بدون کاتالوگ",
+ description: "سنسور پیدا شده اما اطلاعات کاتالوگ کامل نیست.",
+ color: "warning",
+ icon: "tabler-book-off",
+ };
+ }
+
+ if (!log.farm_sensor && log.sensor_catalog_uuid) {
+ return {
+ label: "نیازمند تطبیق",
+ description: "برای این دستگاه هنوز سنسور مزرعه متناظر پیدا نشده است.",
+ color: "error",
+ icon: "tabler-link-off",
+ };
+ }
+
+ return {
+ label: "داده خام",
+ description: "لاگ ثبت شده اما هنوز داده تکمیلی برای آن موجود نیست.",
+ color: "info",
+ icon: "tabler-braces",
+ };
+};
+
+const InsightMetricCard = ({
+ icon,
+ label,
+ value,
+ helper,
+ tone,
+}: {
+ icon: string;
+ label: string;
+ value: string;
+ helper: string;
+ tone: AccentTone;
+}) => {
+ const theme = useTheme();
+ const paletteColor = theme.palette[tone];
+
+ return (
+
+
+
+
+
+
+
+
+
+ {label}
+
+
+ {value}
+
+
+ {helper}
+
+
+
+
+
+ );
+};
+
+const DetailField = ({
+ label,
+ value,
+ mono = false,
+}: {
+ label: string;
+ value: string;
+ mono?: boolean;
+}) => (
+ `1px solid ${theme.palette.divider}`,
+ backgroundColor: (theme) => alpha(theme.palette.action.hover, 0.3),
+ }}
+ >
+
+ {label}
+
+
+ {value}
+
+
+);
+
+const EntityCard = ({
+ title,
+ icon,
+ emptyLabel,
+ lines,
+}: {
+ title: string;
+ icon: string;
+ emptyLabel: string;
+ lines: Array<{ label: string; value: string }>;
+}) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {lines.length ? (
+
+ {lines.map((line) => (
+
+ ))}
+
+ ) : (
+ {emptyLabel}
+ )}
+
+
+ );
+};
+
+const LoadingState = () => (
+
+
+
+
+
+
+);
const Sensor7Page = () => {
+ const theme = useTheme();
const { farmHub } = useFarmHub();
const farmUuid = farmHub?.farm_uuid ?? null;
const { profile, isLoading, error } = useFarmAccessProfile(farmUuid);
+ const canAccessSensor7 = hasAccessByRule(profile, SENSOR_7_ACCESS_RULE);
+
+ const [logs, setLogs] = useState([]);
+ 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(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [selectedLogId, setSelectedLogId] = useState(null);
+ const [lastUpdatedAt, setLastUpdatedAt] = useState(null);
+ const requestIdRef = useRef(0);
+
+ const totalPages = useMemo(
+ () => Math.max(1, Math.ceil(count / pageSize)),
+ [count, pageSize],
+ );
+
+ const selectedLog = useMemo(
+ () => logs.find((item) => item.id === selectedLogId) ?? logs[0] ?? null,
+ [logs, selectedLogId],
+ );
+
+ const uniqueDevicesCount = useMemo(
+ () => new Set(logs.map((item) => item.physical_device_uuid)).size,
+ [logs],
+ );
+
+ const mappedLogsCount = useMemo(
+ () => logs.filter((item) => Boolean(item.farm_sensor)).length,
+ [logs],
+ );
+
+ const catalogedLogsCount = useMemo(
+ () => logs.filter((item) => Boolean(item.sensor_catalog)).length,
+ [logs],
+ );
+
+ const loadLogs = useCallback(
+ async (targetPage = page, targetPageSize = pageSize) => {
+ if (!farmUuid || !canAccessSensor7) {
+ setLogs([]);
+ setCount(0);
+ setSelectedLogId(null);
+ setErrorMessage(null);
+ setLoading(false);
+ setHasLoadedOnce(false);
+ return;
+ }
+
+ const currentRequestId = ++requestIdRef.current;
+ setLoading(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await sensorExternalApiService.listRequestLogs({
+ farmUuid,
+ page: targetPage,
+ pageSize: targetPageSize,
+ });
+
+ if (currentRequestId !== requestIdRef.current) return;
+
+ const nextLogs = Array.isArray(response.data) ? response.data : [];
+
+ setLogs(nextLogs);
+ setCount(typeof response.count === "number" ? response.count : 0);
+ setSelectedLogId((current) =>
+ nextLogs.some((item) => item.id === current)
+ ? current
+ : (nextLogs[0]?.id ?? null),
+ );
+ setHasLoadedOnce(true);
+ setLastUpdatedAt(new Date().toISOString());
+ } catch (requestError) {
+ if (currentRequestId !== requestIdRef.current) return;
+
+ setLogs([]);
+ setCount(0);
+ setSelectedLogId(null);
+ setErrorMessage(
+ resolveErrorMessage(
+ requestError,
+ "دریافت لاگ سنسورهای خارجی با خطا مواجه شد.",
+ ),
+ );
+ setHasLoadedOnce(true);
+ } finally {
+ if (currentRequestId === requestIdRef.current) {
+ setLoading(false);
+ }
+ }
+ },
+ [canAccessSensor7, farmUuid, page, pageSize],
+ );
+
+ useEffect(() => {
+ setPage(1);
+ setSelectedLogId(null);
+ setLogs([]);
+ setCount(0);
+ setErrorMessage(null);
+ setHasLoadedOnce(false);
+ }, [farmUuid]);
+
+ useEffect(() => {
+ if (!farmUuid || !canAccessSensor7) return;
+ void loadLogs(page, pageSize);
+ }, [canAccessSensor7, farmUuid, loadLogs, page, pageSize]);
+
+ const handlePageSizeChange = (event: SelectChangeEvent) => {
+ const nextPageSize = Number(event.target.value);
+ setPageSize(nextPageSize);
+ setPage(1);
+ };
+
+ const heroBorder = alpha(theme.palette.primary.main, 0.18);
+ const heroGlow = alpha(theme.palette.primary.main, 0.2);
if (isLoading) {
return (
-
-
+
+
);
}
@@ -40,10 +468,10 @@ const Sensor7Page = () => {
if (!farmUuid) {
return (
-
- Sensor 7
+
+ مانیتورینگ سنسور خارجی
- برای مشاهده این صفحه ابتدا یک مزرعه انتخاب کنید.
+ برای مشاهده لاگ ها ابتدا یک مزرعه فعال انتخاب کنید.
@@ -53,23 +481,21 @@ const Sensor7Page = () => {
if (error) {
return (
-
- Sensor 7
+
+ مانیتورینگ سنسور خارجی
- {error.message || "خطا در دریافت پروفایل دسترسی مزرعه."}
+ {resolveErrorMessage(error, "خطا در دریافت سطح دسترسی مزرعه.")}
);
}
- const canAccessSensor7 = hasAccessByRule(profile, SENSOR_7_ACCESS_RULE);
-
if (!canAccessSensor7) {
return (
-
- Sensor 7
+
+ مانیتورینگ سنسور خارجی
شما به این صفحه دسترسی ندارید.
@@ -78,36 +504,562 @@ const Sensor7Page = () => {
);
}
+ const metrics = [
+ {
+ icon: "tabler-database",
+ label: "کل لاگ های مزرعه",
+ value: formatPersianNumber(count),
+ helper:
+ totalPages > 1
+ ? `صفحه ${formatPersianNumber(page)} از ${formatPersianNumber(totalPages)}`
+ : "تمام رخدادهای ثبت شده برای مزرعه",
+ tone: "primary" as AccentTone,
+ },
+ {
+ icon: "tabler-stack-2",
+ label: "رکوردهای همین صفحه",
+ value: formatPersianNumber(logs.length),
+ helper: `${formatPersianNumber(pageSize)} رکورد در هر صفحه`,
+ tone: "info" as AccentTone,
+ },
+ {
+ icon: "tabler-devices",
+ label: "دستگاه های یکتا",
+ value: formatPersianNumber(uniqueDevicesCount),
+ helper: "بر پایه physical_device_uuid در همین صفحه",
+ tone: "warning" as AccentTone,
+ },
+ {
+ icon: "tabler-link",
+ label: "لاگ های تطبیق یافته",
+ value: formatPersianNumber(mappedLogsCount),
+ helper: `${formatPersianNumber(catalogedLogsCount)} مورد با کاتالوگ کامل`,
+ tone: "success" as AccentTone,
+ },
+ ];
+
+ const selectedStatus = selectedLog ? getLogStatus(selectedLog) : null;
+ const selectedFarmSensor = selectedLog?.farm_sensor ?? null;
+ const selectedCatalog = selectedLog?.sensor_catalog ?? null;
+
+ const sensorLines: Array<{ label: string; value: string }> = selectedFarmSensor
+ ? [
+ { label: "نام سنسور", value: selectedFarmSensor.name || "-" },
+ { label: "نوع", value: selectedFarmSensor.sensor_type || "-" },
+ {
+ label: "وضعیت",
+ value: selectedFarmSensor.is_active ? "فعال" : "غیرفعال",
+ },
+ {
+ label: "دستگاه فیزیکی",
+ value: selectedFarmSensor.physical_device_uuid || "-",
+ },
+ ]
+ : [];
+
+ const catalogLines: Array<{ label: string; value: string }> = selectedCatalog
+ ? [
+ { label: "نام", value: selectedCatalog.name || "-" },
+ { label: "کد", value: selectedCatalog.code || "-" },
+ {
+ label: "فیلدهای خروجی",
+ value: Array.isArray(selectedCatalog.returned_data_fields)
+ ? selectedCatalog.returned_data_fields.join("، ") || "-"
+ : "-",
+ },
+ {
+ label: "وضعیت",
+ value: selectedCatalog.is_active ? "فعال" : "غیرفعال",
+ },
+ ]
+ : [];
+
return (
-
-
-
- Sensor 7
-
-
-
-
-
- Sensor ID
- Name
- Status
- Last Reading
-
-
-
- {MOCK_SENSOR_ROWS.map((row) => (
-
- {row.id}
- {row.name}
- {row.status}
- {row.lastReading}
-
- ))}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ مرکز مانیتورینگ لاگ سنسورهای خارجی
+
+
+ این صفحه تاریخچه درخواست های ورودی سنسورهای خارجی مزرعه انتخاب شده را
+ به صورت زنده، صفحه بندی شده و همراه با اطلاعات تکمیلی سنسور و کاتالوگ
+ نمایش می دهد.
+
+
+
+
+
+
+
+
+ آخرین همگام سازی
+
+
+ {lastUpdatedAt ? formatDateTime(lastUpdatedAt) : "هنوز دریافت نشده"}
+
+
+ لاگ ها به صورت نزولی و از جدیدترین به قدیمی ترین نمایش داده می شوند.
+
+
+
+
+
+
+
+
+ {metrics.map((metric) => (
+
+
+
+ ))}
+
+
+
+
+
+ {loading && hasLoadedOnce ? : null}
+
+
+
+
+
+ جریان لاگ ها
+
+
+ روی هر ردیف کلیک کنید تا payload و جزئیات سنسور همان لاگ در پنل سمت
+ راست باز شود.
+
+
+
+
+
+ اندازه صفحه
+
+
+
+
+
+ بازه فعلی
+
+
+ {count
+ ? `${formatPersianNumber((page - 1) * pageSize + 1)} تا ${formatPersianNumber(
+ Math.min(page * pageSize, count),
+ )}`
+ : "۰ تا ۰"}
+
+
+
+
+
+
+
+ {!hasLoadedOnce && loading ? (
+
+ ) : errorMessage ? (
+ {errorMessage}
+ ) : logs.length === 0 ? (
+
+ هنوز لاگی برای این مزرعه ثبت نشده است یا در این صفحه داده ای وجود ندارد.
+
+ ) : (
+ <>
+
+
+
+
+ وضعیت
+ زمان ثبت
+ دستگاه
+ سنسور مزرعه
+ کاتالوگ
+ خلاصه payload
+
+
+
+ {logs.map((log) => {
+ const status = getLogStatus(log);
+ const isSelected = selectedLog?.id === log.id;
+
+ return (
+ setSelectedLogId(log.id)}
+ sx={{
+ cursor: "pointer",
+ "& .MuiTableCell-root": {
+ borderColor: alpha(theme.palette.divider, 0.9),
+ },
+ }}
+ >
+
+ }
+ label={status.label}
+ sx={{ fontWeight: 600 }}
+ />
+
+
+
+
+ {formatDateTime(log.created_at)}
+
+
+ ID: {formatPersianNumber(log.id)}
+
+
+
+
+
+
+ {shortenUuid(log.physical_device_uuid)}
+
+
+ {log.physical_device_uuid}
+
+
+
+
+
+ {log.farm_sensor?.name || "تطبیق نشده"}
+
+
+ {log.farm_sensor?.sensor_type || "فاقد سنسور مزرعه"}
+
+
+
+
+ {log.sensor_catalog?.name || "نامشخص"}
+
+
+ {log.sensor_catalog?.code || shortenUuid(log.sensor_catalog_uuid)}
+
+
+
+
+ {formatPayloadPreview(log.payload)}
+
+
+
+ );
+ })}
+
+
+
+
+ {totalPages > 1 ? (
+
+ setPage(value)}
+ />
+
+ ) : null}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ جزئیات لاگ انتخاب شده
+
+
+ اطلاعات enrich شده سنسور و کاتالوگ از همین پنل قابل بررسی است.
+
+
+ {selectedStatus ? (
+
+ ) : null}
+
+
+ {selectedLog ? (
+ <>
+
+ {selectedStatus?.description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ payload دریافتی
+
+
+
+ {formatJson(selectedLog.payload)}
+
+
+ >
+ ) : (
+
+ یک لاگ از جدول سمت چپ انتخاب کنید تا جزئیات آن در این بخش نمایش داده شود.
+
+ )}
+
+
+
+
+
+ راهنمای سریع endpoint
+
+ این ویو با `farm_uuid` فیلتر می شود، فقط `GET` را می پذیرد و صفحه بندی
+ آن با `page` و `page_size` کنترل می شود.
+
+
+
+
+
+
+
+
+
+
+
+ اگر backend هنوز migrate نشده باشد، پاسخ `503` با پیام آماده نبودن جدول ها
+ برمی گردد.
+
+
+
+
+
+
+
);
};