UPDATE
This commit is contained in:
@@ -0,0 +1,312 @@
|
|||||||
|
# Backend Requirements For Sensor Content Page
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
This page is now focused on showing the content of a selected sensor, plus three analytics cards that each consume their own API:
|
||||||
|
|
||||||
|
- `SensorComparisonChart.tsx`
|
||||||
|
- `SensorValuesList.tsx`
|
||||||
|
- `SensorRadarChart.tsx`
|
||||||
|
|
||||||
|
The page currently uses mock data in the frontend. To switch it to real data, the backend needs to provide the APIs and payload contracts below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Main Sensor Logs API
|
||||||
|
|
||||||
|
This API powers:
|
||||||
|
|
||||||
|
- sensor selector
|
||||||
|
- top summary cards
|
||||||
|
- health panel
|
||||||
|
- logs table
|
||||||
|
|
||||||
|
### Suggested endpoint
|
||||||
|
|
||||||
|
`GET /api/sensor-external-api/logs/`
|
||||||
|
|
||||||
|
### Required query params
|
||||||
|
|
||||||
|
- `farm_uuid` - required
|
||||||
|
- `page` - required for pagination
|
||||||
|
- `page_size` - required for pagination
|
||||||
|
|
||||||
|
### Recommended extra query params
|
||||||
|
|
||||||
|
- `physical_device_uuid` - to fetch only one sensor directly from backend
|
||||||
|
- `sensor_type` - optional future filter
|
||||||
|
- `date_from` - optional
|
||||||
|
- `date_to` - optional
|
||||||
|
|
||||||
|
### Required response shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"count": 120,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 108,
|
||||||
|
"farm_uuid": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"sensor_catalog_uuid": "catalog-7in1-01",
|
||||||
|
"physical_device_uuid": "device-7in1-0001",
|
||||||
|
"farm_sensor": {
|
||||||
|
"uuid": "farm-sensor-7in1-01",
|
||||||
|
"sensor_catalog_uuid": "catalog-7in1-01",
|
||||||
|
"physical_device_uuid": "device-7in1-0001",
|
||||||
|
"name": "سنسور خاک 7 در 1 - بلوک شمالی",
|
||||||
|
"sensor_type": "Soil 7 in 1",
|
||||||
|
"is_active": true,
|
||||||
|
"specifications": {},
|
||||||
|
"power_source": {},
|
||||||
|
"created_at": "2025-01-01T08:00:00Z",
|
||||||
|
"updated_at": "2025-01-10T08:00:00Z"
|
||||||
|
},
|
||||||
|
"sensor_catalog": {
|
||||||
|
"uuid": "catalog-7in1-01",
|
||||||
|
"code": "SOIL-7IN1",
|
||||||
|
"name": "کاتالوگ سنسور خاک 7 در 1",
|
||||||
|
"description": "string",
|
||||||
|
"customizable_fields": [],
|
||||||
|
"supported_power_sources": [],
|
||||||
|
"returned_data_fields": [
|
||||||
|
"moisture",
|
||||||
|
"temperature",
|
||||||
|
"humidity",
|
||||||
|
"ph",
|
||||||
|
"ec",
|
||||||
|
"nitrogen",
|
||||||
|
"phosphorus",
|
||||||
|
"potassium"
|
||||||
|
],
|
||||||
|
"sample_payload": {},
|
||||||
|
"is_active": true,
|
||||||
|
"created_at": "2025-01-01T08:00:00Z",
|
||||||
|
"updated_at": "2025-01-10T08:00:00Z"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"moisture": 61,
|
||||||
|
"temperature": 27.4,
|
||||||
|
"humidity": 58,
|
||||||
|
"ph": 6.7,
|
||||||
|
"ec": 1.45,
|
||||||
|
"nitrogen": 44,
|
||||||
|
"phosphorus": 32,
|
||||||
|
"potassium": 39
|
||||||
|
},
|
||||||
|
"created_at": "2025-05-02T11:20:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- `payload` must keep numeric values as numbers, not strings
|
||||||
|
- `physical_device_uuid` is required because the page groups and filters by sensor device
|
||||||
|
- `farm_sensor.name` and `farm_sensor.sensor_type` are needed for sensor dropdown labels
|
||||||
|
- `sensor_catalog.returned_data_fields` is needed to understand sensor structure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Comparison Chart API
|
||||||
|
|
||||||
|
This API powers `src/views/dashboards/farm/SensorComparisonChart.tsx`
|
||||||
|
|
||||||
|
### Suggested endpoint
|
||||||
|
|
||||||
|
`GET /api/sensors/comparison-chart/`
|
||||||
|
|
||||||
|
### Required query params
|
||||||
|
|
||||||
|
- `farm_uuid`
|
||||||
|
- `physical_device_uuid`
|
||||||
|
- `range` - example: `7d`, `30d`
|
||||||
|
|
||||||
|
### Required response shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "moisture",
|
||||||
|
"data": [56, 58, 55, 60, 62, 61, 59]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "temperature",
|
||||||
|
"data": [26.2, 26.7, 27.0, 27.2, 27.5, 27.4, 27.1]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["شنبه", "یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنج شنبه", "جمعه"],
|
||||||
|
"currentValue": 61,
|
||||||
|
"vsLastWeek": "+5.4%"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This endpoint is independent and should not be derived in frontend from logs
|
||||||
|
- `currentValue` is shown in large text in the card
|
||||||
|
- `vsLastWeek` is shown as summary text in the card
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Sensor Values List API
|
||||||
|
|
||||||
|
This API powers `src/views/dashboards/farm/SensorValuesList.tsx`
|
||||||
|
|
||||||
|
### Suggested endpoint
|
||||||
|
|
||||||
|
`GET /api/sensors/values-list/`
|
||||||
|
|
||||||
|
### Required query params
|
||||||
|
|
||||||
|
- `farm_uuid`
|
||||||
|
- `physical_device_uuid`
|
||||||
|
- `range` - example: `1h`, `24h`, `7d`
|
||||||
|
|
||||||
|
### Required response shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sensors": [
|
||||||
|
{
|
||||||
|
"title": "Moisture",
|
||||||
|
"subtitle": "مقدار فعلی: 61%",
|
||||||
|
"trendNumber": 4.8,
|
||||||
|
"trend": "positive",
|
||||||
|
"unit": "%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Temperature",
|
||||||
|
"subtitle": "مقدار فعلی: 27.4°C",
|
||||||
|
"trendNumber": -1.2,
|
||||||
|
"trend": "negative",
|
||||||
|
"unit": "°C"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- `trend` must be either `positive` or `negative`
|
||||||
|
- `trendNumber` should be numeric
|
||||||
|
- `subtitle` can already be formatted by backend if preferred
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Radar Chart API
|
||||||
|
|
||||||
|
This API powers `src/views/dashboards/farm/SensorRadarChart.tsx`
|
||||||
|
|
||||||
|
### Suggested endpoint
|
||||||
|
|
||||||
|
`GET /api/sensors/radar-chart/`
|
||||||
|
|
||||||
|
### Required query params
|
||||||
|
|
||||||
|
- `farm_uuid`
|
||||||
|
- `physical_device_uuid`
|
||||||
|
- `range` - example: `today`, `7d`, `30d`
|
||||||
|
|
||||||
|
### Required response shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"labels": [
|
||||||
|
"Moisture",
|
||||||
|
"Temperature",
|
||||||
|
"Humidity",
|
||||||
|
"PH",
|
||||||
|
"EC",
|
||||||
|
"Nitrogen",
|
||||||
|
"Potassium"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "وضعیت فعلی",
|
||||||
|
"data": [61, 27.4, 58, 6.7, 1.45, 44, 39]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "بازه ایده آل",
|
||||||
|
"data": [60, 26, 55, 6.5, 1.3, 42, 38]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This endpoint is also independent
|
||||||
|
- `labels.length` must match every `series[i].data.length`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Recommended Shared Backend Rules
|
||||||
|
|
||||||
|
These rules should be consistent across all four APIs:
|
||||||
|
|
||||||
|
- all timestamps in ISO 8601 format
|
||||||
|
- all numeric sensor values returned as numbers
|
||||||
|
- all UUID-like identifiers stable and non-null when available
|
||||||
|
- responses filtered by `farm_uuid`
|
||||||
|
- responses filtered by `physical_device_uuid` when a single sensor page is opened
|
||||||
|
- clear 4xx/5xx error messages in `msg` or `detail`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Sensor Metadata Recommended For Future Use
|
||||||
|
|
||||||
|
These fields are not all required immediately, but they will help future UI work:
|
||||||
|
|
||||||
|
- sensor display name
|
||||||
|
- sensor type
|
||||||
|
- installation location
|
||||||
|
- block / zone / greenhouse name
|
||||||
|
- unit for each measurable field
|
||||||
|
- min / max ideal thresholds per field
|
||||||
|
- sensor online / offline state
|
||||||
|
- last sync timestamp
|
||||||
|
- battery status
|
||||||
|
- signal strength
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"field_meta": {
|
||||||
|
"moisture": { "label": "Moisture", "unit": "%", "ideal_min": 45, "ideal_max": 70 },
|
||||||
|
"temperature": { "label": "Temperature", "unit": "°C", "ideal_min": 18, "ideal_max": 30 },
|
||||||
|
"ph": { "label": "PH", "unit": "", "ideal_min": 5.8, "ideal_max": 7.2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Current Frontend State
|
||||||
|
|
||||||
|
Right now the page:
|
||||||
|
|
||||||
|
- uses full mock data for logs and sensor details
|
||||||
|
- uses mock-derived data for charts
|
||||||
|
- has the right-side details panel removed
|
||||||
|
- expects the three chart cards to be fed from three separate APIs later
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Minimum Backend Delivery Checklist
|
||||||
|
|
||||||
|
To connect the page to real backend data, the minimum required items are:
|
||||||
|
|
||||||
|
- one paginated logs API with `farm_uuid` and ideally `physical_device_uuid`
|
||||||
|
- one API for comparison chart card
|
||||||
|
- one API for values list card
|
||||||
|
- one API for radar chart card
|
||||||
|
- numeric sensor payload values
|
||||||
|
- stable sensor identity using `physical_device_uuid`
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,363 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Chip from "@mui/material/Chip";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { alpha, useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
type StatusColor = "success" | "warning" | "error" | "info";
|
||||||
|
type AccentTone = "primary" | "success" | "warning" | "info";
|
||||||
|
type HealthTone = "good" | "medium" | "critical" | "unknown";
|
||||||
|
|
||||||
|
export interface SensorHealthPanelProps {
|
||||||
|
payload?: Record<string, unknown> | null;
|
||||||
|
sensorName?: string | null;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
maxItems?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatPersianNumber = (value: number): string =>
|
||||||
|
value.toLocaleString("fa-IR");
|
||||||
|
|
||||||
|
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 normalizeFieldLabel = (key: string): string =>
|
||||||
|
key
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
const formatFieldValue = (value: unknown): string => {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Number.isInteger(value)
|
||||||
|
? formatPersianNumber(value)
|
||||||
|
: value.toLocaleString("fa-IR", { maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value ? "فعال" : "غیرفعال";
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatScalar(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHealthTone = (key: string, value: unknown): HealthTone => {
|
||||||
|
const normalizedKey = key.toLowerCase();
|
||||||
|
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value ? "good" : "critical";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey.includes("temperature") ||
|
||||||
|
normalizedKey.includes("temp") ||
|
||||||
|
normalizedKey.includes("soil_temp")
|
||||||
|
) {
|
||||||
|
if (value >= 16 && value <= 30) return "good";
|
||||||
|
if ((value >= 10 && value < 16) || (value > 30 && value <= 36)) return "medium";
|
||||||
|
return "critical";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey.includes("humidity") ||
|
||||||
|
normalizedKey.includes("moisture") ||
|
||||||
|
normalizedKey.includes("water")
|
||||||
|
) {
|
||||||
|
if (value >= 35 && value <= 75) return "good";
|
||||||
|
if ((value >= 20 && value < 35) || (value > 75 && value <= 90)) return "medium";
|
||||||
|
return "critical";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedKey.includes("ph")) {
|
||||||
|
if (value >= 5.8 && value <= 7.2) return "good";
|
||||||
|
if ((value >= 5.2 && value < 5.8) || (value > 7.2 && value <= 7.8)) return "medium";
|
||||||
|
return "critical";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedKey.includes("ec") || normalizedKey.includes("salinity")) {
|
||||||
|
if (value >= 0.8 && value <= 2.2) return "good";
|
||||||
|
if ((value >= 0.4 && value < 0.8) || (value > 2.2 && value <= 3.2)) return "medium";
|
||||||
|
return "critical";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedKey.includes("nitrogen") ||
|
||||||
|
normalizedKey === "n" ||
|
||||||
|
normalizedKey.includes("phosphorus") ||
|
||||||
|
normalizedKey === "p" ||
|
||||||
|
normalizedKey.includes("potassium") ||
|
||||||
|
normalizedKey === "k"
|
||||||
|
) {
|
||||||
|
if (value >= 20 && value <= 80) return "good";
|
||||||
|
if ((value >= 10 && value < 20) || (value > 80 && value <= 100)) return "medium";
|
||||||
|
return "critical";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value >= 0 && value <= 100) {
|
||||||
|
if (value >= 35 && value <= 75) return "good";
|
||||||
|
if ((value >= 20 && value < 35) || (value > 75 && value <= 90)) return "medium";
|
||||||
|
return "critical";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHealthToneMeta = (
|
||||||
|
tone: HealthTone,
|
||||||
|
): { label: string; muiColor: StatusColor; paletteKey: AccentTone } => {
|
||||||
|
switch (tone) {
|
||||||
|
case "good":
|
||||||
|
return { label: "سالم", muiColor: "success", paletteKey: "success" };
|
||||||
|
case "medium":
|
||||||
|
return { label: "قابل بررسی", muiColor: "warning", paletteKey: "warning" };
|
||||||
|
case "critical":
|
||||||
|
return { label: "نیازمند رسیدگی", muiColor: "error", paletteKey: "warning" };
|
||||||
|
default:
|
||||||
|
return { label: "نامشخص", muiColor: "info", paletteKey: "info" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SensorHealthPanel = ({
|
||||||
|
payload,
|
||||||
|
sensorName,
|
||||||
|
title = "سلامت بخش های سنسور",
|
||||||
|
description = "این بخش یک نمای تصویری از وضعیت قسمت های مختلف سنسور و داده های آخرین payload را نشان می دهد.",
|
||||||
|
emptyMessage = "هنوز داده کافی برای ساخت نمای سلامت سنسور در آخرین payload وجود ندارد.",
|
||||||
|
maxItems = 6,
|
||||||
|
}: SensorHealthPanelProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const healthItems = useMemo(() => {
|
||||||
|
if (!payload || typeof payload !== "object") return [];
|
||||||
|
|
||||||
|
return Object.entries(payload)
|
||||||
|
.filter(([, value]) =>
|
||||||
|
["number", "string", "boolean"].includes(typeof value) || value === null,
|
||||||
|
)
|
||||||
|
.slice(0, maxItems)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const tone = getHealthTone(key, value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label: normalizeFieldLabel(key),
|
||||||
|
value: formatFieldValue(value),
|
||||||
|
tone,
|
||||||
|
meta: getHealthToneMeta(tone),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [maxItems, payload]);
|
||||||
|
|
||||||
|
const goodCount = healthItems.filter((item) => item.tone === "good").length;
|
||||||
|
const warningCount = healthItems.filter((item) => item.tone === "medium").length;
|
||||||
|
const criticalCount = healthItems.filter((item) => item.tone === "critical").length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 5,
|
||||||
|
border: `1px solid ${alpha(theme.palette.primary.main, 0.14)}`,
|
||||||
|
background: `linear-gradient(135deg, ${alpha(
|
||||||
|
theme.palette.primary.main,
|
||||||
|
0.08,
|
||||||
|
)} 0%, ${alpha(theme.palette.info.main, 0.06)} 48%, ${alpha(
|
||||||
|
theme.palette.background.paper,
|
||||||
|
0.98,
|
||||||
|
)} 100%)`,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-5 sm:p-6">
|
||||||
|
<Grid container spacing={3} alignItems="center">
|
||||||
|
<Grid size={{ xs: 12, lg: 5 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
minHeight: 260,
|
||||||
|
borderRadius: 5,
|
||||||
|
overflow: "hidden",
|
||||||
|
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||||
|
background: `radial-gradient(circle at 50% 20%, ${alpha(
|
||||||
|
theme.palette.info.light,
|
||||||
|
0.36,
|
||||||
|
)} 0%, ${alpha(theme.palette.primary.main, 0.14)} 30%, ${alpha(
|
||||||
|
theme.palette.background.paper,
|
||||||
|
0.92,
|
||||||
|
)} 75%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
backgroundImage: `linear-gradient(${alpha(
|
||||||
|
theme.palette.primary.main,
|
||||||
|
0.08,
|
||||||
|
)} 1px, transparent 1px), linear-gradient(90deg, ${alpha(
|
||||||
|
theme.palette.primary.main,
|
||||||
|
0.08,
|
||||||
|
)} 1px, transparent 1px)`,
|
||||||
|
backgroundSize: "28px 28px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{[0, 1, 2].map((layer) => (
|
||||||
|
<Box
|
||||||
|
key={layer}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
insetInline: "14%",
|
||||||
|
bottom: 30 + layer * 36,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: `1px solid ${alpha(theme.palette.primary.main, 0.18)}`,
|
||||||
|
background: `linear-gradient(180deg, ${alpha(
|
||||||
|
theme.palette.background.paper,
|
||||||
|
0.94 - layer * 0.14,
|
||||||
|
)} 0%, ${alpha(theme.palette.primary.main, 0.12 + layer * 0.04)} 100%)`,
|
||||||
|
transform: `perspective(900px) rotateX(68deg) scale(${1 - layer * 0.08})`,
|
||||||
|
boxShadow: `0 16px 30px ${alpha(theme.palette.primary.main, 0.08)}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{healthItems.slice(0, 4).map((item, index) => {
|
||||||
|
const palette = theme.palette[item.meta.paletteKey];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={item.key}
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 38 + index * 34,
|
||||||
|
insetInlineStart: index % 2 === 0 ? "18%" : "56%",
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.75,
|
||||||
|
borderRadius: 3,
|
||||||
|
border: `1px solid ${alpha(palette.main, 0.28)}`,
|
||||||
|
backgroundColor: alpha(palette.main, 0.12),
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
boxShadow: `0 10px 24px ${alpha(palette.main, 0.14)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" display="block" fontWeight={700}>
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{item.value}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
insetInline: 20,
|
||||||
|
bottom: 18,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chip size="small" color="success" label={`${formatPersianNumber(goodCount)} سالم`} />
|
||||||
|
<Chip size="small" color="warning" label={`${formatPersianNumber(warningCount)} بررسی`} />
|
||||||
|
<Chip size="small" color="error" label={`${formatPersianNumber(criticalCount)} بحرانی`} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, lg: 7 }}>
|
||||||
|
<Stack spacing={2.5}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" className="font-semibold">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" className="mt-1">
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
backgroundColor: alpha(theme.palette.background.paper, 0.74),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" color="text.disabled">
|
||||||
|
سنسور فعال
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" className="mt-1">
|
||||||
|
{sensorName || "سنسور انتخاب نشده"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{healthItems.length ? (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{healthItems.map((item) => (
|
||||||
|
<Grid key={item.key} size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: `1px solid ${alpha(
|
||||||
|
theme.palette[item.meta.paletteKey].main,
|
||||||
|
0.2,
|
||||||
|
)}`,
|
||||||
|
backgroundColor: alpha(
|
||||||
|
theme.palette[item.meta.paletteKey].main,
|
||||||
|
0.08,
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
spacing={1}
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<Typography variant="body2" fontWeight={700}>
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
<Chip size="small" color={item.meta.muiColor} label={item.meta.label} />
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="h6">{item.value}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
) : (
|
||||||
|
<Alert severity="info">{emptyMessage}</Alert>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SensorHealthPanel;
|
||||||
Reference in New Issue
Block a user