first commit
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules;
|
||||
npm - debug.log.git.gitignore;
|
||||
README.md.env.nyc_output;
|
||||
coverage.DS_Store;
|
||||
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
54
README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
rasaddam:
|
||||
build: .
|
||||
image: wixarm/rasaddam:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
restart: unless-stopped
|
||||
16
dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM 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"]
|
||||
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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",
|
||||
},
|
||||
},
|
||||
]);
|
||||
17
index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/images/fav.png" />
|
||||
<link href="/src/styles.css" rel="stylesheet">
|
||||
<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>
|
||||
8
liara.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"app": "rasaddam-front",
|
||||
"platform": "react",
|
||||
"react": {
|
||||
"mirror": true,
|
||||
"sourceMap": false
|
||||
}
|
||||
}
|
||||
7185
package-lock.json
generated
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "rasad-dam-system",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"start": "react-scripts start",
|
||||
"bump": "node scripts/bump-version.js"
|
||||
},
|
||||
"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",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"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",
|
||||
"lottie-react": "^2.4.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-toastify": "^11.0.5",
|
||||
"zod": "^3.25.28",
|
||||
"zustand": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"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 |
38
scripts/bump-version.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = join(__dirname, "..");
|
||||
const versionFile = join(rootDir, "src", "version.txt");
|
||||
|
||||
const version = readFileSync(versionFile, "utf-8").trim();
|
||||
const parts = version.split(".");
|
||||
const major = parseInt(parts[0], 10);
|
||||
const minor = parseInt(parts[1], 10) + 1;
|
||||
const newVersion = `${major.toString().padStart(2, "0")}.${minor
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
|
||||
writeFileSync(versionFile, newVersion + "\n", "utf-8");
|
||||
|
||||
try {
|
||||
execSync(`git add "${versionFile}"`, { cwd: rootDir, stdio: "inherit" });
|
||||
execSync(`git commit -m "version changed to ${newVersion}"`, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
execSync(`git push`, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
execSync(`git pull`, {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
console.log(`Version bumped to ${newVersion} and committed`);
|
||||
} catch (error) {
|
||||
console.error("Failed to commit:", error.message);
|
||||
}
|
||||
33
src/App.css
Normal file
@@ -0,0 +1,33 @@
|
||||
@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;
|
||||
}
|
||||
114
src/App.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
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 { getUserPermissions } from "./utils/getUserAvalableItems";
|
||||
import { ItemWithSubItems } from "./types/userPermissions";
|
||||
|
||||
import versionNumber from "./version.txt";
|
||||
import "./index.css";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { checkIsMobile } from "./utils/checkIsMobile";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function App() {
|
||||
const auth = useUserStore((s) => s.auth);
|
||||
const { profile } = useUserProfileStore();
|
||||
const [isDark] = useDarkMode();
|
||||
|
||||
const menuItems: ItemWithSubItems[] = useMemo(
|
||||
() => getUserPermissions(profile?.permissions ?? []),
|
||||
[profile?.permissions]
|
||||
);
|
||||
|
||||
const router = useMemo(
|
||||
() => makeRouter(auth ?? null, menuItems),
|
||||
[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
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
isDark ? "dark:bg-dark-900 dark-scrollbar" : "bg-white light-scrollbar"
|
||||
}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
<ToastContainer position="bottom-right" />
|
||||
{checkIsMobile() && <div className="h-20"></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
218
src/Pages/Auth.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { motion } from "framer-motion";
|
||||
import img from "../assets/images/auth-bg.png";
|
||||
import logo from "../assets/images/logo.png";
|
||||
import Textfield from "../components/Textfeild/Textfeild";
|
||||
import Button from "../components/Button/Button";
|
||||
import Typography from "../components/Typography/Typography";
|
||||
import {
|
||||
useUserProfileStore,
|
||||
useUserStore,
|
||||
} from "../context/zustand-store/userStore";
|
||||
import { getBase64ImageSrc } from "../utils/getBase64ImageSrc";
|
||||
import { useApiMutation } from "../utils/useApiRequest";
|
||||
import { useEffect, useState } from "react";
|
||||
import noImage from "../assets/images/not-loaded-captcha.png";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { zValidate, zValidateString } from "../data/getFormTypeErrors";
|
||||
import { useToast } from "../hooks/useToast";
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
show: {
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fadeInUp = {
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } },
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
nationalId: zValidateString("کد ملی"),
|
||||
password: zValidateString("کلمه عبور"),
|
||||
captcha: z.coerce
|
||||
.string(zValidate("کپچا"))
|
||||
.min(1, "کپچا الزامی است")
|
||||
.min(1, "فیلد نمیتواند خالی باشد!"),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
interface Captcha {
|
||||
captcha_image: string | null;
|
||||
captcha_key: string | null;
|
||||
}
|
||||
|
||||
export const Auth = () => {
|
||||
const { setUser } = useUserStore();
|
||||
const { setUserProfile } = useUserProfileStore();
|
||||
|
||||
const mutationCaptcha = useApiMutation({
|
||||
api: "/captcha/",
|
||||
method: "post",
|
||||
disableBackdrop: true,
|
||||
});
|
||||
|
||||
const mutationLogin = useApiMutation({
|
||||
api: "/auth/api/v1/login/",
|
||||
method: "post",
|
||||
disableBackdrop: false,
|
||||
});
|
||||
|
||||
const mutationProfile = useApiMutation({
|
||||
api: "/auth/api/v1/user/profile/",
|
||||
method: "get",
|
||||
disableBackdrop: false,
|
||||
});
|
||||
|
||||
const handleGetCaptcha = async () => {
|
||||
const data = await mutationCaptcha.mutateAsync({});
|
||||
setCaptcha(data);
|
||||
};
|
||||
const showToast = useToast();
|
||||
|
||||
const [captcha, setCaptcha] = useState<Captcha>();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
handleGetCaptcha();
|
||||
}, []);
|
||||
|
||||
const getProfile = async () => {
|
||||
const profile = await mutationProfile.mutateAsync({});
|
||||
if (typeof profile === "object") {
|
||||
setUserProfile(profile);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
const response = await mutationLogin.mutateAsync({
|
||||
username: data.nationalId,
|
||||
password: data.password,
|
||||
captcha_code: data.captcha.toString(),
|
||||
captcha_key: `rest_captcha_${captcha?.captcha_key ?? ""}.0`,
|
||||
});
|
||||
|
||||
setUser({ auth: response?.access });
|
||||
getProfile();
|
||||
} catch (error: any) {
|
||||
handleGetCaptcha();
|
||||
if (error?.status === 403) {
|
||||
setValue("captcha", "");
|
||||
showToast("کپچا اشتباه است!", "error");
|
||||
} else {
|
||||
setValue("captcha", "");
|
||||
showToast("نام کاربری یا رمز عبور اشتباه است!", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const imageSrc = getBase64ImageSrc(captcha?.captcha_image ?? "");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-screen flex-col md:flex-row overflow-y-auto dark:bg-dark-900"
|
||||
style={{
|
||||
backgroundImage: `url(${img})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black opacity-0 dark:opacity-50 transition-opacity duration-300 pointer-events-none z-0" />
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="flex w-full h-screen items-center justify-center px-6 sm:px-12 md:px-24 lg:px-40 py-8"
|
||||
>
|
||||
<motion.form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="w-full p-10 bg-white rounded-xl flex flex-col max-w-sm space-y-2 items-center justify-center dark:bg-dark-800"
|
||||
>
|
||||
<motion.img
|
||||
variants={fadeInUp}
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
className="mx-auto w-20 select-none mb-4 dark:brightness-100 dark:opacity-100"
|
||||
/>
|
||||
<motion.div variants={fadeInUp}>
|
||||
<Typography className="mb-4 select-none">سامانه رصدام</Typography>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp} className="w-full">
|
||||
<Textfield
|
||||
fullWidth
|
||||
placeholder="نام کاربری"
|
||||
{...register("nationalId")}
|
||||
error={!!errors.nationalId}
|
||||
helperText={errors.nationalId?.message}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div variants={fadeInUp} className="w-full">
|
||||
<Textfield
|
||||
fullWidth
|
||||
placeholder="کلمه عبور"
|
||||
password
|
||||
{...register("password")}
|
||||
error={!!errors.password}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp} className="flex w-full items-center">
|
||||
<Textfield
|
||||
fullWidth
|
||||
className="w-[100%]"
|
||||
placeholder="کد امنیتی"
|
||||
isNumber
|
||||
{...register("captcha")}
|
||||
value={watch("captcha") || ""}
|
||||
error={!!errors.captcha}
|
||||
helperText={errors.captcha?.message}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetCaptcha}
|
||||
className="p-1 rounded-full hover:bg-gray-200 transition"
|
||||
>
|
||||
<ArrowPathIcon className="h-5 w-5 text-gray-600" />
|
||||
</button>
|
||||
<div className="h-10 w-[180px] overflow-hidden rounded-2xl">
|
||||
<img
|
||||
className="w-full h-full object-cover scale-100"
|
||||
src={imageSrc.endsWith(",") ? noImage : imageSrc}
|
||||
alt="captcha"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={fadeInUp} className="w-full">
|
||||
<Button fullWidth className="mt-1" type="submit">
|
||||
ورود
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.form>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
185
src/Pages/CooperativeRanchers.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { LIVESTOCK_FARMERS } from "../routes/paths";
|
||||
import { TableButton } from "../components/TableButton/TableButton";
|
||||
import { CooperativesDashboardDetails } from "../partials/cooperatives/CooperativesDashboardDetails";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
|
||||
export default function CooperativeRanchers() {
|
||||
const { openModal } = useModalStore();
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [cooperativesTableData, setCooperativesTableData] = useState([]);
|
||||
const { id, name } = useParams({ strict: false });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: cooperativeRanchersData, refetch: cooperativeRanchersRefetch } =
|
||||
useApiRequest({
|
||||
api: `herd/web/api/v1/rancher_org_link/${id}/org_ranchers/`,
|
||||
method: "get",
|
||||
params: {
|
||||
...pagesInfo,
|
||||
},
|
||||
queryKey: ["cooperativeRanchers", pagesInfo],
|
||||
});
|
||||
|
||||
const {
|
||||
data: cooperativeRanchersDashboardData,
|
||||
refetch: cooperativeRanchersDashboardRefetch,
|
||||
} = useApiRequest({
|
||||
api: `herd/web/api/v1/rancher_org_link/${id}/org_ranchers_quota_dashboard/`,
|
||||
method: "get",
|
||||
queryKey: ["cooperativeRanchersDashboard", id],
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const handleUpdate = () => {
|
||||
cooperativeRanchersRefetch();
|
||||
cooperativeRanchersDashboardRefetch();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (cooperativeRanchersData?.results) {
|
||||
const tableData = cooperativeRanchersData.results.map(
|
||||
(item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.ranching_farm,
|
||||
item?.rancher?.first_name,
|
||||
item?.rancher?.last_name,
|
||||
item?.rancher?.national_code,
|
||||
item?.rancher?.mobile,
|
||||
item?.rancher?.activity === "V"
|
||||
? "روستایی"
|
||||
: item?.rancher?.activity === "I"
|
||||
? "صنعتی"
|
||||
: item?.rancher?.activity === "R"
|
||||
? "عشایری"
|
||||
: "-",
|
||||
item?.rancher?.province?.name || "-",
|
||||
item?.rancher?.city?.name || "-",
|
||||
item?.rancher?.address,
|
||||
item?.rancher?.without_herd ? "بدون دام" : "دامدار عادی",
|
||||
item?.rancher?.rancher_type === "N" ? "حقیقی" : "حقوقی",
|
||||
item?.rancher?.union_name || "-",
|
||||
item?.rancher?.union_code || "-",
|
||||
<Popover key={i}>
|
||||
<Tooltip title="مشاهده گله ها" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page="farmer_details"
|
||||
access="See-Herds"
|
||||
onClick={() => {
|
||||
const path =
|
||||
LIVESTOCK_FARMERS +
|
||||
"/" +
|
||||
item?.rancher?.id +
|
||||
"/" +
|
||||
item?.rancher?.ranching_farm;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="farmer_details"
|
||||
access="Delete-Rancher-Herd"
|
||||
api={`herd/web/api/v1/rancher_org_link/${item?.id}/`}
|
||||
getData={handleUpdate}
|
||||
tooltipText="حذف دامدار از تعاونی"
|
||||
title="از حذف دامدار از تعاونی مطمئنید؟"
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
}
|
||||
);
|
||||
setCooperativesTableData(tableData);
|
||||
}
|
||||
}, [cooperativeRanchersData, pagesInfo, openModal]);
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4 mt-2">
|
||||
{id && (
|
||||
<Grid isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
title="خلاصه اطلاعات"
|
||||
noPagination
|
||||
noSearch
|
||||
columns={[
|
||||
"تعداد کل سهمیه ها",
|
||||
"مجموع وزن سهمیه ها (کیلوگرم)",
|
||||
"مجموع وزن توزیع شده (کیلوگرم)",
|
||||
"مجموع وزن باقیمانده (کیلوگرم)",
|
||||
"مجموع وزن فروش رفته (کیلوگرم)",
|
||||
"مجموع وزن ورود به انبار (کیلوگرم)",
|
||||
"جزئیات",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
cooperativeRanchersDashboardData?.quotas_summary?.total_quotas?.toLocaleString() ||
|
||||
0,
|
||||
cooperativeRanchersDashboardData?.quotas_summary?.total_amount?.toLocaleString() ||
|
||||
0,
|
||||
cooperativeRanchersDashboardData?.quotas_summary?.total_distributed?.toLocaleString() ||
|
||||
0,
|
||||
cooperativeRanchersDashboardData?.quotas_summary?.remaining_amount?.toLocaleString() ||
|
||||
0,
|
||||
cooperativeRanchersDashboardData?.quotas_summary?.sold_amount?.toLocaleString() ||
|
||||
0,
|
||||
cooperativeRanchersDashboardData?.quotas_summary?.inventory_received?.toLocaleString() ||
|
||||
0,
|
||||
<TableButton
|
||||
size="small"
|
||||
key="details"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات",
|
||||
content: <CooperativesDashboardDetails orgId={id} />,
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
جزئیات
|
||||
</TableButton>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setPagesInfo}
|
||||
count={cooperativeRanchersData?.count || 10}
|
||||
isPaginated
|
||||
title={id ? `دامداران ${name}` : "دامداران تعاونی"}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام دامداری",
|
||||
"نام",
|
||||
"نام خانوادگی",
|
||||
"کد ملی",
|
||||
"موبایل",
|
||||
"نوع فعالیت",
|
||||
"استان",
|
||||
"شهر",
|
||||
"آدرس",
|
||||
"وضعیت",
|
||||
"نوع دامدار",
|
||||
"نام واحد حقوقی",
|
||||
"شناسه ملی واحد حقوقی",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={cooperativesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
215
src/Pages/Cooperatives.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { ChildOrganizations } from "../partials/cooperatives/ChildOrganizations";
|
||||
import { COOPERATIVE_LIST } from "../routes/paths";
|
||||
import { TableButton } from "../components/TableButton/TableButton";
|
||||
import { CooperativesDashboardDetails } from "../partials/cooperatives/CooperativesDashboardDetails";
|
||||
import { AddActivityType } from "../partials/cooperatives/AddActivityType";
|
||||
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
|
||||
import ShowStringList from "../components/ShowStringList/ShowStringList";
|
||||
|
||||
export default function Cooperatives() {
|
||||
const { openModal } = useModalStore();
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [cooperativesTableData, setCooperativesTableData] = useState([]);
|
||||
const { id, name } = useParams({ strict: false });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: cooperativesData, refetch } = useApiRequest({
|
||||
api: `herd/web/api/v1/rancher_org_link/org_linked_rancher_list${
|
||||
id ? `?org_id=${id}` : ""
|
||||
}`,
|
||||
method: "get",
|
||||
params: {
|
||||
...pagesInfo,
|
||||
},
|
||||
queryKey: [id ? "unioncooperatives" : "cooperatives", pagesInfo],
|
||||
});
|
||||
|
||||
const { data: cooperativesDashboardData } = useApiRequest({
|
||||
api: `herd/web/api/v1/rancher_org_link/${id}/org_ranchers_quota_dashboard/`,
|
||||
method: "get",
|
||||
queryKey: [id ? "unionCooperativeDashboard" : "cooperativeDashboard"],
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cooperativesData?.results) {
|
||||
const formattedData = cooperativesData.results.map(
|
||||
(item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.name || "-",
|
||||
item?.province || "-",
|
||||
item?.city || "-",
|
||||
item?.rancher_count || 0,
|
||||
item?.herd_count || 0,
|
||||
item?.livestock_count || 0,
|
||||
item?.org_service_area?.length ? (
|
||||
<ShowMoreInfo key={i} title="محدوده فعالیت">
|
||||
<Grid
|
||||
container
|
||||
column
|
||||
className="gap-2 p-2 justify-start items-start w-full"
|
||||
>
|
||||
<ShowStringList
|
||||
showSearch={false}
|
||||
strings={item.org_service_area.map(
|
||||
(city: any) => city.name
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</ShowMoreInfo>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
item?.org_purchase_policy === "INTERNAL_ONLY"
|
||||
? "بر اساس تعاونی"
|
||||
: item?.org_purchase_policy === "CROSS_COOP"
|
||||
? "برای کل استان"
|
||||
: "-",
|
||||
<Popover key={i}>
|
||||
<Tooltip title="دامداران تعاونی" position="right">
|
||||
<Button
|
||||
variant="view"
|
||||
page="cooperative_ranchers"
|
||||
access="Show-Cooperative-Ranchers"
|
||||
onClick={() => {
|
||||
const path =
|
||||
COOPERATIVE_LIST +
|
||||
"/ranchers/" +
|
||||
item?.id +
|
||||
"/" +
|
||||
item?.name;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="تعریف نوع فعالیت" position="right">
|
||||
<Button
|
||||
variant="set"
|
||||
page="cooperatives"
|
||||
access="Set-Cooperative-Activity-Type"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: `تعریف نوع فعالیت ${item?.name || ""}`,
|
||||
content: (
|
||||
<AddActivityType getData={refetch} item={item} />
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>,
|
||||
|
||||
<Tooltip title="زیرمجموعه ها" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page="cooperatives"
|
||||
access="Show-Child-Organizations"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: `زیرمجموعه های ${item?.name || ""}`,
|
||||
content: (
|
||||
<ChildOrganizations
|
||||
orgId={item?.id}
|
||||
orgName={item?.name || ""}
|
||||
/>
|
||||
),
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>,
|
||||
];
|
||||
}
|
||||
);
|
||||
setCooperativesTableData(formattedData);
|
||||
}
|
||||
}, [cooperativesData, pagesInfo, openModal, navigate, refetch]);
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4 mt-2">
|
||||
{id && (
|
||||
<Grid isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
title="خلاصه اطلاعات"
|
||||
noPagination
|
||||
noSearch
|
||||
columns={[
|
||||
"تعداد کل سهمیه ها",
|
||||
"مجموع وزن سهمیه ها (کیلوگرم)",
|
||||
"مجموع وزن توزیع شده (کیلوگرم)",
|
||||
"مجموع وزن باقیمانده (کیلوگرم)",
|
||||
"مجموع وزن فروش رفته (کیلوگرم)",
|
||||
"مجموع وزن ورود به انبار (کیلوگرم)",
|
||||
"جزئیات",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
cooperativesDashboardData?.quotas_summary?.total_quotas?.toLocaleString() ||
|
||||
0,
|
||||
cooperativesDashboardData?.quotas_summary?.total_amount?.toLocaleString() ||
|
||||
0,
|
||||
cooperativesDashboardData?.quotas_summary?.total_distributed?.toLocaleString() ||
|
||||
0,
|
||||
cooperativesDashboardData?.quotas_summary?.remaining_amount?.toLocaleString() ||
|
||||
0,
|
||||
cooperativesDashboardData?.quotas_summary?.sold_amount?.toLocaleString() ||
|
||||
0,
|
||||
cooperativesDashboardData?.quotas_summary?.inventory_received?.toLocaleString() ||
|
||||
0,
|
||||
<TableButton
|
||||
size="small"
|
||||
key="details"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات",
|
||||
content: <CooperativesDashboardDetails orgId={id} />,
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
جزئیات
|
||||
</TableButton>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
setPagesInfo(e);
|
||||
}}
|
||||
count={cooperativesData?.count || 10}
|
||||
isPaginated
|
||||
title={id ? `تعاونی های ${name}` : "تعاونی ها"}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام",
|
||||
"استان",
|
||||
"شهر",
|
||||
"تعداد دامدار",
|
||||
"تعداد گله",
|
||||
"تعداد دام",
|
||||
"محدوده فعالیت",
|
||||
"محدودیت دریافت نهاده",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={cooperativesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
447
src/Pages/Dashboard.tsx
Normal file
@@ -0,0 +1,447 @@
|
||||
import moment from "jalali-moment";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Typography from "../components/Typography/Typography";
|
||||
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||
import { ItemWithSubItems } from "../types/userPermissions";
|
||||
import { getUserPermissions } from "../utils/getUserAvalableItems";
|
||||
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
Squares2X2Icon,
|
||||
XMarkIcon,
|
||||
ClockIcon,
|
||||
CalendarIcon,
|
||||
ArrowRightCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { checkIsMobile } from "../utils/checkIsMobile";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { useDashboardTabStore } from "../context/zustand-store/dashboardTabStore";
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
title: string;
|
||||
component: React.ComponentType;
|
||||
path: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { profile } = useUserProfileStore();
|
||||
const { dashboarTabs, setDashboardTabs, activeTabId, setActiveTabId } =
|
||||
useDashboardTabStore();
|
||||
|
||||
const menuItems: ItemWithSubItems[] = getUserPermissions(
|
||||
profile?.permissions
|
||||
);
|
||||
|
||||
const [tabs, setTabs] = useState<Tab[]>(dashboarTabs || []);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setTabs(dashboarTabs || []);
|
||||
}, [dashboarTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
setDashboardTabs(tabs);
|
||||
}, [tabs, setDashboardTabs]);
|
||||
|
||||
const persianDate = moment().locale("fa").format("dddd D MMMM YYYY");
|
||||
const [time, setTime] = useState(
|
||||
new Date().toLocaleTimeString("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTime(
|
||||
new Date().toLocaleTimeString("fa-IR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
);
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const openTab = (subItem: ItemWithSubItems["subItems"][0]) => {
|
||||
const existingTab = tabs.find((tab) => tab.path === subItem.path);
|
||||
|
||||
if (existingTab) {
|
||||
setActiveTabId(existingTab.id);
|
||||
} else {
|
||||
const newTab = {
|
||||
id: `tab-${Date.now()}`,
|
||||
title: getFaPermissions(subItem.name),
|
||||
component: subItem.component,
|
||||
path: subItem.path,
|
||||
};
|
||||
|
||||
setTabs((prev) => [...prev, newTab]);
|
||||
setActiveTabId(newTab.id);
|
||||
}
|
||||
};
|
||||
|
||||
const closeTab = (id: string, e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
const newTabs = tabs.filter((tab) => tab.id !== id);
|
||||
setTabs(newTabs);
|
||||
|
||||
if (activeTabId === id) {
|
||||
setActiveTabId(
|
||||
newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const closeAllTabs = () => {
|
||||
setTabs([]);
|
||||
setActiveTabId(null);
|
||||
};
|
||||
|
||||
const filteredMenuItems = menuItems
|
||||
.map((item) => ({
|
||||
...item,
|
||||
subItems: item.subItems.filter(
|
||||
(subItem) =>
|
||||
!subItem.path.includes("$") &&
|
||||
(search.trim() === "" ||
|
||||
getFaPermissions(subItem.name).includes(search.trim()))
|
||||
),
|
||||
}))
|
||||
.filter((item) => item.subItems.length > 0);
|
||||
|
||||
function findSubItemByPath(
|
||||
items: ItemWithSubItems[],
|
||||
path: string
|
||||
): ItemWithSubItems["subItems"][0] | null {
|
||||
for (const item of items) {
|
||||
for (const subItem of item.subItems) {
|
||||
if (subItem.path === path) return subItem;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTabObj = tabs.find((tab) => tab.id === activeTabId);
|
||||
const activeComponentItem =
|
||||
activeTabObj && findSubItemByPath(menuItems, activeTabObj.path);
|
||||
const ActiveComponent = activeComponentItem?.component || null;
|
||||
|
||||
const draggedTabIndex = useRef<number | null>(null);
|
||||
|
||||
const onDragStart = (e: React.DragEvent<HTMLDivElement>, index: number) => {
|
||||
draggedTabIndex.current = index;
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
};
|
||||
|
||||
const onDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const onDrop = (e: React.DragEvent, dropIndex: number) => {
|
||||
e.preventDefault();
|
||||
if (
|
||||
draggedTabIndex.current === null ||
|
||||
draggedTabIndex.current === dropIndex
|
||||
)
|
||||
return;
|
||||
|
||||
const newTabs = [...tabs];
|
||||
const draggedItem = newTabs[draggedTabIndex.current];
|
||||
|
||||
newTabs.splice(draggedTabIndex.current, 1);
|
||||
newTabs.splice(dropIndex, 0, draggedItem);
|
||||
|
||||
draggedTabIndex.current = null;
|
||||
setTabs(newTabs);
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
draggedTabIndex.current = null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full px-3 py-2 min-h-screen">
|
||||
<header className="backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-2xl shadow-lg border border-white/30 dark:border-dark-700/30 p-4 mb-4">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 rounded-lg bg-primary-600 dark:bg-primary-800 backdrop-blur-sm shadow-lg">
|
||||
<Squares2X2Icon className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<Typography
|
||||
variant="h6"
|
||||
className="text-dark-800 dark:text-dark-100 font-semibold"
|
||||
>
|
||||
داشبورد
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 w-full sm:w-auto">
|
||||
<div className="relative w-full sm:w-48 md:w-64">
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<MagnifyingGlassIcon className="w-4 h-4 text-dark-400 dark:text-dark-100" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجو..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pr-8 pl-3 py-2 text-xs rounded-lg backdrop-blur-sm bg-white/30 dark:bg-dark-800/30 border border-white/40 dark:border-dark-600/40 text-dark-700 dark:text-dark-200 placeholder:text-dark-400 focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/50 dark:focus:bg-dark-700/50 transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center sm:justify-start gap-1.5 px-2 py-1.5 rounded-lg backdrop-blur-sm bg-white/20 dark:bg-dark-800/80 border border-white/30 dark:border-dark-600/30">
|
||||
<CalendarIcon className="w-3 h-3 text-primary-500" />
|
||||
<span className="text-xs text-dark-600 dark:text-dark-300">
|
||||
{persianDate}
|
||||
</span>
|
||||
<span className="mx-1 h-3 w-px bg-dark-300 dark:bg-dark-600"></span>
|
||||
<ClockIcon className="w-3 h-3 text-primary-500" />
|
||||
<span className="text-xs text-dark-600 dark:text-dark-300">
|
||||
{time}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="space-y-3">
|
||||
{filteredMenuItems.length === 0 ? (
|
||||
<div className="backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-xl shadow-lg border border-white/30 dark:border-dark-700/30 p-8">
|
||||
<div className="flex flex-col items-center justify-center text-center space-y-3">
|
||||
<div className="p-3 rounded-full backdrop-blur-sm bg-white/30 dark:bg-dark-700/30">
|
||||
<MagnifyingGlassIcon className="w-6 h-6 text-dark-400" />
|
||||
</div>
|
||||
<Typography
|
||||
variant="body1"
|
||||
className="text-dark-600 dark:text-dark-300 font-medium"
|
||||
>
|
||||
موردی یافت نشد
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-dark-400 dark:text-dark-500"
|
||||
>
|
||||
هیچ آیتمی با عبارت "{search}" مطابقت ندارد
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`${
|
||||
checkIsMobile()
|
||||
? "space-y-3 pb-20"
|
||||
: "flex overflow-x-auto pb-2 gap-3 scrollbar-thin scrollbar-thumb-dark-300 dark:scrollbar-thumb-dark-600"
|
||||
}`}
|
||||
>
|
||||
{checkIsMobile()
|
||||
? filteredMenuItems.map(({ fa, icon: Icon, subItems }, index) => {
|
||||
const filteredSubItems = subItems.filter(
|
||||
(item) =>
|
||||
!item.path.includes("$") &&
|
||||
getFaPermissions(item.name).includes(search.trim())
|
||||
);
|
||||
|
||||
if (filteredSubItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
key={index}
|
||||
className="w-full space-y-5 border border-gray-200 dark:border-dark-600 bg-white dark:bg-dark-800 rounded-2xl p-6 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="w-6 h-6 text-primary-600 dark:text-primary-400" />
|
||||
<h2 className="text-xl font-bold text-dark-900 dark:text-white">
|
||||
{fa}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
{filteredSubItems.map((sub, subIndex) => (
|
||||
<motion.button
|
||||
key={subIndex}
|
||||
onClick={() => navigate({ to: sub.path })}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
className="flex items-center gap-2 cursor-pointer bg-gray-50 dark:bg-dark-700 text-right border border-gray-200 dark:border-dark-600 hover:border-primary-500 dark:hover:border-primary-400 shadow-sm rounded-xl p-3 transition-all duration-200"
|
||||
>
|
||||
<ArrowRightCircleIcon className="w-5 h-5 text-primary-500 dark:text-primary-400" />
|
||||
<span className="text-sm font-medium text-dark-800 dark:text-white">
|
||||
{getFaPermissions(sub.name)}
|
||||
</span>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})
|
||||
: filteredMenuItems.map(({ fa, icon: Icon, subItems }, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="flex-none w-48 backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-xl shadow-lg border border-white/30 dark:border-dark-700/30 overflow-hidden"
|
||||
>
|
||||
<div className="backdrop-blur-sm bg-white/30 dark:bg-dark-700/30 px-3 py-2 border-b border-white/20 dark:border-dark-600/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-md backdrop-blur-sm bg-primary-500/20 dark:bg-primary-400/20">
|
||||
<Icon className="w-3.5 h-3.5 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-dark-800 dark:text-dark-100 truncate">
|
||||
{fa}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-1.5 space-y-0.5">
|
||||
{subItems.map((sub, subIndex) => {
|
||||
const isActive = tabs.some(
|
||||
(tab) =>
|
||||
tab.path === sub.path && activeTabId === tab.id
|
||||
);
|
||||
return (
|
||||
<motion.div
|
||||
key={subIndex}
|
||||
initial={{ opacity: 0, x: -5 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{
|
||||
delay: index * 0.05 + subIndex * 0.02,
|
||||
}}
|
||||
whileHover={{ x: 1 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => openTab(sub)}
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded-md cursor-pointer transition-all duration-200 focus:outline-none ${
|
||||
isActive
|
||||
? "backdrop-blur-sm bg-primary-500/20 dark:bg-primary-400/20 text-primary-700 dark:text-primary-300 "
|
||||
: "hover:backdrop-blur-sm hover:bg-white/30 dark:hover:bg-dark-600/30 border-none"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-1.5 h-1.5 rounded-full ${
|
||||
isActive
|
||||
? "bg-primary-600 dark:bg-primary-400"
|
||||
: "bg-dark-400 dark:bg-dark-500"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`truncate ${
|
||||
isActive
|
||||
? "text-primary-700 dark:text-white font-medium"
|
||||
: "text-dark-600 dark:text-dark-200/80"
|
||||
}`}
|
||||
>
|
||||
{getFaPermissions(sub.name)}
|
||||
</span>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tabs.length > 0 && !checkIsMobile() && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-xl shadow-lg border border-white/30 dark:border-dark-700/30 overflow-hidden mt-4"
|
||||
>
|
||||
<div className="backdrop-blur-sm bg-white/30 dark:bg-dark-700/30 border-b border-white/20 dark:border-dark-600/20">
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500"></div>
|
||||
<span className="text-xs font-medium text-dark-600 dark:text-dark-300">
|
||||
صفحات باز
|
||||
</span>
|
||||
<span className="text-xs text-dark-400 dark:text-dark-500 backdrop-blur-sm bg-white/40 dark:bg-dark-600/40 px-1.5 py-0.5 rounded-full">
|
||||
{tabs.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{tabs.length > 1 && (
|
||||
<button
|
||||
onClick={closeAllTabs}
|
||||
className="flex items-center gap-1 text-xs text-dark-500 dark:text-dark-400 hover:text-red-500 dark:hover:text-red-400 px-2 py-1 rounded-md hover:backdrop-blur-sm hover:bg-red-500/20 dark:hover:bg-red-400/20 transition-all duration-200 focus:outline-none"
|
||||
>
|
||||
<XMarkIcon className="w-3 h-3" />
|
||||
بستن همه
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center overflow-x-auto scrollbar-hide backdrop-blur-sm bg-white/20 dark:bg-dark-700/20 border-b border-white/20 dark:border-dark-600/20">
|
||||
<AnimatePresence initial={false}>
|
||||
{tabs.map((tab, index) => (
|
||||
<motion.div
|
||||
draggable
|
||||
key={tab.id}
|
||||
onDragStart={(e: any) => onDragStart(e, index)}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={(e) => onDrop(e, index)}
|
||||
onDragEnd={onDragEnd}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 10 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
onClick={() => setActiveTabId(tab.id)}
|
||||
className={`group flex items-center gap-1.5 px-3 py-1 cursor-pointer transition-all duration-200 border-b-2 focus:outline-none ${
|
||||
activeTabId === tab.id
|
||||
? "backdrop-blur-sm bg-primary-500/20 dark:bg-primary-400/20 text-primary-700 dark:text-primary-300 border-primary-500 dark:border-primary-400"
|
||||
: "text-dark-600 dark:text-dark-300 hover:backdrop-blur-sm hover:bg-white/30 dark:hover:bg-dark-600/30 border-transparent hover:border-dark-300 dark:hover:border-dark-500"
|
||||
}`}
|
||||
>
|
||||
{tab.icon && (
|
||||
<tab.icon
|
||||
className={`w-3.5 h-3.5 flex-shrink-0 ${
|
||||
activeTabId === tab.id
|
||||
? "text-primary-600 dark:text-primary-400"
|
||||
: "text-dark-400 dark:text-dark-500"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs font-medium whitespace-nowrap">
|
||||
{tab.title}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => closeTab(tab.id, e)}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 rounded-full hover:backdrop-blur-sm hover:bg-white/40 dark:hover:bg-dark-500/40 transition-all duration-200 focus:outline-none"
|
||||
>
|
||||
<XMarkIcon className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTabId && (
|
||||
<motion.div
|
||||
key={activeTabId}
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -5 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
className="p-4 backdrop-blur-sm bg-white/10 dark:bg-dark-800/10"
|
||||
>
|
||||
{ActiveComponent && <ActiveComponent />}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
270
src/Pages/Herds.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import {
|
||||
useDrawerStore,
|
||||
useModalStore,
|
||||
} from "../context/zustand-store/appStore";
|
||||
import { LiveStockAddHerd } from "../partials/live-stock/LiveStockAddHerd";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { LIVESTOCKS } from "../routes/paths";
|
||||
import { LiveStockAddLiveStock } from "../partials/live-stock/LiveStockAddLiveStock";
|
||||
import { TableButton } from "../components/TableButton/TableButton";
|
||||
import { LiveStockHerdDetails } from "../partials/live-stock/LiveStockHerdDetails";
|
||||
|
||||
export default function LiveStocks() {
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [pagesTableData, setPagesTableData] = useState([]);
|
||||
const { openModal } = useModalStore();
|
||||
const { farmid, name } = useParams({ strict: false });
|
||||
const { openDrawer } = useDrawerStore();
|
||||
const { data: pagesData, refetch } = useApiRequest({
|
||||
api: farmid
|
||||
? `herd/web/api/v1/rancher/${farmid}/herds`
|
||||
: "/herd/web/api/v1/herd/",
|
||||
method: "get",
|
||||
params: pagesInfo,
|
||||
queryKey: ["LiveStockFarmers", pagesInfo],
|
||||
});
|
||||
|
||||
const { data: DashboardData, refetch: refetchDashboard } = useApiRequest<any>(
|
||||
{
|
||||
api: farmid
|
||||
? `/herd/web/api/v1/rancher/${farmid}/rancher_dashboard/`
|
||||
: "/herd/web/api/v1/rancher/rancher_main_dashboard/",
|
||||
queryKey: ["HerdsDashboard"],
|
||||
}
|
||||
);
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetch();
|
||||
refetchDashboard();
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (pagesData?.results) {
|
||||
const tableData = pagesData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.unit_unique_id,
|
||||
item?.name,
|
||||
item?.cooperative?.name || "-",
|
||||
item?.contractor?.name || "-",
|
||||
parseInt(item?.capacity)?.toLocaleString(),
|
||||
item?.activity === "I"
|
||||
? "صنعتی"
|
||||
: item?.activity === "V"
|
||||
? "روستایی"
|
||||
: item?.activity === "N"
|
||||
? "عشایری"
|
||||
: "-",
|
||||
item?.epidemiologic,
|
||||
parseInt(item?.light_livestock_number)?.toLocaleString(),
|
||||
parseInt(item?.heavy_livestock_number)?.toLocaleString(),
|
||||
item?.province?.name,
|
||||
item?.city?.name,
|
||||
item?.operating_license_state ? "دارد" : "ندارد",
|
||||
item?.activity_state ? "فعال" : "غیر فعال",
|
||||
item?.postal || "-",
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="livestock_farmers"
|
||||
access="Edit-Rancher"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: "ویرایش گله",
|
||||
content: (
|
||||
<LiveStockAddHerd
|
||||
getData={handleUpdate}
|
||||
item={item}
|
||||
rancher={item?.rancher?.id}
|
||||
/>
|
||||
),
|
||||
isOpen: true,
|
||||
direction: "left",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="ایجاد دام" position="right">
|
||||
<Button
|
||||
variant="submit"
|
||||
page="herds"
|
||||
access="Add-LiveStock"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: "ایجاد دام",
|
||||
content: (
|
||||
<LiveStockAddLiveStock
|
||||
getData={handleUpdate}
|
||||
herdId={item?.id}
|
||||
/>
|
||||
),
|
||||
isOpen: true,
|
||||
direction: "left",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="مشاهده دام ها" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page="herd_livestocks"
|
||||
access="Get-Herd-Livestocks"
|
||||
onClick={() => {
|
||||
const path = LIVESTOCKS + "/" + item?.id + "/" + item?.name;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="farmer_details"
|
||||
access="Delete-Rancher-Herd"
|
||||
api={`herd/web/api/v1/herd/${item?.id}/`}
|
||||
getData={handleUpdate}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setPagesTableData(tableData);
|
||||
}
|
||||
}, [pagesData, pagesInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
<Grid container column isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
noPagination
|
||||
noSearch
|
||||
title="خلاصه اطلاعات"
|
||||
columns={
|
||||
farmid
|
||||
? [
|
||||
"تعداد کل گله ها",
|
||||
"تعداد دام سنگین",
|
||||
"تعداد دام سبک",
|
||||
"مجموع وزن خرید از سهمیه ها",
|
||||
"جزئیات",
|
||||
]
|
||||
: [
|
||||
"تعداد کل گله ها",
|
||||
"تعداد کل دامداران",
|
||||
"تعداد گله های صنعتی",
|
||||
"تعداد گله های روستایی",
|
||||
"تعداد گله های عشایری",
|
||||
"تعداد دام سبک",
|
||||
"تعداد دام سنگین",
|
||||
"تعداد دامداران دارای گله",
|
||||
"تعداد دامداران بدون گله",
|
||||
"تعداد دامداران حقیقی",
|
||||
"تعداد دامداران حقوقی",
|
||||
"تعداد دامداران صنعتی",
|
||||
"تعداد دامداران روستایی",
|
||||
"تعداد دامداران عشایری",
|
||||
]
|
||||
}
|
||||
rows={
|
||||
farmid
|
||||
? [
|
||||
[
|
||||
DashboardData?.total_herds_count?.toLocaleString() || "0",
|
||||
DashboardData?.total_heavy_livestock_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.total_light_livestock_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.total_purchase_weight?.toLocaleString() ||
|
||||
"0",
|
||||
<TableButton
|
||||
size="small"
|
||||
key={DashboardData}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات دامدار",
|
||||
content: (
|
||||
<LiveStockHerdDetails farmid={farmid} name={name} />
|
||||
),
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
]
|
||||
: [
|
||||
[
|
||||
DashboardData?.herd_dashboard?.total_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_ranchers_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_industrial_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_Village_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_nomadic_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_heavy_livestock_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_light_livestock_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_with_herd?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_without_herd?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_natural_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_legal_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_industrial_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_village_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_nomadic_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
],
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
setPagesInfo(e);
|
||||
}}
|
||||
count={pagesData?.count || 10}
|
||||
isPaginated
|
||||
title={name ? `گله های ${name}` : "گله ها"}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"شناسه یکتا",
|
||||
"نام گله",
|
||||
"نام تعاونی",
|
||||
"نام شرکت پیمانکار",
|
||||
"ظرفیت",
|
||||
"نوع فعالیت",
|
||||
"کد اپیدمیولوژیک",
|
||||
"حجم دام سبک",
|
||||
"حجم دام سنگین",
|
||||
"استان",
|
||||
"شهر",
|
||||
"مجوز فعالیت",
|
||||
"وضعیت فعالیت",
|
||||
"کد پستی",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={pagesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
146
src/Pages/IncentivePlans.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { formatJustDate } from "../utils/formatTime";
|
||||
import { AddIncentivePlan } from "../partials/quota/AddIncentivePlan";
|
||||
import AutoComplete from "../components/AutoComplete/AutoComplete";
|
||||
|
||||
export default function IncentivePlans() {
|
||||
const { openModal } = useModalStore();
|
||||
const [params, setParams] = useState({ page: 1, page_size: 10 });
|
||||
const [tableData, setTableData] = useState([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState<(number | string)[]>([]);
|
||||
|
||||
const groupOptions = [
|
||||
{ key: "", value: "همه" },
|
||||
{ key: "rural", value: "روستایی" },
|
||||
{ key: "industrial", value: "صنعتی" },
|
||||
{ key: "nomadic", value: "عشایری" },
|
||||
];
|
||||
|
||||
const { data: apiData, refetch } = useApiRequest({
|
||||
api: `/product/web/api/v1/incentive_plan/active_plans/`,
|
||||
method: "get",
|
||||
params: {
|
||||
...params,
|
||||
...(selectedGroup.length > 0 && { group: selectedGroup[0] }),
|
||||
},
|
||||
queryKey: ["incentivePlans", params, selectedGroup],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (apiData?.results) {
|
||||
const formattedData = apiData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
params.page === 1
|
||||
? i + 1
|
||||
: i + params.page_size * (params.page - 1) + 1,
|
||||
item?.name,
|
||||
item?.description,
|
||||
item?.plan_type,
|
||||
item?.group === "rural"
|
||||
? "روستایی"
|
||||
: item?.group === "nomadic"
|
||||
? "عشایری"
|
||||
: "صنعتی",
|
||||
item?.is_time_unlimited ? "دارد" : "ندارد",
|
||||
formatJustDate(item?.start_date_limit),
|
||||
formatJustDate(item?.end_date_limit),
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش طرح" position="right">
|
||||
<Button
|
||||
page="incentive_plans"
|
||||
access="Update-Incentive-Plan"
|
||||
variant="edit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش طرح",
|
||||
content: <AddIncentivePlan getData={refetch} item={item} />,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<DeleteButtonForPopOver
|
||||
page="incentive_plans"
|
||||
access="Delete-Incentive-Plan"
|
||||
title="از حذف طرح تشویقی اطمینان دارید؟"
|
||||
api={`/product/web/api/v1/incentive_plan/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setTableData(formattedData);
|
||||
}
|
||||
}, [apiData, params]);
|
||||
|
||||
const handleGroupChange = (keys: (number | string)[]) => {
|
||||
setSelectedGroup(keys);
|
||||
setParams({ ...params, page: 1 });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container className="items-center gap-2">
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
page="incentive_plans"
|
||||
access="Post-Incentive-Plan"
|
||||
variant="submit"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
title: "ایجاد طرح تشویقی",
|
||||
content: <AddIncentivePlan getData={refetch} />,
|
||||
})
|
||||
}
|
||||
>
|
||||
ایجاد طرح تشویقی
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<AutoComplete
|
||||
data={groupOptions}
|
||||
selectedKeys={selectedGroup}
|
||||
onChange={handleGroupChange}
|
||||
title="فیلتر بر اساس گروه"
|
||||
size="small"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid className="w-full">
|
||||
<Table
|
||||
className="mt-2"
|
||||
excelInfo={{
|
||||
link: "product/excel/incentive_plan_excel",
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setParams(e);
|
||||
}}
|
||||
title="طرح های تشویقی"
|
||||
isPaginated
|
||||
count={apiData?.count || 10}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام",
|
||||
"توضیحات",
|
||||
"نوع طرح",
|
||||
"گروه",
|
||||
"محدودیت زمانی",
|
||||
"شروع محدودیت",
|
||||
"پایان طرح",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={tableData}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
49
src/Pages/Inventory.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||
import { InventoryStakeHolderAllocations } from "../partials/inventory/InventoryStakeHolderAllocations";
|
||||
import { InventoryWarehouseEntryTab } from "../partials/inventory/InventoryWarehouseEntryTab";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { InventoryEntriesList } from "../partials/inventory/InventoryEntriesList";
|
||||
|
||||
export default function Inventory() {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
const { profile } = useUserProfileStore();
|
||||
const params = useParams({ strict: false });
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
label: "ورودی به انبار",
|
||||
page: "inventory",
|
||||
access: "Entry-Inventory",
|
||||
},
|
||||
{
|
||||
label: "توزیع به زیر مجموعه",
|
||||
visible: profile?.role?.type?.key === "CO",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid container column className="justify-center mt-2">
|
||||
{params?.code ? (
|
||||
<InventoryEntriesList />
|
||||
) : (
|
||||
<>
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
<Grid container column className="mt-2">
|
||||
{selectedTab === 0 ? (
|
||||
<InventoryWarehouseEntryTab />
|
||||
) : (
|
||||
<InventoryStakeHolderAllocations />
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
283
src/Pages/LiveStockFarmers.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import Button from "../components/Button/Button";
|
||||
import { LiveStockAddRancher } from "../partials/live-stock/LiveStockAddRancher";
|
||||
import {
|
||||
useDrawerStore,
|
||||
useModalStore,
|
||||
} from "../context/zustand-store/appStore";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { LiveStockAddHerd } from "../partials/live-stock/LiveStockAddHerd";
|
||||
import { LiveStockAllocateCooperative } from "../partials/live-stock/LiveStockAllocateCooperative";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { LIVESTOCK_FARMERS } from "../routes/paths";
|
||||
import { LiveStockFarmersDashboardResponse } from "../types/LiveStockFarmers";
|
||||
|
||||
export default function LiveStockFarmers() {
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [pagesTableData, setPagesTableData] = useState([]);
|
||||
const { openDrawer } = useDrawerStore();
|
||||
const { openModal } = useModalStore();
|
||||
|
||||
const { data: pagesData, refetch } = useApiRequest({
|
||||
api: "/herd/web/api/v1/rancher/",
|
||||
method: "get",
|
||||
params: pagesInfo,
|
||||
queryKey: ["LiveStockFarmers", pagesInfo],
|
||||
});
|
||||
|
||||
const { data: DashboardData, refetch: refetchDashboard } =
|
||||
useApiRequest<LiveStockFarmersDashboardResponse>({
|
||||
api: `/herd/web/api/v1/rancher/rancher_main_dashboard/`,
|
||||
queryKey: ["LiveStockFarmersDashboard"],
|
||||
});
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetch();
|
||||
refetchDashboard();
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (pagesData?.results) {
|
||||
const tableData = pagesData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.ranching_farm,
|
||||
item?.first_name,
|
||||
item?.last_name,
|
||||
item?.national_code,
|
||||
item?.mobile,
|
||||
item?.organizations
|
||||
?.map((organization: any) => organization?.name)
|
||||
.join(", "),
|
||||
item?.activity === "V"
|
||||
? "روستایی"
|
||||
: item?.activity === "I"
|
||||
? "صنعتی"
|
||||
: item?.activity === "R"
|
||||
? "عشایری"
|
||||
: "-",
|
||||
item?.province?.name || "-",
|
||||
item?.city?.name || "-",
|
||||
item?.address,
|
||||
item?.without_herd ? "بدون دام" : "دامدار عادی",
|
||||
item?.rancher_type === "N" ? "حقیقی" : "حقوقی",
|
||||
item?.union_name || "-",
|
||||
item?.union_code || "-",
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="livestock_farmers"
|
||||
access="Edit-Rancher"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: "ویرایش دامدار",
|
||||
content: (
|
||||
<LiveStockAddRancher getData={handleUpdate} item={item} />
|
||||
),
|
||||
isOpen: true,
|
||||
direction: "left",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="مشاهده گله ها" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page="farmer_details"
|
||||
access="See-Herds"
|
||||
onClick={() => {
|
||||
const path =
|
||||
LIVESTOCK_FARMERS +
|
||||
"/" +
|
||||
item?.id +
|
||||
"/" +
|
||||
item?.ranching_farm;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="ایجاد گله" position="right">
|
||||
<Button
|
||||
variant="submit"
|
||||
page="livestock_farmers"
|
||||
access="Add-Herd"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: "ایجاد گله",
|
||||
content: (
|
||||
<LiveStockAddHerd
|
||||
getData={handleUpdate}
|
||||
rancher={item?.id}
|
||||
/>
|
||||
),
|
||||
isOpen: true,
|
||||
direction: "left",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="طرح های تشویقی" position="right">
|
||||
<Button
|
||||
variant="secondary-submit"
|
||||
page="farmer_plans"
|
||||
access="Get-Rancher-Plans"
|
||||
onClick={() => {
|
||||
const path =
|
||||
LIVESTOCK_FARMERS +
|
||||
"/plans/" +
|
||||
item.id +
|
||||
"/" +
|
||||
item?.ranching_farm;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="تخصیص به تعاونی" position="right">
|
||||
<Button
|
||||
variant="submit"
|
||||
page="livestock_farmers"
|
||||
access="Add-Rancher-Organization"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
title: "تخصیص به تعاونی",
|
||||
content: (
|
||||
<LiveStockAllocateCooperative
|
||||
getData={handleUpdate}
|
||||
item={item}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="livestock_farmers"
|
||||
access="Delete-Rancher"
|
||||
api={`herd/web/api/v1/rancher/${item?.id}/`}
|
||||
getData={handleUpdate}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setPagesTableData(tableData);
|
||||
}
|
||||
}, [pagesData]);
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4">
|
||||
<Grid container column isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
noPagination
|
||||
noSearch
|
||||
title="خلاصه اطلاعات"
|
||||
columns={[
|
||||
"تعداد کل دامداران",
|
||||
"تعداد کل گله ها",
|
||||
"تعداد گله های صنعتی",
|
||||
"تعداد گله های روستایی",
|
||||
"تعداد گله های عشایری",
|
||||
"تعداد دام سبک",
|
||||
"تعداد دام سنگین",
|
||||
"تعداد دامداران دارای گله",
|
||||
"تعداد دامداران بدون گله",
|
||||
"تعداد دامداران حقیقی",
|
||||
"تعداد دامداران حقوقی",
|
||||
"تعداد دامداران صنعتی",
|
||||
"تعداد دامداران روستایی",
|
||||
"تعداد دامداران عشایری",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
DashboardData?.rancher_dashboard?.total_ranchers_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_industrial_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_Village_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_nomadic_herds_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_heavy_livestock_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.herd_dashboard?.total_light_livestock_count?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_with_herd?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_without_herd?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_natural_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_legal_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_industrial_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_village_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
DashboardData?.rancher_dashboard?.total_nomadic_ranchers?.toLocaleString() ||
|
||||
"0",
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
page="livestock_farmers"
|
||||
access="Post-Farmer"
|
||||
variant="submit"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: "ایجاد دامدار",
|
||||
content: <LiveStockAddRancher getData={handleUpdate} />,
|
||||
isOpen: true,
|
||||
direction: "left",
|
||||
})
|
||||
}
|
||||
>
|
||||
ایجاد دامدار
|
||||
</Button>
|
||||
</Grid>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
setPagesInfo(e);
|
||||
}}
|
||||
count={pagesData?.count || 10}
|
||||
isPaginated
|
||||
title="دامداران"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام دامداری",
|
||||
"نام",
|
||||
"نام خانوادگی",
|
||||
"کد ملی",
|
||||
"موبایل",
|
||||
"تعاونی",
|
||||
"نوع فعالیت",
|
||||
"استان",
|
||||
"شهر",
|
||||
"آدرس",
|
||||
"وضعیت",
|
||||
"نوع دامدار",
|
||||
"نام واحد حقوقی",
|
||||
"شناسه ملی واحد حقوقی",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={pagesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
104
src/Pages/LiveStocks.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { formatAgeCalcuation, formatJustDate } from "../utils/formatTime";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { LiveStockAddLiveStock } from "../partials/live-stock/LiveStockAddLiveStock";
|
||||
import { useDrawerStore } from "../context/zustand-store/appStore";
|
||||
|
||||
export default function LiveStocks() {
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [pagesTableData, setPagesTableData] = useState([]);
|
||||
|
||||
const { openDrawer } = useDrawerStore();
|
||||
|
||||
const { herdid, name } = useParams({ strict: false });
|
||||
const { data: pagesData, refetch } = useApiRequest({
|
||||
api: herdid
|
||||
? `herd/web/api/v1/herd/${herdid}/live_stocks/`
|
||||
: "/livestock/web/api/v1/livestock/",
|
||||
method: "get",
|
||||
params: pagesInfo,
|
||||
queryKey: ["LiveStockFarmers", pagesInfo],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pagesData?.results) {
|
||||
const tableData = pagesData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.type?.name,
|
||||
// item?.use_type?.name,
|
||||
formatJustDate(item?.birthdate),
|
||||
formatAgeCalcuation(item?.birthdate),
|
||||
item?.tag?.tag_code || "-",
|
||||
item?.gender === 1 ? "نر" : "ماده",
|
||||
// item?.species?.name,
|
||||
item?.weight_type === "L" ? "سبک" : "سنگین",
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش دام" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="livestocks"
|
||||
access="Edit-LiveStock"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: "ویرایش دام",
|
||||
content: (
|
||||
<LiveStockAddLiveStock
|
||||
getData={refetch}
|
||||
herdId={item?.herd?.id}
|
||||
item={item}
|
||||
/>
|
||||
),
|
||||
isOpen: true,
|
||||
direction: "left",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
api={`livestock/web/api/v1/livestock/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setPagesTableData(tableData);
|
||||
}
|
||||
}, [pagesData, pagesInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
setPagesInfo(e);
|
||||
}}
|
||||
count={pagesData?.count || 10}
|
||||
isPaginated
|
||||
title={name ? `دام های ${name}` : "دام ها"}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"دام",
|
||||
// "نوع دام",
|
||||
"تاریخ تولد",
|
||||
"سن",
|
||||
"پلاک",
|
||||
"جنسیت",
|
||||
// "گونه",
|
||||
"دسته وزنی",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={pagesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
30
src/Pages/Management.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import Pages from "../partials/management/Pages";
|
||||
import Access from "../partials/management/Access";
|
||||
import UnusedAccess from "../partials/management/UnusedAccess";
|
||||
|
||||
const tabItems = [
|
||||
{ label: "صفحات" },
|
||||
{ label: "دسترسی ها" },
|
||||
{ label: "دسترسی های غیر فعال" },
|
||||
];
|
||||
|
||||
export default function Management() {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container column className="justify-center mt-2">
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
<Grid container column className="mt-2">
|
||||
{selectedTab === 0 && <Pages />}
|
||||
{selectedTab === 1 && <Access />}
|
||||
{selectedTab === 2 && <UnusedAccess />}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
162
src/Pages/Menu.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Typography from "../components/Typography/Typography";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||
import { getUserPermissions } from "../utils/getUserAvalableItems";
|
||||
import { ItemWithSubItems } from "../types/userPermissions";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Bars3Icon, QueueListIcon } from "@heroicons/react/24/outline";
|
||||
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||
import SVGImage from "../components/SvgImage/SvgImage";
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.08,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, scale: 0.95, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 20,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const submenuVariants = {
|
||||
hidden: { height: 0, opacity: 0 },
|
||||
visible: {
|
||||
height: "auto",
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: "tween",
|
||||
ease: "easeOut",
|
||||
duration: 0.1,
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Menu = () => {
|
||||
const { profile } = useUserProfileStore();
|
||||
|
||||
const menuItems: ItemWithSubItems[] = getUserPermissions(
|
||||
profile?.permissions
|
||||
);
|
||||
|
||||
const getOpenedItem = () => {
|
||||
if (window.location.pathname !== "/") {
|
||||
const matchedIndex = menuItems.findIndex((item) =>
|
||||
item.subItems.some((sub) => sub.path === window.location.pathname)
|
||||
);
|
||||
return matchedIndex;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(getOpenedItem());
|
||||
const navigate = useNavigate();
|
||||
|
||||
const toggleSubmenu = (index: number) => {
|
||||
setOpenIndex((prev) => (prev === index ? null : index));
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
column
|
||||
className="block md:hidden px-4 pt-6 pb-12 bg-gradient-to-b select-none items-start from-white to-dark-100 dark:from-dark-900 dark:to-dark-800 min-h-screen"
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
className="text-center gap-2 flex justify-center items-center text-dark-800 dark:text-dark-100 font-semibold text-xl mb-6"
|
||||
>
|
||||
<Bars3Icon className="w-4" />
|
||||
داشبورد
|
||||
</Typography>
|
||||
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="flex flex-col items-center gap-4 w-full"
|
||||
>
|
||||
{menuItems.map(({ fa, icon: Icon, subItems }, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
className="w-full max-w-sm"
|
||||
>
|
||||
<motion.button
|
||||
onClick={() => toggleSubmenu(index)}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
className="w-full flex justify-between items-center gap-3 px-4 py-3 rounded-lg shadow-xs dark:shadow-sm shadow-dark-300 dark:shadow-dark-500 backdrop-blur-md bg-gradient-to-r from-transparent to-transparent dark:via-gray-800 via-gray-100 border border-dark-200 dark:border-dark-700 text-dark-800 dark:text-dark-100 transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SVGImage
|
||||
src={Icon}
|
||||
className={` text-primary-800 dark:text-primary-100`}
|
||||
/>
|
||||
|
||||
<span className="text-base font-medium">{fa}</span>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`w-5 h-5 text-dark-500 dark:text-dark-300 transition-transform duration-300 ${
|
||||
openIndex === index ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openIndex === index && (
|
||||
<motion.div
|
||||
key="submenu"
|
||||
variants={submenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="mt-2 mr-4 border-r-2 border-primary-500 dark:border-primary-400 pr-4 flex flex-col gap-2"
|
||||
>
|
||||
{subItems
|
||||
.filter((item) => !item?.path.includes("$"))
|
||||
?.map((sub, subIndex) => (
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
navigate({ to: sub.path });
|
||||
}}
|
||||
key={subIndex}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
className="text-sm flex items-center gap-2 text-dark-700 dark:text-dark-200 bg-white dark:bg-dark-700 shadow-sm px-3 py-2 rounded-lg w-full text-right"
|
||||
>
|
||||
{" "}
|
||||
<QueueListIcon className="w-3" />
|
||||
{getFaPermissions(sub.name)}
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
39
src/Pages/NotFound.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { motion } from "framer-motion";
|
||||
import { HOME } from "../routes/paths";
|
||||
|
||||
export default function NotFound() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-center h-full bg-white dark:bg-dark-900 px-6 text-center text-gray-100">
|
||||
<motion.h1
|
||||
className="text-7xl sm:text-8xl md:text-9xl font-bold mb-4 select-none text-gray-900 dark:text-white"
|
||||
initial={{ opacity: 0, y: -40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 1, ease: "easeOut" }}
|
||||
>
|
||||
۴۰۴
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className="max-w-lg text-lg sm:text-xl mb-10 px-4 text-gray-900 dark:text-white"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6, duration: 1 }}
|
||||
>
|
||||
متأسفیم، صفحهای که دنبال آن هستید یافت نشد!
|
||||
</motion.p>
|
||||
|
||||
<motion.button
|
||||
className="px-8 py-3 cursor-pointer rounded-full bg-primary-600 hover:bg-primary-500 focus:outline-none focus:ring-4 focus:ring-primary-400 text-white font-semibold shadow-lg"
|
||||
initial={{ opacity: 0, scale: 0.85 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 1, duration: 0.3, ease: "easeOut" }}
|
||||
onClick={() => navigate({ to: HOME })}
|
||||
aria-label="بازگشت به صفحه اصلی"
|
||||
>
|
||||
بازگشت به صفحه اصلی
|
||||
</motion.button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
28
src/Pages/Organizations.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import { useState } from "react";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import { OrganizationsList } from "../partials/management/OrganizationsList";
|
||||
import { OrganizationsTypes } from "../partials/management/OrganizationsTypes";
|
||||
|
||||
export default function Organizations() {
|
||||
const tabItems = [
|
||||
{ label: "سازمان ها" },
|
||||
{
|
||||
label: "نهاد",
|
||||
page: "organizations",
|
||||
access: "Show-Organization-Type",
|
||||
},
|
||||
];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-2">
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
{selectedTab === 0 ? <OrganizationsList /> : <OrganizationsTypes />}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
156
src/Pages/Pos.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import Button from "../components/Button/Button";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { AddPos } from "../partials/pos/AddPos";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { AllocatePos } from "../partials/pos/AllocatePos";
|
||||
import { CreditCardIcon } from "@heroicons/react/24/outline";
|
||||
import { POS_POS_LIST } from "../routes/paths";
|
||||
|
||||
export default function Pos() {
|
||||
const { openModal } = useModalStore();
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [pagesTableData, setPagesTableData] = useState([]);
|
||||
const { profile } = useUserProfileStore();
|
||||
const { id, name } = useParams({ strict: false });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: pagesData, refetch } = useApiRequest({
|
||||
api: id
|
||||
? `pos_device/web/v1/pos/device/${id}/devices_by_psp/`
|
||||
: "/pos_device/web/v1/pos/device/my_devices/",
|
||||
method: "get",
|
||||
params: {
|
||||
...pagesInfo,
|
||||
admin: profile?.role?.type?.key === "ADM" ? true : false,
|
||||
},
|
||||
queryKey: ["pos", pagesInfo],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pagesData?.results) {
|
||||
const formattedData = pagesData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.assignment?.client
|
||||
? `${
|
||||
item?.assignment?.client?.organization?.id ? "سازمان" : "صنف"
|
||||
} ${item?.assignment?.client?.organization?.name}`
|
||||
: "-",
|
||||
item?.organization?.name,
|
||||
item?.acceptor,
|
||||
item?.serial,
|
||||
item?.terminal,
|
||||
item?.device_identity,
|
||||
item?.is_activated ? "فعال" : "غیر فعال",
|
||||
<Popover key={i}>
|
||||
<Tooltip title={"تعریف حساب"} position="right">
|
||||
<Button
|
||||
icon={<CreditCardIcon className="w-5 h-5 text-secondary-600" />}
|
||||
page={"pos_accounts"}
|
||||
access="Pos-Accounts"
|
||||
onClick={() => {
|
||||
const path = POS_POS_LIST + "/" + item?.id;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
item?.assignment?.client
|
||||
? "ویرایش تخصیص سازمان"
|
||||
: "تخصیص سازمان"
|
||||
}
|
||||
position="right"
|
||||
>
|
||||
<Button
|
||||
variant={item?.assignment?.client ? "secondary-edit" : "submit"}
|
||||
page={"pos"}
|
||||
access="Pos-Allocate-Organization"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: item?.assignment?.client
|
||||
? "ویرایش تخصیص سازمان"
|
||||
: "تخصیص سازمان",
|
||||
content: <AllocatePos getData={refetch} item={item} />,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="ویرایش" position="right">
|
||||
<Button
|
||||
page={name ? "pos_company_detail" : "pos"}
|
||||
access="Edit-Pos"
|
||||
variant="edit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش کارتخوان",
|
||||
content: <AddPos getData={refetch} item={item} />,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page={name ? "pos_company_detail" : "pos"}
|
||||
access="Delete-Pos-Device"
|
||||
api={`pos_device/web/v1/pos/device/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setPagesTableData(formattedData);
|
||||
}
|
||||
}, [pagesData, pagesInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
{!id && (
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
variant="submit"
|
||||
page="pos"
|
||||
access="Submit-Pos-Device"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ثبت کارتخوان",
|
||||
content: <AddPos getData={refetch} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
ثبت کارتخوان
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setPagesInfo}
|
||||
count={pagesData?.count || 10}
|
||||
isPaginated
|
||||
title={`دستگاه های ${name ? name : "پرداخت"} `}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"مالک",
|
||||
"شرکت psp",
|
||||
"پذیرنده",
|
||||
"سریال",
|
||||
"ترمینال",
|
||||
"کد پشتیبانی",
|
||||
"وضعیت",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={pagesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
246
src/Pages/PosAccounts.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import Button from "../components/Button/Button";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { AddPos } from "../partials/pos/AddPos";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { AllocatePos } from "../partials/pos/AllocatePos";
|
||||
import { PosAllocateOrganizationAccount } from "../partials/pos/PosAllocateOrganizationAccount";
|
||||
import { AllocateAccountToBroker } from "../partials/pos/AllocateAccountToBroker";
|
||||
import { BooleanQuestion } from "../components/BooleanQuestion/BooleanQuestion";
|
||||
|
||||
export default function PosAccounts() {
|
||||
const { openModal } = useModalStore();
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [accountsTableData, setAccountsTableData] = useState([]);
|
||||
|
||||
const { id } = useParams({ strict: false });
|
||||
|
||||
const { data: posData, refetch } = useApiRequest({
|
||||
api: `/pos_device/web/v1/pos/device/${id}/`,
|
||||
method: "get",
|
||||
queryKey: ["posAccountsDashboard", pagesInfo],
|
||||
});
|
||||
|
||||
const { data: posAccountsData, refetch: accountsRefetch } = useApiRequest({
|
||||
api: `pos_device/web/v1/pos/stake_holders/${id}/list_by_device/`,
|
||||
method: "get",
|
||||
params: {
|
||||
...pagesInfo,
|
||||
},
|
||||
queryKey: ["posAccounts", pagesInfo],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (posAccountsData?.results) {
|
||||
const formattedData = posAccountsData.results.map(
|
||||
(item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.organization?.name,
|
||||
item?.bank_account?.card,
|
||||
item?.bank_account?.account,
|
||||
item?.bank_account?.sheba,
|
||||
item?.default ? "حساب اصلی" : "زیر حساب",
|
||||
item?.broker?.name,
|
||||
<Popover key={i}>
|
||||
<Tooltip title={"تخصیص به کارگزار"} position="right">
|
||||
<Button
|
||||
page="pos_accounts"
|
||||
access="Allocate-Account-To-Broker"
|
||||
variant="submit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "تخصیص به کارگزار",
|
||||
content: (
|
||||
<AllocateAccountToBroker
|
||||
getData={accountsRefetch}
|
||||
item={item}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={"تنظیم بعنوان حساب اصلی"} position="right">
|
||||
<Button
|
||||
page="pos_accounts"
|
||||
access="Set-Main-Account"
|
||||
variant="set"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "تنظیم بعنوان حساب اصلی",
|
||||
content: (
|
||||
<BooleanQuestion
|
||||
api="pos_device/web/v1/pos/bank_account_device_link"
|
||||
method="post"
|
||||
getData={handleUpdate}
|
||||
title="آیا از تنظیم این حساب بعنوان حساب اصلی مطمئنید؟"
|
||||
payload={{
|
||||
organization: posData?.organization?.id,
|
||||
bank_account: item?.bank_account?.id,
|
||||
device: item?.id,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="pos_accounts"
|
||||
access="Delete-Account"
|
||||
api={`pos_device/web/v1/pos/stake_holders/${item?.id}/`}
|
||||
getData={handleUpdate}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
}
|
||||
);
|
||||
setAccountsTableData(formattedData);
|
||||
}
|
||||
}, [posAccountsData, pagesInfo]);
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetch();
|
||||
accountsRefetch();
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
<Grid container column isDashboard>
|
||||
<Table
|
||||
noPagination
|
||||
isDashboard
|
||||
className="mt-2"
|
||||
onChange={setPagesInfo}
|
||||
count={posData?.count || 10}
|
||||
isPaginated
|
||||
title={`مشخصات کارتخوان`}
|
||||
columns={[
|
||||
"مالک",
|
||||
"شرکت psp",
|
||||
"پذیرنده",
|
||||
"سریال",
|
||||
"ترمینال",
|
||||
"وضعیت",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
posData?.assignment?.client
|
||||
? `${
|
||||
posData?.assignment?.client?.organization?.id
|
||||
? "سازمان"
|
||||
: "صنف"
|
||||
} ${posData?.assignment?.client?.organization?.name}`
|
||||
: "-",
|
||||
posData?.organization?.name,
|
||||
posData?.acceptor,
|
||||
posData?.serial,
|
||||
posData?.terminal,
|
||||
posData?.is_activated ? "فعال" : "غیر فعال",
|
||||
<Popover key={posData}>
|
||||
<Tooltip
|
||||
title={
|
||||
posData?.assignment?.client
|
||||
? "ویرایش تخصیص"
|
||||
: "تخصیص سازمان"
|
||||
}
|
||||
position="right"
|
||||
>
|
||||
<Button
|
||||
variant="submit"
|
||||
page="pos_accounts"
|
||||
access="Allocate-Pos-Account"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: posData?.assignment?.client
|
||||
? "ویرایش سازمان"
|
||||
: "تخصیص سازمان",
|
||||
content: (
|
||||
<AllocatePos getData={handleUpdate} item={posData} />
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="ویرایش" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="pos_accounts"
|
||||
access="Edit-Pos-Account"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش کارتخوان",
|
||||
content: (
|
||||
<AddPos getData={handleUpdate} item={posData} />
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="pos_accounts"
|
||||
access="Delete-Pos"
|
||||
api={`pos_device/web/v1/pos/device/${posData?.id}/`}
|
||||
getData={handleUpdate}
|
||||
/>
|
||||
</Popover>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid className="mt-16" container column>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
variant="submit"
|
||||
page="pos_accounts"
|
||||
access="Allocate-Account"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "انتخاب لیست زیر حساب",
|
||||
content: (
|
||||
<PosAllocateOrganizationAccount
|
||||
getData={handleUpdate}
|
||||
item={posData}
|
||||
deviceId={id}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
انتخاب لیست زیر حساب
|
||||
</Button>
|
||||
</Grid>
|
||||
<Table
|
||||
className="mt-8"
|
||||
onChange={setPagesInfo}
|
||||
count={posAccountsData?.count || 10}
|
||||
isPaginated
|
||||
title={`زیر حساب ها`}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"سازمان",
|
||||
"شماره کارت",
|
||||
"شماره حساب",
|
||||
"شماره شبا",
|
||||
"نوع",
|
||||
"کارگزار",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={accountsTableData}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
82
src/Pages/PosCompanies.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { POS_COMPANIES } from "../routes/paths";
|
||||
|
||||
export default function PosCompanies() {
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [pagesTableData, setPagesTableData] = useState([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { data: pagesData } = useApiRequest({
|
||||
api: "/pos_device/web/v1/pos/device/psp_organizations/",
|
||||
method: "get",
|
||||
params: { pagesInfo },
|
||||
queryKey: ["posCompanies", pagesInfo],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pagesData?.results) {
|
||||
const formattedData = pagesData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.name,
|
||||
item?.national_unique_id,
|
||||
item?.province
|
||||
? `${item?.province?.name} (${item?.city?.name})`
|
||||
: "-",
|
||||
item?.field_of_activity === "CO"
|
||||
? "کشور"
|
||||
: item?.field_of_activity === "PR"
|
||||
? "استان"
|
||||
: item?.field_of_activity === "CI"
|
||||
? "شهرستان"
|
||||
: "نامشخص",
|
||||
<Popover key={i}>
|
||||
<Tooltip title="نمایش کارتخوان ها" position="right">
|
||||
<Button
|
||||
page="pos_company_detail"
|
||||
access="Show-Pos-Company-Devices"
|
||||
variant="detail"
|
||||
onClick={() => {
|
||||
const path =
|
||||
POS_COMPANIES + "/" + item?.id + "/" + item?.name;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setPagesTableData(formattedData);
|
||||
}
|
||||
}, [pagesData, pagesInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setPagesInfo}
|
||||
count={pagesData?.count || 10}
|
||||
isPaginated
|
||||
title="شرکت های پرداخت"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام",
|
||||
"شناسه یکتا",
|
||||
"استان/شهر",
|
||||
"حوزه فعالیت",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={pagesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
31
src/Pages/Pricing.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import { Attributes } from "../partials/feed-input/Attributes";
|
||||
import { Brokers } from "../partials/feed-input/Brokers";
|
||||
import { SaleUnits } from "../partials/feed-input/SaleUnits";
|
||||
const tabItems = [
|
||||
{ label: "مولفه" },
|
||||
{ label: "کارگزار" },
|
||||
{ label: "واحد فروش" },
|
||||
];
|
||||
|
||||
export default function Pricing() {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-2">
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
{selectedTab === 0 ? (
|
||||
<Attributes />
|
||||
) : selectedTab === 1 ? (
|
||||
<Brokers />
|
||||
) : (
|
||||
<SaleUnits />
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
266
src/Pages/Products.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import sabos from "../assets/images/products/saboos.png";
|
||||
import jo from "../assets/images/products/jo.png";
|
||||
import soya from "../assets/images/products/soya.png";
|
||||
import zorat from "../assets/images/products/zorat.png";
|
||||
import goosfandi from "../assets/images/products/constantre-goosfandi.png";
|
||||
import parvari from "../assets/images/products/constantre-parvari.png";
|
||||
import porTolid from "../assets/images/products/constantre-gave-shiri-por-tolid.png";
|
||||
import shiriMotevaset from "../assets/images/products/constantre-gave-shiri-motevaset.png";
|
||||
import defaultImage from "../assets/images/products/default.png";
|
||||
import Button from "../components/Button/Button";
|
||||
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { AddProduct } from "../partials/feed-input/AddProduct";
|
||||
import { getAbleToSee } from "../utils/getAbleToSee";
|
||||
import { DeleteProduct } from "../partials/feed-input/DeleteProduct";
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Product {
|
||||
id: number;
|
||||
create_date: string;
|
||||
modify_date: string;
|
||||
creator_info: string;
|
||||
modifier_info: string;
|
||||
trash: boolean;
|
||||
name: string;
|
||||
product_id: number;
|
||||
type: string;
|
||||
img: string;
|
||||
created_by: number;
|
||||
modified_by: number;
|
||||
category: Category;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export default function Products() {
|
||||
const { openModal } = useModalStore();
|
||||
|
||||
const { data: productsData, refetch } = useApiRequest({
|
||||
api: "/product/web/api/v1/product/",
|
||||
method: "get",
|
||||
params: { page: 1, page_size: 100 },
|
||||
queryKey: ["products"],
|
||||
});
|
||||
|
||||
const getProductImage = (name: string) => {
|
||||
switch (name) {
|
||||
case "سبوس":
|
||||
return sabos;
|
||||
case "جو":
|
||||
return jo;
|
||||
case "سویا":
|
||||
return soya;
|
||||
case "ذرت":
|
||||
return zorat;
|
||||
case "کنسانتره گوسفندی":
|
||||
return goosfandi;
|
||||
case "کنسانتره گاو شیری پر تولید":
|
||||
return porTolid;
|
||||
case "کنسانتره پرواری":
|
||||
return parvari;
|
||||
case "کنسانتره گاو شیری متوسط":
|
||||
return shiriMotevaset;
|
||||
default:
|
||||
return defaultImage;
|
||||
}
|
||||
};
|
||||
|
||||
const container = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const item = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
show: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: "easeOut",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="p-1 md:p-4 w-full"
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
variants={container}
|
||||
>
|
||||
<Grid>
|
||||
<Button
|
||||
className="mb-2"
|
||||
size="small"
|
||||
page="feed_input_products"
|
||||
access="Post-Product"
|
||||
variant="submit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ایجاد محصول",
|
||||
content: <AddProduct getData={refetch} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
ایجاد محصول
|
||||
</Button>
|
||||
</Grid>
|
||||
<motion.div className=" hidden md:grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 2xl:grid-cols-4 lg:mx-30 xl:mx-40 2xl:mx-70 gap-4 justify-items-center">
|
||||
{productsData?.results?.map((product: Product) => (
|
||||
<motion.div
|
||||
key={product.id}
|
||||
className="bg-white dark:bg-dark-600 rounded-lg shadow-md overflow-hidden flex flex-col max-w-60 w-full"
|
||||
variants={item}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
>
|
||||
<div className="h-50 bg-gray-100 dark:bg-dark-700">
|
||||
<img
|
||||
src={
|
||||
product?.img && product.img !== "empty"
|
||||
? `${product.img}?t=${product.modify_date || Date.now()}`
|
||||
: getProductImage(product.name)
|
||||
}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover dark:opacity-60"
|
||||
key={`${product.id}-${product.modify_date}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3 flex flex-col gap-1 text-sm text-gray-500 dark:text-dark-200">
|
||||
<p className="font-extrabold">{product.name}</p>
|
||||
<p>کد محصول: {product.product_id}</p>
|
||||
<p
|
||||
className={`text-xs p-1 px-2 rounded-lg w-12 text-center ${
|
||||
product.type === "gov"
|
||||
? "text-gray-600 bg-gray1-100"
|
||||
: "text-gray-100 bg-gray-400"
|
||||
}`}
|
||||
>
|
||||
{product.type === "gov" ? "دولتی" : "آزاد"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center gap-2 px-2 pb-3 mt-auto">
|
||||
<Button
|
||||
page="feed_input_products"
|
||||
access="Edit-Product"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش محصول",
|
||||
content: <AddProduct getData={refetch} item={product} />,
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
fullWidth
|
||||
className="bg-primary-500 dark:bg-dark-500 hover:bg-primary-400 dark:hover:bg-dark-400 text-white px-3 w-full rounded-lg text-sm"
|
||||
>
|
||||
ویرایش
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "آیا از حذف محصول اطمینان دارید؟",
|
||||
content: <DeleteProduct getData={refetch} item={product} />,
|
||||
});
|
||||
}}
|
||||
page="feed_input_products"
|
||||
access="Delete-Product"
|
||||
size="small"
|
||||
fullWidth
|
||||
className="bg-white dark:bg-dark-600 text-primary-600 border w-full px-3 rounded-lg hover:bg-red-100 dark:hover:bg-dark-500 text-sm"
|
||||
>
|
||||
حذف
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* mobile */}
|
||||
<motion.div className="grid md:hidden grid-cols-1 gap-2 justify-items-center">
|
||||
{productsData?.results?.map((product: Product) => (
|
||||
<motion.div
|
||||
key={product.id}
|
||||
className="bg-white dark:bg-dark-600 rounded-lg shadow-sm border border-gray-300 overflow-hidden flex justify-between items-center p-2 w-full"
|
||||
variants={item}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
>
|
||||
<div className="h-15 bg-transparent w-1/6 dark:bg-dark-700">
|
||||
<img
|
||||
src={
|
||||
product?.img && product.img !== "empty"
|
||||
? `${product.img}?t=${product.modify_date || Date.now()}`
|
||||
: getProductImage(product.name)
|
||||
}
|
||||
alt={product.name}
|
||||
className="w-15 h-full object-cover rounded-xl dark:opacity-80"
|
||||
key={`${product.id}-${product.modify_date}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-3 flex flex-col text-xs text-gray-500 dark:text-dark-200 w-3/6">
|
||||
<p className="font-extrabold">{product.name}</p>
|
||||
<p>کد محصول: {product.product_id}</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={`text-xs p-1 px-2 rounded-lg max-w-12 text-center w-1/6 ${
|
||||
product.type === "gov"
|
||||
? "text-gray-600 bg-gray1-100"
|
||||
: "text-gray-100 bg-gray-400"
|
||||
}`}
|
||||
>
|
||||
{product.type === "gov" ? "دولتی" : "آزاد"}
|
||||
</p>
|
||||
|
||||
<div className="flex justify-between items-end flex-col gap-4 w-1/6">
|
||||
<button
|
||||
className={`${getAbleToSee(
|
||||
"feed_input_products",
|
||||
"Edit-Product"
|
||||
)} rounded-full text-primary-600 text-sm`}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش محصول",
|
||||
content: <AddProduct getData={refetch} item={product} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "آیا از حذف محصول اطمینان دارید؟",
|
||||
content: <DeleteProduct getData={refetch} item={product} />,
|
||||
});
|
||||
}}
|
||||
className={`${getAbleToSee(
|
||||
"feed_input_products",
|
||||
"Delete-Product"
|
||||
)} text-red-400 rounded-lg text-sm`}
|
||||
>
|
||||
<TrashIcon className="w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
92
src/Pages/ProductsCategories.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { AddProductCategory } from "../partials/feed-input/AddProductCategory";
|
||||
|
||||
export const ProductsCategories = () => {
|
||||
const { openModal } = useModalStore();
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [pagesTableData, setPagesTableData] = useState([]);
|
||||
|
||||
const { data: pagesData, refetch } = useApiRequest({
|
||||
api: "/product/web/api/v1/category/",
|
||||
method: "get",
|
||||
params: pagesInfo,
|
||||
queryKey: ["productCategories", pagesInfo],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pagesData?.results) {
|
||||
const d = pagesData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.name,
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="product_categories"
|
||||
access="Post-Product-Category"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش دسته بندی",
|
||||
content: (
|
||||
<AddProductCategory item={item} getData={refetch} />
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="product_categories"
|
||||
access="Post-Product-Category"
|
||||
api={`product/web/api/v1/category/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setPagesTableData(d);
|
||||
}
|
||||
}, [pagesData, pagesInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
page="product_categories"
|
||||
access="Post-Product-Category"
|
||||
variant="submit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ایجاد دسته بندی",
|
||||
content: <AddProductCategory getData={refetch} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
ایجاد دسته بندی
|
||||
</Button>
|
||||
</Grid>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
setPagesInfo(e);
|
||||
}}
|
||||
count={pagesData?.count || 10}
|
||||
isPaginated
|
||||
title="دسته بندی ها"
|
||||
columns={["ردیف", "نام دسته بندی", "عملیات"]}
|
||||
rows={pagesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
54
src/Pages/Quota.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import { useState } from "react";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import { QuotaActives } from "../partials/quota/QuotaActives";
|
||||
import { QuotaClosed } from "../partials/quota/QuotaClosed";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { QuotaDistributions } from "../partials/quota/QuotaDistributions";
|
||||
import { QuotaAllDistributions } from "../partials/quota/QuotaAllDistributions";
|
||||
|
||||
export default function Quota() {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
label: "سهمیه های فعال",
|
||||
page: "quota",
|
||||
access: "Active-Tab",
|
||||
},
|
||||
{
|
||||
label: "لیست توزیع",
|
||||
page: "quota",
|
||||
access: "Distribution-Tab",
|
||||
},
|
||||
{
|
||||
label: "بایگانی",
|
||||
page: "quota",
|
||||
access: "Closed-Tab",
|
||||
},
|
||||
];
|
||||
|
||||
const params = useParams({ strict: false });
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-2">
|
||||
{params?.code ? (
|
||||
<QuotaDistributions />
|
||||
) : (
|
||||
<>
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
{selectedTab === 0 ? (
|
||||
<QuotaActives />
|
||||
) : selectedTab === 1 ? (
|
||||
<QuotaAllDistributions />
|
||||
) : (
|
||||
<QuotaClosed />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
110
src/Pages/RancherPlans.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { AddIncentivePlan } from "../partials/quota/AddIncentivePlan";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
|
||||
export default function RancherPlans() {
|
||||
const { openModal } = useModalStore();
|
||||
const [params, setParams] = useState({ page: 1, page_size: 10 });
|
||||
const [tableData, setTableData] = useState([]);
|
||||
const { farmid, name } = useParams({ strict: false });
|
||||
|
||||
const { data: apiData, refetch } = useApiRequest({
|
||||
api: `/herd/web/api/v1/rancher/${farmid}/rancher_plans/`,
|
||||
method: "get",
|
||||
params: params,
|
||||
queryKey: ["incentivePlans", params],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (apiData?.results) {
|
||||
const formattedData = apiData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
params.page === 1
|
||||
? i + 1
|
||||
: i + params.page_size * (params.page - 1) + 1,
|
||||
item?.plan_name,
|
||||
item?.livestock_type_name,
|
||||
item?.allowed_quantity?.toLocaleString(),
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش طرح" position="right">
|
||||
<Button
|
||||
page="farmer_plans"
|
||||
access="Edit-Rancher-Incentive-Plan"
|
||||
variant="edit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش طرح",
|
||||
content: <AddIncentivePlan getData={refetch} item={item} />,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<DeleteButtonForPopOver
|
||||
page="farmer_plans"
|
||||
access="Delete-Rancher-Incentive-Plan"
|
||||
title="از حذف طرح تشویقی اطمینان دارید؟"
|
||||
api={`/product/web/api/v1/incentive_plan/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setTableData(formattedData);
|
||||
}
|
||||
}, [apiData, params]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container className="items-center gap-2">
|
||||
<Grid>
|
||||
<Button
|
||||
className="hidden"
|
||||
size="small"
|
||||
page="farmer_plans"
|
||||
access="Post-Rancher-Incentive-Plan"
|
||||
variant="submit"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
title: "تخصیص طرح تشویقی",
|
||||
content: <AddIncentivePlan getData={refetch} />,
|
||||
})
|
||||
}
|
||||
>
|
||||
تخصیص طرح تشویقی
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid className="w-full">
|
||||
<Table
|
||||
className="mt-2"
|
||||
excelInfo={{
|
||||
link: "product/excel/incentive_plan_excel",
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setParams(e);
|
||||
}}
|
||||
title={`طرح های تشویقی دامدار (${name})`}
|
||||
isPaginated
|
||||
count={apiData?.count || 10}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام طرح",
|
||||
"نوع دام",
|
||||
"تعداد راس مجاز",
|
||||
// , "عملیات"
|
||||
]}
|
||||
rows={tableData}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
37
src/Pages/Reporting.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import { QuotaReportingProducts } from "../partials/quota/QuotaReportingProducts";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { QuotaReportingProductDetails } from "../partials/quota/QuotaReportingProductDetails";
|
||||
import { QuotaReportingQuotaDistributions } from "../partials/quota/QuotaReportingQuotaDistributions";
|
||||
|
||||
const tabItems = [
|
||||
{ label: "محصول" },
|
||||
// { label: "نقش ها", visible: false },
|
||||
// { label: "تراکنش ها", visible: false },
|
||||
];
|
||||
|
||||
export default function Reporting() {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
const params = useParams({ strict: false });
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-2">
|
||||
{params?.type === "quota" ? (
|
||||
<QuotaReportingProductDetails />
|
||||
) : params?.type === "distribution" ? (
|
||||
<QuotaReportingQuotaDistributions />
|
||||
) : (
|
||||
<>
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
{selectedTab === 0 ? <QuotaReportingProducts /> : <></>}
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
150
src/Pages/Roles.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import { AddRole } from "../partials/management/AddRole";
|
||||
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||
import ShowStringList from "../components/ShowStringList/ShowStringList";
|
||||
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
|
||||
import Typography from "../components/Typography/Typography";
|
||||
|
||||
export default function Roles() {
|
||||
const { openModal } = useModalStore();
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [pagesTableData, setPagesTableData] = useState([]);
|
||||
|
||||
const { data: pagesData, refetch } = useApiRequest({
|
||||
api: "/auth/api/v1/role/",
|
||||
method: "get",
|
||||
params: pagesInfo,
|
||||
queryKey: ["roles", pagesInfo],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pagesData?.results) {
|
||||
const tableData = pagesData.results.map((item: any, i: number) => {
|
||||
const accessList = Object.values(
|
||||
item?.permissions.reduce((acc: any, item: any) => {
|
||||
if (!acc[item.page]) {
|
||||
acc[item.page] = {
|
||||
page: item.page,
|
||||
names: [],
|
||||
descriptions: [],
|
||||
};
|
||||
}
|
||||
acc[item.page].names.push(item.name);
|
||||
acc[item.page].descriptions.push(item.description);
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.role_name,
|
||||
item?.parent_role?.name || "-",
|
||||
item?.type?.name,
|
||||
item?.type?.key || "-",
|
||||
item?.description,
|
||||
<ShowMoreInfo
|
||||
key={i}
|
||||
title="دسترسی ها"
|
||||
counter={[accessList?.length.toString()]}
|
||||
disabled={!accessList?.length}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
column
|
||||
className="gap-2 p-2 justify-start items-start"
|
||||
>
|
||||
{accessList?.map((opt: any) => (
|
||||
<Grid key={i} container column className="gap-2">
|
||||
<Grid>
|
||||
<Typography variant="subtitle2" sign="arrow">
|
||||
{getFaPermissions(opt.page)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<ShowStringList
|
||||
strings={opt.descriptions}
|
||||
showSearch={false}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>{" "}
|
||||
</ShowMoreInfo>,
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="roles_management"
|
||||
access="Edit-Role"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش نقش",
|
||||
content: <AddRole item={item} getData={refetch} />,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="roles_management"
|
||||
access="Delete-Role"
|
||||
api={`auth/api/v1/role/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setPagesTableData(tableData);
|
||||
}
|
||||
}, [pagesData, pagesInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
page="roles_management"
|
||||
access="Post-Role"
|
||||
variant="submit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ایجاد نقش",
|
||||
content: <AddRole getData={refetch} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
ایجاد نقش
|
||||
</Button>
|
||||
</Grid>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={(e) => {
|
||||
setPagesInfo(e);
|
||||
}}
|
||||
count={pagesData?.count || 10}
|
||||
isPaginated
|
||||
title="لیست نقش ها"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نقش",
|
||||
"نقش والد",
|
||||
"نوع نقش",
|
||||
"کلید",
|
||||
"توضیحات",
|
||||
"دسترسی ها",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={pagesTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
62
src/Pages/SettingsOfUnits.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import SettingCard from "../components/SettingCard/SettingCard";
|
||||
import { ShieldExclamationIcon, MapPinIcon } from "@heroicons/react/24/outline";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { CooperativesSettingsTable } from "../partials/units/CooperativesSettingsTable";
|
||||
|
||||
const tabItems = [
|
||||
{ label: "اتحادیه ها", visible: false },
|
||||
{ label: "تعاونی ها" },
|
||||
];
|
||||
|
||||
export default function SettingsOfUnits() {
|
||||
const { openModal } = useModalStore();
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-2">
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
<Grid container column className="mt-2">
|
||||
{selectedTab === 1 && (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<SettingCard
|
||||
title="محدودیت دریافت نهاده"
|
||||
description="تنظیم و مدیریت محدودیتهای دریافت نهاده برای تعاونیها"
|
||||
icon={ShieldExclamationIcon}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "محدودیت دریافت نهاده تعاونی ها",
|
||||
content: (
|
||||
<CooperativesSettingsTable settingsType="purchase_policy" />
|
||||
),
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SettingCard
|
||||
title="محدوده فعالیت تعاونی ها"
|
||||
description="تعریف و مدیریت محدوده جغرافیایی فعالیت تعاونیها"
|
||||
icon={MapPinIcon}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "محدوده فعالیت تعاونی ها",
|
||||
content: (
|
||||
<CooperativesSettingsTable settingsType="service_area" />
|
||||
),
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
164
src/Pages/Tagging.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Table from "../components/Table/Table";
|
||||
import Button from "../components/Button/Button";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { TagDetails } from "../partials/tagging/TagDetails";
|
||||
import { SubmitNewTags } from "../partials/tagging/SubmitNewTags";
|
||||
|
||||
export default function Tagging() {
|
||||
const { openModal } = useModalStore();
|
||||
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [tagsTableData, setTagsTableData] = useState([]);
|
||||
|
||||
const { data: tagsData, refetch } = useApiRequest({
|
||||
api: "/tag/web/api/v1/tag/",
|
||||
method: "get",
|
||||
queryKey: ["tagsList", tableInfo],
|
||||
params: {
|
||||
...tableInfo,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
|
||||
api: "/tag/web/api/v1/tag/tag_dashboard/",
|
||||
method: "get",
|
||||
queryKey: ["tagDashboard"],
|
||||
});
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetch();
|
||||
updateDashboard();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tagsData?.results) {
|
||||
const formattedData = tagsData.results.map((item: any, index: number) => {
|
||||
return [
|
||||
tableInfo.page === 1
|
||||
? index + 1
|
||||
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
|
||||
item?.tag_code || "-",
|
||||
item?.organization?.name || "بدون سازمان",
|
||||
item?.species_code === 1
|
||||
? "گاو"
|
||||
: item?.species_code === 2
|
||||
? "گاومیش"
|
||||
: item?.species_code === 3
|
||||
? "شتر"
|
||||
: item?.species_code === 4
|
||||
? "گوسفند"
|
||||
: item?.species_code === 5
|
||||
? "بز"
|
||||
: "نامشخص",
|
||||
item?.status === "F"
|
||||
? "آزاد"
|
||||
: item?.status === "A"
|
||||
? "پلاک شده"
|
||||
: item?.status === "R"
|
||||
? "رزرو"
|
||||
: "-",
|
||||
item?.ownership_code || "-",
|
||||
<Popover key={item.id}>
|
||||
<Tooltip title="جزئیات پلاک" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page="tagging"
|
||||
access="Tag-Details"
|
||||
disabled={item?.status === "F"}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات پلاک",
|
||||
content: <TagDetails tagId={item.id} />,
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="tagging"
|
||||
access="Delete-Tag"
|
||||
api={`/tag/web/api/v1/tag/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setTagsTableData(formattedData);
|
||||
}
|
||||
}, [tagsData]);
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4 mt-2">
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
variant="submit"
|
||||
page="tagging"
|
||||
access="Create-Tag"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ایجاد پلاک جدید",
|
||||
content: <SubmitNewTags getData={handleUpdate} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
ایجاد پلاک جدید
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
title="خلاصه اطلاعات"
|
||||
noPagination
|
||||
noSearch
|
||||
columns={[
|
||||
"تعداد کل",
|
||||
"تعداد پلاک های آزاد",
|
||||
"تعداد پلاک شده",
|
||||
"گاو",
|
||||
"گاومیش",
|
||||
"شتر",
|
||||
"گوسفند",
|
||||
"بز",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
tagDashboardData?.count?.toLocaleString() || 0,
|
||||
tagDashboardData?.free_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.assign_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.cow_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.buffalo_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.camel_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.sheep_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.goat_count?.toLocaleString() || 0,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setTableInfo}
|
||||
count={tagsData?.count || 0}
|
||||
isPaginated
|
||||
title="پلاک کوبی"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"پلاک",
|
||||
"سازمان ثبت کننده",
|
||||
"کد گونه",
|
||||
"وضعیت",
|
||||
"کد مالکیت ثبتی",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={tagsTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
118
src/Pages/Training.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { AcademicCapIcon, SparklesIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export default function Training() {
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-center h-full min-h-[60vh] bg-white dark:bg-dark-900 px-6 text-center">
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
>
|
||||
<motion.div
|
||||
className="relative mb-6"
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{
|
||||
delay: 0.2,
|
||||
duration: 0.5,
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<AcademicCapIcon className="w-24 h-24 sm:w-32 sm:h-32 text-primary-500 dark:text-primary-400" />
|
||||
<motion.div
|
||||
className="absolute -top-2 -right-2"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
rotate: [0, 10, -10, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 2,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
>
|
||||
<SparklesIcon className="w-8 h-8 text-warning-500" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4 text-gray-900 dark:text-white select-none"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.6 }}
|
||||
>
|
||||
بخش آموزش
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className="max-w-lg text-lg sm:text-xl mb-2 px-4 text-gray-600 dark:text-dark-300 font-medium"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6, duration: 0.6 }}
|
||||
>
|
||||
این بخش در دست توسعه است!
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
className="max-w-md text-sm sm:text-base px-4 text-gray-500 dark:text-dark-400"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8, duration: 0.6 }}
|
||||
>
|
||||
به زودی ویدیوهای آموزشی سامانه رصدام در این بخش قرار خواهد گرفت.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="mt-8 flex gap-2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1, duration: 0.6 }}
|
||||
>
|
||||
<motion.div
|
||||
className="w-2 h-2 rounded-full bg-primary-500"
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="w-2 h-2 rounded-full bg-primary-500"
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
delay: 0.2,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="w-2 h-2 rounded-full bg-primary-500"
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.5, 1, 0.5],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
delay: 0.4,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
271
src/Pages/Transactions.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Table from "../components/Table/Table";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { formatJustDate, formatJustTime } from "../utils/formatTime";
|
||||
import { TableButton } from "../components/TableButton/TableButton";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import TransactionDetails from "../partials/transactions/TransactionDetails";
|
||||
import { DashboardResponse, ProductSummaryItem } from "../types/transactions";
|
||||
import { ProductSummaryModal } from "../partials/transactions/ProductSummaryModal";
|
||||
import { PaginationParameters } from "../components/PaginationParameters/PaginationParameters";
|
||||
import TransactionSharingDetails from "../partials/transactions/TransactionSharingDetails";
|
||||
import { convertNumberToPersian } from "../utils/convertNumberToPersian";
|
||||
|
||||
type TransactionResponse = {
|
||||
results?: any[];
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export default function Transactions() {
|
||||
const [params, setParams] = useState({ page: 1, page_size: 10 });
|
||||
|
||||
const [publicParams, setPublicParams] = useState({
|
||||
start: null,
|
||||
end: null,
|
||||
search: null,
|
||||
status: "success",
|
||||
});
|
||||
const { openModal } = useModalStore();
|
||||
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
|
||||
const { data, refetch: refetchTransactions } =
|
||||
useApiRequest<TransactionResponse>({
|
||||
api: "/warehouse/web/api/v1/inventory_sale_transaction/",
|
||||
params: { ...params, ...publicParams },
|
||||
queryKey: ["transactions", params],
|
||||
});
|
||||
|
||||
const { data: DashboardData, refetch: refetchDashboard } =
|
||||
useApiRequest<DashboardResponse>({
|
||||
api: `/warehouse/web/api/v1/inventory_sale_transaction/transactions_dashboard/`,
|
||||
params: { ...publicParams },
|
||||
queryKey: ["distributions_dashboard"],
|
||||
});
|
||||
|
||||
const productSummary: ProductSummaryItem[] =
|
||||
DashboardData?.product_summary || [];
|
||||
const hasProductSummary = productSummary.length > 0;
|
||||
|
||||
const handleOpenProductSummaryModal = () => {
|
||||
if (!hasProductSummary) return;
|
||||
|
||||
openModal({
|
||||
title: "آمار محصولات",
|
||||
content: <ProductSummaryModal products={productSummary} />,
|
||||
isFullSize: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetchTransactions();
|
||||
refetchDashboard();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.results) {
|
||||
const formatted = data.results.map((item: any, i: number) => {
|
||||
const items = item?.items || [];
|
||||
const quotaSaleUnits = items
|
||||
.map((p: any) => p?.quota_sale_unit)
|
||||
.filter((unit: any) => unit);
|
||||
const totalWeight = items.reduce(
|
||||
(sum: number, p: any) => sum + (p?.weight || 0),
|
||||
0
|
||||
);
|
||||
|
||||
let weightNature;
|
||||
if (quotaSaleUnits.length > 0) {
|
||||
const allEqual =
|
||||
items.length > 0 &&
|
||||
quotaSaleUnits.length === items.length &&
|
||||
new Set(quotaSaleUnits).size === 1;
|
||||
weightNature = allEqual
|
||||
? `${totalWeight.toLocaleString()} ${quotaSaleUnits[0] || ""}`
|
||||
: "ترکیبی";
|
||||
} else {
|
||||
weightNature = `${totalWeight.toLocaleString()} کیلوگرم`;
|
||||
}
|
||||
|
||||
return [
|
||||
params.page === 1
|
||||
? i + 1
|
||||
: i + params.page_size * (params.page - 1) + 1,
|
||||
item?.seller_organization?.name || "-",
|
||||
item?.rancher_fullname || "آزاد",
|
||||
item?.rancher?.national_code || "-",
|
||||
<Grid container column key={i}>
|
||||
<Grid>{formatJustDate(item?.transaction_date) || "-"}</Grid>
|
||||
<Grid>{formatJustTime(item?.transaction_date) || "-"}</Grid>
|
||||
</Grid>,
|
||||
item?.items?.map((p: any) => `${p?.name}`).join(" - ") || "-",
|
||||
item?.transaction_id || "-",
|
||||
item?.pos_device?.serial || "-",
|
||||
item?.payer_cart || "-",
|
||||
item?.product_type === "gov" ? "دولتی" : "آزاد",
|
||||
item?.price_paid?.toLocaleString() ||
|
||||
item?.transaction_price?.toLocaleString() ||
|
||||
"-",
|
||||
weightNature,
|
||||
item?.transaction_status === "waiting"
|
||||
? "درحال انتظار"
|
||||
: item?.transaction_status === "success"
|
||||
? "موفق"
|
||||
: item?.transaction_status === "failed"
|
||||
? `ناموفق ( ${item?.result_text || "-"} ${
|
||||
item?.transaction_status_code || ""
|
||||
} )`
|
||||
: "-",
|
||||
<TableButton
|
||||
size="small"
|
||||
key={i}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات تراکنش",
|
||||
content: (
|
||||
<TransactionDetails
|
||||
transaction={item}
|
||||
items={item?.items || []}
|
||||
/>
|
||||
),
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
});
|
||||
setTableData(formatted);
|
||||
} else {
|
||||
setTableData([]);
|
||||
}
|
||||
}, [data, params.page, params.page_size]);
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4">
|
||||
<PaginationParameters
|
||||
excelInfo={{
|
||||
link: `/warehouse/excel/inventory_sale_transaction_excel/?status=${
|
||||
publicParams.status
|
||||
}&start=${publicParams.start || ""}&end=${
|
||||
publicParams.end || ""
|
||||
}&search=${publicParams.search || ""}`,
|
||||
title: "لیست تراکنش ها",
|
||||
}}
|
||||
getData={handleUpdate}
|
||||
title="فیلتر تراکنش"
|
||||
onChange={(r) => {
|
||||
setPublicParams((prev) => ({ ...prev, ...(r as any) }));
|
||||
setParams((prev) => ({ ...prev, page: 1 }));
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
key: "status",
|
||||
data: [
|
||||
{ key: "", value: "همه" },
|
||||
{ key: "success", value: "موفق" },
|
||||
{ key: "failed", value: "ناموفق" },
|
||||
{ key: "waiting", value: "درحال انتظار" },
|
||||
],
|
||||
selectedKeys: [publicParams.status],
|
||||
onChange: (keys) => {
|
||||
setPublicParams((prev) => ({
|
||||
...prev,
|
||||
status: keys[0] as string,
|
||||
}));
|
||||
setParams((prev) => ({ ...prev, page: 1 }));
|
||||
},
|
||||
title: "وضعیت",
|
||||
size: "small",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Grid container column isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
noPagination
|
||||
noSearch
|
||||
title="خلاصه تراکنش ها"
|
||||
columns={[
|
||||
"تعداد کل تراکنشها",
|
||||
"تراکنشهای موفق",
|
||||
"تراکنشهای ناموفق",
|
||||
"تراکنشهای در انتظار",
|
||||
"مجموع مبلغ (ریال)",
|
||||
"مبلغ به حروف (ریال)",
|
||||
"مجموع وزن (کیلوگرم)",
|
||||
"جزئیات محصولات",
|
||||
"جزئیات تسهیم",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
DashboardData?.transaction_summary?.total_transactions?.toLocaleString() ||
|
||||
0,
|
||||
DashboardData?.transaction_summary?.success_transactions?.toLocaleString() ||
|
||||
0,
|
||||
DashboardData?.transaction_summary?.failed_transactions?.toLocaleString() ||
|
||||
0,
|
||||
DashboardData?.transaction_summary?.waiting_transactions?.toLocaleString() ||
|
||||
0,
|
||||
DashboardData?.transaction_summary?.total_amount?.toLocaleString() ||
|
||||
0,
|
||||
convertNumberToPersian(
|
||||
DashboardData?.transaction_summary?.total_amount || 0
|
||||
),
|
||||
DashboardData?.transaction_summary?.total_weight?.toLocaleString() ||
|
||||
0,
|
||||
<TableButton
|
||||
size="small"
|
||||
key={DashboardData?.product_summary?.length}
|
||||
disabled={!hasProductSummary}
|
||||
onClick={handleOpenProductSummaryModal}
|
||||
/>,
|
||||
<TableButton
|
||||
size="small"
|
||||
key={DashboardData?.product_summary?.length}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات تسهیم",
|
||||
content: (
|
||||
<TransactionSharingDetails
|
||||
data={DashboardData?.brokers_sharing_summary}
|
||||
/>
|
||||
),
|
||||
isFullSize: true,
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid className="w-full">
|
||||
<Table
|
||||
onChange={setParams}
|
||||
count={data?.count || 0}
|
||||
isPaginated
|
||||
noSearch
|
||||
title="لیست تراکنش ها"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"تعاونی",
|
||||
"دامدار",
|
||||
"کد ملی دامدار",
|
||||
"تاریخ",
|
||||
"محصولات",
|
||||
"شناسه تراکنش",
|
||||
"سریال دستگاه",
|
||||
"شماره کارت",
|
||||
"نوع فروش",
|
||||
"مبلغ",
|
||||
"ماهیت وزن",
|
||||
"وضعیت",
|
||||
"جزئیات تراکنش",
|
||||
]}
|
||||
rows={tableData}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
82
src/Pages/Unions.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
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 { COOPERATIVE_LIST } from "../routes/paths";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export default function Unions() {
|
||||
const [pagesInfo, setPagesInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [unionsTableData, setUnionsTableData] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: unionsData } = useApiRequest({
|
||||
api: "/auth/api/v1/organization/get_union_orgs/",
|
||||
method: "get",
|
||||
params: {
|
||||
...pagesInfo,
|
||||
},
|
||||
queryKey: ["unions", pagesInfo],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (unionsData?.results) {
|
||||
const formattedData = unionsData.results.map((item: any, i: number) => {
|
||||
return [
|
||||
pagesInfo.page === 1
|
||||
? i + 1
|
||||
: i + pagesInfo.page_size * (pagesInfo.page - 1) + 1,
|
||||
item?.name || "-",
|
||||
item?.type?.name || "-",
|
||||
item?.province?.name || "-",
|
||||
item?.city?.name || "-",
|
||||
item?.parent_organization?.name || "-",
|
||||
item?.national_unique_id || "-",
|
||||
item?.address || "-",
|
||||
<Popover key={i}>
|
||||
<Tooltip title="نمایش تعاونی ها" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page={"union_cooperatives"}
|
||||
access="Show-Union-Cooperatives"
|
||||
onClick={() => {
|
||||
const path =
|
||||
COOPERATIVE_LIST + "/" + item?.id + "/" + item?.name;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setUnionsTableData(formattedData);
|
||||
}
|
||||
}, [unionsData, pagesInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setPagesInfo}
|
||||
count={unionsData?.count || 10}
|
||||
isPaginated
|
||||
title="اتحادیه ها"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام",
|
||||
"نوع سازمان",
|
||||
"استان",
|
||||
"شهر",
|
||||
"سازمان والد",
|
||||
"شناسه کشوری",
|
||||
"آدرس",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={unionsTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
286
src/Pages/UserProfile.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import {
|
||||
PhoneIcon,
|
||||
MapPinIcon,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
ArrowLeftStartOnRectangleIcon,
|
||||
IdentificationIcon,
|
||||
CalendarIcon,
|
||||
HomeIcon,
|
||||
UserIcon,
|
||||
BuildingOfficeIcon,
|
||||
BookOpenIcon,
|
||||
InformationCircleIcon,
|
||||
} 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 { formatJustDate } from "../utils/formatTime";
|
||||
import bg from "../assets/images/profile-bg.png";
|
||||
import bgdark from "../assets/images/profile-bg-dark.png";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { TRAINING } from "../routes/paths";
|
||||
import { useFetchProfile } from "../hooks/useFetchProfile";
|
||||
import { useEffect } from "react";
|
||||
import versionRaw from "../version.txt?raw";
|
||||
|
||||
interface ProfileCardProps {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
value: string | null;
|
||||
show?: boolean;
|
||||
variant?: "default" | "highlight" | "accent";
|
||||
}
|
||||
|
||||
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 break-words">
|
||||
{value}
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default function UserProfile() {
|
||||
const [isDark, setIsDark] = useDarkMode();
|
||||
const navigate = useNavigate();
|
||||
const { profile } = useUserProfileStore();
|
||||
const { getProfile } = useFetchProfile();
|
||||
|
||||
useEffect(() => {
|
||||
getProfile();
|
||||
}, []);
|
||||
|
||||
const { openModal } = useModalStore();
|
||||
const version = versionRaw.trim();
|
||||
|
||||
return (
|
||||
<Grid className="min-h-screen bg-gray-50 dark:bg-dark-900">
|
||||
<div
|
||||
className="relative overflow-hidden bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url(${isDark ? bgdark : bg})` }}
|
||||
>
|
||||
<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">
|
||||
<UserIcon className="h-24 w-24 md:h-28 md:w-28 text-gray-600 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-gray-600 dark:text-dark-100 bg-clip-text drop-shadow-sm">
|
||||
{profile?.user?.first_name} {profile?.user?.last_name}
|
||||
</h1>
|
||||
|
||||
<Grid container className="w-auto gap-2 justify-start">
|
||||
<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">
|
||||
{profile?.role?.role_name}
|
||||
</span>
|
||||
</Grid>
|
||||
{profile?.organization?.name && (
|
||||
<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">
|
||||
سازمان: {profile?.organization?.name}
|
||||
</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-gray-600 dark:text-dark-100" />
|
||||
) : (
|
||||
<MoonIcon className="w-5 h-5 text-gray-600 dark:text-dark-100" />
|
||||
)}
|
||||
</Grid>
|
||||
<span className="text-gray-700 dark:text-dark-100 font-semibold text-sm">
|
||||
{isDark ? "حالت روشن" : "حالت تاریک"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const path = TRAINING;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<BookOpenIcon className="h-5 w-5 text-green-500" />
|
||||
</Grid>
|
||||
<span className="text-green-600 font-semibold text-sm">
|
||||
آموزش
|
||||
</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-red-700" />
|
||||
</Grid>
|
||||
<span className="text-red-700 font-semibold text-sm">
|
||||
خروج از سامانه
|
||||
</span>
|
||||
</button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<Grid className="pb-8 mt-4">
|
||||
<Grid className="max-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?.user?.username || "بدون نام کاربری"}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={PhoneIcon}
|
||||
label="شماره موبایل"
|
||||
value={profile?.user?.mobile || "بدون شماره موبایل"}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={MapPinIcon}
|
||||
label="موقعیت"
|
||||
value={`${profile?.organization?.province?.name || ""}، ${
|
||||
profile?.organization?.city?.name || ""
|
||||
}`}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={IdentificationIcon}
|
||||
label="کد ملی"
|
||||
value={profile?.user?.national_code || "بدون کد ملی"}
|
||||
show={!!profile?.user?.national_code}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={CalendarIcon}
|
||||
label="تاریخ تولد"
|
||||
value={
|
||||
profile?.user?.birthdate
|
||||
? formatJustDate(profile.user.birthdate)
|
||||
: "بدون تاریخ تولد"
|
||||
}
|
||||
show={!!profile?.user?.birthdate}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={HomeIcon}
|
||||
label="آدرس"
|
||||
value={profile?.user?.address || "بدون آدرس"}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={BuildingOfficeIcon}
|
||||
label="نوع کاربر"
|
||||
value={profile?.user?.ownership === "L" ? "حقوقی" : "حقیقی"}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={BuildingOfficeIcon}
|
||||
label="نام واحد"
|
||||
value={profile?.user?.unit_name || "بدون نام واحد"}
|
||||
show={profile?.user?.ownership === "L"}
|
||||
/>
|
||||
|
||||
<ProfileCard
|
||||
icon={IdentificationIcon}
|
||||
label="شناسه ملی واحد"
|
||||
value={profile?.user?.unit_national_id || "بدون شناسه ملی واحد"}
|
||||
show={profile?.user?.ownership === "L"}
|
||||
/>
|
||||
</Grid>
|
||||
{version && (
|
||||
<Grid className="mt-6 flex justify-end">
|
||||
<Grid className="inline-flex items-center gap-1 bg-white dark:bg-dark-600 backdrop-blur-sm rounded-lg py-1 px-3 border border-sky-200/40">
|
||||
<InformationCircleIcon className="h-3 w-3 text-sky-600" />
|
||||
<span className="text-sky-600 dark:text-dark-100 font-semibold text-xs">
|
||||
نسخه {version}
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
208
src/Pages/Users.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import Table from "../components/Table/Table";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFaPermissions } from "../utils/getFaPermissions";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import Button from "../components/Button/Button";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import {
|
||||
useDrawerStore,
|
||||
useModalStore,
|
||||
} from "../context/zustand-store/appStore";
|
||||
import { EditAccess } from "../partials/management/EditAccess";
|
||||
import { AddUser } from "../partials/management/AddUser";
|
||||
import ShowStringList from "../components/ShowStringList/ShowStringList";
|
||||
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
|
||||
import AutoComplete from "../components/AutoComplete/AutoComplete";
|
||||
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||
|
||||
type PermissionType = {
|
||||
page_name: string;
|
||||
};
|
||||
|
||||
export default function Users() {
|
||||
const { openModal } = useModalStore();
|
||||
const { openDrawer } = useDrawerStore();
|
||||
const [selectedRolesKeys, setSelectedRolesKeys] = useState<
|
||||
(string | number)[]
|
||||
>([]);
|
||||
const { profile } = useUserProfileStore();
|
||||
const [params, setParams] = useState({ page: 1, page_size: 10 });
|
||||
const [tableData, setTableData] = useState([]);
|
||||
|
||||
const { data: apiData, refetch } = useApiRequest({
|
||||
api: `/auth/api/v1/user-relations/?role=${selectedRolesKeys[0] || ""}`,
|
||||
method: "get",
|
||||
params: params,
|
||||
queryKey: ["users", params, selectedRolesKeys],
|
||||
});
|
||||
|
||||
const { data: rolesData } = useApiRequest({
|
||||
api: "/auth/api/v1/role/",
|
||||
method: "get",
|
||||
params: { page: 1, page_size: 1000 },
|
||||
queryKey: ["roles"],
|
||||
});
|
||||
|
||||
const formattedRolesData = [
|
||||
{
|
||||
key: "",
|
||||
value: "کل نقش ها",
|
||||
},
|
||||
...(rolesData?.results
|
||||
?.filter((role: any) => {
|
||||
if (profile?.role?.type?.key !== "ADM") {
|
||||
return role.type?.key !== "ADM";
|
||||
}
|
||||
return true;
|
||||
})
|
||||
?.map((role: any) => ({
|
||||
key: role.id,
|
||||
value: role.role_name,
|
||||
})) || []),
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (apiData?.results) {
|
||||
const formattedData = apiData.results.map((item: any, i: number) => {
|
||||
const permissionLength = item?.permissions?.length;
|
||||
return [
|
||||
params.page === 1
|
||||
? i + 1
|
||||
: i + params.page_size * (params.page - 1) + 1,
|
||||
item?.user?.username,
|
||||
item?.user?.first_name,
|
||||
item?.user?.last_name,
|
||||
item?.organization?.name,
|
||||
item?.role?.role_name,
|
||||
item?.user?.mobile,
|
||||
item?.user?.national_code,
|
||||
item?.user?.province_name
|
||||
? `${item?.user?.province_name} (${item?.user?.city_name})`
|
||||
: "-",
|
||||
item?.user?.address || "-",
|
||||
item?.user?.ownership === "L" ? "حقوقی" : "حقیقی",
|
||||
item?.user?.unit_name || "-",
|
||||
item?.user?.unit_national_id || "-",
|
||||
item?.user?.is_active ? "فعال" : "غیر فعال",
|
||||
<ShowMoreInfo
|
||||
key={i}
|
||||
title="دسترسی ها"
|
||||
disabled={!permissionLength}
|
||||
counter={permissionLength}
|
||||
>
|
||||
<ShowStringList
|
||||
strings={item?.permissions?.map((option: PermissionType) =>
|
||||
getFaPermissions(option?.page_name)
|
||||
)}
|
||||
/>
|
||||
</ShowMoreInfo>,
|
||||
<Popover key={i}>
|
||||
<Tooltip title="ویرایش پروفایل کاربر" position="right">
|
||||
<Button
|
||||
page="users"
|
||||
access="Update-User-Profile"
|
||||
variant="edit"
|
||||
onClick={() => {
|
||||
openDrawer({
|
||||
title: "ویرایش پروفایل کاربر",
|
||||
content: <AddUser getData={refetch} item={item} />,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="ویرایش دسترسی کاربر" position="right">
|
||||
<Button
|
||||
page="users"
|
||||
access="Update-User-Access"
|
||||
variant="secondary-edit"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش دسترسی کاربر",
|
||||
content: <EditAccess getData={refetch} item={item} />,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="users"
|
||||
access="Delete-User"
|
||||
title="از حذف کاربر اطمینان دارید؟"
|
||||
api={`/auth/api/v1/user/${item?.user?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setTableData(formattedData);
|
||||
}
|
||||
}, [apiData, params]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container className="items-center gap-2">
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
page="users"
|
||||
access="Create-User"
|
||||
variant="submit"
|
||||
onClick={() =>
|
||||
openDrawer({
|
||||
title: "ایجاد کاربر",
|
||||
content: <AddUser getData={refetch} />,
|
||||
isOpen: true,
|
||||
direction: "left",
|
||||
})
|
||||
}
|
||||
>
|
||||
ایجاد کاربر
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<AutoComplete
|
||||
inPage
|
||||
size="small"
|
||||
data={formattedRolesData}
|
||||
selectedKeys={selectedRolesKeys}
|
||||
onChange={setSelectedRolesKeys}
|
||||
title="فیلتر نقش"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid className="w-full">
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setParams}
|
||||
excelInfo={{
|
||||
link: "auth/excel/user_relations_excel/",
|
||||
}}
|
||||
title="کاربران"
|
||||
isPaginated
|
||||
count={apiData?.count || 10}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"نام کاربری",
|
||||
"نام",
|
||||
"نام خانوادگی",
|
||||
"سازمان",
|
||||
"نقش",
|
||||
"موبایل",
|
||||
"کد ملی",
|
||||
"استان/شهر",
|
||||
"آدرس",
|
||||
"مالکیت",
|
||||
"نام واحد",
|
||||
"شناسه ملی واحد",
|
||||
"وضعیت",
|
||||
"دسترسی ها",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={tableData}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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
129
src/assets/fonts/fonts.css
Normal file
@@ -0,0 +1,129 @@
|
||||
@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/active-stepper.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/auth-bg.png
Normal file
|
After Width: | Height: | Size: 898 KiB |
BIN
src/assets/images/auth.jpg
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
src/assets/images/banks/ansar.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/assets/images/banks/ayandeh.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
src/assets/images/banks/eghtesad-novin.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
src/assets/images/banks/keshavarzi.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/assets/images/banks/maskan.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/images/banks/mehriran.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/images/banks/meli.png
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
src/assets/images/banks/mellat.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
src/assets/images/banks/pasargad.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
src/assets/images/banks/saderat.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
src/assets/images/banks/saman.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
src/assets/images/banks/sina.png
Normal file
|
After Width: | Height: | Size: 59 KiB |