UPDATE
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
import Sensor7Page from "@/views/dashboards/farm/sensor7/Sensor7Page";
|
||||||
|
|
||||||
|
const Sensor7 = async () => {
|
||||||
|
return <Sensor7Page />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sensor7;
|
||||||
@@ -16,6 +16,9 @@ import CustomChip from "@core/components/mui/Chip";
|
|||||||
|
|
||||||
// Hook Imports
|
// Hook Imports
|
||||||
import useVerticalNav from "@menu/hooks/useVerticalNav";
|
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
|
// Styled Component Imports
|
||||||
import StyledVerticalNavExpandIcon from "@menu/styles/vertical/StyledVerticalNavExpandIcon";
|
import StyledVerticalNavExpandIcon from "@menu/styles/vertical/StyledVerticalNavExpandIcon";
|
||||||
@@ -52,6 +55,10 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
|
|||||||
const t = useTranslations('navigation')
|
const t = useTranslations('navigation')
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const verticalNavOptions = useVerticalNav();
|
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
|
// Vars
|
||||||
const { isBreakpointReached, transitionDuration } = verticalNavOptions;
|
const { isBreakpointReached, transitionDuration } = verticalNavOptions;
|
||||||
@@ -104,6 +111,11 @@ const VerticalMenu = ({ scrollMenu }: Props) => {
|
|||||||
<MenuItem href="/crop-zoning" icon={<i className="tabler-map-2" />}>
|
<MenuItem href="/crop-zoning" icon={<i className="tabler-map-2" />}>
|
||||||
{t('cropZoning')}
|
{t('cropZoning')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{canShowSensor7Menu && (
|
||||||
|
<MenuItem href="/sensor-7" icon={<i className="tabler-sensor" />}>
|
||||||
|
Sensor 7
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
<MenuSection label={t('simulator')}>
|
<MenuSection label={t('simulator')}>
|
||||||
<MenuItem href="/plant-simulator" icon={<i className="tabler-flower" />}>
|
<MenuItem href="/plant-simulator" icon={<i className="tabler-flower" />}>
|
||||||
|
|||||||
@@ -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<FarmAccessProfile | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(Boolean(farmUuid));
|
||||||
|
const [error, setError] = useState<ApiError | null>(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 };
|
||||||
|
};
|
||||||
@@ -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<FarmAccessProfile> {
|
||||||
|
const response = await apiClient.get<AccessProfileEnvelope | FarmAccessProfile>(
|
||||||
|
`${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";
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<Box className="flex items-center justify-center min-h-[260px]">
|
||||||
|
<CircularProgress size={28} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!farmUuid) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6">Sensor 7</Typography>
|
||||||
|
<Typography color="text.secondary" className="mt-2">
|
||||||
|
برای مشاهده این صفحه ابتدا یک مزرعه انتخاب کنید.
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6">Sensor 7</Typography>
|
||||||
|
<Typography color="error" className="mt-2">
|
||||||
|
{error.message || "خطا در دریافت پروفایل دسترسی مزرعه."}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canAccessSensor7 = hasAccessByRule(profile, SENSOR_7_ACCESS_RULE);
|
||||||
|
|
||||||
|
if (!canAccessSensor7) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6">Sensor 7</Typography>
|
||||||
|
<Typography color="text.secondary" className="mt-2">
|
||||||
|
شما به این صفحه دسترسی ندارید.
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h5" className="mb-4">
|
||||||
|
Sensor 7
|
||||||
|
</Typography>
|
||||||
|
<TableContainer component={Paper} variant="outlined">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Sensor ID</TableCell>
|
||||||
|
<TableCell>Name</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell align="right">Last Reading</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{MOCK_SENSOR_ROWS.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
<TableCell>{row.id}</TableCell>
|
||||||
|
<TableCell>{row.name}</TableCell>
|
||||||
|
<TableCell>{row.status}</TableCell>
|
||||||
|
<TableCell align="right">{row.lastReading}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sensor7Page;
|
||||||
Reference in New Issue
Block a user