first commit

This commit is contained in:
2026-01-19 13:08:58 +03:30
commit 850b4a3f1e
293 changed files with 51775 additions and 0 deletions

4
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,8 @@
{
"app": "rasaddam-front",
"platform": "react",
"react": {
"mirror": true,
"sourceMap": false
}
}

7185
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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>
);
};

View 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
View 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
View 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"
>
هیچ آیتمی با عبارت &quot;{search}&quot; مطابقت ندارد
</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
View 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>
);
}

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

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

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

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

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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>
</>
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

129
src/assets/fonts/fonts.css Normal file
View 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;
} */

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 199 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 221 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 201 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 198 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 233 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 222 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 219 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

BIN
src/assets/images/auth.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Some files were not shown because too many files have changed in this diff Show More