Compare commits

...

2 Commits

Author SHA1 Message Date
e51f998a91 feat: postal inquiry 2026-02-03 14:59:54 +03:30
67517e99c8 fix: ionspection add users and infractions 2026-02-03 12:31:56 +03:30
9 changed files with 417 additions and 34 deletions

View 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;

View File

@@ -33,6 +33,11 @@ export const getInspectionMenuItems = (
path: "/veterinarytransfer", path: "/veterinarytransfer",
component: () => import("../Pages/VeterinaryTransfer"), component: () => import("../Pages/VeterinaryTransfer"),
}, },
{
name: "postcodeinquiry",
path: "/postcode-inquiry",
component: () => import("../Pages/PostcodeInquiry"),
},
], ],
}); });
} }

View File

@@ -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 (
<motion.div
initial={{ opacity: 0, scale: 0.96 }}
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 ( return (
<Grid container column> <motion.div
<Typography variant="body1">این بخش در دست توسعه است</Typography> variants={containerVariants}
</Grid> 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>
); );
}; };

View File

@@ -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}

View File

@@ -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",
@@ -226,8 +222,8 @@ const MainSubmitInspection: React.FC<MainSubmitInspectionProps> = ({
(Array.isArray(field.value) (Array.isArray(field.value)
? field.value ? field.value
: field.value : field.value
? [field.value] ? [field.value]
: []) as (string | number)[] : []) as (string | number)[]
} }
onChange={(keys) => setValue("license_type", keys as any)} onChange={(keys) => setValue("license_type", keys as any)}
title="نوع پروانه کسب" title="نوع پروانه کسب"
@@ -307,8 +303,8 @@ const MainSubmitInspection: React.FC<MainSubmitInspectionProps> = ({
(Array.isArray(field.value) (Array.isArray(field.value)
? field.value ? field.value
: field.value : field.value
? [field.value] ? [field.value]
: []) as (string | number)[] : []) as (string | number)[]
} }
onChange={(keys) => setValue("ownership_type", keys as any)} onChange={(keys) => setValue("ownership_type", keys as any)}
title="نوع مالکیت" title="نوع مالکیت"
@@ -328,8 +324,8 @@ const MainSubmitInspection: React.FC<MainSubmitInspectionProps> = ({
(Array.isArray(field.value) (Array.isArray(field.value)
? field.value ? field.value
: field.value : field.value
? [field.value] ? [field.value]
: []) as (string | number)[] : []) as (string | number)[]
} }
onChange={(keys) => setValue("unit_type", keys as any)} onChange={(keys) => setValue("unit_type", keys as any)}
title="نوع واحد" title="نوع واحد"

View File

@@ -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";

View File

@@ -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()) {

View File

@@ -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] || [];

View File

@@ -22,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;