Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e51f998a91 | |||
| 67517e99c8 | |||
| 58ea683c1c | |||
| 438e46d9ae | |||
| 9b876c8dd7 | |||
| 25aefe2b49 | |||
| 8b5ec3c6f9 | |||
| 62dad94d5d | |||
| 1fab9dc2d0 |
@@ -1,5 +1,6 @@
|
|||||||
FROM registry.hamdocker.ir/seniorkian/node:18-alpine
|
FROM registry.hamdocker.ir/seniorkian/node:18-alpine
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { makeRouter } from "./routes/routes";
|
|||||||
import { useDarkMode } from "./hooks/useDarkMode";
|
import { useDarkMode } from "./hooks/useDarkMode";
|
||||||
import { ItemWithSubItems } from "./types/userPermissions";
|
import { ItemWithSubItems } from "./types/userPermissions";
|
||||||
import { useFetchProfile } from "./hooks/useFetchProfile";
|
import { useFetchProfile } from "./hooks/useFetchProfile";
|
||||||
import { getInspectionMenuItems } from "./screen/SideBar";
|
import { getInspectionMenuItems } from "./config/menuItems";
|
||||||
|
|
||||||
const versionNumber = "/src/version.txt";
|
const versionNumber = "/src/version.txt";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@@ -127,7 +127,7 @@ export default function App() {
|
|||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
{},
|
{},
|
||||||
document.title,
|
document.title,
|
||||||
url.pathname + url.search
|
url.pathname + url.search,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -4,99 +4,26 @@ import { useUserProfileStore } from "../context/zustand-store/userStore";
|
|||||||
import { getFaPermissions } from "../utils/getFaPermissions";
|
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import {
|
import {
|
||||||
UsersIcon,
|
|
||||||
DocumentTextIcon,
|
|
||||||
ChartBarIcon,
|
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
UserIcon,
|
|
||||||
GlobeAsiaAustraliaIcon,
|
GlobeAsiaAustraliaIcon,
|
||||||
|
MapPinIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { ItemWithSubItems } from "../types/userPermissions";
|
import { getInspectionMenuItems } from "../config/menuItems";
|
||||||
import { checkMenuPermission } from "../utils/checkMenuPermission";
|
|
||||||
|
|
||||||
const getInspectionMenuItems = (
|
const ADMIN_PROVINCES = [
|
||||||
permissions: string[] = []
|
{ value: "hamedan", label: "همدان" },
|
||||||
): ItemWithSubItems[] => {
|
{ value: "markazi", label: "مرکزی" },
|
||||||
const items: ItemWithSubItems[] = [];
|
] as const;
|
||||||
|
|
||||||
if (checkMenuPermission("nationalinfo", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "nationalinfo",
|
|
||||||
fa: "اطلاعات",
|
|
||||||
icon: () => <UserIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "nationalinfo",
|
|
||||||
path: "/nationalinfo",
|
|
||||||
component: () => import("./NationalInfo"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ladinginfo",
|
|
||||||
path: "/ladinginfo",
|
|
||||||
component: () => import("./LadingInfo"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "veterinarytransfer",
|
|
||||||
path: "/veterinarytransfer",
|
|
||||||
component: () => import("./VeterinaryTransfer"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkMenuPermission("users", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "users",
|
|
||||||
fa: "کاربران",
|
|
||||||
icon: () => <UsersIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "users",
|
|
||||||
path: "/users",
|
|
||||||
component: () => import("./Users"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkMenuPermission("inspections", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "inspections",
|
|
||||||
fa: "سوابق بازرسی",
|
|
||||||
icon: () => <DocumentTextIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "inspections",
|
|
||||||
path: "/inspections",
|
|
||||||
component: () => import("./UserInspections"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkMenuPermission("statics", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "statics",
|
|
||||||
fa: "آمار",
|
|
||||||
icon: () => <ChartBarIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "statics",
|
|
||||||
path: "/statics",
|
|
||||||
component: () => import("./Statics"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Menu = () => {
|
export const Menu = () => {
|
||||||
const { profile } = useUserProfileStore();
|
const { profile, updateProfile } = useUserProfileStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const menuItems = getInspectionMenuItems(profile?.permissions || []);
|
const menuItems = getInspectionMenuItems(profile?.permissions || []);
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||||
|
const hasAdmin =
|
||||||
|
Array.isArray(profile?.permissions) &&
|
||||||
|
profile.permissions.includes("admin");
|
||||||
|
const currentProvince = profile?.province || "hamedan";
|
||||||
|
|
||||||
const toggleSubmenu = (index: number) => {
|
const toggleSubmenu = (index: number) => {
|
||||||
setOpenIndex((prev) => (prev === index ? null : index));
|
setOpenIndex((prev) => (prev === index ? null : index));
|
||||||
@@ -110,7 +37,7 @@ export const Menu = () => {
|
|||||||
منو
|
منو
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4 space-y-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate({ to: "/" })}
|
onClick={() => navigate({ to: "/" })}
|
||||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl bg-primary-500 hover:bg-primary-600 cursor-pointer dark:bg-primary-800 dark:hover:bg-primary-700 text-white font-semibold text-sm transition-all duration-200 shadow-md hover:shadow-lg"
|
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl bg-primary-500 hover:bg-primary-600 cursor-pointer dark:bg-primary-800 dark:hover:bg-primary-700 text-white font-semibold text-sm transition-all duration-200 shadow-md hover:shadow-lg"
|
||||||
@@ -118,6 +45,30 @@ export const Menu = () => {
|
|||||||
<GlobeAsiaAustraliaIcon className="w-5 h-5" />
|
<GlobeAsiaAustraliaIcon className="w-5 h-5" />
|
||||||
<span>مشاهده نقشه</span>
|
<span>مشاهده نقشه</span>
|
||||||
</button>
|
</button>
|
||||||
|
{hasAdmin && (
|
||||||
|
<div className="flex flex-col gap-2 p-3 rounded-xl bg-white dark:bg-dark-700 border border-gray-200 dark:border-dark-600">
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
<MapPinIcon className="w-4 h-4 shrink-0" />
|
||||||
|
<span>استان</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{ADMIN_PROVINCES.map(({ value, label }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateProfile({ province: value })}
|
||||||
|
className={`px-4 py-2.5 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
currentProvince === value
|
||||||
|
? "bg-primary-500 text-white dark:bg-primary-600 shadow-md"
|
||||||
|
: "bg-gray-100 dark:bg-dark-600 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-dark-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
290
src/Pages/PostcodeInquiry.tsx
Normal file
290
src/Pages/PostcodeInquiry.tsx
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Grid } from "../components/Grid/Grid";
|
||||||
|
import Textfield from "../components/Textfeild/Textfeild";
|
||||||
|
import { useApiMutation } from "../utils/useApiRequest";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import Typography from "../components/Typography/Typography";
|
||||||
|
import { useToast } from "../hooks/useToast";
|
||||||
|
import {
|
||||||
|
MapPinIcon,
|
||||||
|
BuildingOfficeIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import Button from "../components/Button/Button";
|
||||||
|
|
||||||
|
interface PostcodeInquiryData {
|
||||||
|
avenue: string;
|
||||||
|
buildingName: string;
|
||||||
|
description: string;
|
||||||
|
floorNo: number | null;
|
||||||
|
houseNo: string;
|
||||||
|
houseNoSpecified: boolean;
|
||||||
|
location: string;
|
||||||
|
locationCode: string;
|
||||||
|
locationType: string;
|
||||||
|
parish: string | null;
|
||||||
|
postCode: string;
|
||||||
|
preAvenue: string;
|
||||||
|
sideFloor: number | null;
|
||||||
|
state: string;
|
||||||
|
townShip: string;
|
||||||
|
village: string;
|
||||||
|
zone: string;
|
||||||
|
address: string;
|
||||||
|
hasError: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
errorCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostcodeInquiryResponse {
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number;
|
||||||
|
data: PostcodeInquiryData;
|
||||||
|
apiLogId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostcodeInquiry: React.FC = () => {
|
||||||
|
const [postcode, setPostcode] = useState("");
|
||||||
|
const [inquiryData, setInquiryData] = useState<PostcodeInquiryData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
|
const inquiryMutation = useApiMutation<PostcodeInquiryResponse>({
|
||||||
|
api: "postcode-inquiry",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
const trimmed = postcode.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
showToast("لطفا کد پستی را وارد کنید", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.length !== 10) {
|
||||||
|
showToast("کد پستی باید 10 رقم باشد", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await inquiryMutation.mutateAsync({ postcode: trimmed });
|
||||||
|
|
||||||
|
if (result?.status && result?.data) {
|
||||||
|
if (result.data.hasError) {
|
||||||
|
setInquiryData(null);
|
||||||
|
showToast(result.data.errorMessage || "نتیجهای یافت نشد", "info");
|
||||||
|
} else {
|
||||||
|
setInquiryData(result.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setInquiryData(null);
|
||||||
|
showToast("نتیجهای یافت نشد", "info");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Postcode inquiry error:", error);
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message || "خطا در استعلام کد پستی",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
setInquiryData(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePostcodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const numericValue = value.replace(/\D/g, "").slice(0, 10);
|
||||||
|
setPostcode(numericValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container column className="gap-4 p-4 max-w-6xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.1 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
column
|
||||||
|
className="gap-4 p-6 bg-white dark:bg-dark-800 rounded-xl shadow-lg border border-gray-200 dark:border-dark-600"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-bold mb-2"
|
||||||
|
>
|
||||||
|
استعلام کد پستی
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container className="gap-4 items-end">
|
||||||
|
<Grid container className="flex-1" column>
|
||||||
|
<Textfield
|
||||||
|
placeholder="کد پستی (10 رقم)"
|
||||||
|
value={postcode}
|
||||||
|
onChange={handlePostcodeChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={inquiryMutation.isPending}
|
||||||
|
className="px-6 py-2.5"
|
||||||
|
>
|
||||||
|
{inquiryMutation.isPending ? "در حال جستجو..." : "جستجو"}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{inquiryData && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<div className="bg-white dark:bg-dark-800 rounded-xl shadow-lg border border-gray-200 dark:border-dark-600 p-6">
|
||||||
|
<Grid container column className="gap-4">
|
||||||
|
<div className="flex items-start justify-between pb-4 border-b border-gray-200 dark:border-dark-600">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-3 bg-primary-100 dark:bg-primary-900/30 rounded-lg">
|
||||||
|
<MapPinIcon className="w-6 h-6 text-primary-600 dark:text-primary-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-bold"
|
||||||
|
>
|
||||||
|
نتیجه استعلام کد پستی
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-600 dark:text-gray-400 mt-1"
|
||||||
|
>
|
||||||
|
کد پستی: {inquiryData.postCode}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{inquiryData.address && (
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg md:col-span-2">
|
||||||
|
<MapPinIcon className="w-5 h-5 text-gray-600 dark:text-gray-400 shrink-0 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-500 dark:text-gray-400 text-xs mb-1"
|
||||||
|
>
|
||||||
|
آدرس
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium"
|
||||||
|
>
|
||||||
|
{inquiryData.address}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inquiryData.buildingName && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<BuildingOfficeIcon className="w-5 h-5 text-gray-600 dark:text-gray-400 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-500 dark:text-gray-400 text-xs"
|
||||||
|
>
|
||||||
|
نام ساختمان
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium"
|
||||||
|
>
|
||||||
|
{inquiryData.buildingName}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inquiryData.state && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<MapPinIcon className="w-5 h-5 text-gray-600 dark:text-gray-400 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-500 dark:text-gray-400 text-xs"
|
||||||
|
>
|
||||||
|
استان
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium"
|
||||||
|
>
|
||||||
|
{inquiryData.state}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inquiryData.townShip && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<MapPinIcon className="w-5 h-5 text-gray-600 dark:text-gray-400 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-500 dark:text-gray-400 text-xs"
|
||||||
|
>
|
||||||
|
شهرستان
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium"
|
||||||
|
>
|
||||||
|
{inquiryData.townShip}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inquiryData.locationCode && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<IdentificationIcon className="w-5 h-5 text-gray-600 dark:text-gray-400 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-500 dark:text-gray-400 text-xs"
|
||||||
|
>
|
||||||
|
کد شهر
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium"
|
||||||
|
>
|
||||||
|
{inquiryData.locationCode}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostcodeInquiry;
|
||||||
@@ -12,6 +12,7 @@ import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButt
|
|||||||
import { getFaPermissions } from "../utils/getFaPermissions";
|
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||||
import { getFaProvince } from "../utils/getFaProvince";
|
import { getFaProvince } from "../utils/getFaProvince";
|
||||||
import { getFaCityName } from "../utils/getFaCityName";
|
import { getFaCityName } from "../utils/getFaCityName";
|
||||||
|
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||||
|
|
||||||
const Users: React.FC = () => {
|
const Users: React.FC = () => {
|
||||||
const { profile } = useUserProfileStore();
|
const { profile } = useUserProfileStore();
|
||||||
@@ -38,19 +39,32 @@ const Users: React.FC = () => {
|
|||||||
)) || "-",
|
)) || "-",
|
||||||
getFaProvince(item?.province || ""),
|
getFaProvince(item?.province || ""),
|
||||||
getFaCityName(item?.city || ""),
|
getFaCityName(item?.city || ""),
|
||||||
item?.mobile === profile?.mobile ? (
|
|
||||||
<Typography variant="body2" className="text-gray-400">
|
|
||||||
-
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<Popover key={i}>
|
<Popover key={i}>
|
||||||
|
<Tooltip title="ویرایش" position="right">
|
||||||
|
<Button
|
||||||
|
variant="edit"
|
||||||
|
access="add"
|
||||||
|
onClick={() => {
|
||||||
|
openDrawer({
|
||||||
|
title: "ویرایش کاربر",
|
||||||
|
content: (
|
||||||
|
<SubmitNewUser
|
||||||
|
province={profile?.province || ""}
|
||||||
|
onSuccess={refetch}
|
||||||
|
item={item}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
<DeleteButtonForPopOver
|
<DeleteButtonForPopOver
|
||||||
access="add"
|
access="add"
|
||||||
api={`users/${item?._id || item?.Id}`}
|
api={`users/${item?._id || item?.Id}`}
|
||||||
getData={refetch}
|
getData={refetch}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>,
|
||||||
),
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
setTableData(d);
|
setTableData(d);
|
||||||
|
|||||||
91
src/config/menuItems.tsx
Normal file
91
src/config/menuItems.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
UsersIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
ChartBarIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { ItemWithSubItems } from "../types/userPermissions";
|
||||||
|
import { checkMenuPermission } from "../utils/checkMenuPermission";
|
||||||
|
|
||||||
|
export const getInspectionMenuItems = (
|
||||||
|
permissions: string[] = [],
|
||||||
|
): ItemWithSubItems[] => {
|
||||||
|
const items: ItemWithSubItems[] = [];
|
||||||
|
|
||||||
|
if (checkMenuPermission("nationalinfo", permissions)) {
|
||||||
|
items.push({
|
||||||
|
en: "nationalinfo",
|
||||||
|
fa: "اطلاعات",
|
||||||
|
icon: () => <UserIcon className="w-6 h-6" />,
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
name: "nationalinfo",
|
||||||
|
path: "/nationalinfo",
|
||||||
|
component: () => import("../Pages/NationalInfo"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ladinginfo",
|
||||||
|
path: "/ladinginfo",
|
||||||
|
component: () => import("../Pages/LadingInfo"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "veterinarytransfer",
|
||||||
|
path: "/veterinarytransfer",
|
||||||
|
component: () => import("../Pages/VeterinaryTransfer"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "postcodeinquiry",
|
||||||
|
path: "/postcode-inquiry",
|
||||||
|
component: () => import("../Pages/PostcodeInquiry"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkMenuPermission("users", permissions)) {
|
||||||
|
items.push({
|
||||||
|
en: "users",
|
||||||
|
fa: "کاربران",
|
||||||
|
icon: () => <UsersIcon className="w-6 h-6" />,
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
name: "users",
|
||||||
|
path: "/users",
|
||||||
|
component: () => import("../Pages/Users"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkMenuPermission("inspections", permissions)) {
|
||||||
|
items.push({
|
||||||
|
en: "inspections",
|
||||||
|
fa: "سوابق بازرسی",
|
||||||
|
icon: () => <DocumentTextIcon className="w-6 h-6" />,
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
name: "inspections",
|
||||||
|
path: "/inspections",
|
||||||
|
component: () => import("../Pages/UserInspections"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkMenuPermission("statics", permissions)) {
|
||||||
|
items.push({
|
||||||
|
en: "statics",
|
||||||
|
fa: "آمار",
|
||||||
|
icon: () => <ChartBarIcon className="w-6 h-6" />,
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
name: "statics",
|
||||||
|
path: "/statics",
|
||||||
|
component: () => import("../Pages/Statics"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
@@ -5,12 +5,13 @@ import { useDashboardTabStore } from "./dashboardTabStore";
|
|||||||
interface UseUserProfileStore {
|
interface UseUserProfileStore {
|
||||||
profile?: Record<string, any> | null;
|
profile?: Record<string, any> | null;
|
||||||
setUserProfile: (profile: Record<string, any>) => void;
|
setUserProfile: (profile: Record<string, any>) => void;
|
||||||
|
updateProfile: (updates: Partial<Record<string, any>>) => void;
|
||||||
clearProfile: () => void;
|
clearProfile: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const arePermissionsEqual = (
|
const arePermissionsEqual = (
|
||||||
permissions1?: string[],
|
permissions1?: string[],
|
||||||
permissions2?: string[]
|
permissions2?: string[],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!permissions1 && !permissions2) return true;
|
if (!permissions1 && !permissions2) return true;
|
||||||
if (!permissions1 || !permissions2) return false;
|
if (!permissions1 || !permissions2) return false;
|
||||||
@@ -24,7 +25,7 @@ const arePermissionsEqual = (
|
|||||||
|
|
||||||
const areProfilesEqual = (
|
const areProfilesEqual = (
|
||||||
currentProfile: Record<string, any> | null | undefined,
|
currentProfile: Record<string, any> | null | undefined,
|
||||||
newProfile: Record<string, any>
|
newProfile: Record<string, any>,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!currentProfile) return false;
|
if (!currentProfile) return false;
|
||||||
|
|
||||||
@@ -35,10 +36,10 @@ const areProfilesEqual = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentKeys = Object.keys(currentProfile).filter(
|
const currentKeys = Object.keys(currentProfile).filter(
|
||||||
(key) => key !== "permissions"
|
(key) => key !== "permissions",
|
||||||
);
|
);
|
||||||
const newKeys = Object.keys(newProfile).filter(
|
const newKeys = Object.keys(newProfile).filter(
|
||||||
(key) => key !== "permissions"
|
(key) => key !== "permissions",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (currentKeys.length !== newKeys.length) return false;
|
if (currentKeys.length !== newKeys.length) return false;
|
||||||
@@ -72,10 +73,15 @@ export const useUserProfileStore = create<UseUserProfileStore>()(
|
|||||||
console.log("profile", profile);
|
console.log("profile", profile);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateProfile: (updates) => {
|
||||||
|
const currentProfile = get().profile;
|
||||||
|
if (!currentProfile) return;
|
||||||
|
set({ profile: { ...currentProfile, ...updates } });
|
||||||
|
},
|
||||||
clearProfile: () => set({ profile: null }),
|
clearProfile: () => set({ profile: null }),
|
||||||
}),
|
}),
|
||||||
{ name: "userprofile" }
|
{ name: "userprofile" },
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
interface UseUserStore {
|
interface UseUserStore {
|
||||||
@@ -97,6 +103,6 @@ export const useUserStore = create<UseUserStore>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "user",
|
name: "user",
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import { motion, Variants } from "framer-motion";
|
import { motion, Variants } from "framer-motion";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useApiMutation } from "../../utils/useApiRequest";
|
import { useApiMutation } from "../../utils/useApiRequest";
|
||||||
@@ -52,12 +52,6 @@ const Login: React.FC = () => {
|
|||||||
disableBackdrop: false,
|
disableBackdrop: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (phoneNumber && phoneNumber.length >= 10) {
|
|
||||||
handleGetCaptcha();
|
|
||||||
}
|
|
||||||
}, [phoneNumber]);
|
|
||||||
|
|
||||||
const handleGetCaptcha = async () => {
|
const handleGetCaptcha = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await mutationCaptcha.mutateAsync({
|
const data = await mutationCaptcha.mutateAsync({
|
||||||
|
|||||||
@@ -1,17 +1,103 @@
|
|||||||
import Typography from "../../components/Typography/Typography";
|
import Typography from "../../components/Typography/Typography";
|
||||||
import { Grid } from "../../components/Grid/Grid";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.08, delayChildren: 0.05 },
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: { staggerChildren: 0.05, staggerDirection: -1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 12 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
exit: { opacity: 0, y: -8 },
|
||||||
|
};
|
||||||
|
|
||||||
export const MainInfractions = ({
|
export const MainInfractions = ({
|
||||||
data,
|
data,
|
||||||
handleUpdate,
|
handleUpdate: _handleUpdate,
|
||||||
}: {
|
}: {
|
||||||
data: any;
|
data: any;
|
||||||
handleUpdate: () => void;
|
handleUpdate: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
console.log(data, handleUpdate);
|
const infractions = data?.infractions || [];
|
||||||
|
|
||||||
|
if (infractions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Grid container column>
|
<motion.div
|
||||||
<Typography variant="body1">این بخش در دست توسعه است</Typography>
|
initial={{ opacity: 0, scale: 0.96 }}
|
||||||
</Grid>
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="flex flex-col items-center justify-center py-12 px-6 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<CheckCircleIcon className="w-14 h-14 text-emerald-500 dark:text-emerald-400 mb-4" />
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-500 dark:text-gray-400 text-center"
|
||||||
|
>
|
||||||
|
تخلفی ثبت نشده است
|
||||||
|
</Typography>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="exit"
|
||||||
|
className="flex flex-col gap-3"
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{infractions.map(
|
||||||
|
(
|
||||||
|
infraction: { _id: string; title: string; description?: string },
|
||||||
|
index: number,
|
||||||
|
) => (
|
||||||
|
<motion.div
|
||||||
|
key={infraction._id || index}
|
||||||
|
variants={itemVariants}
|
||||||
|
layout
|
||||||
|
className="group flex gap-4 p-4 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/80 hover:bg-gray-50 dark:hover:bg-gray-800 hover:border-red-200 dark:hover:border-red-900/50 shadow-sm hover:shadow-md transition-all duration-200"
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 w-10 h-10 rounded-lg bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
|
||||||
|
<ExclamationTriangleIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className="font-semibold mb-1 text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
{index + 1}. {infraction.title}
|
||||||
|
</Typography>
|
||||||
|
{infraction.description && (
|
||||||
|
<div className="flex items-start gap-2 mt-2">
|
||||||
|
<DocumentTextIcon className="w-4 h-4 text-gray-400 dark:text-gray-500 flex-shrink-0 mt-0.5" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-600 dark:text-gray-400 leading-relaxed"
|
||||||
|
>
|
||||||
|
{infraction.description}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const MainPlaceInfo: React.FC<MainPlaceInfoProps> = ({
|
|||||||
const [personRegistryData, setPersonRegistryData] =
|
const [personRegistryData, setPersonRegistryData] =
|
||||||
useState<PersonRegistryData | null>(null);
|
useState<PersonRegistryData | null>(null);
|
||||||
const [unitRegistryData, setUnitRegistryData] = useState<UnitRegistryData[]>(
|
const [unitRegistryData, setUnitRegistryData] = useState<UnitRegistryData[]>(
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
const [isLoadingRegistry, setIsLoadingRegistry] = useState(false);
|
const [isLoadingRegistry, setIsLoadingRegistry] = useState(false);
|
||||||
const { openDrawer } = useDrawerStore();
|
const { openDrawer } = useDrawerStore();
|
||||||
@@ -201,7 +201,7 @@ const MainPlaceInfo: React.FC<MainPlaceInfoProps> = ({
|
|||||||
const unitsCandidate =
|
const unitsCandidate =
|
||||||
unitResult?.data ?? (Array.isArray(unitResult) ? unitResult : []);
|
unitResult?.data ?? (Array.isArray(unitResult) ? unitResult : []);
|
||||||
setUnitRegistryData(
|
setUnitRegistryData(
|
||||||
Array.isArray(unitsCandidate) ? unitsCandidate : []
|
Array.isArray(unitsCandidate) ? unitsCandidate : [],
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Unit registry error:", error);
|
console.error("Unit registry error:", error);
|
||||||
@@ -320,7 +320,6 @@ const MainPlaceInfo: React.FC<MainPlaceInfoProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Inspection Button */}
|
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleOpenInspection}
|
onClick={handleOpenInspection}
|
||||||
|
|||||||
@@ -93,30 +93,28 @@ const MainSubmitInspection: React.FC<MainSubmitInspectionProps> = ({
|
|||||||
{ key: "عمده فروش", value: "عمده فروش" },
|
{ key: "عمده فروش", value: "عمده فروش" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const usersList = Array.isArray(usersData)
|
||||||
|
? usersData
|
||||||
|
: ((usersData as any)?.data ?? []);
|
||||||
|
|
||||||
const inspectorOptions = useMemo(() => {
|
const inspectorOptions = useMemo(() => {
|
||||||
if (!usersData?.data) return [];
|
if (!usersList?.length) return [];
|
||||||
return usersData.data
|
return usersList
|
||||||
.filter(
|
.filter((user: any) => user.province === profile?.province)
|
||||||
(user: any) =>
|
|
||||||
user.province === profile?.province &&
|
|
||||||
!user?.permissions?.includes("admin")
|
|
||||||
)
|
|
||||||
.map((user: any) => ({
|
.map((user: any) => ({
|
||||||
key: `${user.fullname} / ${user.mobile}`,
|
key: `${user.fullname} / ${user.mobile}`,
|
||||||
value: `${user.fullname} / ${user.mobile}`,
|
value: `${user.fullname} / ${user.mobile}`,
|
||||||
}));
|
}));
|
||||||
}, [usersData, profile]);
|
}, [usersList, profile]);
|
||||||
|
|
||||||
const defaultInspectorKeys = useMemo(() => {
|
const defaultInspectorKeys = useMemo(() => {
|
||||||
if (!isEdit || !item?.inspectors) return [];
|
if (!isEdit || !item?.inspectors) return [];
|
||||||
return item.inspectors.map((insp: any) => {
|
return item.inspectors.map((insp: any) => {
|
||||||
if (typeof insp === "string") return insp;
|
if (typeof insp === "string") return insp;
|
||||||
const user = usersData?.data?.find(
|
const user = usersList?.find((u: any) => u.fullname === insp.fullname);
|
||||||
(u: any) => u.fullname === insp.fullname
|
|
||||||
);
|
|
||||||
return user ? `${insp.fullname} / ${user.mobile}` : insp.fullname;
|
return user ? `${insp.fullname} / ${user.mobile}` : insp.fullname;
|
||||||
});
|
});
|
||||||
}, [item, isEdit, usersData]);
|
}, [item, isEdit, usersList]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -150,17 +148,15 @@ const MainSubmitInspection: React.FC<MainSubmitInspectionProps> = ({
|
|||||||
const watchedInfractions = watch("infractions");
|
const watchedInfractions = watch("infractions");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit && item?.inspectors && usersData?.data) {
|
if (isEdit && item?.inspectors && usersList?.length) {
|
||||||
const inspectorStrings = item.inspectors.map((insp: any) => {
|
const inspectorStrings = item.inspectors.map((insp: any) => {
|
||||||
if (typeof insp === "string") return insp;
|
if (typeof insp === "string") return insp;
|
||||||
const user = usersData.data.find(
|
const user = usersList.find((u: any) => u.fullname === insp.fullname);
|
||||||
(u: any) => u.fullname === insp.fullname
|
|
||||||
);
|
|
||||||
return user ? `${insp.fullname} / ${user.mobile}` : insp.fullname;
|
return user ? `${insp.fullname} / ${user.mobile}` : insp.fullname;
|
||||||
});
|
});
|
||||||
setValue("inspectors", inspectorStrings);
|
setValue("inspectors", inspectorStrings);
|
||||||
}
|
}
|
||||||
}, [item, isEdit, usersData, setValue]);
|
}, [item, isEdit, usersList, setValue]);
|
||||||
|
|
||||||
const submitInspectionMutation = useApiMutation({
|
const submitInspectionMutation = useApiMutation({
|
||||||
api: "inspections",
|
api: "inspections",
|
||||||
|
|||||||
@@ -17,25 +17,42 @@ import {
|
|||||||
zValidateAutoComplete,
|
zValidateAutoComplete,
|
||||||
} from "../../data/getFormTypeErrors";
|
} from "../../data/getFormTypeErrors";
|
||||||
|
|
||||||
|
interface UserItem {
|
||||||
|
_id?: string;
|
||||||
|
Id?: string;
|
||||||
|
mobile?: string;
|
||||||
|
fullname?: string;
|
||||||
|
permissions?: string[];
|
||||||
|
province?: string;
|
||||||
|
city?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface SubmitNewUserProps {
|
interface SubmitNewUserProps {
|
||||||
province: string;
|
province: string;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
|
item?: UserItem | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = z.object({
|
const getSchema = (isEdit: boolean) =>
|
||||||
|
z.object({
|
||||||
mobile: zValidateMobile("موبایل"),
|
mobile: zValidateMobile("موبایل"),
|
||||||
password: zValidateString("کلمه عبور"),
|
password: isEdit ? z.string().optional() : zValidateString("کلمه عبور"),
|
||||||
fullname: zValidateString("نام کامل"),
|
fullname: zValidateString("نام کامل"),
|
||||||
permissions: zValidateAutoComplete("دسترسیها"),
|
permissions: zValidateAutoComplete("دسترسیها"),
|
||||||
city: zValidateAutoComplete("شهر"),
|
city: zValidateAutoComplete("شهر"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<ReturnType<typeof getSchema>>;
|
||||||
|
|
||||||
export const SubmitNewUser: React.FC<SubmitNewUserProps> = ({ onSuccess }) => {
|
export const SubmitNewUser: React.FC<SubmitNewUserProps> = ({
|
||||||
|
onSuccess,
|
||||||
|
item,
|
||||||
|
}) => {
|
||||||
const { profile } = useUserProfileStore();
|
const { profile } = useUserProfileStore();
|
||||||
const showToast = useToast();
|
const showToast = useToast();
|
||||||
const { closeDrawer } = useDrawerStore();
|
const { closeDrawer } = useDrawerStore();
|
||||||
|
const isEdit = !!item;
|
||||||
|
const schema = useMemo(() => getSchema(isEdit), [isEdit]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -44,7 +61,15 @@ export const SubmitNewUser: React.FC<SubmitNewUserProps> = ({ onSuccess }) => {
|
|||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<FormValues>({
|
} = useForm<FormValues>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: item
|
||||||
|
? {
|
||||||
|
mobile: item.mobile ?? "",
|
||||||
|
password: "",
|
||||||
|
fullname: item.fullname ?? "",
|
||||||
|
permissions: Array.isArray(item.permissions) ? item.permissions : [],
|
||||||
|
city: item.city ? [item.city] : [],
|
||||||
|
}
|
||||||
|
: {
|
||||||
mobile: "",
|
mobile: "",
|
||||||
password: "",
|
password: "",
|
||||||
fullname: "",
|
fullname: "",
|
||||||
@@ -53,11 +78,16 @@ export const SubmitNewUser: React.FC<SubmitNewUserProps> = ({ onSuccess }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitUserMutation = useApiMutation({
|
const createUserMutation = useApiMutation({
|
||||||
api: "user",
|
api: "user",
|
||||||
method: "post",
|
method: "post",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateUserMutation = useApiMutation({
|
||||||
|
api: `user/${item?._id ?? item?.Id ?? ""}`,
|
||||||
|
method: "put",
|
||||||
|
});
|
||||||
|
|
||||||
const cityOptions = useMemo(() => {
|
const cityOptions = useMemo(() => {
|
||||||
return getCitiesOfProvinceInfo(profile?.province || "").map((item) => ({
|
return getCitiesOfProvinceInfo(profile?.province || "").map((item) => ({
|
||||||
key: item.en,
|
key: item.en,
|
||||||
@@ -65,18 +95,23 @@ export const SubmitNewUser: React.FC<SubmitNewUserProps> = ({ onSuccess }) => {
|
|||||||
}));
|
}));
|
||||||
}, [profile?.province]);
|
}, [profile?.province]);
|
||||||
|
|
||||||
|
const hasAdminPermission = profile?.permissions?.includes("admin");
|
||||||
|
|
||||||
const permissionOptions = useMemo(() => {
|
const permissionOptions = useMemo(() => {
|
||||||
return [
|
const options = [
|
||||||
{ key: "add", value: "ثبت کاربر" },
|
{ key: "add", value: "ثبت کاربر" },
|
||||||
{ key: "submit", value: "ثبت بازرسی" },
|
{ key: "submit", value: "ثبت بازرسی" },
|
||||||
];
|
];
|
||||||
}, []);
|
if (hasAdminPermission) {
|
||||||
|
options.push({ key: "admin", value: "ادمین" });
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}, [hasAdminPermission]);
|
||||||
|
|
||||||
const onSubmit = async (data: FormValues) => {
|
const onSubmit = async (data: FormValues) => {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const basePayload = {
|
||||||
mobile: data.mobile,
|
mobile: data.mobile,
|
||||||
password: data.password,
|
|
||||||
fullname: data.fullname,
|
fullname: data.fullname,
|
||||||
pic: "",
|
pic: "",
|
||||||
permissions: data.permissions as string[],
|
permissions: data.permissions as string[],
|
||||||
@@ -87,15 +122,34 @@ export const SubmitNewUser: React.FC<SubmitNewUserProps> = ({ onSuccess }) => {
|
|||||||
: "",
|
: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
await submitUserMutation.mutateAsync(payload);
|
if (isEdit) {
|
||||||
|
const payload =
|
||||||
|
data.password && String(data.password).trim() !== ""
|
||||||
|
? { ...basePayload, password: data.password }
|
||||||
|
: basePayload;
|
||||||
|
await updateUserMutation.mutateAsync(payload);
|
||||||
|
showToast("کاربر با موفقیت ویرایش شد", "success");
|
||||||
|
} else {
|
||||||
|
if (!data.password || String(data.password).trim() === "") {
|
||||||
|
showToast("کلمه عبور را وارد کنید", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = { ...basePayload, password: data.password };
|
||||||
|
await createUserMutation.mutateAsync(payload);
|
||||||
showToast("کاربر با موفقیت ثبت شد", "success");
|
showToast("کاربر با موفقیت ثبت شد", "success");
|
||||||
|
}
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess();
|
onSuccess();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
showToast("مشکلی پیش آمده است، ممکن است کاربر تکراری باشد!", "error");
|
showToast(
|
||||||
|
isEdit
|
||||||
|
? "مشکلی پیش آمده است!"
|
||||||
|
: "مشکلی پیش آمده است، ممکن است کاربر تکراری باشد!",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,9 +252,13 @@ export const SubmitNewUser: React.FC<SubmitNewUserProps> = ({ onSuccess }) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="submit"
|
variant="submit"
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={isSubmitting || submitUserMutation.isPending}
|
disabled={
|
||||||
|
isSubmitting ||
|
||||||
|
createUserMutation.isPending ||
|
||||||
|
updateUserMutation.isPending
|
||||||
|
}
|
||||||
>
|
>
|
||||||
ثبت کاربر
|
{isEdit ? "ذخیره تغییرات" : "ثبت کاربر"}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export const AUTO_LOGIN_WITH_PARAMS = "/autologin/:key/:province";
|
|||||||
export const NATIONAL_INFO = "/nationalinfo";
|
export const NATIONAL_INFO = "/nationalinfo";
|
||||||
export const LADING_INFO = "/ladinginfo";
|
export const LADING_INFO = "/ladinginfo";
|
||||||
export const VETERINARY_TRANSFER = "/veterinarytransfer";
|
export const VETERINARY_TRANSFER = "/veterinarytransfer";
|
||||||
|
export const POSTCODE_INQUIRY = "/postcode-inquiry";
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import AutoLogin from "../Pages/AutoLogin";
|
|||||||
import NationalInfo from "../Pages/NationalInfo";
|
import NationalInfo from "../Pages/NationalInfo";
|
||||||
import LadingInfo from "../Pages/LadingInfo";
|
import LadingInfo from "../Pages/LadingInfo";
|
||||||
import VeterinaryTransfer from "../Pages/VeterinaryTransfer";
|
import VeterinaryTransfer from "../Pages/VeterinaryTransfer";
|
||||||
|
import PostcodeInquiry from "../Pages/PostcodeInquiry";
|
||||||
|
|
||||||
interface Route {
|
interface Route {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -30,6 +31,7 @@ export const getRoutes = (auth: string | null): Route[] => {
|
|||||||
{ path: R.NATIONAL_INFO, component: NationalInfo },
|
{ path: R.NATIONAL_INFO, component: NationalInfo },
|
||||||
{ path: R.LADING_INFO, component: LadingInfo },
|
{ path: R.LADING_INFO, component: LadingInfo },
|
||||||
{ path: R.VETERINARY_TRANSFER, component: VeterinaryTransfer },
|
{ path: R.VETERINARY_TRANSFER, component: VeterinaryTransfer },
|
||||||
|
{ path: R.POSTCODE_INQUIRY, component: PostcodeInquiry },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (checkIsMobile()) {
|
if (checkIsMobile()) {
|
||||||
|
|||||||
@@ -17,15 +17,7 @@ export function makeRouter(auth: string | null) {
|
|||||||
try {
|
try {
|
||||||
const routeConfigs = getRoutes(auth);
|
const routeConfigs = getRoutes(auth);
|
||||||
|
|
||||||
console.log(
|
|
||||||
"Creating router with routes:",
|
|
||||||
routeConfigs.length,
|
|
||||||
"auth:",
|
|
||||||
!!auth
|
|
||||||
);
|
|
||||||
|
|
||||||
if (routeConfigs.length === 0) {
|
if (routeConfigs.length === 0) {
|
||||||
console.log("No routes found, adding default Auth route");
|
|
||||||
routeConfigs.push({ path: "/", component: Auth });
|
routeConfigs.push({ path: "/", component: Auth });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +49,6 @@ export function makeRouter(auth: string | null) {
|
|||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Router created successfully");
|
|
||||||
return router;
|
return router;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating router:", error);
|
console.error("Error creating router:", error);
|
||||||
|
|||||||
@@ -4,24 +4,25 @@ import { useState } from "react";
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||||
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||||
import { ItemWithSubItems } from "../types/userPermissions";
|
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { getFaPermissions } from "../utils/getFaPermissions";
|
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||||
import { useSideBarStore } from "../context/zustand-store/appStore";
|
import { useSideBarStore } from "../context/zustand-store/appStore";
|
||||||
import SVGImage from "../components/SvgImage/SvgImage";
|
import SVGImage from "../components/SvgImage/SvgImage";
|
||||||
import { checkMenuPermission } from "../utils/checkMenuPermission";
|
import { getInspectionMenuItems } from "../config/menuItems";
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
BuildingOfficeIcon,
|
BuildingOfficeIcon,
|
||||||
UsersIcon,
|
|
||||||
DocumentTextIcon,
|
|
||||||
ChartBarIcon,
|
|
||||||
UserIcon,
|
|
||||||
GlobeAsiaAustraliaIcon,
|
GlobeAsiaAustraliaIcon,
|
||||||
|
MapPinIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
const ADMIN_PROVINCES = [
|
||||||
|
{ value: "hamedan", label: "همدان" },
|
||||||
|
{ value: "markazi", label: "مرکزی" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: {},
|
hidden: {},
|
||||||
visible: { transition: { staggerChildren: 0.03 } },
|
visible: { transition: { staggerChildren: 0.03 } },
|
||||||
@@ -69,91 +70,14 @@ const sidebarVariants = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getInspectionMenuItems = (
|
|
||||||
permissions: string[] = []
|
|
||||||
): ItemWithSubItems[] => {
|
|
||||||
const items: ItemWithSubItems[] = [];
|
|
||||||
|
|
||||||
if (checkMenuPermission("nationalinfo", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "nationalinfo",
|
|
||||||
fa: "اطلاعات",
|
|
||||||
icon: () => <UserIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "nationalinfo",
|
|
||||||
path: "/nationalinfo",
|
|
||||||
component: () => import("../Pages/NationalInfo"),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "ladinginfo",
|
|
||||||
path: "/ladinginfo",
|
|
||||||
component: () => import("../Pages/LadingInfo"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "veterinarytransfer",
|
|
||||||
path: "/veterinarytransfer",
|
|
||||||
component: () => import("../Pages/VeterinaryTransfer"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkMenuPermission("users", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "users",
|
|
||||||
fa: "کاربران",
|
|
||||||
icon: () => <UsersIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "users",
|
|
||||||
path: "/users",
|
|
||||||
component: () => import("../Pages/Users"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkMenuPermission("inspections", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "inspections",
|
|
||||||
fa: "سوابق بازرسی",
|
|
||||||
icon: () => <DocumentTextIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "inspections",
|
|
||||||
path: "/inspections",
|
|
||||||
component: () => import("../Pages/UserInspections"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkMenuPermission("statics", permissions)) {
|
|
||||||
items.push({
|
|
||||||
en: "statics",
|
|
||||||
fa: "آمار",
|
|
||||||
icon: () => <ChartBarIcon className="w-6 h-6" />,
|
|
||||||
subItems: [
|
|
||||||
{
|
|
||||||
name: "statics",
|
|
||||||
path: "/statics",
|
|
||||||
component: () => import("../Pages/Statics"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SideBar = () => {
|
export const SideBar = () => {
|
||||||
const isMobile = checkIsMobile();
|
const isMobile = checkIsMobile();
|
||||||
const { profile } = useUserProfileStore();
|
const { profile, updateProfile } = useUserProfileStore();
|
||||||
const menuItems: ItemWithSubItems[] = getInspectionMenuItems(
|
const menuItems = getInspectionMenuItems(profile?.permissions || []);
|
||||||
profile?.permissions || []
|
const hasAdmin =
|
||||||
);
|
Array.isArray(profile?.permissions) &&
|
||||||
|
profile.permissions.includes("admin");
|
||||||
|
const currentProvince = profile?.province || "hamedan";
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const { isSideBarOpen, toggleSideBar } = useSideBarStore();
|
const { isSideBarOpen, toggleSideBar } = useSideBarStore();
|
||||||
@@ -161,7 +85,7 @@ export const SideBar = () => {
|
|||||||
const getOpenedItem = () => {
|
const getOpenedItem = () => {
|
||||||
if (window.location.pathname !== "/") {
|
if (window.location.pathname !== "/") {
|
||||||
const matchedIndex = menuItems.findIndex((item) =>
|
const matchedIndex = menuItems.findIndex((item) =>
|
||||||
item.subItems.some((sub) => sub.path === window.location.pathname)
|
item.subItems.some((sub) => sub.path === window.location.pathname),
|
||||||
);
|
);
|
||||||
return matchedIndex;
|
return matchedIndex;
|
||||||
} else {
|
} else {
|
||||||
@@ -180,7 +104,7 @@ export const SideBar = () => {
|
|||||||
getFaPermissions(subItem.name)
|
getFaPermissions(subItem.name)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(search.toLowerCase()) ||
|
.includes(search.toLowerCase()) ||
|
||||||
subItem.path.toLowerCase().includes(search.toLowerCase())
|
subItem.path.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -191,7 +115,7 @@ export const SideBar = () => {
|
|||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.subItems.length > 0 ||
|
item.subItems.length > 0 ||
|
||||||
item.fa.toLowerCase().includes(search.toLowerCase())
|
item.fa.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) return null;
|
if (isMobile) return null;
|
||||||
@@ -287,6 +211,30 @@ export const SideBar = () => {
|
|||||||
<span>مشاهده نقشه</span>
|
<span>مشاهده نقشه</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
|
{isSideBarOpen && hasAdmin && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
<MapPinIcon className="w-4 h-4 shrink-0" />
|
||||||
|
<span>استان</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{ADMIN_PROVINCES.map(({ value, label }) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateProfile({ province: value })}
|
||||||
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||||
|
currentProvince === value
|
||||||
|
? "bg-primary-500 text-white dark:bg-primary-600 shadow-md"
|
||||||
|
: "bg-gray-100 dark:bg-dark-600 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-dark-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ api.interceptors.request.use(
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
(error) => Promise.reject(error),
|
||||||
);
|
);
|
||||||
|
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
@@ -50,12 +50,13 @@ api.interceptors.response.use(
|
|||||||
rtl: true,
|
rtl: true,
|
||||||
});
|
});
|
||||||
if (typeof logOut === "function") {
|
if (typeof logOut === "function") {
|
||||||
|
window.location.href = "/";
|
||||||
logOut();
|
logOut();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const checkMenuPermission = (
|
export const checkMenuPermission = (
|
||||||
menuItemName: string,
|
menuItemName: string,
|
||||||
userPermissions: string[] = []
|
userPermissions: string[] = [],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (userPermissions.includes("admin")) {
|
if (userPermissions.includes("admin")) {
|
||||||
return true;
|
return true;
|
||||||
@@ -24,7 +24,7 @@ export const checkMenuPermission = (
|
|||||||
|
|
||||||
export const checkRoutePermission = (
|
export const checkRoutePermission = (
|
||||||
routePath: string,
|
routePath: string,
|
||||||
userPermissions: string[] = []
|
userPermissions: string[] = [],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (userPermissions.includes("admin")) {
|
if (userPermissions.includes("admin")) {
|
||||||
return true;
|
return true;
|
||||||
@@ -35,6 +35,7 @@ export const checkRoutePermission = (
|
|||||||
"/inspections": ["admin", "submit"],
|
"/inspections": ["admin", "submit"],
|
||||||
"/statics": [],
|
"/statics": [],
|
||||||
"/nationalinfo": ["admin"],
|
"/nationalinfo": ["admin"],
|
||||||
|
"/postcode-inquiry": ["admin"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const requiredPermissions = routePermissionMap[routePath] || [];
|
const requiredPermissions = routePermissionMap[routePath] || [];
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
export function getFaPermissions(permission: string) {
|
export function getFaPermissions(permission: string) {
|
||||||
let faPermission = "";
|
let faPermission = "";
|
||||||
switch (permission) {
|
switch (permission) {
|
||||||
|
case "admin":
|
||||||
|
faPermission = "ادمین";
|
||||||
|
break;
|
||||||
case "users":
|
case "users":
|
||||||
faPermission = "کاربران";
|
faPermission = "کاربران";
|
||||||
break;
|
break;
|
||||||
@@ -19,6 +22,9 @@ export function getFaPermissions(permission: string) {
|
|||||||
case "veterinarytransfer":
|
case "veterinarytransfer":
|
||||||
faPermission = "اطلاعات گواهی حمل";
|
faPermission = "اطلاعات گواهی حمل";
|
||||||
break;
|
break;
|
||||||
|
case "postcodeinquiry":
|
||||||
|
faPermission = "استعلام کد پستی";
|
||||||
|
break;
|
||||||
case "main":
|
case "main":
|
||||||
faPermission = "صفحه اصلی";
|
faPermission = "صفحه اصلی";
|
||||||
break;
|
break;
|
||||||
@@ -28,9 +34,7 @@ export function getFaPermissions(permission: string) {
|
|||||||
case "submit":
|
case "submit":
|
||||||
faPermission = "ثبت بازرسی";
|
faPermission = "ثبت بازرسی";
|
||||||
break;
|
break;
|
||||||
case "admin":
|
|
||||||
faPermission = "مدیر";
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
faPermission = permission;
|
faPermission = permission;
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user