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;