first commit
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
rasaddam:
|
||||||
|
build: .
|
||||||
|
image: wixarm/inspection:latest
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
16
dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM ghcr.io/eic/node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
RUN ls -la
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["npx", "vite", "preview", "--host", "0.0.0.0", "--port", "3000"]
|
||||||
29
eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import pluginReact from "eslint-plugin-react";
|
||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||||
|
plugins: { js },
|
||||||
|
extends: ["js/recommended"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||||
|
languageOptions: { globals: globals.browser },
|
||||||
|
},
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
pluginReact.configs.flat.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"@typescript-eslint/no-unused-expressions": "off",
|
||||||
|
"prefer-const": "off",
|
||||||
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/src/assets/images/fav.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>سامانه بازرسی</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body dir="rtl" class="bg-white dark:bg-dark-900">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
7641
package-lock.json
generated
Normal file
50
package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "inspection_frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
|
"@tailwindcss/vite": "^4.1.5",
|
||||||
|
"@tanstack/react-query": "^5.76.1",
|
||||||
|
"@tanstack/react-router": "^1.119.0",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns-jalali": "^4.1.0-0",
|
||||||
|
"framer-motion": "^12.10.5",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
|
"jalali-moment": "^3.3.11",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"lottie-react": "^2.4.1",
|
||||||
|
"maplibre-gl": "^5.15.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-hook-form": "^7.56.4",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
|
"react-map-gl": "^8.1.0",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
|
"zod": "^3.25.28",
|
||||||
|
"zustand": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.28.0",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
|
"@types/react": "^19.1.4",
|
||||||
|
"@types/react-dom": "^19.1.5",
|
||||||
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"eslint": "^9.28.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"globals": "^16.2.0",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.33.0",
|
||||||
|
"vite": "^6.3.1",
|
||||||
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
34
src/App.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
@import url("./assets/fonts/fonts.css");
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "iranyekan", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
||||||
|
"Helvetica Neue", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
/* zoom: 90%; */
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-family: "iranyekan" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Toastify__toast {
|
||||||
|
font-family: "iranyekan" !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-scrollbar {
|
||||||
|
scrollbar-color: #323232 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-scrollbar {
|
||||||
|
scrollbar-color: #d9d9d9 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
159
src/App.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { useEffect, useMemo, useCallback } from "react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { RouterProvider } from "@tanstack/react-router";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useUserProfileStore,
|
||||||
|
useUserStore,
|
||||||
|
} from "./context/zustand-store/userStore";
|
||||||
|
import { makeRouter } from "./routes/routes";
|
||||||
|
import { useDarkMode } from "./hooks/useDarkMode";
|
||||||
|
import { ItemWithSubItems } from "./types/userPermissions";
|
||||||
|
import { useFetchProfile } from "./hooks/useFetchProfile";
|
||||||
|
import { getInspectionMenuItems } from "./screen/SideBar";
|
||||||
|
|
||||||
|
const versionNumber = "/src/version.txt";
|
||||||
|
import "./index.css";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
import { checkIsMobile } from "./utils/checkIsMobile";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
const auth = useUserStore((s) => s.auth);
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
const { getProfile } = useFetchProfile();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (auth && !profile) {
|
||||||
|
getProfile();
|
||||||
|
}
|
||||||
|
}, [auth, profile, getProfile]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const auth = useUserStore((s) => s.auth);
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
const [isDark] = useDarkMode();
|
||||||
|
|
||||||
|
const menuItems: ItemWithSubItems[] = useMemo(() => {
|
||||||
|
const userPermissions = profile?.permissions || [];
|
||||||
|
|
||||||
|
const permissionsArray = Array.isArray(userPermissions)
|
||||||
|
? userPermissions.filter((p): p is string => typeof p === "string")
|
||||||
|
: [];
|
||||||
|
return getInspectionMenuItems(permissionsArray);
|
||||||
|
}, [profile?.permissions]);
|
||||||
|
|
||||||
|
const router = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const newRouter = makeRouter(auth ?? null);
|
||||||
|
if (!newRouter) {
|
||||||
|
console.error("Router creation returned null");
|
||||||
|
return makeRouter(null);
|
||||||
|
}
|
||||||
|
return newRouter;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Router creation error:", error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return makeRouter(null);
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error("Fallback router creation failed:", fallbackError);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [auth, menuItems]);
|
||||||
|
|
||||||
|
const hardRefresh = useCallback(() => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("refresh", Date.now().toString());
|
||||||
|
window.location.href = url.toString();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const runWhenIdle = useCallback((fn: () => void) => {
|
||||||
|
const ric = (window as any).requestIdleCallback;
|
||||||
|
if (typeof ric === "function") {
|
||||||
|
ric(fn);
|
||||||
|
} else {
|
||||||
|
setTimeout(fn, 300);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let aborted = false;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const checkVersion = () => {
|
||||||
|
if (document.visibilityState !== "visible") return;
|
||||||
|
fetch(versionNumber + `?_=${Date.now()}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
cache: "no-store",
|
||||||
|
})
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then(async (txt) => {
|
||||||
|
if (aborted) return;
|
||||||
|
const latest = txt.trim();
|
||||||
|
const stored = localStorage.getItem("AppVersion");
|
||||||
|
if (latest && latest !== stored) {
|
||||||
|
localStorage.setItem("AppVersion", latest);
|
||||||
|
const clearAndReload = async () => {
|
||||||
|
if ("caches" in window) {
|
||||||
|
const names = await caches.keys();
|
||||||
|
for (const n of names) {
|
||||||
|
await caches.delete(n).catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(hardRefresh, 200);
|
||||||
|
};
|
||||||
|
runWhenIdle(clearAndReload);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
runWhenIdle(checkVersion);
|
||||||
|
return () => {
|
||||||
|
aborted = true;
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [hardRefresh, runWhenIdle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (url.searchParams.has("refresh")) {
|
||||||
|
url.searchParams.delete("refresh");
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
document.title,
|
||||||
|
url.pathname + url.search
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!router) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">در حال بارگذاری...</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDark ? "dark:bg-dark-900 dark-scrollbar" : "bg-white light-scrollbar"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<AppContent />
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
<ToastContainer position="bottom-right" rtl={true} theme="light" />
|
||||||
|
{checkIsMobile() && <div className="h-20"></div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/Pages/Auth.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import Login from "../partials/auth/Login";
|
||||||
|
import authBg from "../assets/images/auth-bg.png";
|
||||||
|
|
||||||
|
export const Auth = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative flex items-center justify-center min-h-screen h-screen overflow-y-auto bg-cover bg-center bg-no-repeat"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${authBg})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundAttachment: "fixed",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-black/10 dark:bg-black/30" />
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="relative z-10 w-full max-w-md px-4 py-4"
|
||||||
|
>
|
||||||
|
<Login />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
12
src/Pages/AutoLogin.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const AutoLogin = () => {
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">ورود خودکار</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
صفحه ورود خودکار - در حال توسعه
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutoLogin;
|
||||||
510
src/Pages/LadingInfo.tsx
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Grid } from "../components/Grid/Grid";
|
||||||
|
import Textfield from "../components/Textfeild/Textfeild";
|
||||||
|
import DatePicker from "../components/date-picker/DatePicker";
|
||||||
|
import { useApiMutation } from "../utils/useApiRequest";
|
||||||
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||||
|
import Typography from "../components/Typography/Typography";
|
||||||
|
import { useToast } from "../hooks/useToast";
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
UserIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import Button from "../components/Button/Button";
|
||||||
|
import moment from "jalali-moment";
|
||||||
|
|
||||||
|
interface KalaItem {
|
||||||
|
gOODHSCODE: number;
|
||||||
|
gOODCODE: number;
|
||||||
|
wEIGHT: number;
|
||||||
|
pACKINGTYPECODE: number;
|
||||||
|
gOODVALUE: number;
|
||||||
|
iRANCODE: string;
|
||||||
|
kALADESC: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LadingData {
|
||||||
|
bARNAMEHID: number;
|
||||||
|
bARNUMBER: number;
|
||||||
|
bARSERIAL: string;
|
||||||
|
iSSUDATE: string;
|
||||||
|
sENDERNAME: string;
|
||||||
|
sENDERLASTNAME: string;
|
||||||
|
sENDERNATIONALCODE: string;
|
||||||
|
sENDERPOSTALCODE: string;
|
||||||
|
rCIEVERNAME: string;
|
||||||
|
rECIEVERLASTNAME: string;
|
||||||
|
rECIEVERNATIONALCODE: string;
|
||||||
|
rECIEVERPOSTALCODE: string;
|
||||||
|
fIRSTDRIVERCARDID: string;
|
||||||
|
sECONDDRIVERCARDID: string;
|
||||||
|
fRIGHTERCARDID: string;
|
||||||
|
lANDERTYPEID: number;
|
||||||
|
lANDERCODE: number;
|
||||||
|
sTATUS: string;
|
||||||
|
cOST: number;
|
||||||
|
kOOTAJNO: number;
|
||||||
|
kOOTAJDATE: string;
|
||||||
|
cUSTOMCODE: string;
|
||||||
|
tRANSITTYPEID: number;
|
||||||
|
cREATEDATE: string;
|
||||||
|
tRACECODE: string;
|
||||||
|
kalaList: KalaItem[];
|
||||||
|
introduceTraceCode: string | null;
|
||||||
|
cmpDesc: string;
|
||||||
|
originCityDesc: string;
|
||||||
|
destCityDesc: string;
|
||||||
|
pLAQUE_ID: string;
|
||||||
|
pLAQUE_SN: string;
|
||||||
|
firstDriverName: string;
|
||||||
|
firstDriverFamily: string;
|
||||||
|
secondDriverName: string;
|
||||||
|
secondDriverFamily: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LadingResponse {
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number;
|
||||||
|
data: LadingData[];
|
||||||
|
apiLogId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LadingInfo: React.FC = () => {
|
||||||
|
const [startDate, setStartDate] = useState("");
|
||||||
|
const [endDate, setEndDate] = useState("");
|
||||||
|
const [postalCode, setPostalCode] = useState("");
|
||||||
|
const [ladingData, setLadingData] = useState<LadingData[]>([]);
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
|
const ladingMutation = useApiMutation<LadingResponse>({
|
||||||
|
api: "ladinginfo",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
const convertToPersianDate = (gregorianDate: string): string => {
|
||||||
|
if (!gregorianDate) return "";
|
||||||
|
const jDate = moment(gregorianDate, "YYYY-MM-DD").locale("fa");
|
||||||
|
return jDate.format("jYYYY/jMM/jDD");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
showToast("لطفا تاریخ شروع و پایان را وارد کنید", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!postalCode) {
|
||||||
|
showToast("لطفا کد پستی را وارد کنید", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postalCode.length !== 10) {
|
||||||
|
showToast("کد پستی باید 10 رقم باشد", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const persianStartDate = convertToPersianDate(startDate);
|
||||||
|
const persianEndDate = convertToPersianDate(endDate);
|
||||||
|
|
||||||
|
const result = await ladingMutation.mutateAsync({
|
||||||
|
start: persianStartDate,
|
||||||
|
end: persianEndDate,
|
||||||
|
postal: postalCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.status && result?.data && result.data.length > 0) {
|
||||||
|
setLadingData(result.data);
|
||||||
|
} else {
|
||||||
|
setLadingData([]);
|
||||||
|
showToast("نتیجهای یافت نشد", "info");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Lading info error:", error);
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message || "خطا در دریافت اطلاعات بارنامه",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
setLadingData([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePostalCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const numericValue = value.replace(/\D/g, "").slice(0, 10);
|
||||||
|
setPostalCode(numericValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20, scale: 0.95 },
|
||||||
|
visible: (i: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
delay: i * 0.1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "easeOut",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.95,
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
<DatePicker
|
||||||
|
label="تاریخ شروع"
|
||||||
|
value={startDate}
|
||||||
|
onChange={setStartDate}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid container className="flex-1" column>
|
||||||
|
<DatePicker
|
||||||
|
label="تاریخ پایان"
|
||||||
|
value={endDate}
|
||||||
|
onChange={setEndDate}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid container className="flex-1" column>
|
||||||
|
<Textfield
|
||||||
|
placeholder="کد پستی"
|
||||||
|
value={postalCode}
|
||||||
|
onChange={handlePostalCodeChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={ladingMutation.isPending}
|
||||||
|
className="px-6 py-2.5"
|
||||||
|
>
|
||||||
|
{ladingMutation.isPending ? "در حال جستجو..." : "جستجو"}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{ladingData.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="mb-4 text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
نتایج جستجو ({ladingData.length})
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container column className="gap-6">
|
||||||
|
{ladingData.map((lading, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={lading.bARNAMEHID}
|
||||||
|
custom={index}
|
||||||
|
variants={cardVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="exit"
|
||||||
|
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 hover:shadow-xl transition-shadow">
|
||||||
|
<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">
|
||||||
|
<DocumentTextIcon 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"
|
||||||
|
>
|
||||||
|
بارنامه ردیف {index + 1}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<CalendarIcon 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"
|
||||||
|
>
|
||||||
|
{lading.iSSUDATE}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<DocumentTextIcon 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"
|
||||||
|
>
|
||||||
|
{lading.bARNUMBER}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{lading.originCityDesc}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{lading.destCityDesc}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<UserIcon 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"
|
||||||
|
>
|
||||||
|
{lading.sENDERNAME} {lading.sENDERLASTNAME}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<UserIcon 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"
|
||||||
|
>
|
||||||
|
{lading.rCIEVERNAME} {lading.rECIEVERLASTNAME}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 break-all"
|
||||||
|
>
|
||||||
|
{lading.tRACECODE}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{lading.pLAQUE_ID}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{lading.pLAQUE_SN}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<UserIcon 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"
|
||||||
|
>
|
||||||
|
{lading.firstDriverName}{" "}
|
||||||
|
{lading.firstDriverFamily}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lading.kalaList && lading.kalaList.length > 0 && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<DocumentTextIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
لیست کالا
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100 dark:bg-dark-700">
|
||||||
|
<th className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
ردیف
|
||||||
|
</th>
|
||||||
|
<th className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
نام کالا
|
||||||
|
</th>
|
||||||
|
<th className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
وزن
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{lading.kalaList.map((kala, idx) => (
|
||||||
|
<tr
|
||||||
|
key={idx}
|
||||||
|
className="hover:bg-gray-50 dark:hover:bg-dark-700"
|
||||||
|
>
|
||||||
|
<td className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-sm text-gray-900 dark:text-gray-100 text-center">
|
||||||
|
{idx + 1}
|
||||||
|
</td>
|
||||||
|
<td className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-sm text-gray-900 dark:text-gray-100 text-center">
|
||||||
|
{kala.kALADESC}
|
||||||
|
</td>
|
||||||
|
<td className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-sm text-gray-900 dark:text-gray-100 text-center">
|
||||||
|
{kala.wEIGHT.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LadingInfo;
|
||||||
1097
src/Pages/MainPage.tsx
Normal file
182
src/Pages/Menu.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||||
|
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
UsersIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
ChartBarIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
UserIcon,
|
||||||
|
GlobeAsiaAustraliaIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { ItemWithSubItems } from "../types/userPermissions";
|
||||||
|
import { checkMenuPermission } from "../utils/checkMenuPermission";
|
||||||
|
|
||||||
|
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("./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 = () => {
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const menuItems = getInspectionMenuItems(profile?.permissions || []);
|
||||||
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const toggleSubmenu = (index: number) => {
|
||||||
|
setOpenIndex((prev) => (prev === index ? null : index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-2xl font-bold mb-6 text-gray-900 dark:text-white">
|
||||||
|
منو
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<GlobeAsiaAustraliaIcon className="w-5 h-5" />
|
||||||
|
<span>مشاهده نقشه</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{menuItems.map(({ fa, icon: Icon, subItems }, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-white dark:bg-dark-700 rounded-lg shadow-sm border border-gray-200 dark:border-dark-600 overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSubmenu(index)}
|
||||||
|
className="w-full flex justify-between items-center px-4 py-3 text-right hover:bg-gray-50 dark:hover:bg-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-gray-600 dark:text-primary-100">
|
||||||
|
<Icon />
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-900 dark:text-white font-semibold">
|
||||||
|
{fa}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-300 ${
|
||||||
|
openIndex === index ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{openIndex === index && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-4 pb-2 space-y-1 border-t border-gray-200 dark:border-dark-600">
|
||||||
|
{subItems
|
||||||
|
.filter((item) => !item?.path.includes("$"))
|
||||||
|
.map((sub, subIndex) => (
|
||||||
|
<button
|
||||||
|
key={subIndex}
|
||||||
|
onClick={() => navigate({ to: sub.path })}
|
||||||
|
className={`w-full text-right px-3 py-2 rounded-lg text-sm transition-colors ${
|
||||||
|
currentPath === sub.path
|
||||||
|
? "bg-primary-100 dark:bg-dark-500 text-primary-700 dark:text-primary-300"
|
||||||
|
: "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-dark-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getFaPermissions(sub.name)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
988
src/Pages/NationalInfo.tsx
Normal file
@@ -0,0 +1,988 @@
|
|||||||
|
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, Variants } from "framer-motion";
|
||||||
|
import Typography from "../components/Typography/Typography";
|
||||||
|
import { useToast } from "../hooks/useToast";
|
||||||
|
import {
|
||||||
|
UserIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
PhoneIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
CreditCardIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
ClipboardDocumentIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import Button from "../components/Button/Button";
|
||||||
|
import Checkbox from "../components/CheckBox/CheckBox";
|
||||||
|
|
||||||
|
interface PersonData {
|
||||||
|
_id: string;
|
||||||
|
nationalcode: string;
|
||||||
|
name: string;
|
||||||
|
family: string;
|
||||||
|
father: string;
|
||||||
|
birthdate: string;
|
||||||
|
mobile: string[];
|
||||||
|
info: string[];
|
||||||
|
card: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
data: PersonData[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonRegistryData {
|
||||||
|
registeryOfficePersonId: number;
|
||||||
|
nationalCode: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
fatherName: string;
|
||||||
|
supervisorNationalCode: string | null;
|
||||||
|
gender: boolean;
|
||||||
|
identityNo: string;
|
||||||
|
identitySeries: string;
|
||||||
|
identitySerial: string;
|
||||||
|
birthDate: string;
|
||||||
|
birthDateUnix: number;
|
||||||
|
isLive: boolean;
|
||||||
|
deathDate: string | null;
|
||||||
|
city: string;
|
||||||
|
vilage: string | null;
|
||||||
|
town: string | null;
|
||||||
|
errorCode: number;
|
||||||
|
errorDescription: string | null;
|
||||||
|
createdOn: string;
|
||||||
|
errorCodeSpecified: boolean | null;
|
||||||
|
birthDateDay: number;
|
||||||
|
birthDateMonth: number;
|
||||||
|
birthDateYear: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersonRegistryResponse {
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number;
|
||||||
|
data: PersonRegistryData;
|
||||||
|
apiLogId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnitRegistryData {
|
||||||
|
counter: number;
|
||||||
|
serviceType: number;
|
||||||
|
title: string;
|
||||||
|
isicname: string;
|
||||||
|
fullname: string;
|
||||||
|
state: string;
|
||||||
|
city: string;
|
||||||
|
address: string;
|
||||||
|
licenseNumber: string;
|
||||||
|
licenseExpireDate: string;
|
||||||
|
licenseType: string;
|
||||||
|
licenseStatus: string;
|
||||||
|
layerTwo: {
|
||||||
|
phonenumber: string;
|
||||||
|
unionName: string;
|
||||||
|
postalcode: string;
|
||||||
|
nationalcode: string;
|
||||||
|
mobilenumber: string;
|
||||||
|
nationalId: string | null;
|
||||||
|
corporationName: string | null;
|
||||||
|
licenseIssueDate: string;
|
||||||
|
jobType: string;
|
||||||
|
hasPartner: string;
|
||||||
|
hasSteward: string;
|
||||||
|
isForeigner: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnitRegistryResponse {
|
||||||
|
status?: boolean;
|
||||||
|
statusCode?: number;
|
||||||
|
data?: UnitRegistryData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const NationalInfo: React.FC = () => {
|
||||||
|
const [searchValue, setSearchValue] = useState("");
|
||||||
|
const [searchResults, setSearchResults] = useState<PersonData[]>([]);
|
||||||
|
const [personRegistryChecked, setPersonRegistryChecked] = useState(false);
|
||||||
|
const [unitRegistryChecked, setUnitRegistryChecked] = useState(false);
|
||||||
|
const [personRegistryData, setPersonRegistryData] =
|
||||||
|
useState<PersonRegistryData | null>(null);
|
||||||
|
const [unitRegistryData, setUnitRegistryData] = useState<UnitRegistryData[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
|
const searchMutation = useApiMutation<ApiResponse>({
|
||||||
|
api: "people_info",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
const personRegistryMutation = useApiMutation<PersonRegistryResponse>({
|
||||||
|
api: "national-documents",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
const unitRegistryMutation = useApiMutation<UnitRegistryResponse>({
|
||||||
|
api: "national-documents",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
const trimmedValue = searchValue.trim();
|
||||||
|
|
||||||
|
if (!trimmedValue) {
|
||||||
|
showToast("لطفا مقدار جستجو را وارد کنید", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchField: "mobile" | "nationalcode";
|
||||||
|
if (trimmedValue.length === 11 && trimmedValue.startsWith("09")) {
|
||||||
|
searchField = "mobile";
|
||||||
|
} else if (trimmedValue.length === 10) {
|
||||||
|
searchField = "nationalcode";
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
"لطفا 10 رقم (کد ملی) یا 11 رقم (شماره موبایل) وارد کنید",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await searchMutation.mutateAsync({
|
||||||
|
searchfield: searchField,
|
||||||
|
value: trimmedValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.data && result.data.length > 0) {
|
||||||
|
setSearchResults(result.data);
|
||||||
|
} else {
|
||||||
|
setSearchResults([]);
|
||||||
|
showToast("نتیجهای یافت نشد", "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
let nationalCodeForRegistry: string | null = null;
|
||||||
|
|
||||||
|
if (trimmedValue.length === 10) {
|
||||||
|
nationalCodeForRegistry = trimmedValue;
|
||||||
|
} else if (result?.data && result.data.length > 0) {
|
||||||
|
const personWithNationalCode = result.data.find(
|
||||||
|
(person) => person.nationalcode && person.nationalcode.length === 10
|
||||||
|
);
|
||||||
|
if (personWithNationalCode) {
|
||||||
|
nationalCodeForRegistry = personWithNationalCode.nationalcode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (personRegistryChecked) {
|
||||||
|
if (!nationalCodeForRegistry) {
|
||||||
|
showToast("کد ملی برای استعلام ثبت احوال یافت نشد", "error");
|
||||||
|
setPersonRegistryData(null);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const personResult = await personRegistryMutation.mutateAsync({
|
||||||
|
type: "person",
|
||||||
|
info: nationalCodeForRegistry,
|
||||||
|
});
|
||||||
|
if (personResult?.status && personResult?.data) {
|
||||||
|
setPersonRegistryData(personResult.data);
|
||||||
|
} else {
|
||||||
|
setPersonRegistryData(null);
|
||||||
|
showToast("نتیجهای از ثبت احوال یافت نشد", "info");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Person registry error:", error);
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message || "خطا در استعلام ثبت احوال",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
setPersonRegistryData(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setPersonRegistryData(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unitRegistryChecked) {
|
||||||
|
if (!nationalCodeForRegistry) {
|
||||||
|
showToast("کد ملی برای استعلام واحد صنفی یافت نشد", "error");
|
||||||
|
setUnitRegistryData([]);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const unitResult = await unitRegistryMutation.mutateAsync({
|
||||||
|
type: "guild",
|
||||||
|
info: nationalCodeForRegistry,
|
||||||
|
});
|
||||||
|
const unitsCandidate =
|
||||||
|
unitResult?.data ?? (Array.isArray(unitResult) ? unitResult : []);
|
||||||
|
setUnitRegistryData(
|
||||||
|
Array.isArray(unitsCandidate) ? unitsCandidate : []
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Unit registry error:", error);
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message || "خطا در استعلام واحد صنفی",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
setUnitRegistryData([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setUnitRegistryData([]);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Search error:", error);
|
||||||
|
showToast(error?.response?.data?.message || "خطا در جستجو", "error");
|
||||||
|
setSearchResults([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const numericValue = value.replace(/\D/g, "").slice(0, 11);
|
||||||
|
setSearchValue(numericValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayName = (person: PersonData) => {
|
||||||
|
if (person.family && person.name.includes(person.family)) {
|
||||||
|
return person.name;
|
||||||
|
}
|
||||||
|
return person.family ? `${person.name} ${person.family}` : person.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20, scale: 0.95 },
|
||||||
|
visible: (i: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
delay: i * 0.1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "easeOut",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.95,
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const useClipboard = () => {
|
||||||
|
const showToast = useToast();
|
||||||
|
const copyToClipboard = async (text: string, label?: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
showToast(label ? `${label} کپی شد` : "کپی شد", "success");
|
||||||
|
} catch {
|
||||||
|
showToast("خطا در کپی کردن به کلیپبورد", "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return copyToClipboard;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CopyIconButton: React.FC<{
|
||||||
|
text: string;
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
}> = ({ text, label, className }) => {
|
||||||
|
const copyToClipboard = useClipboard();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => copyToClipboard(text, label)}
|
||||||
|
title="کپی به کلیپبورد"
|
||||||
|
className={
|
||||||
|
className ??
|
||||||
|
"ml-2 inline-flex items-center justify-center p-1 rounded hover:bg-gray-100 dark:hover:bg-dark-600 transition"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ClipboardDocumentIcon className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Grid container className="gap-2 items-center justify-center">
|
||||||
|
<Grid container className="flex-1" column>
|
||||||
|
<Textfield
|
||||||
|
placeholder="کد ملی یا شماره موبایل"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={
|
||||||
|
searchMutation.isPending ||
|
||||||
|
personRegistryMutation.isPending ||
|
||||||
|
unitRegistryMutation.isPending
|
||||||
|
}
|
||||||
|
className="px-6 py-2.5"
|
||||||
|
>
|
||||||
|
{searchMutation.isPending ||
|
||||||
|
personRegistryMutation.isPending ||
|
||||||
|
unitRegistryMutation.isPending
|
||||||
|
? "در حال جستجو..."
|
||||||
|
: "جستجو"}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-500 dark:text-gray-400 text-xs"
|
||||||
|
>
|
||||||
|
شماره موبایل باید با 09 آغاز شود
|
||||||
|
</Typography>
|
||||||
|
<Grid container className="gap-4 justify-center">
|
||||||
|
<Checkbox
|
||||||
|
label="استعلام از ثبت احوال"
|
||||||
|
checked={personRegistryChecked}
|
||||||
|
onChange={(e) => setPersonRegistryChecked(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="استعلام واحد صنفی"
|
||||||
|
checked={unitRegistryChecked}
|
||||||
|
onChange={(e) => setUnitRegistryChecked(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{searchResults.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="mb-4 text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
نتایج جستجو ({searchResults.length})
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container column className="gap-4">
|
||||||
|
{searchResults.map((person, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={person._id}
|
||||||
|
custom={index}
|
||||||
|
variants={cardVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="exit"
|
||||||
|
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 hover:shadow-xl transition-shadow">
|
||||||
|
<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">
|
||||||
|
<UserIcon 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 flex items-center"
|
||||||
|
>
|
||||||
|
{getDisplayName(person)}
|
||||||
|
{/* کپی نام کامل */}
|
||||||
|
<CopyIconButton
|
||||||
|
text={getDisplayName(person)}
|
||||||
|
label="نام"
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
{person.father && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-600 dark:text-gray-400 mt-1 flex items-center"
|
||||||
|
>
|
||||||
|
فرزند {person.father}
|
||||||
|
<CopyIconButton
|
||||||
|
text={person.father}
|
||||||
|
label="نام پدر"
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium"
|
||||||
|
>
|
||||||
|
{person.nationalcode}
|
||||||
|
</Typography>
|
||||||
|
{person.nationalcode && (
|
||||||
|
<CopyIconButton
|
||||||
|
text={person.nationalcode}
|
||||||
|
label="کد ملی"
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{person.birthdate && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<CalendarIcon 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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium"
|
||||||
|
>
|
||||||
|
{person.birthdate}
|
||||||
|
</Typography>
|
||||||
|
<CopyIconButton
|
||||||
|
text={person.birthdate}
|
||||||
|
label="تاریخ تولد"
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{person.mobile && person.mobile.length > 0 && (
|
||||||
|
<div className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<PhoneIcon 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>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{person.mobile.map((mobile, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-1 bg-primary-50 dark:bg-primary-900/20 text-primary-700 dark:text-primary-300 rounded text-sm font-medium"
|
||||||
|
>
|
||||||
|
{mobile}
|
||||||
|
<CopyIconButton
|
||||||
|
text={mobile}
|
||||||
|
label="شماره تلفن"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{person.card && person.card.length > 0 && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<CreditCardIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
کارت های بانکی
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{person.card.map((card, idx) => (
|
||||||
|
<motion.span
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
className="inline-flex items-center gap-1 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 rounded-lg text-sm font-mono border border-blue-200 dark:border-blue-800"
|
||||||
|
>
|
||||||
|
{card}
|
||||||
|
<CopyIconButton
|
||||||
|
text={card}
|
||||||
|
label="شماره کارت"
|
||||||
|
/>
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{person.info && person.info.length > 0 && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<InformationCircleIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
اطلاعات جمع آوری شده توسط هوش مصنوعی
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{person.info.map((info, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg border-r-4 border-primary-500 flex items-start justify-between"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
{info}
|
||||||
|
</Typography>
|
||||||
|
<CopyIconButton
|
||||||
|
text={info}
|
||||||
|
label="اطلاعات"
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{personRegistryData && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="mb-4 text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
استعلام از ثبت احوال
|
||||||
|
</Typography>
|
||||||
|
<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">
|
||||||
|
<UserIcon 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"
|
||||||
|
>
|
||||||
|
{personRegistryData.firstName}{" "}
|
||||||
|
{personRegistryData.lastName}
|
||||||
|
</Typography>
|
||||||
|
{personRegistryData.fatherName && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-600 dark:text-gray-400 mt-1"
|
||||||
|
>
|
||||||
|
فرزند {personRegistryData.fatherName}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{personRegistryData.nationalCode}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{personRegistryData.birthDate && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<CalendarIcon 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"
|
||||||
|
>
|
||||||
|
{personRegistryData.birthDate}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{personRegistryData.city && (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<InformationCircleIcon 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"
|
||||||
|
>
|
||||||
|
{personRegistryData.city}
|
||||||
|
{personRegistryData.vilage &&
|
||||||
|
` - ${personRegistryData.vilage}`}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<InformationCircleIcon 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"
|
||||||
|
>
|
||||||
|
{personRegistryData.gender ? "مرد" : "زن"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<InformationCircleIcon 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"
|
||||||
|
>
|
||||||
|
{personRegistryData.isLive ? "زنده" : "فوت شده"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{unitRegistryData.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="mb-4 text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
استعلام واحد صنفی ({unitRegistryData.length})
|
||||||
|
</Typography>
|
||||||
|
<Grid container column className="gap-4">
|
||||||
|
{unitRegistryData.map((unit, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
custom={index}
|
||||||
|
variants={cardVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="exit"
|
||||||
|
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 hover:shadow-xl transition-shadow">
|
||||||
|
<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">
|
||||||
|
<CreditCardIcon 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"
|
||||||
|
>
|
||||||
|
{unit.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-600 dark:text-gray-400 mt-1"
|
||||||
|
>
|
||||||
|
{unit.fullname}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{unit.layerTwo.nationalcode}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<PhoneIcon 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"
|
||||||
|
>
|
||||||
|
{unit.layerTwo.mobilenumber}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<PhoneIcon 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"
|
||||||
|
>
|
||||||
|
{unit.layerTwo.phonenumber}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<CreditCardIcon 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"
|
||||||
|
>
|
||||||
|
{unit.licenseNumber}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<InformationCircleIcon 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"
|
||||||
|
>
|
||||||
|
{unit.licenseType}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<InformationCircleIcon 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"
|
||||||
|
>
|
||||||
|
{unit.licenseStatus}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<CalendarIcon 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"
|
||||||
|
>
|
||||||
|
{unit.licenseExpireDate}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<InformationCircleIcon 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"
|
||||||
|
>
|
||||||
|
{unit.city} - {unit.state}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{unit.address && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<InformationCircleIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
آدرس
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg"
|
||||||
|
>
|
||||||
|
{unit.address}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unit.layerTwo.unionName && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<InformationCircleIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
نام اتحادیه
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 p-3 bg-gray-50 dark:bg-dark-700 rounded-lg"
|
||||||
|
>
|
||||||
|
{unit.layerTwo.unionName}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NationalInfo;
|
||||||
12
src/Pages/NotFound.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">404</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">صفحه یافت نشد</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
12
src/Pages/Statics.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Grid } from "../components/Grid/Grid";
|
||||||
|
import Typography from "../components/Typography/Typography";
|
||||||
|
const Statics: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Grid container column>
|
||||||
|
<Typography variant="body1">این بخش در دست توسعه است</Typography>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Statics;
|
||||||
142
src/Pages/UserInspections.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Grid } from "../components/Grid/Grid";
|
||||||
|
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||||
|
import { useApiRequest } from "../utils/useApiRequest";
|
||||||
|
import { formatJustDate, formatJustTime } from "../utils/formatTime";
|
||||||
|
import Typography from "../components/Typography/Typography";
|
||||||
|
import { EyeIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { useModalStore } from "../context/zustand-store/appStore";
|
||||||
|
import { MainInfractions } from "../partials/main/MainInfractions";
|
||||||
|
import MainSubmitInspection from "../partials/main/MainSubmitInspection";
|
||||||
|
import { useDrawerStore } from "../context/zustand-store/appStore";
|
||||||
|
import Table from "../components/Table/Table";
|
||||||
|
import { Popover } from "../components/PopOver/PopOver";
|
||||||
|
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||||
|
import Button from "../components/Button/Button";
|
||||||
|
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||||
|
|
||||||
|
const UserInspections: React.FC = () => {
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
const { openModal } = useModalStore();
|
||||||
|
const { openDrawer } = useDrawerStore();
|
||||||
|
const [tableData, setTableData] = useState<any[][]>([]);
|
||||||
|
|
||||||
|
const { data: inspectionsData, refetch } = useApiRequest({
|
||||||
|
api: `/userinspections/${profile?._id || profile?.Id}`,
|
||||||
|
method: "get",
|
||||||
|
queryKey: ["userinspections"],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inspectionsData) {
|
||||||
|
const d = inspectionsData.map((field: any, i: number) => {
|
||||||
|
return [
|
||||||
|
i + 1,
|
||||||
|
`${formatJustDate(field?.createdAt)} ساعت ${formatJustTime(
|
||||||
|
field?.createdAt
|
||||||
|
)}`,
|
||||||
|
field.inspectors?.map((item: any, idx: number) => (
|
||||||
|
<Typography key={idx} variant="body2" className="text-xs">
|
||||||
|
{item.fullname}
|
||||||
|
</Typography>
|
||||||
|
)) || "-",
|
||||||
|
field?.license_type,
|
||||||
|
field?.issuer,
|
||||||
|
field?.registration_number,
|
||||||
|
field?.ownership_type,
|
||||||
|
field?.unit_type,
|
||||||
|
field?.economic_code,
|
||||||
|
field?.document_number,
|
||||||
|
field?.infractions?.length ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Typography variant="body2" className="text-red-600">
|
||||||
|
دارد
|
||||||
|
</Typography>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
openModal({
|
||||||
|
title: "مشاهده تخلفات",
|
||||||
|
content: (
|
||||||
|
<MainInfractions data={field} handleUpdate={refetch} />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||||
|
>
|
||||||
|
<EyeIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" className="text-blue-600">
|
||||||
|
ندارد
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
parseInt(field?.violation_amount || "0")?.toLocaleString() !== "NaN"
|
||||||
|
? parseInt(field?.violation_amount || "0")?.toLocaleString()
|
||||||
|
: "-",
|
||||||
|
parseInt(field?.plaintiff_damage || "0")?.toLocaleString() !== "NaN"
|
||||||
|
? parseInt(field?.plaintiff_damage || "0")?.toLocaleString()
|
||||||
|
: "-",
|
||||||
|
field?.description || "-",
|
||||||
|
<Popover key={i}>
|
||||||
|
<Tooltip title="ویرایش" position="right">
|
||||||
|
<Button
|
||||||
|
variant="edit"
|
||||||
|
access="submit"
|
||||||
|
onClick={() => {
|
||||||
|
openDrawer({
|
||||||
|
title: "ویرایش بازرسی",
|
||||||
|
content: (
|
||||||
|
<MainSubmitInspection
|
||||||
|
item={field}
|
||||||
|
isEdit
|
||||||
|
inspectId={field?._id || field?.Id}
|
||||||
|
handleUpdate={refetch}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<DeleteButtonForPopOver
|
||||||
|
access="submit"
|
||||||
|
api={`inspections/${field?._id || field?.Id}`}
|
||||||
|
getData={refetch}
|
||||||
|
/>
|
||||||
|
</Popover>,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
setTableData(d);
|
||||||
|
}
|
||||||
|
}, [inspectionsData, profile]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container column className="justify-center">
|
||||||
|
<Grid container>
|
||||||
|
<Table
|
||||||
|
title="سوابق بازرسی من"
|
||||||
|
columns={[
|
||||||
|
"ردیف",
|
||||||
|
"تاریخ بازرسی",
|
||||||
|
"بازرسان همراه",
|
||||||
|
"نوع پروانه کسب",
|
||||||
|
"صادر کننده پروانه",
|
||||||
|
"شماره ثبت",
|
||||||
|
"نوع مالکیت",
|
||||||
|
"نوع واحد",
|
||||||
|
"کد اقتصادی",
|
||||||
|
"شماره پرونده یا مجوز",
|
||||||
|
"تخلف",
|
||||||
|
"جمع کل تخلف",
|
||||||
|
"جمع کل خسارت به شاکی",
|
||||||
|
"توضیحات",
|
||||||
|
"عملیات",
|
||||||
|
]}
|
||||||
|
rows={tableData}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserInspections;
|
||||||
212
src/Pages/UserProfile.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
PhoneIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
MoonIcon,
|
||||||
|
SunIcon,
|
||||||
|
ArrowLeftStartOnRectangleIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { Grid } from "../components/Grid/Grid";
|
||||||
|
import { useDarkMode } from "../hooks/useDarkMode";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useModalStore } from "../context/zustand-store/appStore";
|
||||||
|
import { Logout } from "../partials/auth/Logout";
|
||||||
|
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||||
|
import { getFaProvince } from "../utils/getFaProvince";
|
||||||
|
import { getFaCityName } from "../utils/getFaCityName";
|
||||||
|
import userImage from "../assets/images/user.png";
|
||||||
|
|
||||||
|
interface ProfileCardProps {
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
label: string;
|
||||||
|
value: string | null;
|
||||||
|
show?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileCard = ({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
show = true,
|
||||||
|
}: ProfileCardProps) => {
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
className={clsx(
|
||||||
|
"group relative p-4 transition-all duration-500 hover:-translate-y-1 hover:scale-[1.01]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Grid className="relative flex items-start gap-4">
|
||||||
|
<Grid
|
||||||
|
className={clsx(
|
||||||
|
"rounded-xl p-3 shadow-lg group-hover:shadow-xl transition-all duration-500 group-hover:scale-110",
|
||||||
|
"bg-primary-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5 text-white" />
|
||||||
|
</Grid>
|
||||||
|
<Grid className="flex flex-col min-w-0">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-white font-semibold uppercase tracking-wider mb-2">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="text-primary-600 font-bold text-sm leading-relaxed wrap-break-word">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserProfile: React.FC = () => {
|
||||||
|
const [isDark, setIsDark] = useDarkMode();
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
|
||||||
|
const { openModal } = useModalStore();
|
||||||
|
|
||||||
|
const getUserRole = (permissions: string[] = []): string => {
|
||||||
|
if (permissions.includes("admin")) {
|
||||||
|
return "بازرس ارشد";
|
||||||
|
} else if (permissions.includes("submit")) {
|
||||||
|
return "بازرس";
|
||||||
|
} else {
|
||||||
|
return "ناظر";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid className="min-h-screen bg-gray-50 dark:bg-dark-900">
|
||||||
|
<div className="relative overflow-hidden bg-linear-to-br from-primary-500 via-primary-600 to-primary-700 dark:from-dark-800 dark:via-dark-700 dark:to-dark-800">
|
||||||
|
<Grid className="relative px-6 py-12 md:px-12 md:py-8 items-center">
|
||||||
|
<Grid className="mx-auto">
|
||||||
|
<Grid className="flex flex-col lg:flex-row items-center gap-8 lg:gap-12">
|
||||||
|
<Grid className="relative group">
|
||||||
|
<Grid className="relative">
|
||||||
|
<Grid className="relative bg-white/80 dark:bg-dark-600 backdrop-blur-xl rounded-full p-4 border border-white/20 shadow-2xl">
|
||||||
|
<img
|
||||||
|
src={userImage}
|
||||||
|
alt="User"
|
||||||
|
className="h-24 w-24 md:h-28 md:w-28 rounded-full object-cover drop-shadow-lg"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid className="flex-1 text-center lg:text-right">
|
||||||
|
<Grid className="space-y-4">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold text-white dark:text-dark-100 bg-clip-text drop-shadow-sm">
|
||||||
|
{profile?.fullname || "کاربر"}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<Grid container className="w-auto gap-2 justify-start">
|
||||||
|
{profile?.permissions && profile.permissions.length > 0 && (
|
||||||
|
<Grid className="inline-flex items-center gap-2 bg-white dark:bg-dark-600 backdrop-blur-sm rounded-xl py-3 px-6 border border-primary-200/30">
|
||||||
|
<span className="text-gray-600 dark:text-dark-100 font-semibold text-sm">
|
||||||
|
{getUserRole(profile.permissions)}
|
||||||
|
</span>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{profile?.province && (
|
||||||
|
<Grid className="inline-flex items-center gap-2 bg-white dark:bg-dark-600 backdrop-blur-sm rounded-xl py-2 px-4 border border-emerald-200/30">
|
||||||
|
<span className="text-gray-600 dark:text-dark-100 font-semibold text-sm">
|
||||||
|
استان: {getFaProvince(profile.province)}
|
||||||
|
</span>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid className="flex flex-col justify-center items-end gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsDark(!isDark)}
|
||||||
|
className={clsx(
|
||||||
|
"group relative flex items-center gap-3 rounded-2xl transition-all duration-500 cursor-pointer"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
className={clsx(
|
||||||
|
"rounded-xl transition-all duration-500 group-hover:scale-110 bg-transparent"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isDark ? (
|
||||||
|
<SunIcon className="w-5 h-5 text-white dark:text-dark-100" />
|
||||||
|
) : (
|
||||||
|
<MoonIcon className="w-5 h-5 text-white dark:text-dark-100" />
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<span className="text-white dark:text-dark-100 font-semibold text-sm">
|
||||||
|
{isDark ? "حالت روشن" : "حالت تاریک"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
openModal({
|
||||||
|
title: "از سامانه خارج میشوید؟",
|
||||||
|
content: <Logout />,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
"group relative flex items-center gap-3 rounded-2xl transition-all duration-500 cursor-pointer"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
className={clsx(
|
||||||
|
"rounded-xl transition-all duration-500 group-hover:scale-110 bg-transparent"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ArrowLeftStartOnRectangleIcon className="h-5 w-5 text-white" />
|
||||||
|
</Grid>
|
||||||
|
<span className="text-white font-semibold text-sm">
|
||||||
|
خروج از سامانه
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Grid className="pb-8 mt-4">
|
||||||
|
<Grid className="max-w-full mx-auto bg-white dark:bg-gray-900 p-4 rounded-2xl">
|
||||||
|
<Grid className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-bold text-gray-600 bg-[#FFF2E5] p-2 rounded-xl">
|
||||||
|
اطلاعات کاربری
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 border-t-2 border-dotted border-primary-800"></div>
|
||||||
|
</Grid>
|
||||||
|
<Grid className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-6 gap-6 mt-2">
|
||||||
|
<ProfileCard
|
||||||
|
icon={UserIcon}
|
||||||
|
label="نام کامل"
|
||||||
|
value={profile?.fullname || "بدون نام"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProfileCard
|
||||||
|
icon={PhoneIcon}
|
||||||
|
label="شماره موبایل"
|
||||||
|
value={profile?.mobile || "بدون شماره موبایل"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProfileCard
|
||||||
|
icon={MapPinIcon}
|
||||||
|
label="موقعیت"
|
||||||
|
value={
|
||||||
|
profile?.province
|
||||||
|
? `${getFaProvince(profile.province)}${
|
||||||
|
profile?.city ? `، ${getFaCityName(profile.city)}` : ""
|
||||||
|
}`
|
||||||
|
: "نامشخص"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfile;
|
||||||
99
src/Pages/Users.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Grid } from "../components/Grid/Grid";
|
||||||
|
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||||
|
import { useApiRequest } from "../utils/useApiRequest";
|
||||||
|
import Table from "../components/Table/Table";
|
||||||
|
import Typography from "../components/Typography/Typography";
|
||||||
|
import Button from "../components/Button/Button";
|
||||||
|
import { useDrawerStore } from "../context/zustand-store/appStore";
|
||||||
|
import { SubmitNewUser } from "../partials/users/SubmitNewUser";
|
||||||
|
import { Popover } from "../components/PopOver/PopOver";
|
||||||
|
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||||
|
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||||
|
import { getFaProvince } from "../utils/getFaProvince";
|
||||||
|
import { getFaCityName } from "../utils/getFaCityName";
|
||||||
|
|
||||||
|
const Users: React.FC = () => {
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
const { openDrawer } = useDrawerStore();
|
||||||
|
const [tableData, setTableData] = useState<any[][]>([]);
|
||||||
|
|
||||||
|
const { data: usersData, refetch } = useApiRequest({
|
||||||
|
api: `/users/${profile?.province || "hamedan"}`,
|
||||||
|
method: "get",
|
||||||
|
queryKey: ["users", profile?.province],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (usersData) {
|
||||||
|
const d = usersData.map((item: any, i: number) => {
|
||||||
|
return [
|
||||||
|
i + 1,
|
||||||
|
item?.fullname || "-",
|
||||||
|
item?.mobile || "-",
|
||||||
|
item?.permissions?.map((perm: string, idx: number) => (
|
||||||
|
<Typography key={idx} variant="body2" className="text-xs">
|
||||||
|
{getFaPermissions(perm)}
|
||||||
|
</Typography>
|
||||||
|
)) || "-",
|
||||||
|
getFaProvince(item?.province || ""),
|
||||||
|
getFaCityName(item?.city || ""),
|
||||||
|
item?.mobile === profile?.mobile ? (
|
||||||
|
<Typography variant="body2" className="text-gray-400">
|
||||||
|
-
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Popover key={i}>
|
||||||
|
<DeleteButtonForPopOver
|
||||||
|
access="add"
|
||||||
|
api={`users/${item?._id || item?.Id}`}
|
||||||
|
getData={refetch}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
setTableData(d);
|
||||||
|
}
|
||||||
|
}, [usersData, profile]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container column className="justify-center">
|
||||||
|
<Grid>
|
||||||
|
<Button
|
||||||
|
variant="submit"
|
||||||
|
access="add"
|
||||||
|
onClick={() => {
|
||||||
|
openDrawer({
|
||||||
|
title: "ثبت کاربر جدید",
|
||||||
|
content: (
|
||||||
|
<SubmitNewUser
|
||||||
|
province={profile?.province || ""}
|
||||||
|
onSuccess={refetch}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ثبت کاربر جدید
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
title="کاربران"
|
||||||
|
columns={[
|
||||||
|
"ردیف",
|
||||||
|
"نام کامل",
|
||||||
|
"شماره موبایل",
|
||||||
|
"دسترسیها",
|
||||||
|
"استان",
|
||||||
|
"شهر",
|
||||||
|
"عملیات",
|
||||||
|
]}
|
||||||
|
rows={tableData}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Users;
|
||||||
876
src/Pages/VeterinaryTransfer.tsx
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
||||||
|
import Typography from "../components/Typography/Typography";
|
||||||
|
import { Grid } from "../components/Grid/Grid";
|
||||||
|
import Textfield from "../components/Textfeild/Textfeild";
|
||||||
|
import Button from "../components/Button/Button";
|
||||||
|
import { useToast } from "../hooks/useToast";
|
||||||
|
import { useApiMutation } from "../utils/useApiRequest";
|
||||||
|
import {
|
||||||
|
DocumentTextIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
ArrowPathIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
interface TransferGood {
|
||||||
|
goodCode: number;
|
||||||
|
goodAmount: number;
|
||||||
|
goodUnit: number;
|
||||||
|
goodUnitStr: string;
|
||||||
|
goodStr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VeterinaryTransferItem {
|
||||||
|
trIDCode: string;
|
||||||
|
trTypeCode: number;
|
||||||
|
trTypeCodeStr: string;
|
||||||
|
trStatus: number;
|
||||||
|
trStatusStr: string;
|
||||||
|
sourcePartIDCode: string;
|
||||||
|
sourceCertID: string;
|
||||||
|
desPartIDCode: string;
|
||||||
|
desCertID: string;
|
||||||
|
issueDate: string;
|
||||||
|
issueDateStr: string;
|
||||||
|
resideDate: string;
|
||||||
|
resideDateStr: string;
|
||||||
|
listofGoods: TransferGood[];
|
||||||
|
message: string | null;
|
||||||
|
errorCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VeterinaryTransferResponse {
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number;
|
||||||
|
data: VeterinaryTransferItem[];
|
||||||
|
apiLogId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnitProperty {
|
||||||
|
partIDCode: string | null;
|
||||||
|
unitPostalCode: string | null;
|
||||||
|
postalAddress: string | null;
|
||||||
|
detailAddress: string | null;
|
||||||
|
unitName: string | null;
|
||||||
|
unitGroupStr: string | null;
|
||||||
|
unitTypeStr: string | null;
|
||||||
|
licenseStatusStr: string | null;
|
||||||
|
licenseID: string | null;
|
||||||
|
ownerFullName: string | null;
|
||||||
|
ownerCompany: string | null;
|
||||||
|
lesseeFullName: string | null;
|
||||||
|
activityTypeName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InquiryFarmData {
|
||||||
|
listUnitProperty?: UnitProperty[];
|
||||||
|
errorCode?: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InquiryFarmResponse {
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number;
|
||||||
|
data: InquiryFarmData;
|
||||||
|
apiLogId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VeterinaryTransfer: React.FC = () => {
|
||||||
|
const [trIDCode, setTrIDCode] = useState("");
|
||||||
|
const [transferData, setTransferData] = useState<VeterinaryTransferItem[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [farmInfoByPartId, setFarmInfoByPartId] = useState<
|
||||||
|
Record<string, InquiryFarmResponse | null>
|
||||||
|
>({});
|
||||||
|
const [farmLoadingByPartId, setFarmLoadingByPartId] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
|
const transferMutation = useApiMutation<VeterinaryTransferResponse>({
|
||||||
|
api: "veterinary-transfer",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
|
||||||
|
const inquiryFarmMutation = useApiMutation<InquiryFarmResponse>({
|
||||||
|
api: "inquiry-farm",
|
||||||
|
method: "get",
|
||||||
|
disableBackdrop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
const trimmed = trIDCode.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
showToast("لطفا کد رهگیری گواهی بهداشت حمل را وارد کنید", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await transferMutation.mutateAsync({ trIDCode: trimmed });
|
||||||
|
if (result?.status && Array.isArray(result?.data) && result.data.length) {
|
||||||
|
setTransferData(result.data);
|
||||||
|
} else {
|
||||||
|
setTransferData([]);
|
||||||
|
showToast("نتیجهای یافت نشد", "info");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Veterinary transfer error:", error);
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
"خطا در دریافت اطلاعات گواهی بهداشتی حمل",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
setTransferData([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFarmInquiry = async (partIdCode: string) => {
|
||||||
|
const code = String(partIdCode || "").trim();
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
|
setFarmLoadingByPartId((prev) => ({ ...prev, [code]: true }));
|
||||||
|
try {
|
||||||
|
const res = await inquiryFarmMutation.mutateAsync({ PartIdCode: code });
|
||||||
|
setFarmInfoByPartId((prev) => ({ ...prev, [code]: res }));
|
||||||
|
if (!res?.status) {
|
||||||
|
showToast("خطا در استعلام واحد کشاورزی", "error");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Inquiry farm error:", error);
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.message || "خطا در استعلام واحد کشاورزی",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
setFarmInfoByPartId((prev) => ({ ...prev, [code]: null }));
|
||||||
|
} finally {
|
||||||
|
setFarmLoadingByPartId((prev) => ({ ...prev, [code]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFirstUnitProperty = (res?: InquiryFarmResponse | null) => {
|
||||||
|
const list = res?.data?.listUnitProperty;
|
||||||
|
if (!Array.isArray(list) || list.length === 0) return null;
|
||||||
|
return list[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20, scale: 0.95 },
|
||||||
|
visible: (i: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
delay: i * 0.1,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "easeOut",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
scale: 0.95,
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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="کد رهگیری گواهی بهداشت حمل"
|
||||||
|
value={trIDCode}
|
||||||
|
onChange={(e) => setTrIDCode(e.target.value)}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={transferMutation.isPending}
|
||||||
|
className="px-6 py-2.5"
|
||||||
|
>
|
||||||
|
{transferMutation.isPending ? "در حال جستجو..." : "جستجو"}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{transferData.length > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="mb-4 text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
نتایج جستجو ({transferData.length})
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container column className="gap-6">
|
||||||
|
{transferData.map((item, index) => {
|
||||||
|
const sourceCode = String(item.sourcePartIDCode || "").trim();
|
||||||
|
const destCode = String(item.desPartIDCode || "").trim();
|
||||||
|
const sourceInquiry = farmInfoByPartId[sourceCode];
|
||||||
|
const destInquiry = farmInfoByPartId[destCode];
|
||||||
|
const sourceUnit = getFirstUnitProperty(sourceInquiry);
|
||||||
|
const destUnit = getFirstUnitProperty(destInquiry);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={`${item.trIDCode}-${index}`}
|
||||||
|
custom={index}
|
||||||
|
variants={cardVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="exit"
|
||||||
|
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 hover:shadow-xl transition-shadow">
|
||||||
|
<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">
|
||||||
|
<DocumentTextIcon 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"
|
||||||
|
>
|
||||||
|
ردیف {index + 1}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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 break-all"
|
||||||
|
>
|
||||||
|
{item.trIDCode}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{item.trTypeCodeStr}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{item.trStatusStr}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{item.sourceCertID || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{item.desCertID || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{item.issueDateStr}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{item.resideDateStr}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<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 break-all"
|
||||||
|
>
|
||||||
|
{item.sourcePartIDCode}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => fetchFarmInquiry(sourceCode)}
|
||||||
|
disabled={!!farmLoadingByPartId[sourceCode]}
|
||||||
|
className="px-4 py-2"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<ArrowPathIcon className="w-5 h-5" />
|
||||||
|
{farmLoadingByPartId[sourceCode]
|
||||||
|
? "..."
|
||||||
|
: "استعلام"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<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 break-all"
|
||||||
|
>
|
||||||
|
{item.desPartIDCode}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => fetchFarmInquiry(destCode)}
|
||||||
|
disabled={!!farmLoadingByPartId[destCode]}
|
||||||
|
className="px-4 py-2"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<ArrowPathIcon className="w-5 h-5" />
|
||||||
|
{farmLoadingByPartId[destCode]
|
||||||
|
? "..."
|
||||||
|
: "استعلام"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.listofGoods && item.listofGoods.length > 0 && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<DocumentTextIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
لیست کالا
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100 dark:bg-dark-700">
|
||||||
|
<th className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
ردیف
|
||||||
|
</th>
|
||||||
|
<th className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
نام کالا
|
||||||
|
</th>
|
||||||
|
<th className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
مقدار
|
||||||
|
</th>
|
||||||
|
<th className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
واحد
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{item.listofGoods.map((g, idx) => (
|
||||||
|
<tr
|
||||||
|
key={idx}
|
||||||
|
className="hover:bg-gray-50 dark:hover:bg-dark-700"
|
||||||
|
>
|
||||||
|
<td className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-sm text-gray-900 dark:text-gray-100 text-center">
|
||||||
|
{idx + 1}
|
||||||
|
</td>
|
||||||
|
<td className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-sm text-gray-900 dark:text-gray-100 text-center">
|
||||||
|
{g.goodStr}
|
||||||
|
</td>
|
||||||
|
<td className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-sm text-gray-900 dark:text-gray-100 text-center">
|
||||||
|
{Number(g.goodAmount).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="border border-gray-300 dark:border-dark-600 px-4 py-2 text-sm text-gray-900 dark:text-gray-100 text-center">
|
||||||
|
{g.goodUnitStr}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(sourceUnit || destUnit) && (
|
||||||
|
<div className="pt-2">
|
||||||
|
{sourceUnit && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold mb-3"
|
||||||
|
>
|
||||||
|
شناسه یکتای واحد کشاورزی (مبداء)
|
||||||
|
</Typography>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.unitPostalCode || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.detailAddress ||
|
||||||
|
sourceUnit.postalAddress ||
|
||||||
|
"-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.unitName || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.unitGroupStr || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.unitTypeStr || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.licenseStatusStr || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.licenseID || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.ownerFullName || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.ownerCompany || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.lesseeFullName?.trim() || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{sourceUnit.activityTypeName || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{destUnit && (
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold mb-3"
|
||||||
|
>
|
||||||
|
شناسه یکتای واحد کشاورزی (مقصد)
|
||||||
|
</Typography>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.unitPostalCode || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.detailAddress ||
|
||||||
|
destUnit.postalAddress ||
|
||||||
|
"-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.unitName || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.unitGroupStr || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.unitTypeStr || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.licenseStatusStr || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.licenseID || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.ownerFullName || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.ownerCompany || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.lesseeFullName?.trim() || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{destUnit.activityTypeName || "-"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.message && (
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-dark-700 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IdentificationIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className="text-gray-700 dark:text-gray-300 font-semibold"
|
||||||
|
>
|
||||||
|
پیام
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className="text-gray-900 dark:text-gray-100 font-medium mt-2"
|
||||||
|
>
|
||||||
|
{item.message}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VeterinaryTransfer;
|
||||||
1
src/assets/animations/nodata.json
Normal file
1
src/assets/animations/waiting.json
Normal file
BIN
src/assets/fonts/eot/iranyekanwebblackfanum.eot
Normal file
BIN
src/assets/fonts/eot/iranyekanwebboldfanum.eot
Normal file
BIN
src/assets/fonts/eot/iranyekanwebextrablackfanum.eot
Normal file
BIN
src/assets/fonts/eot/iranyekanwebextraboldfanum.eot
Normal file
BIN
src/assets/fonts/eot/iranyekanweblightfanum.eot
Normal file
BIN
src/assets/fonts/eot/iranyekanwebmediumfanum.eot
Normal file
BIN
src/assets/fonts/eot/iranyekanwebregularfanum.eot
Normal file
BIN
src/assets/fonts/eot/iranyekanwebthinfanum.eot
Normal file
127
src/assets/fonts/fonts.css
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: bold;
|
||||||
|
src: url("./eot/iranyekanwebboldfanum.eot");
|
||||||
|
src: url("./eot/iranyekanwebboldfanum.eot?#iefix") format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanwebboldfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("./ttf/iranyekanwebboldfanum.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
src: url("./eot/iranyekanwebthinfanum.eot");
|
||||||
|
src: url("./eot/iranyekanwebthinfanum.eot?#iefix") format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanwebthinfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("./ttf/iranyekanwebthinfanum.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url("./eot/iranyekanweblightfanum.eot");
|
||||||
|
src: url("./eot/iranyekanweblightfanum.eot?#iefix")
|
||||||
|
format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanweblightfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("./ttf/iranyekanweblightfanum.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
src: url("./eot/iranyekanwebregularfanum.eot");
|
||||||
|
src: url("./eot/iranyekanwebregularfanum.eot?#iefix")
|
||||||
|
format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanwebregularfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||||
|
url("./ttf/iranyekanwebregularfanum.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url("./eot/iranyekanwebmediumfanum.eot");
|
||||||
|
src: url("./eot/iranyekanwebmediumfanum.eot?#iefix") format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanwebmediumfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("./ttf/iranyekanwebmediumfanum.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
src: url("./eot/iranyekanwebextraboldfanum.eot");
|
||||||
|
src: url("./eot/iranyekanwebextraboldfanum.eot?#iefix")
|
||||||
|
format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanwebextraboldfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||||
|
url("./ttf/iranyekanwebextraboldfanum.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 850;
|
||||||
|
src: url("./eot/iranyekanwebblackfanum.eot");
|
||||||
|
src: url("./eot/iranyekanwebblackfanum.eot?#iefix") format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanwebblackfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("./ttf/iranyekanwebblackfanum.ttf")
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: iranyekan;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
src: url("./eot/iranyekanwebextrablackfanum.eot");
|
||||||
|
src: url("./eot/iranyekanwebextrablackfanum.eot?#iefix")
|
||||||
|
format("embedded-opentype"),
|
||||||
|
/* IE6-8 */ url("./woff/iranyekanwebextrablackfanum.woff") format("woff"),
|
||||||
|
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
|
||||||
|
url("./ttf/iranyekanwebextrablackfanum.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "nazanin";
|
||||||
|
src: local("nazanin"), url("./ttf/B-NAZANIN.TTF") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "titr";
|
||||||
|
src: local("titr"), url("./ttf/Titr.ttf") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @font-face {
|
||||||
|
font-family: "vazir";
|
||||||
|
src: local("vazir"), url("./ttf/Vazir-Medium.ttf") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "nima";
|
||||||
|
src: local("vazir"), url("./ttf/Nima.ttf") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "sharif";
|
||||||
|
src: local("vazir"), url("./ttf/Sharif.ttf") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "azar";
|
||||||
|
src: local("vazir"), url("./ttf/AzarMehr.ttf") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
} */
|
||||||
1475
src/assets/fonts/svg/iranyekanwebblackfanum.svg
Normal file
|
After Width: | Height: | Size: 199 KiB |
1578
src/assets/fonts/svg/iranyekanwebboldfanum.svg
Normal file
|
After Width: | Height: | Size: 221 KiB |
1489
src/assets/fonts/svg/iranyekanwebextrablackfanum.svg
Normal file
|
After Width: | Height: | Size: 201 KiB |
1478
src/assets/fonts/svg/iranyekanwebextraboldfanum.svg
Normal file
|
After Width: | Height: | Size: 198 KiB |
1628
src/assets/fonts/svg/iranyekanweblightfanum.svg
Normal file
|
After Width: | Height: | Size: 233 KiB |
1584
src/assets/fonts/svg/iranyekanwebmediumfanum.svg
Normal file
|
After Width: | Height: | Size: 222 KiB |
1560
src/assets/fonts/svg/iranyekanwebregularfanum.svg
Normal file
|
After Width: | Height: | Size: 219 KiB |
1651
src/assets/fonts/svg/iranyekanwebthinfanum.svg
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
src/assets/fonts/ttf/AzarMehr.ttf
Normal file
BIN
src/assets/fonts/ttf/B-NAZANIN.TTF
Normal file
BIN
src/assets/fonts/ttf/Nima.ttf
Normal file
BIN
src/assets/fonts/ttf/Sharif.ttf
Normal file
BIN
src/assets/fonts/ttf/Titr.ttf
Normal file
BIN
src/assets/fonts/ttf/Vazir-Medium.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanwebblackfanum.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanwebboldfanum.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanwebextrablackfanum.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanwebextraboldfanum.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanweblightfanum.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanwebmediumfanum.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanwebregularfanum.ttf
Normal file
BIN
src/assets/fonts/ttf/iranyekanwebthinfanum.ttf
Normal file
BIN
src/assets/fonts/woff/iranyekanwebblackfanum.woff
Normal file
BIN
src/assets/fonts/woff/iranyekanwebboldfanum.woff
Normal file
BIN
src/assets/fonts/woff/iranyekanwebextrablackfanum.woff
Normal file
BIN
src/assets/fonts/woff/iranyekanwebextraboldfanum.woff
Normal file
BIN
src/assets/fonts/woff/iranyekanweblightfanum.woff
Normal file
BIN
src/assets/fonts/woff/iranyekanwebmediumfanum.woff
Normal file
BIN
src/assets/fonts/woff/iranyekanwebregularfanum.woff
Normal file
BIN
src/assets/fonts/woff/iranyekanwebthinfanum.woff
Normal file
BIN
src/assets/images/IranOutlined.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
src/assets/images/auth-bg.png
Normal file
|
After Width: | Height: | Size: 612 KiB |
BIN
src/assets/images/chicken.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/images/dark.png
Normal file
|
After Width: | Height: | Size: 980 KiB |
BIN
src/assets/images/fav.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/images/green_dot.png
Normal file
|
After Width: | Height: | Size: 991 B |
BIN
src/assets/images/hen.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/images/light.png
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
src/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
src/assets/images/marker.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/images/navlogo.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/images/orange_dot.png
Normal file
|
After Width: | Height: | Size: 959 B |
BIN
src/assets/images/red_dot.png
Normal file
|
After Width: | Height: | Size: 961 B |
BIN
src/assets/images/sheep.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/images/store.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
3
src/assets/images/svg/excel.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><g fill='none'><path d='M24 0v24H0V0zM12.593 23.258l-.011.002-.071.035-.02.004-.014-.004-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113-.013.002-.185.093-.01.01-.003.011.018.43.005.012.008.007.201.093c.012.004.023 0 .029-.008l.004-.014-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014-.034.614c0 .012.007.02.017.024l.015-.002.201-.093.01-.008.004-.011.017-.43-.003-.012-.01-.01z'/><path fill='currentColor' d='M13.586 2A2 2 0 0 1 15 2.586L19.414 7A2 2 0 0 1 20 8.414V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zM12 4H6v16h12V10h-4.5A1.5 1.5 0 0 1 12 8.5zm-1.06 8.525 1.06 1.06 1.06-1.06a1 1 0 0 1 1.415 1.414L13.415 15l1.06 1.06a1 1 0 0 1-1.414 1.415L12 16.415l-1.06 1.06a1 1 0 0 1-1.415-1.414l1.06-1.06-1.06-1.062a1 1 0 1 1 1.414-1.414ZM14 4.415V8h3.586L14 4.414Z'/></g></svg>
|
||||||
|
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/images/user.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/images/yellow_dot.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
653
src/components/AutoComplete/AutoComplete.tsx
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useId,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
} from "react";
|
||||||
|
import { ChevronDownIcon, CheckIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { getSizeStyles } from "../../data/getInputSizes";
|
||||||
|
import Textfield from "../Textfeild/Textfeild";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Tooltip } from "../Tooltip/Tooltip";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { checkIsMobile } from "../../utils/checkIsMobile";
|
||||||
|
|
||||||
|
interface DataItem {
|
||||||
|
key: number | string;
|
||||||
|
value: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
isGroupHeader?: boolean;
|
||||||
|
originalGroupKey?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoCompleteProps {
|
||||||
|
data: DataItem[];
|
||||||
|
multiselect?: boolean;
|
||||||
|
inPage?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
selectedKeys: (number | string)[];
|
||||||
|
onChange: (keys: (number | string)[]) => void | [];
|
||||||
|
width?: string;
|
||||||
|
buttonHeight?: number | string;
|
||||||
|
title?: string;
|
||||||
|
error?: boolean;
|
||||||
|
size?: "small" | "medium" | "large";
|
||||||
|
helperText?: string;
|
||||||
|
onChangeValue?: (data: { value: string; key: number | string }) => void;
|
||||||
|
onGroupHeaderClick?: (groupKey: string | number) => void;
|
||||||
|
selectField?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoComplete: React.FC<AutoCompleteProps> = ({
|
||||||
|
data,
|
||||||
|
multiselect = false,
|
||||||
|
selectedKeys,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
inPage = false,
|
||||||
|
title = "",
|
||||||
|
error = false,
|
||||||
|
size = "medium",
|
||||||
|
helperText,
|
||||||
|
onChangeValue,
|
||||||
|
onGroupHeaderClick,
|
||||||
|
selectField = false,
|
||||||
|
}) => {
|
||||||
|
const [filteredData, setFilteredData] = useState<DataItem[]>(data);
|
||||||
|
const [showOptions, setShowOptions] = useState<boolean>(false);
|
||||||
|
const [dropdownWidth, setDropdownWidth] = useState<number>(0);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState<{
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
}>({ top: 0, left: 0 });
|
||||||
|
const [maxHeight, setMaxHeight] = useState<number>(240);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLUListElement>(null);
|
||||||
|
const uniqueId = useId();
|
||||||
|
const selectedKeysRef = useRef<(number | string)[]>(selectedKeys);
|
||||||
|
const isInternalChangeRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateDropdownDimensions = () => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
const rect = inputRef.current.getBoundingClientRect();
|
||||||
|
const defaultMaxHeight = 240;
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
|
const availableHeight = Math.max(100, spaceBelow - 10);
|
||||||
|
const calculatedMaxHeight =
|
||||||
|
spaceBelow < defaultMaxHeight ? availableHeight : defaultMaxHeight;
|
||||||
|
|
||||||
|
setDropdownWidth(rect.width);
|
||||||
|
setMaxHeight(calculatedMaxHeight);
|
||||||
|
setDropdownPosition({
|
||||||
|
top: rect.bottom + window.scrollY,
|
||||||
|
left: rect.left + window.scrollX,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDropdownDimensions();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateDropdownDimensions);
|
||||||
|
if (inputRef.current) {
|
||||||
|
resizeObserver.observe(inputRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", updateDropdownDimensions);
|
||||||
|
window.addEventListener("scroll", updateDropdownDimensions);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
window.removeEventListener("resize", updateDropdownDimensions);
|
||||||
|
window.removeEventListener("scroll", updateDropdownDimensions);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showOptions) return;
|
||||||
|
|
||||||
|
let animationFrameId: number;
|
||||||
|
let isActive = true;
|
||||||
|
let lastTop = 0;
|
||||||
|
let lastLeft = 0;
|
||||||
|
let lastWidth = 0;
|
||||||
|
let lastMaxHeight = 0;
|
||||||
|
|
||||||
|
const updatePosition = (force = false) => {
|
||||||
|
if (!isActive || !inputRef.current) return;
|
||||||
|
|
||||||
|
const rect = inputRef.current.getBoundingClientRect();
|
||||||
|
const defaultMaxHeight = 240;
|
||||||
|
const viewportHeight =
|
||||||
|
window.visualViewport?.height || window.innerHeight;
|
||||||
|
const spaceBelow = viewportHeight - rect.bottom;
|
||||||
|
const availableHeight = Math.max(100, spaceBelow - 10);
|
||||||
|
const calculatedMaxHeight =
|
||||||
|
spaceBelow < defaultMaxHeight ? availableHeight : defaultMaxHeight;
|
||||||
|
|
||||||
|
const newTop = rect.bottom + window.scrollY;
|
||||||
|
const newLeft = rect.left + window.scrollX;
|
||||||
|
const newWidth = rect.width;
|
||||||
|
|
||||||
|
if (
|
||||||
|
force ||
|
||||||
|
Math.abs(newTop - lastTop) > 0.5 ||
|
||||||
|
Math.abs(newLeft - lastLeft) > 0.5 ||
|
||||||
|
Math.abs(newWidth - lastWidth) > 0.5 ||
|
||||||
|
Math.abs(calculatedMaxHeight - lastMaxHeight) > 1
|
||||||
|
) {
|
||||||
|
setDropdownWidth(newWidth);
|
||||||
|
setMaxHeight(calculatedMaxHeight - 30);
|
||||||
|
setDropdownPosition({
|
||||||
|
top: newTop,
|
||||||
|
left: newLeft,
|
||||||
|
});
|
||||||
|
lastTop = newTop;
|
||||||
|
lastLeft = newLeft;
|
||||||
|
lastWidth = newWidth;
|
||||||
|
lastMaxHeight = calculatedMaxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive && showOptions) {
|
||||||
|
animationFrameId = requestAnimationFrame(() => updatePosition(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePosition();
|
||||||
|
|
||||||
|
const handleResize = () => updatePosition(true);
|
||||||
|
const handleScroll = () => updatePosition();
|
||||||
|
let lastViewportHeight =
|
||||||
|
window.visualViewport?.height || window.innerHeight;
|
||||||
|
const handleVisualViewportResize = () => {
|
||||||
|
const currentHeight = window.visualViewport?.height || window.innerHeight;
|
||||||
|
const heightDiff = Math.abs(currentHeight - lastViewportHeight);
|
||||||
|
lastViewportHeight = currentHeight;
|
||||||
|
|
||||||
|
if (heightDiff > 50) {
|
||||||
|
setTimeout(() => {
|
||||||
|
updatePosition(true);
|
||||||
|
setTimeout(() => updatePosition(true), 200);
|
||||||
|
setTimeout(() => updatePosition(true), 400);
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => updatePosition(true), 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleVisualViewportScroll = () => updatePosition();
|
||||||
|
const handleFocus = () => {
|
||||||
|
setTimeout(() => updatePosition(true), 300);
|
||||||
|
};
|
||||||
|
const handleBlur = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
updatePosition(true);
|
||||||
|
setTimeout(() => updatePosition(true), 200);
|
||||||
|
setTimeout(() => updatePosition(true), 400);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
window.addEventListener("scroll", handleScroll, true);
|
||||||
|
|
||||||
|
if (checkIsMobile()) {
|
||||||
|
if (window.visualViewport) {
|
||||||
|
window.visualViewport.addEventListener(
|
||||||
|
"resize",
|
||||||
|
handleVisualViewportResize
|
||||||
|
);
|
||||||
|
window.visualViewport.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
handleVisualViewportScroll
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const inputElement = inputRef.current;
|
||||||
|
if (inputElement) {
|
||||||
|
inputElement.addEventListener("focus", handleFocus);
|
||||||
|
inputElement.addEventListener("blur", handleBlur);
|
||||||
|
inputElement.addEventListener("touchstart", handleFocus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isActive = false;
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
window.removeEventListener("scroll", handleScroll, true);
|
||||||
|
if (checkIsMobile()) {
|
||||||
|
if (window.visualViewport) {
|
||||||
|
window.visualViewport.removeEventListener(
|
||||||
|
"resize",
|
||||||
|
handleVisualViewportResize
|
||||||
|
);
|
||||||
|
window.visualViewport.removeEventListener(
|
||||||
|
"scroll",
|
||||||
|
handleVisualViewportScroll
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const inputElement = inputRef.current;
|
||||||
|
if (inputElement) {
|
||||||
|
inputElement.removeEventListener("focus", handleFocus);
|
||||||
|
inputElement.removeEventListener("blur", handleBlur);
|
||||||
|
inputElement.removeEventListener("touchstart", handleFocus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [showOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const clickedInsideCurrent = target.closest(`.select-group-${uniqueId}`);
|
||||||
|
const clickedOnAnotherAutocomplete =
|
||||||
|
target.closest(".select-group") && !clickedInsideCurrent;
|
||||||
|
|
||||||
|
const clickedOnPortalDropdown = target.closest(
|
||||||
|
`[data-autocomplete-portal="${uniqueId}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clickedOnAnotherAutocomplete) {
|
||||||
|
setShowOptions(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clickedInsideCurrent && !clickedOnPortalDropdown) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const isInputFocused = document.activeElement === inputRef.current;
|
||||||
|
if (!isInputFocused) {
|
||||||
|
setShowOptions(false);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [uniqueId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilteredData(data);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedKeysRef.current = selectedKeys;
|
||||||
|
}, [selectedKeys]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInternalChangeRef.current) {
|
||||||
|
isInternalChangeRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedKeys?.length > 0 && onChangeValue) {
|
||||||
|
const selectedItem = data.find((item) => item.key === selectedKeys[0]);
|
||||||
|
if (selectedItem) {
|
||||||
|
onChangeValue({
|
||||||
|
value: selectedItem.value.trim(),
|
||||||
|
key: selectedItem.key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedKeys, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showOptions) {
|
||||||
|
setIsTyping(false);
|
||||||
|
}
|
||||||
|
}, [showOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showOptions || !checkIsMobile()) return;
|
||||||
|
|
||||||
|
const originalOverflow = window.getComputedStyle(document.body).overflow;
|
||||||
|
const originalPosition = window.getComputedStyle(document.body).position;
|
||||||
|
const originalTop = document.body.style.top;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
document.body.style.position = "fixed";
|
||||||
|
document.body.style.top = `-${scrollY}px`;
|
||||||
|
document.body.style.width = "100%";
|
||||||
|
|
||||||
|
const preventTouchMove = (e: TouchEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const dropdown = document.querySelector(
|
||||||
|
`[data-autocomplete-portal="${uniqueId}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dropdown) {
|
||||||
|
const touch = e.touches[0] || e.changedTouches[0];
|
||||||
|
if (touch) {
|
||||||
|
const elementAtPoint = document.elementFromPoint(
|
||||||
|
touch.clientX,
|
||||||
|
touch.clientY
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
elementAtPoint &&
|
||||||
|
(dropdown.contains(elementAtPoint) || dropdown.contains(target))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (dropdown.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("touchmove", preventTouchMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = originalOverflow;
|
||||||
|
document.body.style.position = originalPosition;
|
||||||
|
document.body.style.top = originalTop;
|
||||||
|
document.body.style.width = "";
|
||||||
|
window.scrollTo(0, scrollY);
|
||||||
|
document.removeEventListener("touchmove", preventTouchMove);
|
||||||
|
};
|
||||||
|
}, [showOptions, uniqueId]);
|
||||||
|
|
||||||
|
const inputValue = useMemo(() => {
|
||||||
|
if (selectedKeys?.length > 0) {
|
||||||
|
const selectedValues = data
|
||||||
|
.filter((item) => selectedKeys?.includes(item.key))
|
||||||
|
.map((item) => item.value);
|
||||||
|
return selectedValues?.join(", ");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, [selectedKeys, data]);
|
||||||
|
|
||||||
|
const [localInputValue, setLocalInputValue] = useState<string>("");
|
||||||
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setLocalInputValue(value);
|
||||||
|
setIsTyping(true);
|
||||||
|
const filtered = data.filter((item) =>
|
||||||
|
item.value.toLowerCase().includes(value.toLowerCase())
|
||||||
|
);
|
||||||
|
setFilteredData(filtered);
|
||||||
|
setShowOptions(true);
|
||||||
|
},
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newSelectedKeys: (number | string)[]) => {
|
||||||
|
isInternalChangeRef.current = true;
|
||||||
|
onChange(newSelectedKeys);
|
||||||
|
|
||||||
|
if (onChangeValue && newSelectedKeys.length > 0) {
|
||||||
|
const selectedItem = data.find(
|
||||||
|
(item) => item.key === newSelectedKeys[0]
|
||||||
|
);
|
||||||
|
if (selectedItem) {
|
||||||
|
onChangeValue({
|
||||||
|
value: selectedItem.value.trim(),
|
||||||
|
key: selectedItem.key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange, onChangeValue, data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionClick = useCallback(
|
||||||
|
(key: number | string) => {
|
||||||
|
const currentSelectedKeys = selectedKeysRef.current;
|
||||||
|
let newSelectedKeys: (number | string)[];
|
||||||
|
|
||||||
|
if (multiselect) {
|
||||||
|
if (currentSelectedKeys.includes(key)) {
|
||||||
|
newSelectedKeys = currentSelectedKeys.filter((item) => item !== key);
|
||||||
|
} else {
|
||||||
|
newSelectedKeys = [...currentSelectedKeys, key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentSelectedKeys.includes(key)) {
|
||||||
|
newSelectedKeys = currentSelectedKeys.filter((item) => item !== key);
|
||||||
|
} else {
|
||||||
|
newSelectedKeys = [key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(newSelectedKeys);
|
||||||
|
|
||||||
|
setIsTyping(false);
|
||||||
|
if (!multiselect) {
|
||||||
|
setLocalInputValue("");
|
||||||
|
setShowOptions(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[multiselect, handleChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputClick = useCallback(() => {
|
||||||
|
document.querySelectorAll(".select-group").forEach((el) => {
|
||||||
|
if (!el.classList.contains(`select-group-${uniqueId}`)) {
|
||||||
|
const input = el.querySelector("input");
|
||||||
|
if (input) {
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowOptions(true);
|
||||||
|
setFilteredData(data);
|
||||||
|
setLocalInputValue("");
|
||||||
|
setIsTyping(false);
|
||||||
|
}, [uniqueId, data]);
|
||||||
|
|
||||||
|
const handleCloseInput = useCallback(() => {
|
||||||
|
setShowOptions(false);
|
||||||
|
setIsTyping(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedKeysSet = useMemo(() => new Set(selectedKeys), [selectedKeys]);
|
||||||
|
|
||||||
|
const handleSelectAll = useCallback(() => {
|
||||||
|
const enabledItems = filteredData.filter((item) => !item.disabled);
|
||||||
|
const allEnabledKeys = enabledItems.map((item) => item.key);
|
||||||
|
handleChange(allEnabledKeys);
|
||||||
|
}, [filteredData, handleChange]);
|
||||||
|
|
||||||
|
const handleDeselectAll = useCallback(() => {
|
||||||
|
handleChange([]);
|
||||||
|
}, [handleChange]);
|
||||||
|
|
||||||
|
const areAllSelected = useMemo(() => {
|
||||||
|
const enabledItems = filteredData.filter((item) => !item.disabled);
|
||||||
|
return (
|
||||||
|
enabledItems.length > 0 &&
|
||||||
|
enabledItems.every((item) => selectedKeysSet.has(item.key))
|
||||||
|
);
|
||||||
|
}, [filteredData, selectedKeysSet]);
|
||||||
|
|
||||||
|
const dropdownOptions = useMemo(() => {
|
||||||
|
if (filteredData.length === 0) {
|
||||||
|
return (
|
||||||
|
<li className="px-4 py-3 text-gray-500 dark:text-dark-400 text-center">
|
||||||
|
نتیجهای یافت نشد
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAllHeader = multiselect ? (
|
||||||
|
<li
|
||||||
|
key="select-all-header"
|
||||||
|
onClick={areAllSelected ? handleDeselectAll : handleSelectAll}
|
||||||
|
className="flex items-center my-1 justify-start gap-2 px-4 py-2 cursor-pointer transition-colors duration-150 rounded-md border border-gray-200 dark:border-dark-600 hover:bg-primary-100 text-dark-800 dark:text-dark-100 dark:hover:bg-dark-700 bg-gray-50 dark:bg-dark-700 font-semibold"
|
||||||
|
>
|
||||||
|
<span className="text-sm">
|
||||||
|
{areAllSelected ? "عدم انتخاب همه" : "انتخاب همه"}
|
||||||
|
</span>
|
||||||
|
{areAllSelected && (
|
||||||
|
<CheckIcon className="w-4 h-4 text-primary-600 dark:text-primary-400 shrink-0" />
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const options = filteredData.map((item) => {
|
||||||
|
const isSelected = selectedKeysSet.has(item.key);
|
||||||
|
const isGroupHeader = item.isGroupHeader;
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isGroupHeader && onGroupHeaderClick) {
|
||||||
|
const groupKey =
|
||||||
|
item.originalGroupKey !== undefined
|
||||||
|
? item.originalGroupKey
|
||||||
|
: String(item.key).startsWith("__group__")
|
||||||
|
? String(item.key).slice(11)
|
||||||
|
: item.key;
|
||||||
|
onGroupHeaderClick(groupKey);
|
||||||
|
} else if (!item.disabled) {
|
||||||
|
handleOptionClick(item.key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`${item.key}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
className={`flex items-center justify-between px-4 py-2 transition-colors duration-150 rounded-md
|
||||||
|
${
|
||||||
|
isGroupHeader && onGroupHeaderClick
|
||||||
|
? "cursor-pointer opacity-55 hover:bg-gray-100 text-dark-800 dark:text-dark-100 dark:hover:bg-primary-900/90 font-semibold bg-gray-200 dark:bg-primary-900"
|
||||||
|
: item.disabled
|
||||||
|
? "text-gray-400 dark:text-dark-500 cursor-not-allowed"
|
||||||
|
: "cursor-pointer hover:bg-primary-100 text-dark-800 dark:text-dark-100 dark:hover:bg-dark-700"
|
||||||
|
}
|
||||||
|
${
|
||||||
|
isSelected && !isGroupHeader
|
||||||
|
? "bg-primary-50 dark:bg-dark-700 font-semibold"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
aria-disabled={item?.disabled && !isGroupHeader}
|
||||||
|
>
|
||||||
|
{checkIsMobile() ? (
|
||||||
|
<span
|
||||||
|
className={`truncate ${
|
||||||
|
item?.value.length > 55 ? "text-xs" : "text-sm"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Tooltip
|
||||||
|
title={item.value}
|
||||||
|
hidden={item?.value?.length < 55}
|
||||||
|
position="right"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`truncate ${
|
||||||
|
item?.value.length > 55 ? "text-xs" : "text-sm"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{isSelected && (
|
||||||
|
<CheckIcon className="w-4 h-4 text-primary-600 dark:text-primary-400 shrink-0" />
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return selectAllHeader ? [selectAllHeader, ...options] : options;
|
||||||
|
}, [
|
||||||
|
filteredData,
|
||||||
|
selectedKeysSet,
|
||||||
|
handleOptionClick,
|
||||||
|
multiselect,
|
||||||
|
areAllSelected,
|
||||||
|
handleSelectAll,
|
||||||
|
handleDeselectAll,
|
||||||
|
onGroupHeaderClick,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dropdownPortalContent = useMemo(() => {
|
||||||
|
if (!showOptions) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<motion.ul
|
||||||
|
ref={dropdownRef}
|
||||||
|
data-autocomplete-portal={`${uniqueId}`}
|
||||||
|
initial={{ opacity: 0, scaleY: 0.95, y: -5 }}
|
||||||
|
animate={{ opacity: 1, scaleY: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.25, ease: "easeOut" }}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: dropdownPosition.top,
|
||||||
|
left: dropdownPosition.left,
|
||||||
|
width: `${dropdownWidth}px`,
|
||||||
|
maxHeight: `${maxHeight}px`,
|
||||||
|
zIndex: 9999,
|
||||||
|
transformOrigin: "top center",
|
||||||
|
scrollbarWidth: "thin",
|
||||||
|
scrollbarColor: "#cbd5e1 transparent",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
}}
|
||||||
|
className={`overflow-y-auto border border-gray-200 dark:border-dark-500 bg-white dark:bg-dark-800 divide-y divide-gray-100 dark:divide-dark-600 text-sm backdrop-blur-lg rounded-xl shadow-2xl modern-scrollbar`}
|
||||||
|
>
|
||||||
|
{dropdownOptions}
|
||||||
|
</motion.ul>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
showOptions,
|
||||||
|
dropdownPosition,
|
||||||
|
dropdownWidth,
|
||||||
|
uniqueId,
|
||||||
|
dropdownOptions,
|
||||||
|
maxHeight,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`select-group select-group-${uniqueId} ${
|
||||||
|
inPage ? "w-auto" : "w-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<div className="relative">
|
||||||
|
<Textfield
|
||||||
|
disabled={disabled}
|
||||||
|
readOnly={selectField}
|
||||||
|
inputMode={selectField ? "none" : undefined}
|
||||||
|
handleCloseInput={handleCloseInput}
|
||||||
|
error={error}
|
||||||
|
helperText={helperText}
|
||||||
|
ref={inputRef}
|
||||||
|
isAutoComplete
|
||||||
|
inputSize={size}
|
||||||
|
value={isTyping ? localInputValue : inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
className="selected-value w-full p-3 pl-10 outline-0 rounded-lg border border-black-100 transition-all duration-200 text-right"
|
||||||
|
placeholder={title || "انتخاب کنید..."}
|
||||||
|
/>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`absolute left-3 text-dark-400 dark:text-dark-100 transition-transform duration-200 ${
|
||||||
|
showOptions ? "transform rotate-180" : ""
|
||||||
|
} ${getSizeStyles(size).icon}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dropdownPortalContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutoComplete;
|
||||||
69
src/components/BackDrop/Backdrop.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useBackdropStore } from "../../context/zustand-store/appStore";
|
||||||
|
import Lottie from "lottie-react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import waiting from "../../assets/animations/waiting.json";
|
||||||
|
|
||||||
|
const Backdrop: React.FC = () => {
|
||||||
|
const { isOpen, closeBackdrop } = useBackdropStore();
|
||||||
|
const backdropRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeBackdrop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
setTimeout(() => {
|
||||||
|
backdropRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
ref={backdropRef}
|
||||||
|
key="backdrop"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 backdrop-blur-sm"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
tabIndex={-1}
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.8, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||||
|
className="flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="w-32 h-32">
|
||||||
|
<Lottie animationData={waiting} loop={true} />
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-white text-lg font-medium select-none">
|
||||||
|
لطفا صبر کنید ...
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Backdrop;
|
||||||
232
src/components/Button/Button.tsx
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import React, { ReactNode, ReactElement, ButtonHTMLAttributes } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import {
|
||||||
|
ChartBarIcon,
|
||||||
|
DocumentChartBarIcon,
|
||||||
|
EyeIcon,
|
||||||
|
FolderPlusIcon,
|
||||||
|
PencilIcon,
|
||||||
|
PencilSquareIcon,
|
||||||
|
TrashIcon,
|
||||||
|
ViewfinderCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import {
|
||||||
|
bgPrimaryColor,
|
||||||
|
mobileBorders,
|
||||||
|
textColorOnPrimary,
|
||||||
|
} from "../../data/getColorBasedOnMode";
|
||||||
|
import { checkIsMobile } from "../../utils/checkIsMobile";
|
||||||
|
import { inputWidths } from "../../data/getItemsWidth";
|
||||||
|
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||||
|
import excel from "../../assets/images/svg/excel.svg?react";
|
||||||
|
import SVGImage from "../SvgImage/SvgImage";
|
||||||
|
import api from "../../utils/axios";
|
||||||
|
import { useBackdropStore } from "../../context/zustand-store/appStore";
|
||||||
|
import { useToast } from "../../hooks/useToast";
|
||||||
|
|
||||||
|
type ExcelProps = {
|
||||||
|
link: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ButtonProps = {
|
||||||
|
children?: ReactNode | string;
|
||||||
|
icon?: ReactElement;
|
||||||
|
direction?: "row" | "row-reverse" | "col" | "col-reverse";
|
||||||
|
iconColor?: string;
|
||||||
|
iconBgColor?: string;
|
||||||
|
iconSize?: number | string;
|
||||||
|
className?: string;
|
||||||
|
variant?:
|
||||||
|
| "submit"
|
||||||
|
| "secondary-submit"
|
||||||
|
| "edit"
|
||||||
|
| "secondary-edit"
|
||||||
|
| "detail"
|
||||||
|
| "delete"
|
||||||
|
| "view"
|
||||||
|
| "info"
|
||||||
|
| "chart";
|
||||||
|
access?: string;
|
||||||
|
height?: string | number;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
excelInfo?: ExcelProps;
|
||||||
|
rounded?: boolean;
|
||||||
|
size?: "small" | "medium" | "large";
|
||||||
|
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
|
const Button: React.FC<ButtonProps> = ({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
direction = "row",
|
||||||
|
iconSize,
|
||||||
|
className = "",
|
||||||
|
variant = "",
|
||||||
|
access = "",
|
||||||
|
height,
|
||||||
|
fullWidth = false,
|
||||||
|
excelInfo,
|
||||||
|
rounded = false,
|
||||||
|
size = "medium",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const directionClass = {
|
||||||
|
row: "flex-row",
|
||||||
|
"row-reverse": "flex-row-reverse",
|
||||||
|
col: "flex-col",
|
||||||
|
"col-reverse": "flex-col-reverse",
|
||||||
|
}[direction];
|
||||||
|
|
||||||
|
const sizeStyles = {
|
||||||
|
small: {
|
||||||
|
padding: "h-[32px] px-2",
|
||||||
|
text: "text-xs",
|
||||||
|
icon: iconSize ?? 14,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
padding: "h-[40px] px-2",
|
||||||
|
text: "text-sm",
|
||||||
|
icon: iconSize ?? 18,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
padding: "h-[48px] px-2",
|
||||||
|
text: "text-base",
|
||||||
|
icon: iconSize ?? 20,
|
||||||
|
},
|
||||||
|
}[size] ?? {
|
||||||
|
padding: "px-4 py-2",
|
||||||
|
text: "text-sm",
|
||||||
|
icon: iconSize ?? 18,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVariantIcon = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case "submit":
|
||||||
|
return (
|
||||||
|
<PlusIcon
|
||||||
|
className={`w-5 h-5 ${
|
||||||
|
children ? "text-white" : "text-purple-400 dark:text-white"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "secondary-submit":
|
||||||
|
return (
|
||||||
|
<FolderPlusIcon
|
||||||
|
className={`w-5 h-5 ${
|
||||||
|
children ? "text-white" : "text-purple-400 dark:text-white"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "edit":
|
||||||
|
return (
|
||||||
|
<PencilIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||||
|
);
|
||||||
|
case "secondary-edit":
|
||||||
|
return (
|
||||||
|
<PencilSquareIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||||
|
);
|
||||||
|
case "detail":
|
||||||
|
return (
|
||||||
|
<EyeIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||||
|
);
|
||||||
|
case "view":
|
||||||
|
return (
|
||||||
|
<ViewfinderCircleIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||||
|
);
|
||||||
|
case "delete":
|
||||||
|
return <TrashIcon className="w-5 h-5 text-red-500" />;
|
||||||
|
case "info":
|
||||||
|
return (
|
||||||
|
<DocumentChartBarIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||||
|
);
|
||||||
|
case "chart":
|
||||||
|
return (
|
||||||
|
<ChartBarIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
const { openBackdrop, closeBackdrop } = useBackdropStore();
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
|
const ableToSeeButton = () => {
|
||||||
|
if (!access) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const permissions = profile?.permissions || [];
|
||||||
|
// Check if access exists in the permissions array (simple array of strings)
|
||||||
|
return permissions.includes(access);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={clsx(
|
||||||
|
`${
|
||||||
|
ableToSeeButton() ? "flex" : "hidden"
|
||||||
|
} flex items-center justify-center gap-1 backdrop-blur-md transition-all duration-200 focus:outline-none cursor-pointer`,
|
||||||
|
fullWidth ? "w-full" : inputWidths,
|
||||||
|
!className.includes("bg-") ? children && bgPrimaryColor : "hover-",
|
||||||
|
directionClass,
|
||||||
|
!className.includes("text-") && textColorOnPrimary,
|
||||||
|
rounded ? "rounded-2xl" : "rounded-[8px]",
|
||||||
|
sizeStyles.padding,
|
||||||
|
sizeStyles.text,
|
||||||
|
className,
|
||||||
|
checkIsMobile() && !icon && !variant && children && mobileBorders
|
||||||
|
)}
|
||||||
|
style={{ height }}
|
||||||
|
>
|
||||||
|
<div className="w-full flex justify-center items-center">
|
||||||
|
{variant && !icon && <>{getVariantIcon()}</>}
|
||||||
|
<span className="whitespace-nowrap">{children}</span>
|
||||||
|
{icon && <div>{icon}</div>}
|
||||||
|
{excelInfo && (
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
openBackdrop();
|
||||||
|
api
|
||||||
|
.get(excelInfo?.link || "", {
|
||||||
|
responseType: "blob",
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
closeBackdrop();
|
||||||
|
const url = window.URL.createObjectURL(
|
||||||
|
new Blob([response.data])
|
||||||
|
);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
link.setAttribute("download", `${excelInfo?.title}.xlsx`);
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error downloading file:", error);
|
||||||
|
closeBackdrop();
|
||||||
|
showToast("خطا در دانلود فایل", "error");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SVGImage
|
||||||
|
src={excel}
|
||||||
|
className={` text-primary-600 dark:text-primary-100 w-5 h-5`}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
288
src/components/Captcha/Captcha.tsx
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { ArrowPathIcon, SpeakerWaveIcon } from "@heroicons/react/24/outline";
|
||||||
|
import Textfield from "../Textfeild/Textfeild";
|
||||||
|
|
||||||
|
interface CaptchaProps {
|
||||||
|
onChange: (isValid: boolean) => void;
|
||||||
|
captchaImage?: string;
|
||||||
|
captchaKey?: string;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Captcha: React.FC<CaptchaProps> = ({
|
||||||
|
onChange,
|
||||||
|
captchaImage,
|
||||||
|
onRefresh,
|
||||||
|
}) => {
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [solution, setSolution] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (solution !== null) {
|
||||||
|
drawCaptcha();
|
||||||
|
}
|
||||||
|
}, [solution]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
generateNewCaptcha();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getRandomInt = (min: number, max: number) => {
|
||||||
|
min = Math.ceil(min);
|
||||||
|
max = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRandomFloat = (min: number, max: number) => {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateNewCaptcha = () => {
|
||||||
|
const newSolution = getRandomInt(111111, 999999);
|
||||||
|
setSolution(newSolution);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawCaptcha = () => {
|
||||||
|
if (!canvasRef.current || solution === null) return;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const { width, height } = canvas;
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
||||||
|
const colors = ["#f0f4f8", "#e8f0f5", "#f5f5f5", "#fafafa", "#f0f0f0"];
|
||||||
|
gradient.addColorStop(0, colors[getRandomInt(0, colors.length)]);
|
||||||
|
gradient.addColorStop(1, colors[getRandomInt(0, colors.length)]);
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
|
||||||
|
for (let i = 0; i < 150; i++) {
|
||||||
|
const x = getRandomInt(0, width);
|
||||||
|
const y = getRandomInt(0, height);
|
||||||
|
const size = getRandomInt(1, 3);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = "rgba(150, 150, 150, 0.3)";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
const startX = getRandomInt(0, width);
|
||||||
|
const startY = getRandomInt(0, height);
|
||||||
|
ctx.moveTo(startX, startY);
|
||||||
|
|
||||||
|
for (let j = 0; j < 3; j++) {
|
||||||
|
const cpX = getRandomInt(0, width);
|
||||||
|
const cpY = getRandomInt(0, height);
|
||||||
|
const endX = getRandomInt(0, width);
|
||||||
|
const endY = getRandomInt(0, height);
|
||||||
|
ctx.quadraticCurveTo(cpX, cpY, endX, endY);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = "rgba(200, 200, 200, 0.4)";
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
const y = getRandomInt(0, height);
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
for (let x = 0; x < width; x += 5) {
|
||||||
|
const waveY = y + Math.sin(x * 0.1 + i) * 6;
|
||||||
|
ctx.lineTo(x, waveY);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
const solutionStr = solution.toString();
|
||||||
|
const charWidth = width / (solutionStr.length + 1);
|
||||||
|
const baseY = height / 2;
|
||||||
|
const fonts = [
|
||||||
|
"Arial",
|
||||||
|
"Verdana",
|
||||||
|
"Courier New",
|
||||||
|
"Georgia",
|
||||||
|
"Times New Roman",
|
||||||
|
];
|
||||||
|
const colors_text = ["#1a1a1a", "#2d2d2d", "#1f2937", "#111827", "#0f172a"];
|
||||||
|
|
||||||
|
solutionStr.split("").forEach((char, index) => {
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
const rotation = getRandomFloat(-0.3, 0.3);
|
||||||
|
const x = charWidth * (index + 1);
|
||||||
|
const yOffset = getRandomInt(-6, 6);
|
||||||
|
const fontSize = getRandomInt(28, 36);
|
||||||
|
const fontFamily = fonts[getRandomInt(0, fonts.length)];
|
||||||
|
|
||||||
|
ctx.translate(x, baseY + yOffset);
|
||||||
|
ctx.rotate(rotation);
|
||||||
|
|
||||||
|
ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
|
||||||
|
ctx.shadowBlur = 2;
|
||||||
|
ctx.shadowOffsetX = 1;
|
||||||
|
ctx.shadowOffsetY = 1;
|
||||||
|
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
const charGradient = ctx.createLinearGradient(-15, -20, 15, 20);
|
||||||
|
charGradient.addColorStop(
|
||||||
|
0,
|
||||||
|
colors_text[getRandomInt(0, colors_text.length)]
|
||||||
|
);
|
||||||
|
charGradient.addColorStop(
|
||||||
|
1,
|
||||||
|
colors_text[getRandomInt(0, colors_text.length)]
|
||||||
|
);
|
||||||
|
ctx.fillStyle = charGradient;
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = colors_text[getRandomInt(0, colors_text.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.font = `bold ${fontSize}px ${fontFamily}`;
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(char, 0, 0);
|
||||||
|
|
||||||
|
if (Math.random() > 0.6) {
|
||||||
|
ctx.strokeStyle = "rgba(100, 100, 100, 0.3)";
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
ctx.strokeText(char, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.globalAlpha = 0.15;
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const shapeType = getRandomInt(0, 3);
|
||||||
|
const x = getRandomInt(0, width);
|
||||||
|
const y = getRandomInt(0, height);
|
||||||
|
const size = getRandomInt(8, 20);
|
||||||
|
|
||||||
|
ctx.fillStyle = `hsl(${getRandomInt(0, 360)}, 50%, 50%)`;
|
||||||
|
|
||||||
|
if (shapeType === 0) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
} else if (shapeType === 1) {
|
||||||
|
ctx.fillRect(x - size / 2, y - size / 2, size, size);
|
||||||
|
} else {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y - size / 2);
|
||||||
|
ctx.lineTo(x - size / 2, y + size / 2);
|
||||||
|
ctx.lineTo(x + size / 2, y + size / 2);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1.0;
|
||||||
|
|
||||||
|
ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const x = getRandomInt(0, width);
|
||||||
|
const y = getRandomInt(0, height);
|
||||||
|
const size = getRandomInt(1, 2);
|
||||||
|
ctx.fillRect(x, y, size, size);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
generateNewCaptcha();
|
||||||
|
setInput("");
|
||||||
|
if (onRefresh) {
|
||||||
|
onRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const playAudio = () => {
|
||||||
|
if (solution === null) return;
|
||||||
|
const audio = new SpeechSynthesisUtterance(
|
||||||
|
solution.toString().split("").join(" ")
|
||||||
|
);
|
||||||
|
audio.rate = 0.25;
|
||||||
|
window.speechSynthesis.speak(audio);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setInput(value);
|
||||||
|
if (solution !== null) {
|
||||||
|
onChange(value === solution.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (captchaImage) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Textfield
|
||||||
|
fullWidth
|
||||||
|
placeholder="کد امنیتی"
|
||||||
|
value={input}
|
||||||
|
onChange={handleChange}
|
||||||
|
isNumber
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRefresh}
|
||||||
|
className="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-dark-600 transition"
|
||||||
|
>
|
||||||
|
<ArrowPathIcon className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
||||||
|
</button>
|
||||||
|
<div className="h-10 w-[180px] overflow-hidden rounded-lg border border-gray-300 dark:border-dark-600">
|
||||||
|
<img
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
src={captchaImage}
|
||||||
|
alt="captcha"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-full items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={200}
|
||||||
|
height={40}
|
||||||
|
className="border border-gray-300 dark:border-dark-600 rounded-lg"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={refresh}
|
||||||
|
className="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-dark-600 transition"
|
||||||
|
aria-label="get new captcha"
|
||||||
|
>
|
||||||
|
<ArrowPathIcon className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={playAudio}
|
||||||
|
className="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-dark-600 transition"
|
||||||
|
aria-label="play audio"
|
||||||
|
>
|
||||||
|
<SpeakerWaveIcon className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Textfield
|
||||||
|
fullWidth
|
||||||
|
placeholder="کد امنیتی"
|
||||||
|
value={input}
|
||||||
|
onChange={handleChange}
|
||||||
|
isNumber
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Captcha;
|
||||||
66
src/components/CheckBox/CheckBox.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { textColor } from "../../data/getColorBasedOnMode";
|
||||||
|
import { getSizeStyles } from "../../data/getInputSizes";
|
||||||
|
|
||||||
|
interface CheckboxProps
|
||||||
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
|
||||||
|
label?: string;
|
||||||
|
size?: "small" | "medium" | "large";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Checkbox: React.FC<CheckboxProps> = ({
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
size,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={"flex items-center"}>
|
||||||
|
<label className="inline-flex items-center space-x-2 cursor-pointer select-none">
|
||||||
|
<span className="relative inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`${getSizeStyles(size).check} cursor-pointer
|
||||||
|
appearance-none border-1 rounded
|
||||||
|
checked:border-primary-600 border-dark-300 bg-gray-100 dark:border-gray-50 checked:bg-primary-600
|
||||||
|
checked:border-none focus:outline-none
|
||||||
|
disabled:cursor-not-allowed disabled:opacity-50
|
||||||
|
peer
|
||||||
|
`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
className={` ${getSizeStyles(size).check} ${textColor}
|
||||||
|
absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none
|
||||||
|
hidden peer-checked:block text-white
|
||||||
|
`}
|
||||||
|
viewBox="1 1 24 20"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{label && (
|
||||||
|
<span
|
||||||
|
className={`${size === "small" ? "text-xs" : "text-sm"} ${
|
||||||
|
disabled ? "text-dark-400 cursor-not-allowed" : textColor
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Checkbox;
|
||||||
46
src/components/Divider/Divider.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type DividerProps = {
|
||||||
|
size?: "fullWidth" | "middle" | "inset";
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Divider({
|
||||||
|
size = "fullWidth",
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
}: DividerProps) {
|
||||||
|
const getWidthClass = () => {
|
||||||
|
switch (size) {
|
||||||
|
case "fullWidth":
|
||||||
|
return "w-full";
|
||||||
|
case "middle":
|
||||||
|
return "w-1/2 mx-auto";
|
||||||
|
case "inset":
|
||||||
|
return "w-1/3 ml-6";
|
||||||
|
default:
|
||||||
|
return "w-full";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-4 text-md text-dark-700 dark:text-primary-100 nt-medium ${getWidthClass()} ${className}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-primary-600 to-transparent rounded-full opacity-70" />
|
||||||
|
<span className="whitespace-nowrap">{children}</span>
|
||||||
|
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-primary-600 to-transparent rounded-full opacity-70" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`h-px bg-gradient-to-r from-transparent via-dark-300 to-transparent rounded-full opacity-70 ${getWidthClass()} ${className}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
135
src/components/Drawer/Drawer.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import { ArrowRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { useDrawerStore } from "../../context/zustand-store/appStore";
|
||||||
|
import { checkIsMobile } from "../../utils/checkIsMobile";
|
||||||
|
import { panelBgAndTextColors } from "../../data/getColorBasedOnMode";
|
||||||
|
import Divider from "../Divider/Divider";
|
||||||
|
|
||||||
|
type Direction = "top" | "bottom" | "left" | "right" | null;
|
||||||
|
|
||||||
|
const Drawer: React.FC = () => {
|
||||||
|
const { drawerState, closeDrawer } = useDrawerStore();
|
||||||
|
const [shouldRender, setShouldRender] = useState<boolean>(
|
||||||
|
!!drawerState.isOpen
|
||||||
|
);
|
||||||
|
const [animate, setAnimate] = useState<boolean>(false);
|
||||||
|
const [mouseDownTarget, setMouseDownTarget] = useState<EventTarget | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const drawerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const rawDirection = drawerState.direction;
|
||||||
|
|
||||||
|
const direction: Direction = checkIsMobile()
|
||||||
|
? "top"
|
||||||
|
: rawDirection && ["top", "bottom", "left", "right"].includes(rawDirection)
|
||||||
|
? (rawDirection as Direction)
|
||||||
|
: "left";
|
||||||
|
|
||||||
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||||
|
if (e.target === mouseDownTarget && e.target === e.currentTarget) {
|
||||||
|
closeDrawer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
setMouseDownTarget(e.target);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (drawerState.isOpen) {
|
||||||
|
setShouldRender(true);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setAnimate(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAnimate(false);
|
||||||
|
const timer = setTimeout(() => setShouldRender(false), 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [drawerState.isOpen]);
|
||||||
|
|
||||||
|
if (!shouldRender) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directionClasses: Record<Exclude<Direction, null>, string> = {
|
||||||
|
top: "transform -translate-y-full",
|
||||||
|
bottom: "transform translate-y-full",
|
||||||
|
left: "transform -translate-x-full",
|
||||||
|
right: "transform translate-x-full",
|
||||||
|
};
|
||||||
|
|
||||||
|
const openClasses: Record<Exclude<Direction, null>, string> = {
|
||||||
|
top: "top-0 left-0 right-0 w-full h-full sm:h-1/3 sm:w-auto translate-y-0 top-0",
|
||||||
|
bottom:
|
||||||
|
"bottom-0 left-0 right-0 w-full h-full sm:h-1/3 sm:w-auto translate-y-0 bottom-0",
|
||||||
|
left: "left-0 top-0 bottom-0 h-full sm:w-1/2 md:w-2/4 lg:w-[400px] translate-x-0",
|
||||||
|
right:
|
||||||
|
"right-0 top-0 bottom-0 h-full sm:w-1/2 md:w-2/4 lg:w-[400px] translate-x-0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMobile = checkIsMobile();
|
||||||
|
if ((direction === "left" || direction === "right") && isMobile) {
|
||||||
|
openClasses.left = "left-0 top-0 bottom-0 w-full h-full translate-x-0";
|
||||||
|
openClasses.right = "right-0 top-0 bottom-0 w-full h-full translate-x-0";
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentClasses =
|
||||||
|
direction && animate && direction in openClasses
|
||||||
|
? openClasses[direction]
|
||||||
|
: direction
|
||||||
|
? directionClasses[direction]
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 bg-opacity-50 transition-opacity duration-300
|
||||||
|
${drawerState.isOpen ? "opacity-100" : "opacity-0 pointer-events-none"}
|
||||||
|
flex z-[1000]`}
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={drawerRef}
|
||||||
|
className={`${panelBgAndTextColors} fixed shadow-lg p-4 transition-transform duration-300 ease-in-out
|
||||||
|
${currentClasses} ${!animate ? "opacity-0" : "opacity-100 "}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{checkIsMobile() ? (
|
||||||
|
<div className=" items-center justify-between pb-2">
|
||||||
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
<button
|
||||||
|
onClick={closeDrawer}
|
||||||
|
className="text-primary-500 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowRightIcon className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<h2 className="text-base font-medium text-gray-900 dark:text-white">
|
||||||
|
{drawerState?.title}
|
||||||
|
</h2>
|
||||||
|
<div className="w-5" />
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-between items-center border-b pb-2 ">
|
||||||
|
<h2 className="text-lg ">{drawerState.title}</h2>
|
||||||
|
<button
|
||||||
|
onClick={closeDrawer}
|
||||||
|
className="mr-1 text-primary-500 hover:text-red-700 dark:text-white rounded-full p-0.5 transition-colors cursor-pointer "
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-4 overflow-y-auto max-h-[94vh] scrollbar-hidden pb-24">
|
||||||
|
{drawerState.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Drawer;
|
||||||
54
src/components/Grid/Grid.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface GridProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
container?: boolean;
|
||||||
|
column?: boolean;
|
||||||
|
isDashboard?: boolean;
|
||||||
|
xs?: string;
|
||||||
|
sm?: string;
|
||||||
|
md?: string;
|
||||||
|
lg?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Grid: React.FC<GridProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
container,
|
||||||
|
column,
|
||||||
|
isDashboard,
|
||||||
|
xs,
|
||||||
|
sm,
|
||||||
|
md,
|
||||||
|
lg,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const getWidthSizes = () => {
|
||||||
|
let sizes;
|
||||||
|
|
||||||
|
if (xs || sm || md || lg) {
|
||||||
|
sizes = `w-${xs} ${sm && `w-${sm}`} ${md && `md:w-${md}`} ${
|
||||||
|
lg && `lg:w-${lg}`
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
{...props}
|
||||||
|
className={`${
|
||||||
|
isDashboard && "shadow-xl rounded-2xl shadow-red-500/10"
|
||||||
|
} ${className} ${container && "flex"} ${getWidthSizes()} ${
|
||||||
|
column && "flex-col"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
123
src/components/Modal/Modal.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||||
|
import { XMarkIcon } from "@heroicons/react/16/solid";
|
||||||
|
import { FC, useRef, useState, useEffect } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
const Modal: FC = () => {
|
||||||
|
const { isOpen, title, content, closeModal, isFullSize } = useModalStore();
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [mouseDownTarget, setMouseDownTarget] = useState<EventTarget | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||||
|
if (e.target === mouseDownTarget && e.target === e.currentTarget) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
setMouseDownTarget(e.target);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
const originalStyle = window.getComputedStyle(document.body).overflow;
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = originalStyle;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="hidden md:flex fixed inset-0 z-[999] backdrop-blur-[2px] justify-center items-center z-40 p-4"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
ref={modalRef}
|
||||||
|
className={`bg-white dark:bg-gray-900 rounded-xl shadow-2xl overflow-y-auto ${
|
||||||
|
isFullSize ? "w-[80%] h-[90%] p-3" : "max-w-md w-full p-6"
|
||||||
|
} max-h-[90vh]`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.95, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between pb-2 mb-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2
|
||||||
|
className={`${
|
||||||
|
isFullSize ? "text-sm" : "text-md font-semibold"
|
||||||
|
} text-gray-900 dark:text-white`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className={`rounded-full cursor-pointer ${
|
||||||
|
isFullSize ? "border-1 " : "p-2"
|
||||||
|
} hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors duration-200`}
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-hidden pt-1">{content}</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="fixed inset-0 z-[999] md:hidden"
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="hidden"
|
||||||
|
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1 } }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-opacity-30 backdrop-blur-[1px]"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-x-0 bottom-0 border-t-5 border-primary-100 bg-white rounded-t-3xl shadow-2xl p-4 pt-2 max-h-[80vh] overflow-y-auto dark:bg-dark-700 dark:border-none"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
initial={{ y: "100%" }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
exit={{ y: "100%" }}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-1.5 bg-gray-300 rounded-full mx-auto mb-4" />
|
||||||
|
<div className="flex items-center justify-center pb-2 mb-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-base font-medium text-gray-900 dark:text-white">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-800 dark:text-gray-200 mt-2 overflow-hidden pt-1">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
225
src/components/PopOver/PopOver.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { useState, useRef, useEffect, createContext, useContext } from "react";
|
||||||
|
import { Cog6ToothIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Grid } from "../Grid/Grid";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
|
export const PopOverContext = createContext<boolean>(false);
|
||||||
|
export const usePopOverContext = () => useContext(PopOverContext);
|
||||||
|
|
||||||
|
export const Popover = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode | any;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => setIsMobile(window.innerWidth < 640);
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener("resize", checkMobile);
|
||||||
|
return () => window.removeEventListener("resize", checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const buttonRect = buttonRef?.current?.getBoundingClientRect();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (
|
||||||
|
!buttonRef.current?.contains(e.target as Node) &&
|
||||||
|
!popoverRef.current?.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
if (isMobile) document.body.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
if (isMobile) document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [isOpen, isMobile]);
|
||||||
|
|
||||||
|
const togglePopover = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || isMobile) return;
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
const buttonRect = buttonRef.current?.getBoundingClientRect();
|
||||||
|
const popoverElement = popoverRef.current;
|
||||||
|
|
||||||
|
if (!buttonRect) return;
|
||||||
|
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const scrollX = window.scrollX;
|
||||||
|
|
||||||
|
const padding = 16;
|
||||||
|
const maxPopoverHeight = viewportHeight - padding * 2;
|
||||||
|
|
||||||
|
let top = buttonRect.top + scrollY;
|
||||||
|
let left = buttonRect.right + scrollX + 8;
|
||||||
|
|
||||||
|
if (popoverElement) {
|
||||||
|
const popoverRect = popoverElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
const popoverHeight = Math.min(popoverRect.height, maxPopoverHeight);
|
||||||
|
const popoverWidth = popoverRect.width;
|
||||||
|
|
||||||
|
const popoverBottomInViewport = buttonRect.top + popoverHeight;
|
||||||
|
|
||||||
|
if (popoverBottomInViewport > viewportHeight - padding) {
|
||||||
|
const overflow = popoverBottomInViewport - (viewportHeight - padding);
|
||||||
|
|
||||||
|
top = buttonRect.top + scrollY - overflow;
|
||||||
|
|
||||||
|
if (top < scrollY + padding) {
|
||||||
|
top = scrollY + padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const popoverRightInViewport = buttonRect.right + popoverWidth + 8;
|
||||||
|
if (popoverRightInViewport > viewportWidth - padding) {
|
||||||
|
left = buttonRect.left + scrollX - popoverWidth - 8;
|
||||||
|
|
||||||
|
if (left < scrollX + padding) {
|
||||||
|
left = scrollX + padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const estimatedHeight = Math.min(
|
||||||
|
(Array.isArray(children) ? children.length : 1) * 50 + 100,
|
||||||
|
maxPopoverHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
if (buttonRect.top + estimatedHeight > viewportHeight - padding) {
|
||||||
|
const overflow =
|
||||||
|
buttonRect.top + estimatedHeight - (viewportHeight - padding);
|
||||||
|
|
||||||
|
top = buttonRect.top + scrollY - overflow;
|
||||||
|
if (top < scrollY + padding) {
|
||||||
|
top = scrollY + padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimatedWidth = 300;
|
||||||
|
if (buttonRect.right + estimatedWidth + 8 > viewportWidth - padding) {
|
||||||
|
left = buttonRect.left + scrollX - estimatedWidth - 8;
|
||||||
|
if (left < scrollX + padding) {
|
||||||
|
left = scrollX + padding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition({ top, left });
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePosition();
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(updatePosition, 0);
|
||||||
|
|
||||||
|
window.addEventListener("scroll", updatePosition, true);
|
||||||
|
window.addEventListener("resize", updatePosition);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
window.removeEventListener("scroll", updatePosition, true);
|
||||||
|
window.removeEventListener("resize", updatePosition);
|
||||||
|
};
|
||||||
|
}, [isOpen, isMobile, children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopOverContext.Provider value={true}>
|
||||||
|
<div className={`relative ${className}`}>
|
||||||
|
<motion.button
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={togglePopover}
|
||||||
|
className={`p-2 rounded-full hover:bg-gray-100/50 dark:hover:bg-gray-800 transition-colors`}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<motion.div animate={{ rotate: isOpen ? 90 : 0 }}>
|
||||||
|
<Cog6ToothIcon
|
||||||
|
className={`h-5 w-5 ${
|
||||||
|
isOpen ? "text-primary-600" : "text-gray-600 dark:text-gray-300"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{isOpen &&
|
||||||
|
createPortal(
|
||||||
|
<AnimatePresence>
|
||||||
|
<>
|
||||||
|
{isMobile && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 0.4 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-9998 bg-black/40 backdrop-blur-sm"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
ref={popoverRef}
|
||||||
|
id="popover-content"
|
||||||
|
initial={isMobile ? { y: "100%" } : { opacity: 0, y: -10 }}
|
||||||
|
animate={isMobile ? { y: 0 } : { opacity: 1, y: 0 }}
|
||||||
|
exit={isMobile ? { y: "100%" } : { opacity: 0, y: -10 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||||
|
className={`z-9999 bg-white dark:bg-dark-700 rounded-lg shadow-lg ring-1 ring-black/10 dark:ring-white/10 ${
|
||||||
|
isMobile
|
||||||
|
? "fixed bottom-0 inset-x-0 rounded-t-3xl pb-[env(safe-area-inset-bottom)]"
|
||||||
|
: "fixed max-h-[calc(100vh-32px)]"
|
||||||
|
}`}
|
||||||
|
style={
|
||||||
|
!isMobile
|
||||||
|
? {
|
||||||
|
top: `${
|
||||||
|
buttonRect?.bottom && buttonRect?.bottom > 700
|
||||||
|
? position.top - 100
|
||||||
|
: position.top
|
||||||
|
}px`,
|
||||||
|
left: `${position.left}px`,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isMobile && (
|
||||||
|
<div className="flex justify-center py-2">
|
||||||
|
<div className="w-10 h-1 bg-gray-300 dark:bg-gray-600 rounded-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PopOverContext.Provider value={true}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
column
|
||||||
|
className="gap-2 p-2 max-h-[calc(100vh-32px)] overflow-y-auto"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Grid>
|
||||||
|
</PopOverContext.Provider>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
</AnimatePresence>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopOverContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
114
src/components/PopOverButtons/PopOverButtons.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||||
|
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||||
|
import { useToast } from "../../hooks/useToast";
|
||||||
|
import { useApiMutation } from "../../utils/useApiRequest";
|
||||||
|
import Button from "../Button/Button";
|
||||||
|
import { Grid } from "../Grid/Grid";
|
||||||
|
import { Tooltip } from "../Tooltip/Tooltip";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
api?: string;
|
||||||
|
title?: string;
|
||||||
|
getData?: () => void;
|
||||||
|
access?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteButtonForPopOver = ({
|
||||||
|
api,
|
||||||
|
title,
|
||||||
|
getData,
|
||||||
|
access = "",
|
||||||
|
}: Props) => {
|
||||||
|
const { openModal, closeModal } = useModalStore();
|
||||||
|
const showToast = useToast();
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
|
||||||
|
const mutation = useApiMutation({
|
||||||
|
api: api || "",
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ableToSeeButton = () => {
|
||||||
|
if (!access) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const permissions = profile?.permissions || [];
|
||||||
|
// Check if access exists in the permissions array (simple array of strings)
|
||||||
|
return permissions.includes(access);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await mutation.mutateAsync({});
|
||||||
|
showToast("عملیات با موفقیت انجام شد", "success");
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
if (getData) {
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.status === 400 || error?.status === 403) {
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.detail ||
|
||||||
|
error?.response?.data?.message + " !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast("مشکلی پیش آمده است!", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ableToSeeButton()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title="حذف" position="right">
|
||||||
|
<Button
|
||||||
|
variant="delete"
|
||||||
|
onClick={() => {
|
||||||
|
openModal({
|
||||||
|
title: title || "از حذف این مورد مطمئنید؟",
|
||||||
|
content: (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
xs="full"
|
||||||
|
column
|
||||||
|
className="flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="w-full max-w-md p-4"
|
||||||
|
>
|
||||||
|
<Grid container className="flex-row space-y-0 space-x-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onSubmit();
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
className="bg-[#eb5757] hover:bg-[#d44e4e] text-white py-3 rounded-lg transition-all duration-300 transform hover:scale-[1.02] shadow-md"
|
||||||
|
>
|
||||||
|
بله
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => closeModal()}
|
||||||
|
fullWidth
|
||||||
|
className="bg-gray-200 text-gray-700 hover:bg-gray-100 py-3 rounded-lg transition-all duration-300 transform hover:scale-[1.02] shadow-md"
|
||||||
|
>
|
||||||
|
خیر
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
</Grid>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||||
|
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||||
|
import { useToast } from "../../hooks/useToast";
|
||||||
|
import { useApiMutation } from "../../utils/useApiRequest";
|
||||||
|
import Button from "../Button/Button";
|
||||||
|
import { Grid } from "../Grid/Grid";
|
||||||
|
import { Tooltip } from "../Tooltip/Tooltip";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
api?: string;
|
||||||
|
title?: string;
|
||||||
|
tooltipText?: string;
|
||||||
|
getData?: () => void;
|
||||||
|
page?: string;
|
||||||
|
access?: string;
|
||||||
|
method?: "delete" | "post" | "put" | "patch";
|
||||||
|
icon?: ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PopoverCustomModalOperation = ({
|
||||||
|
api,
|
||||||
|
title,
|
||||||
|
method = "delete",
|
||||||
|
tooltipText,
|
||||||
|
getData,
|
||||||
|
page = "",
|
||||||
|
access = "",
|
||||||
|
icon,
|
||||||
|
}: Props) => {
|
||||||
|
const { openModal, closeModal } = useModalStore();
|
||||||
|
const showToast = useToast();
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
|
||||||
|
const mutation = useApiMutation({
|
||||||
|
api: api || "",
|
||||||
|
method: method || "delete",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ableToSeeButton = () => {
|
||||||
|
if (!access || !page) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const finded = profile?.permissions?.find(
|
||||||
|
(item: any) => item.page_name === page
|
||||||
|
);
|
||||||
|
if (finded && finded.page_access.includes(access)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await mutation.mutateAsync({});
|
||||||
|
showToast("عملیات با موفقیت انجام شد", "success");
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
if (getData) {
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.status === 400) {
|
||||||
|
showToast(
|
||||||
|
error?.response?.data?.detail ||
|
||||||
|
error?.response?.data?.message + " !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast("مشکلی پیش آمده است!", "error");
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ableToSeeButton()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={tooltipText || ""} position="right">
|
||||||
|
<Button
|
||||||
|
icon={icon}
|
||||||
|
onClick={() => {
|
||||||
|
openModal({
|
||||||
|
title: title || "آیا از انجام عملیات مطمئنید؟",
|
||||||
|
content: (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
xs="full"
|
||||||
|
column
|
||||||
|
className="flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="w-full max-w-md p-4"
|
||||||
|
>
|
||||||
|
<Grid container className="flex-row space-y-0 space-x-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onSubmit();
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
className="bg-[#eb5757] hover:bg-[#d44e4e] text-white py-3 rounded-lg transition-all duration-300 transform hover:scale-[1.02] shadow-md"
|
||||||
|
>
|
||||||
|
بله
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => closeModal()}
|
||||||
|
fullWidth
|
||||||
|
className="bg-gray-200 text-gray-700 hover:bg-gray-100 py-3 rounded-lg transition-all duration-300 transform hover:scale-[1.02] shadow-md"
|
||||||
|
>
|
||||||
|
خیر
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</motion.div>
|
||||||
|
</Grid>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
70
src/components/RadioButton/RadioButton.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
export type RadioSize = "small" | "medium" | "large";
|
||||||
|
|
||||||
|
interface RadioButtonProps {
|
||||||
|
name: string;
|
||||||
|
value: string | any;
|
||||||
|
checked?: boolean;
|
||||||
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
label?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
size?: RadioSize;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeMap: Record<RadioSize, string> = {
|
||||||
|
small: "w-3 h-3",
|
||||||
|
medium: "w-4 h-4",
|
||||||
|
large: "w-5 h-5",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RadioButton: React.FC<RadioButtonProps> = ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
checked = false,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
disabled = false,
|
||||||
|
isError = false,
|
||||||
|
size = "medium",
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={"w-full items-center flex sm:w-auto md:w-auto lg:w-auto"}>
|
||||||
|
<label
|
||||||
|
className={clsx(
|
||||||
|
"inline-flex items-center gap-1 cursor-pointer",
|
||||||
|
disabled && "cursor-not-allowed opacity-60",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
className={clsx(
|
||||||
|
"appearance-none rounded-full w-5 h-5 border-2 transition-all duration-150",
|
||||||
|
sizeMap[size],
|
||||||
|
isError ? "border-red-500" : "border-dark-400",
|
||||||
|
checked
|
||||||
|
? "border-transparent bg-primary-600 dark:bg-dark-400 dark:ring-1 ring-gray-300 dark:ring-white"
|
||||||
|
: "bg-white dark:bg-dark-600"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{label && (
|
||||||
|
<span
|
||||||
|
className={`text-sm text-gray-700 dark:text-dark-100 select-none`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
73
src/components/RadioButton/RadioGroup.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { RadioButton, RadioSize } from "./RadioButton";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { inputWidths } from "../../data/getItemsWidth";
|
||||||
|
import Typography from "../Typography/Typography";
|
||||||
|
|
||||||
|
interface RadioOption {
|
||||||
|
value?: string | any;
|
||||||
|
label?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RadioGroupProps {
|
||||||
|
name?: string;
|
||||||
|
groupTitle?: string;
|
||||||
|
options?: RadioOption[];
|
||||||
|
value?: string | any;
|
||||||
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
size?: RadioSize;
|
||||||
|
direction?: "row" | "column";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RadioGroup: React.FC<RadioGroupProps> = ({
|
||||||
|
name = "",
|
||||||
|
groupTitle = "",
|
||||||
|
options = [],
|
||||||
|
value = "",
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
isError = false,
|
||||||
|
size = "medium",
|
||||||
|
direction = "column",
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"flex ",
|
||||||
|
direction === "column"
|
||||||
|
? "flex-col space-y-2 items-start"
|
||||||
|
: "flex-row space-x-4 items-center",
|
||||||
|
inputWidths,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{groupTitle && (
|
||||||
|
<Typography
|
||||||
|
color="text-gray-700 dark:text-primary-100"
|
||||||
|
variant="body2"
|
||||||
|
className="text-nowrap "
|
||||||
|
>
|
||||||
|
{groupTitle}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{options.map((option) => (
|
||||||
|
<RadioButton
|
||||||
|
key={option.value ?? ""}
|
||||||
|
name={name}
|
||||||
|
value={option.value ?? ""}
|
||||||
|
label={option.label ?? ""}
|
||||||
|
checked={value === option.value}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={option.disabled || disabled}
|
||||||
|
isError={isError}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
170
src/components/ShowImage/ShowImage.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
ArrowDownTrayIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
ArrowPathIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import sampleImage from "../../assets/images/no-image.png";
|
||||||
|
|
||||||
|
const imageExtensions = [
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".gif",
|
||||||
|
".bmp",
|
||||||
|
".webp",
|
||||||
|
".svg",
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ShowImageProps {
|
||||||
|
src?: string;
|
||||||
|
size?: number | string;
|
||||||
|
className?: string;
|
||||||
|
noOpen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowImage: React.FC<ShowImageProps> = ({
|
||||||
|
src = sampleImage,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
noOpen = false,
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [rotation, setRotation] = useState(0);
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
!noOpen && setOpen(true);
|
||||||
|
};
|
||||||
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (!src) return;
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = src;
|
||||||
|
const filename = src.split("/").pop() || "document";
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRotate = () => {
|
||||||
|
setRotation((prev) => prev + 90);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileExtension = () => {
|
||||||
|
if (!src) return "";
|
||||||
|
const filename = src.split("/").pop() || "";
|
||||||
|
const lastDotIndex = filename.lastIndexOf(".");
|
||||||
|
if (lastDotIndex === -1) return "";
|
||||||
|
return filename.substring(lastDotIndex + 1).toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isImage = () => {
|
||||||
|
if (!src) return false;
|
||||||
|
const ext = getFileExtension();
|
||||||
|
return imageExtensions.includes(`.${ext}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!src) {
|
||||||
|
return <span className="text-gray-400 italic">-</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isImage()) {
|
||||||
|
const ext = getFileExtension();
|
||||||
|
const buttonText = ext ? `دانلود سند ${ext}` : "دانلود سند";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="inline-flex items-center gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg shadow-lg transition-shadow duration-300 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ArrowDownTrayIcon className="w-5 h-5" />
|
||||||
|
{buttonText}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt="thumbnail"
|
||||||
|
onClick={handleOpen}
|
||||||
|
className={`${className} cursor-pointer rounded-lg select-none transition-transform duration-300 ease-in-out hover:scale-105 ${
|
||||||
|
size
|
||||||
|
? typeof size === "number"
|
||||||
|
? `w-[${size}px] h-[${size}px]`
|
||||||
|
: `w-full h-full`
|
||||||
|
: "w-16 h-16"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: typeof size === "number" ? size : undefined,
|
||||||
|
height: typeof size === "number" ? size : undefined,
|
||||||
|
}}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-opacity-70 backdrop-blur-sm"
|
||||||
|
aria-modal="true"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.85, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.85, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
|
className="relative max-w-[90vw] max-h-[90vh] min-w-[40vw] min-h-[40vh] rounded-2xl overflow-hidden bg-white dark:bg-dark-600 shadow-2xl flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt="full-size"
|
||||||
|
style={{ transform: `rotate(${rotation}deg)` }}
|
||||||
|
className="max-w-full max-h-[80vh] transition-transform duration-500 ease-in-out select-none"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute top-4 right-4 flex space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
title="دانلود تصویر"
|
||||||
|
className="flex items-center cursor-pointer justify-center bg-white bg-opacity-90 hover:bg-opacity-100 p-3 rounded-full shadow-lg transition-shadow duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ArrowDownTrayIcon className="w-6 h-6 text-primary-700" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleRotate}
|
||||||
|
title="چرخش تصویر"
|
||||||
|
className="flex items-center cursor-pointer justify-center bg-white bg-opacity-90 hover:bg-opacity-100 p-3 rounded-full shadow-lg transition-shadow duration-200 focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<ArrowPathIcon className="w-6 h-6 text-primary-700" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="absolute cursor-pointer top-4 left-4 bg-white bg-opacity-90 hover:bg-opacity-100 p-3 rounded-full shadow-lg transition-shadow duration-200 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||||
|
type="button"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="w-6 h-6 text-red-600" />
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowImage;
|
||||||
226
src/components/ShowStringList/ShowStringList.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import React, { useState, useMemo, useCallback } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
HashtagIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
interface ShowStringListProps {
|
||||||
|
strings: string[];
|
||||||
|
title?: string;
|
||||||
|
maxItems?: number;
|
||||||
|
showSearch?: boolean;
|
||||||
|
showNumbers?: boolean;
|
||||||
|
className?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
onItemClick?: (item: string, index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowStringList: React.FC<ShowStringListProps> = ({
|
||||||
|
strings = [],
|
||||||
|
title,
|
||||||
|
maxItems = 50,
|
||||||
|
showSearch = true,
|
||||||
|
showNumbers = true,
|
||||||
|
className = "",
|
||||||
|
emptyMessage = "هیچ آیتمی یافت نشد",
|
||||||
|
searchPlaceholder = "جستجو...",
|
||||||
|
onItemClick,
|
||||||
|
}) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||||
|
|
||||||
|
const filteredStrings = useMemo(() => {
|
||||||
|
if (!searchTerm.trim()) return strings.slice(0, maxItems);
|
||||||
|
return strings
|
||||||
|
.filter((str) => str.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
|
.slice(0, maxItems);
|
||||||
|
}, [strings, searchTerm, maxItems]);
|
||||||
|
|
||||||
|
const hasMoreItems = strings.length > maxItems;
|
||||||
|
const hasSearchResults = searchTerm.trim() && filteredStrings.length > 0;
|
||||||
|
|
||||||
|
const handleSearchChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearSearch = useCallback(() => {
|
||||||
|
setSearchTerm("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleItemClick = useCallback(
|
||||||
|
(item: string, index: number) => {
|
||||||
|
onItemClick?.(item, index);
|
||||||
|
},
|
||||||
|
[onItemClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 8, scale: 0.9, filter: "blur(4px)" },
|
||||||
|
visible: (i: number) => ({
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
transition: {
|
||||||
|
delay: i * 0.03,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.16, 1, 0.3, 1] as const,
|
||||||
|
filter: { duration: 0.3 },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -8,
|
||||||
|
scale: 0.9,
|
||||||
|
filter: "blur(4px)",
|
||||||
|
transition: {
|
||||||
|
duration: 0.25,
|
||||||
|
ease: [0.4, 0, 1, 1] as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
scale: 1.02,
|
||||||
|
y: -2,
|
||||||
|
transition: { duration: 0.2, ease: [0.16, 1, 0.3, 1] as const },
|
||||||
|
},
|
||||||
|
tap: {
|
||||||
|
scale: 0.98,
|
||||||
|
transition: { duration: 0.1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.03,
|
||||||
|
delayChildren: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!strings || strings.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={`text-center py-8 ${className}`}>
|
||||||
|
<div className="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
{emptyMessage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-3 ${className}`}>
|
||||||
|
{title && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HashtagIcon className="w-4 h-4 text-primary-500" />
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
({strings.length})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSearch && strings.length > 1 && (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="relative">
|
||||||
|
<MagnifyingGlassIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
onFocus={() => setIsSearchFocused(true)}
|
||||||
|
onBlur={() => setIsSearchFocused(false)}
|
||||||
|
className={`w-full px-3 py-1.5 pr-10 text-sm border rounded-md transition-all duration-200 ${
|
||||||
|
isSearchFocused
|
||||||
|
? "border-primary-500 ring-1 ring-primary-500/30 shadow-sm"
|
||||||
|
: "border-gray-300 dark:border-gray-600"
|
||||||
|
} bg-white dark:bg-dark-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none`}
|
||||||
|
aria-label="جستجو در لیست"
|
||||||
|
/>
|
||||||
|
{searchTerm && (
|
||||||
|
<button
|
||||||
|
onClick={clearSearch}
|
||||||
|
className="absolute left-2.5 top-1/2 transform -translate-y-1/2 p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-full transition-colors"
|
||||||
|
aria-label="پاک کردن جستجو"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="w-3 h-3 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{hasSearchResults && (
|
||||||
|
<div className="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{filteredStrings.length} نتیجه از {strings.length} آیتم
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="flex flex-wrap items-center gap-2"
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{filteredStrings.map((str, index) => (
|
||||||
|
<motion.button
|
||||||
|
key={`${str}-${index}`}
|
||||||
|
custom={index}
|
||||||
|
variants={itemVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="exit"
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
onClick={() => handleItemClick(str, index)}
|
||||||
|
className={`group inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium focus:outline-none focus:ring-2 focus:ring-primary-500/50 ${
|
||||||
|
index % 2 === 0
|
||||||
|
? "bg-linear-to-r from-primary-100/80 to-primary-50/80 dark:from-dark-600/80 dark:to-dark-700/80 text-primary-700 dark:text-primary-300 border border-primary-200/50 dark:border-dark-500/50"
|
||||||
|
: "bg-linear-to-r from-secondary-100/80 to-secondary-50/80 dark:from-dark-700/80 dark:to-dark-800/80 text-secondary-700 dark:text-secondary-300 border border-secondary-200/50 dark:border-dark-600/50"
|
||||||
|
} hover:shadow-lg hover:border-opacity-100`}
|
||||||
|
aria-label={`آیتم ${index + 1}: ${str}`}
|
||||||
|
>
|
||||||
|
{showNumbers && (
|
||||||
|
<span className="shrink-0 w-4 h-4 bg-white/60 dark:bg-black/30 rounded-full flex items-center justify-center text-[10px] font-bold">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="truncate max-w-[180px]" title={str}>
|
||||||
|
{str}
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{hasMoreItems && !searchTerm && (
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded-md">
|
||||||
|
و {strings.length - maxItems} آیتم دیگر...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchTerm && filteredStrings.length === 0 && (
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<div className="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
هیچ نتیجهای برای "{searchTerm}" یافت نشد
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowStringList;
|
||||||
19
src/components/ShowWeight/ShowWeight.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
type Props = {
|
||||||
|
weight: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
export const ShowWeight = ({ weight, type = "کیلوگرم" }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="!grid !justify-center !items-center">
|
||||||
|
<span className="text-[14px] gap-1 justify-center items-center flex text-center mb-1 border-b-[0.5px] border-gray-400 dark:text-white">
|
||||||
|
{weight?.toLocaleString()}{" "}
|
||||||
|
<span className="sm:block md:hidden text-red-300 text-[9px] dark:text-white">
|
||||||
|
{type}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-center select-none hidden md:block dark:text-white">
|
||||||
|
{type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
28
src/components/SvgImage copy/SvgImage.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface SVGImageProps extends React.SVGProps<SVGSVGElement> {
|
||||||
|
src: React.FC<React.SVGProps<SVGSVGElement>> | any;
|
||||||
|
width?: string | number;
|
||||||
|
height?: string | number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SVGImage: React.FC<SVGImageProps> = ({
|
||||||
|
src: IconComponent,
|
||||||
|
width = "30px",
|
||||||
|
height = "30px",
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
fill="currentColor"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={`inline-block ${className}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SVGImage;
|
||||||
30
src/components/SvgImage/SvgImage.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface SVGImageProps extends React.SVGProps<SVGSVGElement> {
|
||||||
|
src: React.FC<React.SVGProps<SVGSVGElement>> | any;
|
||||||
|
width?: string | number;
|
||||||
|
height?: string | number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SVGImage: React.FC<SVGImageProps> = ({
|
||||||
|
src: IconComponent,
|
||||||
|
width = "30px",
|
||||||
|
height = "30px",
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
fill="currentColor"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={`inline-block ${className}`}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SVGImage;
|
||||||
|
|
||||||
|
|
||||||