From ae1bbc126f71428c7b94b84ed5ee12036921e127 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Sat, 4 Apr 2026 01:16:36 +0330 Subject: [PATCH] UPDATE --- .../(dashboard)/(private)/sensor-7/page.tsx | 7 ++ .../layout/vertical/VerticalMenu.tsx | 12 ++ src/hooks/useFarmAccessProfile.ts | 60 +++++++++ src/libs/api/services/accessControlService.ts | 75 ++++++++++++ .../dashboards/farm/sensor7/Sensor7Page.tsx | 114 ++++++++++++++++++ 5 files changed, 268 insertions(+) create mode 100644 src/app/(dashboard)/(private)/sensor-7/page.tsx create mode 100644 src/hooks/useFarmAccessProfile.ts create mode 100644 src/libs/api/services/accessControlService.ts create mode 100644 src/views/dashboards/farm/sensor7/Sensor7Page.tsx diff --git a/src/app/(dashboard)/(private)/sensor-7/page.tsx b/src/app/(dashboard)/(private)/sensor-7/page.tsx new file mode 100644 index 0000000..8ffd30e --- /dev/null +++ b/src/app/(dashboard)/(private)/sensor-7/page.tsx @@ -0,0 +1,7 @@ +import Sensor7Page from "@/views/dashboards/farm/sensor7/Sensor7Page"; + +const Sensor7 = async () => { + return ; +}; + +export default Sensor7; diff --git a/src/components/layout/vertical/VerticalMenu.tsx b/src/components/layout/vertical/VerticalMenu.tsx index 9d690bc..4cd06f2 100644 --- a/src/components/layout/vertical/VerticalMenu.tsx +++ b/src/components/layout/vertical/VerticalMenu.tsx @@ -16,6 +16,9 @@ import CustomChip from "@core/components/mui/Chip"; // Hook Imports import useVerticalNav from "@menu/hooks/useVerticalNav"; +import { useFarmHub } from "@/hooks/useFarmHub"; +import { useFarmAccessProfile } from "@/hooks/useFarmAccessProfile"; +import { hasAccessByRule } from "@/libs/api/services/accessControlService"; // Styled Component Imports import StyledVerticalNavExpandIcon from "@menu/styles/vertical/StyledVerticalNavExpandIcon"; @@ -52,6 +55,10 @@ const VerticalMenu = ({ scrollMenu }: Props) => { const t = useTranslations('navigation') const theme = useTheme(); const verticalNavOptions = useVerticalNav(); + const { farmHub } = useFarmHub(); + const farmUuid = farmHub?.farm_uuid ?? null; + const { profile } = useFarmAccessProfile(farmUuid); + const canShowSensor7Menu = hasAccessByRule(profile, "sensor-7-page-access"); // Vars const { isBreakpointReached, transitionDuration } = verticalNavOptions; @@ -104,6 +111,11 @@ const VerticalMenu = ({ scrollMenu }: Props) => { }> {t('cropZoning')} + {canShowSensor7Menu && ( + }> + Sensor 7 + + )} }> diff --git a/src/hooks/useFarmAccessProfile.ts b/src/hooks/useFarmAccessProfile.ts new file mode 100644 index 0000000..f928f7e --- /dev/null +++ b/src/hooks/useFarmAccessProfile.ts @@ -0,0 +1,60 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { ApiError } from "@/libs/api/client"; +import { + accessControlService, + type FarmAccessProfile, +} from "@/libs/api/services/accessControlService"; + +interface UseFarmAccessProfileResult { + profile: FarmAccessProfile | null; + isLoading: boolean; + error: ApiError | null; +} + +export const useFarmAccessProfile = ( + farmUuid: string | null | undefined, +): UseFarmAccessProfileResult => { + const [profile, setProfile] = useState(null); + const [isLoading, setIsLoading] = useState(Boolean(farmUuid)); + const [error, setError] = useState(null); + + useEffect(() => { + let active = true; + + if (!farmUuid) { + setProfile(null); + setError(null); + setIsLoading(false); + return () => { + active = false; + }; + } + + setIsLoading(true); + setError(null); + + accessControlService + .getFarmAccessProfile(farmUuid) + .then((nextProfile) => { + if (!active) return; + setProfile(nextProfile); + }) + .catch((nextError: ApiError) => { + if (!active) return; + setProfile(null); + setError(nextError); + }) + .finally(() => { + if (!active) return; + setIsLoading(false); + }); + + return () => { + active = false; + }; + }, [farmUuid]); + + return { profile, isLoading, error }; +}; diff --git a/src/libs/api/services/accessControlService.ts b/src/libs/api/services/accessControlService.ts new file mode 100644 index 0000000..71a0e13 --- /dev/null +++ b/src/libs/api/services/accessControlService.ts @@ -0,0 +1,75 @@ +import { apiClient } from "../client"; + +export interface AccessMatchedRule { + code: string; + name: string; + effect: "allow" | "deny" | string; + priority: number; +} + +export interface AccessSubscriptionPlan { + uuid: string; + code: string; + name: string; +} + +export interface FarmAccessProfile { + farm_uuid: string; + subscription_plan?: AccessSubscriptionPlan | null; + matched_rules: AccessMatchedRule[]; + resolved_from_profile: boolean; +} + +interface AccessProfileEnvelope { + code?: number; + msg?: string; + data?: FarmAccessProfile; +} + +const ACCESS_PREFIX = "/api/access-control/farms"; + +export const accessControlService = { + async getFarmAccessProfile(farmUuid: string): Promise { + const response = await apiClient.get( + `${ACCESS_PREFIX}/${encodeURIComponent(farmUuid)}/profile/`, + ); + + const payload = + response && typeof response === "object" && "data" in response + ? response.data + : response; + + return { + farm_uuid: payload?.farm_uuid ?? farmUuid, + subscription_plan: payload?.subscription_plan ?? null, + matched_rules: Array.isArray(payload?.matched_rules) + ? payload.matched_rules + : [], + resolved_from_profile: Boolean(payload?.resolved_from_profile), + }; + }, +}; + +export const hasAccessByRule = ( + profile: FarmAccessProfile | null, + requiredRuleCode: string, +): boolean => { + if (!profile) return false; + + const relevantRules = profile.matched_rules.filter((rule) => + rule.code === requiredRuleCode, + ); + + if (!relevantRules.length) return false; + + const sortedRules = [...relevantRules].sort((left, right) => { + if (right.priority !== left.priority) { + return right.priority - left.priority; + } + + if (left.effect === right.effect) return 0; + return left.effect === "deny" ? -1 : 1; + }); + + return sortedRules[0].effect === "allow"; +}; diff --git a/src/views/dashboards/farm/sensor7/Sensor7Page.tsx b/src/views/dashboards/farm/sensor7/Sensor7Page.tsx new file mode 100644 index 0000000..5156f82 --- /dev/null +++ b/src/views/dashboards/farm/sensor7/Sensor7Page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import CircularProgress from "@mui/material/CircularProgress"; +import Typography from "@mui/material/Typography"; +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 { useFarmHub } from "@/hooks/useFarmHub"; +import { useFarmAccessProfile } from "@/hooks/useFarmAccessProfile"; +import { hasAccessByRule } from "@/libs/api/services/accessControlService"; + +const SENSOR_7_ACCESS_RULE = "sensor-7-page-access"; + +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" }, +]; + +const Sensor7Page = () => { + const { farmHub } = useFarmHub(); + const farmUuid = farmHub?.farm_uuid ?? null; + const { profile, isLoading, error } = useFarmAccessProfile(farmUuid); + + if (isLoading) { + return ( + + + + ); + } + + if (!farmUuid) { + return ( + + + Sensor 7 + + برای مشاهده این صفحه ابتدا یک مزرعه انتخاب کنید. + + + + ); + } + + if (error) { + return ( + + + Sensor 7 + + {error.message || "خطا در دریافت پروفایل دسترسی مزرعه."} + + + + ); + } + + const canAccessSensor7 = hasAccessByRule(profile, SENSOR_7_ACCESS_RULE); + + if (!canAccessSensor7) { + return ( + + + Sensor 7 + + شما به این صفحه دسترسی ندارید. + + + + ); + } + + return ( + + + + Sensor 7 + + + + + + Sensor ID + Name + Status + Last Reading + + + + {MOCK_SENSOR_ROWS.map((row) => ( + + {row.id} + {row.name} + {row.status} + {row.lastReading} + + ))} + +
+
+
+
+ ); +}; + +export default Sensor7Page;