Compare commits
47 Commits
850b4a3f1e
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 93867ce7ee | |||
| 4210dbd7e2 | |||
| 3624b3bc70 | |||
| 4d00b0d492 | |||
| ed7b257ed8 | |||
| 03136f5f30 | |||
| 908a69ce0e | |||
| a818683247 | |||
| 95780cfbc9 | |||
| 071c3e159b | |||
| 90f51c6899 | |||
| e967329108 | |||
| f8d2da4f28 | |||
| bb1d5b3315 | |||
| d8d415a8f5 | |||
| f58c8e6c58 | |||
| 3a17fcb448 | |||
| 6e219aca1a | |||
| 9b74be078f | |||
| 5fd55c4b10 | |||
| 6b5276ce36 | |||
| e465843eb9 | |||
| e5402f9037 | |||
| e342a7cdd5 | |||
| f5de2f68b5 | |||
| 44ea5974eb | |||
| cb87251d62 | |||
| a1b430ad8e | |||
| 7b88a664b0 | |||
| 2a6d978dba | |||
| b9f9e6cd06 | |||
| de31fa9e6d | |||
| a705d0360b | |||
| f1ba276c6c | |||
| 983a072487 | |||
| 0c951f7b4c | |||
| 4a719c9d1c | |||
| bb1b22152a | |||
| de16f203d4 | |||
| e0633245cd | |||
| 576fc434dc | |||
| cfc4b8cc53 | |||
| 3550e1fec7 | |||
| b573a16e89 | |||
| 2e8a74bb9c | |||
| 3dfc4d51d9 | |||
| a1aefdff8f |
@@ -1,9 +1,12 @@
|
||||
FROM node:18-alpine
|
||||
FROM registry.hamdocker.ir/seniorkian/node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
run npm config set registry https://mirror-npm.runflare.com
|
||||
|
||||
RUN npm install --force
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import { OrganizationsTypes } from "../partials/management/OrganizationsTypes";
|
||||
|
||||
export default function Organizations() {
|
||||
const tabItems = [
|
||||
{ label: "سازمان ها" },
|
||||
{
|
||||
label: "نهاد",
|
||||
page: "organizations",
|
||||
access: "Show-Organization-Type",
|
||||
},
|
||||
{ label: "سازمان ها" },
|
||||
];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
@@ -22,7 +22,7 @@ export default function Organizations() {
|
||||
return (
|
||||
<Grid container column className="gap-2">
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
{selectedTab === 0 ? <OrganizationsList /> : <OrganizationsTypes />}
|
||||
{selectedTab === 0 ? <OrganizationsTypes /> : <OrganizationsList />}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
38
src/Pages/TagDistribution.tsx
Normal file
38
src/Pages/TagDistribution.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useState } from "react";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Tabs from "../components/Tab/Tab";
|
||||
import TagActiveDistributions from "../partials/tagging/TagActiveDistributions";
|
||||
import TagCanceledDistributions from "../partials/tagging/TagCanceledDistributions";
|
||||
|
||||
export default function TagDistribtution() {
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
label: "توزیع فعال",
|
||||
page: "tag_distribution",
|
||||
access: "Tag-Distribution-Actives",
|
||||
},
|
||||
{
|
||||
label: "توزیع های لغو شده",
|
||||
page: "tag_distribution",
|
||||
access: "Tag-Distribution-Cancels",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid container column className="justify-center mt-2">
|
||||
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
|
||||
<Grid container column className="mt-2">
|
||||
{selectedTab === 0 ? (
|
||||
<TagActiveDistributions />
|
||||
) : (
|
||||
<TagCanceledDistributions />
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
412
src/Pages/TagDistributionDetails.tsx
Normal file
412
src/Pages/TagDistributionDetails.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import { Bars3Icon, CubeIcon, SparklesIcon } from "@heroicons/react/24/outline";
|
||||
import { useApiRequest } from "../utils/useApiRequest";
|
||||
import { useModalStore } from "../context/zustand-store/appStore";
|
||||
import { formatJustDate, formatJustTime } from "../utils/formatTime";
|
||||
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
|
||||
import { Grid } from "../components/Grid/Grid";
|
||||
import Typography from "../components/Typography/Typography";
|
||||
import Table from "../components/Table/Table";
|
||||
import { Popover } from "../components/PopOver/PopOver";
|
||||
import Button from "../components/Button/Button";
|
||||
import { Tooltip } from "../components/Tooltip/Tooltip";
|
||||
import { DistributeFromDistribution } from "../partials/tagging/DistributeFromDistribution";
|
||||
import { DocumentOperation } from "../components/DocumentOperation/DocumentOperation";
|
||||
import { DocumentDownloader } from "../components/DocumentDownloader/DocumentDownloader";
|
||||
import { BooleanQuestion } from "../components/BooleanQuestion/BooleanQuestion";
|
||||
import { useUserProfileStore } from "../context/zustand-store/userStore";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
|
||||
const speciesMap: Record<number, string> = {
|
||||
1: "گاو",
|
||||
2: "گاومیش",
|
||||
3: "شتر",
|
||||
4: "گوسفند",
|
||||
5: "بز",
|
||||
};
|
||||
|
||||
export default function TagDistribtutionDetails() {
|
||||
const { id } = useParams({ strict: false });
|
||||
const { openModal } = useModalStore();
|
||||
const [childTableInfo, setChildTableInfo] = useState({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
});
|
||||
const [childTableData, setChildTableData] = useState([]);
|
||||
|
||||
const { data, refetch: refetchData } = useApiRequest({
|
||||
api: `/tag/web/api/v1/tag_distribution_batch/${id}/`,
|
||||
method: "get",
|
||||
queryKey: ["tagBatchInnerDashboard", id],
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const { data: childData, refetch: refetchChildList } = useApiRequest({
|
||||
api: `/tag/web/api/v1/tag_distribution_batch/${id}/child_list/`,
|
||||
method: "get",
|
||||
queryKey: ["tagDistributionChildList", id, childTableInfo],
|
||||
params: {
|
||||
...childTableInfo,
|
||||
},
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const { profile } = useUserProfileStore();
|
||||
|
||||
const showAssignDocColumn =
|
||||
childData?.results?.some(
|
||||
(item: any) =>
|
||||
profile?.role?.type?.key === "ADM" ||
|
||||
profile?.organization?.id === item?.assigned_org?.id,
|
||||
) ?? false;
|
||||
|
||||
const AbleToSeeAssignDoc = (item: any) => {
|
||||
if (
|
||||
profile?.role?.type?.key === "ADM" ||
|
||||
profile?.organization?.id === item?.assigned_org?.id
|
||||
) {
|
||||
return (
|
||||
<DocumentOperation
|
||||
key={item?.id}
|
||||
downloadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/distribution_pdf_view/`}
|
||||
payloadKey="dist_exit_document"
|
||||
validFiles={["pdf"]}
|
||||
page="tag_distribution"
|
||||
access="Upload-Assign-Document"
|
||||
uploadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/assign_document/`}
|
||||
onUploadSuccess={handleUpdate}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetchData();
|
||||
refetchChildList();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (childData?.results) {
|
||||
const formattedData = childData.results.map(
|
||||
(item: any, index: number) => {
|
||||
const dist = item?.distributions;
|
||||
|
||||
return [
|
||||
childTableInfo.page === 1
|
||||
? index + 1
|
||||
: index +
|
||||
childTableInfo.page_size * (childTableInfo.page - 1) +
|
||||
1,
|
||||
item?.dist_batch_identity ?? "-",
|
||||
`${formatJustDate(item?.create_date) ?? "-"} (${
|
||||
formatJustDate(item?.create_date)
|
||||
? (formatJustTime(item?.create_date) ?? "-")
|
||||
: "-"
|
||||
})`,
|
||||
item?.assigner_org?.name ?? "-",
|
||||
item?.assigned_org?.name ?? "-",
|
||||
item?.total_tag_count?.toLocaleString() ?? "-",
|
||||
item?.total_distributed_tag_count?.toLocaleString() ?? "-",
|
||||
item?.remaining_tag_count?.toLocaleString() ?? "-",
|
||||
item?.distribution_type === "batch"
|
||||
? "توزیع گروهی"
|
||||
: "توزیع تصادفی",
|
||||
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
|
||||
<Grid container column className="gap-4 w-full">
|
||||
{dist?.map((opt: any, idx: number) => (
|
||||
<Grid
|
||||
key={idx}
|
||||
container
|
||||
column
|
||||
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
|
||||
>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
گونه:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{speciesMap[opt?.species_code] ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{item?.distribution_type === "batch" &&
|
||||
opt?.serial_from != null && (
|
||||
<Grid container className="gap-2 items-center">
|
||||
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
بازه سریال:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
از {opt?.serial_from ?? "-"} تا{" "}
|
||||
{opt?.serial_to ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
تعداد پلاک:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.total_tag_count?.toLocaleString() ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
پلاک های توزیع شده:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.distributed_number?.toLocaleString() ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
پلاک های باقیمانده:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.remaining_number?.toLocaleString() ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</ShowMoreInfo>,
|
||||
...(showAssignDocColumn ? [AbleToSeeAssignDoc(item)] : []),
|
||||
<DocumentDownloader
|
||||
key={`doc-${item?.id}`}
|
||||
link={item?.warehouse_exit_doc}
|
||||
title="دانلود"
|
||||
/>,
|
||||
item?.exit_doc_status ? (
|
||||
"تایید شده"
|
||||
) : (
|
||||
<Button
|
||||
key={`btn-${item?.id}`}
|
||||
page="tag_distribution"
|
||||
access="Accept-Assign-Document"
|
||||
size="small"
|
||||
disabled={item?.exit_doc_status}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "تایید سند خروج",
|
||||
content: (
|
||||
<BooleanQuestion
|
||||
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/accept_exit_doc/`}
|
||||
method="post"
|
||||
getData={handleUpdate}
|
||||
title="آیا از تایید سند خروج مطمئنید؟"
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
تایید سند خروج
|
||||
</Button>
|
||||
),
|
||||
<Popover key={`popover-${item?.id}`}>
|
||||
<Tooltip title="ویرایش توزیع" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="tag_distribution"
|
||||
access="Submit-Tag-Distribution"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش توزیع",
|
||||
content: (
|
||||
<DistributeFromDistribution
|
||||
getData={handleUpdate}
|
||||
item={item}
|
||||
isEdit
|
||||
parentDistributions={data?.distributions}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="tag_distribution"
|
||||
access="Delete-Tag-Distribution"
|
||||
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
|
||||
getData={handleUpdate}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
},
|
||||
);
|
||||
setChildTableData(formattedData);
|
||||
} else {
|
||||
setChildTableData([]);
|
||||
}
|
||||
}, [childData, childTableInfo]);
|
||||
|
||||
const dist = data?.distributions;
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4 mt-2">
|
||||
<Grid isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
title="مشخصات توزیع پلاک"
|
||||
noSearch
|
||||
noPagination
|
||||
columns={[
|
||||
"شناسه توزیع",
|
||||
"تاریخ ثبت",
|
||||
"توزیع کننده",
|
||||
"دریافت کننده",
|
||||
"تعداد کل پلاک",
|
||||
"پلاک های توزیع شده",
|
||||
"پلاک های باقیمانده",
|
||||
"نوع توزیع",
|
||||
"جزئیات توزیع",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
data?.dist_batch_identity ?? "-",
|
||||
`${formatJustDate(data?.create_date) ?? "-"} (${
|
||||
formatJustDate(data?.create_date)
|
||||
? (formatJustTime(data?.create_date) ?? "-")
|
||||
: "-"
|
||||
})`,
|
||||
data?.assigner_org?.name ?? "-",
|
||||
data?.assigned_org?.name ?? "-",
|
||||
data?.total_tag_count?.toLocaleString() ?? "-",
|
||||
data?.total_distributed_tag_count?.toLocaleString() ?? "-",
|
||||
data?.remaining_tag_count?.toLocaleString() ?? "-",
|
||||
data?.distribution_type === "batch"
|
||||
? "توزیع گروهی"
|
||||
: "توزیع تصادفی",
|
||||
<ShowMoreInfo key={data?.id} title="جزئیات توزیع">
|
||||
<Grid container column className="gap-4 w-full">
|
||||
{dist?.map((opt: any, index: number) => (
|
||||
<Grid
|
||||
key={index}
|
||||
container
|
||||
column
|
||||
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
|
||||
>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
گونه:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{speciesMap[opt?.species_code] ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{data?.distribution_type === "batch" &&
|
||||
opt?.serial_from != null && (
|
||||
<Grid container className="gap-2 items-center">
|
||||
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
بازه سریال:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
از {opt?.serial_from ?? "-"} تا{" "}
|
||||
{opt?.serial_to ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
تعداد پلاک:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.total_tag_count?.toLocaleString() ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
پلاک های توزیع شده:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.distributed_number?.toLocaleString() ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
پلاک های باقیمانده:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.remaining_number?.toLocaleString() ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</ShowMoreInfo>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setChildTableInfo}
|
||||
count={childData?.count || 0}
|
||||
isPaginated
|
||||
title="توزیع های مجدد"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"شناسه توزیع",
|
||||
"تاریخ ثبت",
|
||||
"توزیع کننده",
|
||||
"دریافت کننده",
|
||||
"تعداد کل پلاک",
|
||||
"پلاک های توزیع شده",
|
||||
"پلاک های باقیمانده",
|
||||
"نوع توزیع",
|
||||
"جزئیات توزیع",
|
||||
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
|
||||
"سند خروج از انبار",
|
||||
"تایید سند خروج",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={childTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -6,26 +6,42 @@ 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";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { TAGS } from "../routes/paths";
|
||||
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
|
||||
import { TableButton } from "../components/TableButton/TableButton";
|
||||
import AutoComplete from "../components/AutoComplete/AutoComplete";
|
||||
|
||||
const speciesMap: Record<number, string> = {
|
||||
1: "گاو",
|
||||
2: "گاومیش",
|
||||
3: "شتر",
|
||||
4: "گوسفند",
|
||||
5: "بز",
|
||||
};
|
||||
|
||||
export default function Tagging() {
|
||||
const { openModal } = useModalStore();
|
||||
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [tagsTableData, setTagsTableData] = useState([]);
|
||||
const [tagsTableData, setTagsTableData] = useState<any[]>([]);
|
||||
const [selectedSpecie, setSelectedSpecie] = useState<
|
||||
(string | number)[] | any
|
||||
>([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: tagsData, refetch } = useApiRequest({
|
||||
api: "/tag/web/api/v1/tag/",
|
||||
api: `/tag/web/api/v1/tag_batch/?species_code=${
|
||||
selectedSpecie.length ? selectedSpecie[0] : ""
|
||||
}`,
|
||||
method: "get",
|
||||
queryKey: ["tagsList", tableInfo],
|
||||
params: {
|
||||
...tableInfo,
|
||||
},
|
||||
queryKey: ["tagsList", tableInfo, selectedSpecie],
|
||||
params: { ...tableInfo },
|
||||
});
|
||||
|
||||
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
|
||||
api: "/tag/web/api/v1/tag/tag_dashboard/",
|
||||
api: "/tag/web/api/v1/tag_batch/main_dashboard/",
|
||||
method: "get",
|
||||
queryKey: ["tagDashboard"],
|
||||
});
|
||||
@@ -42,39 +58,52 @@ export default function Tagging() {
|
||||
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 || "-",
|
||||
? "گاومیش"
|
||||
: item?.species_code === 3
|
||||
? "شتر"
|
||||
: item?.species_code === 4
|
||||
? "گوسفند"
|
||||
: item?.species_code === 5
|
||||
? "بز"
|
||||
: "نامشخص",
|
||||
item?.serial_from || "-",
|
||||
item?.serial_to || "-",
|
||||
item?.total_distributed_tags || 0,
|
||||
item?.total_remaining_tags || 0,
|
||||
<Popover key={item.id}>
|
||||
<Tooltip title="جزئیات پلاک" position="right">
|
||||
<Tooltip title="مشاهده پلاک ها" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page="tagging"
|
||||
access="Tag-Details"
|
||||
disabled={item?.status === "F"}
|
||||
page="tags"
|
||||
access="Show-Tags-Page"
|
||||
onClick={() => {
|
||||
const path =
|
||||
TAGS +
|
||||
"/" +
|
||||
item?.id +
|
||||
"/" +
|
||||
item?.serial_from +
|
||||
"/" +
|
||||
item?.serial_to;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="ویرایش" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="livestock_farmers"
|
||||
access="Edit-Rancher"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات پلاک",
|
||||
content: <TagDetails tagId={item.id} />,
|
||||
isFullSize: true,
|
||||
title: "ایجاد پلاک جدید",
|
||||
content: (
|
||||
<SubmitNewTags getData={handleUpdate} item={item} />
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -82,18 +111,36 @@ export default function Tagging() {
|
||||
<DeleteButtonForPopOver
|
||||
page="tagging"
|
||||
access="Delete-Tag"
|
||||
api={`/tag/web/api/v1/tag/${item?.id}/`}
|
||||
api={`/tag/web/api/v1/tag_batch/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
setTagsTableData(formattedData);
|
||||
} else {
|
||||
setTagsTableData([]);
|
||||
}
|
||||
}, [tagsData]);
|
||||
}, [tagsData, tableInfo.page, tableInfo.page_size, refetch]);
|
||||
|
||||
const { data: speciesData } = useApiRequest({
|
||||
api: "/livestock/web/api/v1/livestock_species",
|
||||
method: "get",
|
||||
params: { page: 1, pageSize: 1000 },
|
||||
queryKey: ["species"],
|
||||
});
|
||||
|
||||
const speciesOptions = () => {
|
||||
return speciesData?.results?.map((opt: any) => {
|
||||
return {
|
||||
key: opt?.value,
|
||||
value: opt?.name,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4 mt-2">
|
||||
<Grid container column className="gap-4 mt-2 rtl">
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -118,30 +165,112 @@ export default function Tagging() {
|
||||
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,
|
||||
tagDashboardData?.batch_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.tag_count_created_by_batch?.toLocaleString() ||
|
||||
0,
|
||||
tagDashboardData?.has_distributed_batches_number?.toLocaleString() ||
|
||||
0,
|
||||
tagDashboardData?.total_distributed_tags?.toLocaleString() || 0,
|
||||
tagDashboardData?.total_remaining_tags?.toLocaleString() || 0,
|
||||
<TableButton
|
||||
key="species-stats"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
title: "آمار گونهای",
|
||||
isFullSize: true,
|
||||
content: (
|
||||
<BatchBySpeciesModal
|
||||
batchData={
|
||||
tagDashboardData?.batch_data_by_species || []
|
||||
}
|
||||
onRowAction={(row) => {
|
||||
openModal({
|
||||
title: `جزئیات ${
|
||||
speciesMap[row?.species_code] ?? "-"
|
||||
}`,
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
تعداد بچ
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.batch_count?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک تولید شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.tag_count_created_by_batch?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک توزیع شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_distributed_tags?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک باقیمانده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_remaining_tags?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
بچهای توزیعشده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.has_distributed_batches_number?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
مشاهده
|
||||
</TableButton>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid container className="items-center gap-2">
|
||||
<Grid>
|
||||
{speciesOptions() && (
|
||||
<AutoComplete
|
||||
data={speciesOptions()}
|
||||
selectedKeys={selectedSpecie}
|
||||
onChange={setSelectedSpecie}
|
||||
title="گونه"
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setTableInfo}
|
||||
@@ -150,11 +279,12 @@ export default function Tagging() {
|
||||
title="پلاک کوبی"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"پلاک",
|
||||
"سازمان ثبت کننده",
|
||||
"کد گونه",
|
||||
"وضعیت",
|
||||
"کد مالکیت ثبتی",
|
||||
"گونه",
|
||||
"از بازه سریال",
|
||||
"تا بازه سریال",
|
||||
"پلاک های توزیع شده",
|
||||
"پلاک های باقیمانده",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={tagsTableData}
|
||||
@@ -162,3 +292,78 @@ export default function Tagging() {
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function BatchBySpeciesModal({
|
||||
batchData = [],
|
||||
}: {
|
||||
batchData: Array<any>;
|
||||
onRowAction?: (row: any, index: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{batchData?.map((row, idx) => {
|
||||
const speciesName = speciesMap[row?.species_code] ?? "-";
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm p-4 flex flex-col"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-primary/70"></div>
|
||||
<span className="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||
{speciesName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
تعداد گروه پلاک
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.batch_count?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
گروه پلاک های توزیعشده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.has_distributed_batches_number?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک تولید شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.tag_count_created_by_batch?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک توزیع شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_distributed_tags?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک باقیمانده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_remaining_tags?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
353
src/Pages/Tags.tsx
Normal file
353
src/Pages/Tags.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
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 { useParams } from "@tanstack/react-router";
|
||||
import { TableButton } from "../components/TableButton/TableButton";
|
||||
import AutoComplete from "../components/AutoComplete/AutoComplete";
|
||||
|
||||
const speciesMap: Record<number, string> = {
|
||||
1: "گاو",
|
||||
2: "گاومیش",
|
||||
3: "شتر",
|
||||
4: "گوسفند",
|
||||
5: "بز",
|
||||
};
|
||||
|
||||
const statusOptions = [
|
||||
{ key: "F", value: "آزاد" },
|
||||
{ key: "A", value: "پلاک شده" },
|
||||
{ key: "R", value: "رزرو" },
|
||||
];
|
||||
|
||||
export default function Tags() {
|
||||
const { openModal } = useModalStore();
|
||||
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [tagsTableData, setTagsTableData] = useState([]);
|
||||
const { id, from, to } = useParams({ strict: false });
|
||||
const [selectedStatus, setSelectedStatus] = useState<(string | number)[]>([]);
|
||||
|
||||
const { data: tagsData, refetch } = useApiRequest({
|
||||
api: `/tag/web/api/v1/tag/${id ? id + "/tags_by_batch" : ""}`,
|
||||
method: "get",
|
||||
queryKey: ["tagsList", tableInfo, selectedStatus],
|
||||
params: {
|
||||
...tableInfo,
|
||||
status: selectedStatus.length ? selectedStatus[0] : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: tagDashboardData } = useApiRequest({
|
||||
api: id
|
||||
? `/tag/web/api/v1/tag_batch/${id}/inner_dashboard`
|
||||
: "/tag/web/api/v1/tag/tag_dashboard/",
|
||||
method: "get",
|
||||
queryKey: ["tagDashboard"],
|
||||
});
|
||||
|
||||
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">
|
||||
{tagDashboardData && (
|
||||
<Grid isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
title="خلاصه اطلاعات"
|
||||
noPagination
|
||||
noSearch
|
||||
columns={
|
||||
id
|
||||
? [
|
||||
"تعداد پلاک",
|
||||
"پلاک های توزیع شده",
|
||||
"تعداد پلاک های گروه پلاک",
|
||||
"تعداد پلاک های توزیع شده",
|
||||
"تعداد پلاک های باقیمانده",
|
||||
"آمار گونه ای",
|
||||
]
|
||||
: [
|
||||
"تعداد کل",
|
||||
"تعداد پلاک های آزاد",
|
||||
"تعداد پلاک شده",
|
||||
"گاو",
|
||||
"گاومیش",
|
||||
"شتر",
|
||||
"گوسفند",
|
||||
"بز",
|
||||
]
|
||||
}
|
||||
rows={
|
||||
id
|
||||
? [
|
||||
[
|
||||
tagDashboardData?.batch_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.has_distributed_batches_number?.toLocaleString() ||
|
||||
0,
|
||||
tagDashboardData?.tag_count_created_by_batch?.toLocaleString() ||
|
||||
0,
|
||||
tagDashboardData?.total_distributed_tags?.toLocaleString() ||
|
||||
0,
|
||||
tagDashboardData?.total_remaining_tags?.toLocaleString() ||
|
||||
0,
|
||||
<TableButton
|
||||
key="species-stats"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
title: "آمار گونهای",
|
||||
isFullSize: true,
|
||||
content: (
|
||||
<BatchBySpeciesModal
|
||||
batchData={
|
||||
tagDashboardData?.batch_data_by_species || []
|
||||
}
|
||||
onRowAction={(row) => {
|
||||
openModal({
|
||||
title: `جزئیات ${
|
||||
speciesMap[row?.species_code] ?? "-"
|
||||
}`,
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
تعداد بچ
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.batch_count?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک تولید شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.tag_count_created_by_batch?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک توزیع شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_distributed_tags?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک باقیمانده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_remaining_tags?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
بچهای توزیعشده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.has_distributed_batches_number?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
مشاهده
|
||||
</TableButton>,
|
||||
],
|
||||
]
|
||||
: [
|
||||
[
|
||||
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>
|
||||
)}
|
||||
|
||||
<Grid container className="items-center gap-2">
|
||||
<Grid>
|
||||
<AutoComplete
|
||||
data={statusOptions}
|
||||
selectedKeys={selectedStatus}
|
||||
onChange={setSelectedStatus}
|
||||
title="فیلتر پلاک ها"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setTableInfo}
|
||||
count={tagsData?.count || 0}
|
||||
isPaginated
|
||||
title={!id ? "لیست پلاک" : "لیست پلاک های بازه " + from + " تا " + to}
|
||||
columns={[
|
||||
"ردیف",
|
||||
"پلاک",
|
||||
"سازمان ثبت کننده",
|
||||
"کد گونه",
|
||||
"وضعیت",
|
||||
"کد مالکیت ثبتی",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={tagsTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function BatchBySpeciesModal({
|
||||
batchData = [],
|
||||
}: {
|
||||
batchData: Array<any>;
|
||||
onRowAction?: (row: any, index: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{batchData?.map((row, idx) => {
|
||||
const speciesName = speciesMap[row?.species_code] ?? "-";
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm p-4 flex flex-col"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-primary/70"></div>
|
||||
<span className="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||
{speciesName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
تعداد گروه پلاک
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.batch_count?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
گروه پلاک های توزیعشده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.has_distributed_batches_number?.toLocaleString?.() ??
|
||||
0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک تولید شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.tag_count_created_by_batch?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک توزیع شده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_distributed_tags?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
پلاک باقیمانده
|
||||
</span>
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
||||
{row?.total_remaining_tags?.toLocaleString?.() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ interface AutoCompleteProps {
|
||||
multiselect?: boolean;
|
||||
inPage?: boolean;
|
||||
disabled?: boolean;
|
||||
selectedKeys: (number | string)[];
|
||||
selectedKeys: (number | string)[] | any;
|
||||
onChange: (keys: (number | string)[]) => void | [];
|
||||
width?: string;
|
||||
buttonHeight?: number | string;
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
} from "react";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ArrowUpCircleIcon,
|
||||
ChartBarIcon,
|
||||
DocumentChartBarIcon,
|
||||
EyeIcon,
|
||||
@@ -56,7 +57,8 @@ type ButtonProps = {
|
||||
| "view"
|
||||
| "info"
|
||||
| "chart"
|
||||
| "set";
|
||||
| "set"
|
||||
| "share";
|
||||
page?: string;
|
||||
access?: string;
|
||||
height?: string | number;
|
||||
@@ -161,6 +163,10 @@ const Button: React.FC<ButtonProps> = ({
|
||||
return (
|
||||
<WrenchIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||
);
|
||||
case "share":
|
||||
return (
|
||||
<ArrowUpCircleIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" />
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -181,7 +187,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
return true;
|
||||
} else {
|
||||
const finded = profile?.permissions?.find(
|
||||
(item: any) => item.page_name === page
|
||||
(item: any) => item.page_name === page,
|
||||
);
|
||||
if (finded && finded.page_access.includes(access)) {
|
||||
return true;
|
||||
@@ -237,7 +243,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
sizeStyles.padding,
|
||||
sizeStyles.text,
|
||||
className,
|
||||
checkIsMobile() && !icon && !variant && children && mobileBorders
|
||||
checkIsMobile() && !icon && !variant && children && mobileBorders,
|
||||
)}
|
||||
style={{ height }}
|
||||
>
|
||||
@@ -256,7 +262,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
.then((response) => {
|
||||
closeBackdrop();
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([response.data])
|
||||
new Blob([response.data]),
|
||||
);
|
||||
|
||||
const link = document.createElement("a");
|
||||
|
||||
236
src/components/DocumentOperation/DocumentOperation.tsx
Normal file
236
src/components/DocumentOperation/DocumentOperation.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import React, { useRef, useState, useEffect, ChangeEvent } from "react";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
CheckCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import api from "../../utils/axios";
|
||||
import { useBackdropStore } from "../../context/zustand-store/appStore";
|
||||
import { useToast } from "../../hooks/useToast";
|
||||
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||
import { RolesContextMenu } from "../Button/RolesContextMenu";
|
||||
|
||||
interface DocumentOperationProps {
|
||||
downloadLink: string;
|
||||
uploadLink: string;
|
||||
validFiles?: string[];
|
||||
payloadKey: string;
|
||||
onUploadSuccess?: () => void;
|
||||
page?: string;
|
||||
access?: string;
|
||||
limitSize?: number;
|
||||
}
|
||||
|
||||
const buildAcceptString = (extensions: string[]): string => {
|
||||
const mimeTypes: string[] = [];
|
||||
|
||||
extensions.forEach((ext) => {
|
||||
const lower = ext.toLowerCase().replace(".", "");
|
||||
|
||||
if (lower === "img" || lower === "image") {
|
||||
mimeTypes.push("image/*");
|
||||
} else {
|
||||
mimeTypes.push(`.${lower}`);
|
||||
}
|
||||
});
|
||||
|
||||
return mimeTypes.join(",");
|
||||
};
|
||||
|
||||
export const DocumentOperation = ({
|
||||
downloadLink,
|
||||
uploadLink,
|
||||
validFiles = [],
|
||||
payloadKey,
|
||||
onUploadSuccess,
|
||||
page = "",
|
||||
access = "",
|
||||
limitSize,
|
||||
}: DocumentOperationProps) => {
|
||||
const { openBackdrop, closeBackdrop } = useBackdropStore();
|
||||
const showToast = useToast();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadedFileName, setUploadedFileName] = useState<string>("");
|
||||
const { profile } = useUserProfileStore();
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const isAdmin = profile?.role?.type?.key === "ADM";
|
||||
|
||||
const ableToSee = () => {
|
||||
if (!access || !page) {
|
||||
return true;
|
||||
}
|
||||
const found = profile?.permissions?.find(
|
||||
(item: any) => item.page_name === page,
|
||||
);
|
||||
if (found && found.page_access.includes(access)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isAdmin && page && access) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setContextMenu({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = () => {
|
||||
if (contextMenu) {
|
||||
setContextMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (contextMenu) {
|
||||
document.addEventListener("click", handleClick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
};
|
||||
}, [contextMenu]);
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!downloadLink) return;
|
||||
|
||||
openBackdrop();
|
||||
try {
|
||||
const response = await api.get(downloadLink, {
|
||||
responseType: "blob",
|
||||
});
|
||||
|
||||
const contentDisposition = response.headers["content-disposition"];
|
||||
let fileName = "document";
|
||||
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(
|
||||
/filename\*?=(?:UTF-8''|"?)([^";]+)/i,
|
||||
);
|
||||
if (match?.[1]) {
|
||||
fileName = decodeURIComponent(match[1].replace(/"/g, ""));
|
||||
}
|
||||
} else {
|
||||
const urlParts = downloadLink.split("/").filter(Boolean);
|
||||
const lastPart = urlParts[urlParts.length - 1];
|
||||
if (lastPart && lastPart.includes(".")) {
|
||||
fileName = lastPart;
|
||||
}
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
showToast("فایل با موفقیت دانلود شد", "success");
|
||||
} catch {
|
||||
showToast("خطا در دانلود فایل", "error");
|
||||
} finally {
|
||||
closeBackdrop();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
|
||||
if (limitSize && file.size > limitSize * 1024 * 1024) {
|
||||
showToast(`حداکثر حجم فایل ${limitSize} مگابایت است`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
openBackdrop();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append(payloadKey, file);
|
||||
|
||||
await api.post(uploadLink, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
|
||||
setUploadedFileName(file.name);
|
||||
showToast("فایل با موفقیت آپلود شد", "success");
|
||||
onUploadSuccess?.();
|
||||
} catch {
|
||||
showToast("خطا در آپلود فایل", "error");
|
||||
} finally {
|
||||
closeBackdrop();
|
||||
}
|
||||
};
|
||||
|
||||
const acceptString =
|
||||
validFiles.length > 0 ? buildAcceptString(validFiles) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="inline-flex items-center"
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
accept={acceptString}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
disabled={!downloadLink || !ableToSee()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-r-lg border border-l-0 border-primary-300 dark:border-dark-400 bg-primary-50 dark:bg-dark-600 text-primary-700 dark:text-primary-200 hover:bg-primary-100 dark:hover:bg-dark-500 transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ArrowDownTrayIcon className="w-4 h-4" />
|
||||
<span>دانلود</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUploadClick}
|
||||
disabled={!uploadLink || !ableToSee()}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-l-lg border border-primary-300 dark:border-dark-400 bg-primary-600 dark:bg-primary-700 text-white hover:bg-primary-500 dark:hover:bg-primary-800 transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{uploadedFileName ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-300" />
|
||||
) : (
|
||||
<ArrowUpTrayIcon className="w-4 h-4" />
|
||||
)}
|
||||
<span>آپلود</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{contextMenu && page && access && (
|
||||
<RolesContextMenu
|
||||
page={page}
|
||||
access={access}
|
||||
position={contextMenu}
|
||||
onClose={() => setContextMenu(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { getNestedValue } from "../../utils/getNestedValue";
|
||||
type FormEnterLocationsProps = {
|
||||
title: string;
|
||||
api: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
error?: boolean;
|
||||
errorMessage?: any;
|
||||
multiple?: boolean;
|
||||
@@ -31,6 +32,7 @@ type FormEnterLocationsProps = {
|
||||
export const FormApiBasedAutoComplete = ({
|
||||
title,
|
||||
api,
|
||||
size,
|
||||
error,
|
||||
errorMessage,
|
||||
onChange,
|
||||
@@ -94,7 +96,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
if (filterAddress && filterValue) {
|
||||
data = apiData.results?.filter(
|
||||
(item: any) =>
|
||||
!filterValue.includes(getNestedValue(item, filterAddress))
|
||||
!filterValue.includes(getNestedValue(item, filterAddress)),
|
||||
);
|
||||
} else {
|
||||
data = apiData.results;
|
||||
@@ -119,10 +121,10 @@ export const FormApiBasedAutoComplete = ({
|
||||
valueField === "page"
|
||||
? getFaPermissions(option[valueField])
|
||||
: typeof valueField === "string"
|
||||
? option[valueField]
|
||||
: getNestedValue(option, valueField),
|
||||
"v1"
|
||||
)
|
||||
? option[valueField]
|
||||
: getNestedValue(option, valueField),
|
||||
"v1",
|
||||
),
|
||||
)
|
||||
.replace(
|
||||
/v2/g,
|
||||
@@ -132,8 +134,8 @@ export const FormApiBasedAutoComplete = ({
|
||||
? option[valueField2]
|
||||
: getNestedValue(option, valueField2)
|
||||
: "",
|
||||
"v2"
|
||||
)
|
||||
"v2",
|
||||
),
|
||||
)
|
||||
.replace(
|
||||
/v3/g,
|
||||
@@ -143,15 +145,15 @@ export const FormApiBasedAutoComplete = ({
|
||||
? option[valueField3]
|
||||
: getNestedValue(option, valueField3)
|
||||
: "",
|
||||
"v3"
|
||||
)
|
||||
"v3",
|
||||
),
|
||||
)
|
||||
: `${
|
||||
valueField === "page"
|
||||
? getFaPermissions(option[valueField])
|
||||
: typeof valueField === "string"
|
||||
? option[valueField]
|
||||
: getNestedValue(option, valueField)
|
||||
? option[valueField]
|
||||
: getNestedValue(option, valueField)
|
||||
} ${
|
||||
valueField2
|
||||
? " - " +
|
||||
@@ -208,7 +210,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
setData(finalData);
|
||||
|
||||
const actualDataItems = finalData.filter(
|
||||
(item: any) => !item.isGroupHeader && !item.disabled
|
||||
(item: any) => !item.isGroupHeader && !item.disabled,
|
||||
);
|
||||
|
||||
if (defaultKey !== undefined && defaultKey !== null) {
|
||||
@@ -218,25 +220,33 @@ export const FormApiBasedAutoComplete = ({
|
||||
setSelectedKeys([]);
|
||||
} else {
|
||||
const defaultIds = defaultKey.map((item: any) =>
|
||||
typeof item === "object" ? item[keyField] : item
|
||||
typeof item === "object" ? item[keyField] : item,
|
||||
);
|
||||
const defaultItems = actualDataItems.filter((item: any) =>
|
||||
defaultIds.includes(item.key)
|
||||
defaultIds.includes(item.key),
|
||||
);
|
||||
setSelectedKeys(defaultItems.map((item: any) => item.key));
|
||||
if (onChange) {
|
||||
if (secondaryKey) {
|
||||
onChange({
|
||||
key1: defaultItems.map((item: any) => item.key),
|
||||
key2: defaultItems.map((item: any) => item.secondaryKey),
|
||||
...(tertiaryKey
|
||||
? {
|
||||
key3: defaultItems.map(
|
||||
(item: any) => item.tertiaryKey
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
console.log("dksk0", defaultItems);
|
||||
onChange(
|
||||
defaultItems.map((item: any) => {
|
||||
return {
|
||||
key: item?.key,
|
||||
key2: item?.secondaryKey,
|
||||
...(tertiaryKey
|
||||
? {
|
||||
key3: item?.tertiaryKey,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (onChangeValue) {
|
||||
onChangeValue(
|
||||
defaultItems.map((item: any) => item.value.trim()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onChange(defaultItems.map((item: any) => item.key));
|
||||
}
|
||||
@@ -250,7 +260,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
? defaultKey[keyField]
|
||||
: defaultKey;
|
||||
const defaultItem = actualDataItems.find(
|
||||
(item: any) => item.key === keyToFind
|
||||
(item: any) => item.key === keyToFind,
|
||||
);
|
||||
if (defaultItem) {
|
||||
setSelectedKeys([keyToFind]);
|
||||
@@ -288,18 +298,18 @@ export const FormApiBasedAutoComplete = ({
|
||||
|
||||
const groupItemKeys = groupItems.map((item: any) => item.key);
|
||||
const allGroupItemsSelected = groupItemKeys.every((key) =>
|
||||
selectedKeys.includes(key)
|
||||
selectedKeys.includes(key),
|
||||
);
|
||||
|
||||
let newSelectedKeys: (string | number)[];
|
||||
|
||||
if (allGroupItemsSelected) {
|
||||
newSelectedKeys = selectedKeys.filter(
|
||||
(key) => !groupItemKeys.includes(key)
|
||||
(key) => !groupItemKeys.includes(key),
|
||||
);
|
||||
} else {
|
||||
const newKeys = groupItemKeys.filter(
|
||||
(key) => !selectedKeys.includes(key)
|
||||
(key) => !selectedKeys.includes(key),
|
||||
);
|
||||
newSelectedKeys = [...selectedKeys, ...newKeys];
|
||||
}
|
||||
@@ -312,7 +322,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
(item: any) =>
|
||||
newSelectedKeys.includes(item.key) &&
|
||||
!item.isGroupHeader &&
|
||||
!item.disabled
|
||||
!item.disabled,
|
||||
);
|
||||
|
||||
if (secondaryKey) {
|
||||
@@ -321,7 +331,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
key1: item.key,
|
||||
key2: item.secondaryKey,
|
||||
...(tertiaryKey ? { key3: item.tertiaryKey } : {}),
|
||||
}))
|
||||
})),
|
||||
);
|
||||
if (onChangeValue) {
|
||||
onChangeValue(selectedItems.map((item: any) => item.value.trim()));
|
||||
@@ -336,6 +346,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
<AutoComplete
|
||||
multiselect={multiple}
|
||||
selectField={selectField}
|
||||
size={size}
|
||||
data={data}
|
||||
selectedKeys={selectedKeys}
|
||||
onChange={(newSelectedKeys) => {
|
||||
@@ -349,7 +360,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
(item: any) =>
|
||||
newSelectedKeys.includes(item.key) &&
|
||||
!item.isGroupHeader &&
|
||||
!item.disabled
|
||||
!item.disabled,
|
||||
);
|
||||
|
||||
onChange(
|
||||
@@ -357,16 +368,16 @@ export const FormApiBasedAutoComplete = ({
|
||||
key1: item.key,
|
||||
key2: item.secondaryKey,
|
||||
...(tertiaryKey ? { key3: item.tertiaryKey } : {}),
|
||||
}))
|
||||
})),
|
||||
);
|
||||
if (onChangeValue) {
|
||||
onChangeValue(
|
||||
selectedItems.map((item: any) => item.value.trim())
|
||||
selectedItems.map((item: any) => item.value.trim()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const validKeys = newSelectedKeys.filter(
|
||||
(key) => !String(key).startsWith("__group__")
|
||||
(key) => !String(key).startsWith("__group__"),
|
||||
);
|
||||
onChange(validKeys);
|
||||
}
|
||||
@@ -376,7 +387,7 @@ export const FormApiBasedAutoComplete = ({
|
||||
(item: any) =>
|
||||
item.key === newSelectedKeys[0] &&
|
||||
!item.isGroupHeader &&
|
||||
!item.disabled
|
||||
!item.disabled,
|
||||
);
|
||||
if (onChangeValue) {
|
||||
onChangeValue({
|
||||
|
||||
@@ -21,16 +21,17 @@ import { getToastResponse } from "../../data/getToastResponse";
|
||||
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||
import { useState } from "react";
|
||||
import Checkbox from "../../components/CheckBox/CheckBox";
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
const schema = z.object({
|
||||
name: zValidateString("نام سازمان"),
|
||||
national_unique_id: zValidateString("شناسه کشوری"),
|
||||
address: zValidateStringOptional("آدرس"),
|
||||
field_of_activity: zValidateAutoComplete("حوزه فعالیت"),
|
||||
province: zValidateNumber("استان"),
|
||||
city: zValidateNumber("شهر"),
|
||||
organization: zValidateNumberOptional("سازمان"),
|
||||
organizationType: zValidateNumber("سازمان"),
|
||||
unique_unit_identity: zValidateStringOptional("شناسه یکتا واحد"),
|
||||
is_repeatable: z.boolean(),
|
||||
free_visibility_by_scope: z.boolean(),
|
||||
});
|
||||
@@ -75,8 +76,8 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: item?.name || "",
|
||||
address: item?.address || "",
|
||||
national_unique_id: item?.national_unique_id || "",
|
||||
unique_unit_identity: item?.unique_unit_identity || "",
|
||||
free_visibility_by_scope: item?.free_visibility_by_scope || false,
|
||||
field_of_activity:
|
||||
item && item?.field_of_activity !== "EM"
|
||||
@@ -95,22 +96,57 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
||||
city: string | any;
|
||||
}>({ province: "", city: "" });
|
||||
|
||||
const [addresses, setAddresses] = useState<
|
||||
{ postal_code: string; address: string }[]
|
||||
>(
|
||||
item?.addresses?.length
|
||||
? item.addresses.map((a: any) => ({
|
||||
postal_code: a.postal_code || "",
|
||||
address: a.address || "",
|
||||
}))
|
||||
: [{ postal_code: "", address: "" }],
|
||||
);
|
||||
|
||||
const handleAddAddress = () => {
|
||||
setAddresses((prev) => [...prev, { postal_code: "", address: "" }]);
|
||||
};
|
||||
|
||||
const handleRemoveAddress = (index: number) => {
|
||||
setAddresses((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleAddressChange = (
|
||||
index: number,
|
||||
field: "postal_code" | "address",
|
||||
value: string,
|
||||
) => {
|
||||
setAddresses((prev) =>
|
||||
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
try {
|
||||
await mutation.mutateAsync({
|
||||
addresses: addresses.filter(
|
||||
(a) => a.postal_code.trim() || a.address.trim(),
|
||||
),
|
||||
organization: {
|
||||
name: `${data?.name} ${
|
||||
data?.is_repeatable
|
||||
? ""
|
||||
: data.field_of_activity[0] === "CI"
|
||||
? LocationValues.city
|
||||
: LocationValues.province
|
||||
? LocationValues.city
|
||||
: LocationValues.province
|
||||
}`,
|
||||
|
||||
...(data.organizationType !== undefined && {
|
||||
type: data.organizationType,
|
||||
}),
|
||||
national_unique_id: data?.national_unique_id,
|
||||
...(data?.unique_unit_identity && {
|
||||
unique_unit_identity: data.unique_unit_identity,
|
||||
}),
|
||||
province: data?.province,
|
||||
city: data?.city,
|
||||
...(data.organization && {
|
||||
@@ -118,7 +154,6 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
||||
}),
|
||||
field_of_activity: data.field_of_activity[0],
|
||||
free_visibility_by_scope: data.free_visibility_by_scope,
|
||||
address: data.address,
|
||||
},
|
||||
});
|
||||
showToast(getToastResponse(item, "سازمان"), "success");
|
||||
@@ -128,12 +163,12 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
||||
if (error?.status === 403) {
|
||||
showToast(
|
||||
error?.response?.data?.message || "این سازمان تکراری است!",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
showToast(
|
||||
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
|
||||
"error"
|
||||
"error",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -223,6 +258,21 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="unique_unit_identity"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Textfield
|
||||
fullWidth
|
||||
placeholder="شناسه یکتا واحد (اختیاری)"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
error={!!errors.unique_unit_identity}
|
||||
helperText={errors.unique_unit_identity?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="province"
|
||||
control={control}
|
||||
@@ -258,7 +308,7 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
||||
defaultKey={item?.parent_organization?.id}
|
||||
title="سازمان والد (اختیاری)"
|
||||
api={`auth/api/v1/organization/organizations_by_province?province=${getValues(
|
||||
"province"
|
||||
"province",
|
||||
)}`}
|
||||
keyField="id"
|
||||
valueField="name"
|
||||
@@ -273,20 +323,53 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Textfield
|
||||
fullWidth
|
||||
placeholder="آدرس (اختیاری)"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
error={!!errors.address}
|
||||
helperText={errors.address?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">آدرسها</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddAddress}
|
||||
className="flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
افزودن آدرس
|
||||
</button>
|
||||
</div>
|
||||
{addresses.map((addr, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-2 p-3 border border-gray-200 rounded-lg"
|
||||
>
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Textfield
|
||||
fullWidth
|
||||
placeholder="کد پستی"
|
||||
value={addr.postal_code}
|
||||
onChange={(e) =>
|
||||
handleAddressChange(index, "postal_code", e.target.value)
|
||||
}
|
||||
/>
|
||||
<Textfield
|
||||
fullWidth
|
||||
placeholder="آدرس"
|
||||
value={addr.address}
|
||||
onChange={(e) =>
|
||||
handleAddressChange(index, "address", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{addresses.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveAddress(index)}
|
||||
className="mt-2 text-red-500 hover:text-red-700 transition-colors shrink-0"
|
||||
>
|
||||
<TrashIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="free_visibility_by_scope"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Grid } from "../../components/Grid/Grid";
|
||||
import Button from "../../components/Button/Button";
|
||||
import { AddOrganization } from "./AddOrganization";
|
||||
import AutoComplete from "../../components/AutoComplete/AutoComplete";
|
||||
import { FormApiBasedAutoComplete } from "../../components/FormItems/FormApiBasedAutoComplete";
|
||||
import Table from "../../components/Table/Table";
|
||||
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||
import { useApiRequest } from "../../utils/useApiRequest";
|
||||
@@ -19,6 +20,9 @@ export const OrganizationsList = () => {
|
||||
const [selectedProvinceKeys, setSelectedProvinceKeys] = useState<
|
||||
(string | number)[]
|
||||
>([]);
|
||||
const [selectedOrganizationType, setSelectedOrganizationType] = useState<
|
||||
string | number
|
||||
>("");
|
||||
const [params, setParams] = useState({ page: 1, page_size: 10 });
|
||||
const [tableData, setTableData] = useState([]);
|
||||
const { profile } = useUserProfileStore();
|
||||
@@ -32,11 +36,16 @@ export const OrganizationsList = () => {
|
||||
|
||||
const { data: apiData, refetch } = useApiRequest({
|
||||
api: selectedProvinceKeys?.length
|
||||
? `/auth/api/v1/organization/organizations_by_province?province=${selectedProvinceKeys[0]}`
|
||||
: "/auth/api/v1/organization/",
|
||||
? `/auth/api/v1/organization/organizations_by_province?province=${selectedProvinceKeys[0]}${selectedOrganizationType ? `&org_type=${selectedOrganizationType}` : ""}`
|
||||
: `/auth/api/v1/organization/${selectedOrganizationType ? `?org_type=${selectedOrganizationType}` : ""}`,
|
||||
method: "get",
|
||||
params: params,
|
||||
queryKey: ["organizations", params, selectedProvinceKeys],
|
||||
queryKey: [
|
||||
"organizations",
|
||||
params,
|
||||
selectedProvinceKeys,
|
||||
selectedOrganizationType,
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,16 +59,24 @@ export const OrganizationsList = () => {
|
||||
`${item?.type?.name}`,
|
||||
item?.parent_organization?.name,
|
||||
item?.national_unique_id,
|
||||
item?.unique_unit_identity || "-",
|
||||
item?.field_of_activity === "CO"
|
||||
? "کشور"
|
||||
: item?.field_of_activity === "PR"
|
||||
? "استان"
|
||||
: item?.field_of_activity === "CI"
|
||||
? "شهرستان"
|
||||
: "نامشخص",
|
||||
? "استان"
|
||||
: item?.field_of_activity === "CI"
|
||||
? "شهرستان"
|
||||
: "نامشخص",
|
||||
item?.province?.name,
|
||||
item?.city?.name,
|
||||
item?.address || "-",
|
||||
<ShowMoreInfo
|
||||
key={`address-${i}`}
|
||||
title="آدرسها"
|
||||
disabled={!item?.addresses?.length}
|
||||
data={item?.addresses}
|
||||
columns={["کد پستی", "آدرس"]}
|
||||
accessKeys={[["postal_code"], ["address"]]}
|
||||
/>,
|
||||
<ShowMoreInfo
|
||||
key={i}
|
||||
title="اطلاعات حساب"
|
||||
@@ -151,6 +168,16 @@ export const OrganizationsList = () => {
|
||||
ایجاد سازمان
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<FormApiBasedAutoComplete
|
||||
size="small"
|
||||
title="فیلتر نهاد"
|
||||
api={`auth/api/v1/organization-type`}
|
||||
keyField="id"
|
||||
valueField="name"
|
||||
onChange={(r) => setSelectedOrganizationType(r)}
|
||||
/>
|
||||
</Grid>
|
||||
{profile?.organization?.type?.org_type_field === "CO" && (
|
||||
<Grid>
|
||||
<AutoComplete
|
||||
@@ -177,6 +204,7 @@ export const OrganizationsList = () => {
|
||||
"نهاد",
|
||||
"سازمان والد",
|
||||
"شناسه کشوری",
|
||||
"شناسه یکتا واحد",
|
||||
"حوزه فعالیت",
|
||||
"استان",
|
||||
"شهر",
|
||||
|
||||
290
src/partials/tagging/DistributeFromDistribution.tsx
Normal file
290
src/partials/tagging/DistributeFromDistribution.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import { z } from "zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Grid } from "../../components/Grid/Grid";
|
||||
import Button from "../../components/Button/Button";
|
||||
import Textfield from "../../components/Textfeild/Textfeild";
|
||||
import { FormApiBasedAutoComplete } from "../../components/FormItems/FormApiBasedAutoComplete";
|
||||
import AutoComplete from "../../components/AutoComplete/AutoComplete";
|
||||
import { zValidateAutoComplete } from "../../data/getFormTypeErrors";
|
||||
import { useApiMutation, useApiRequest } from "../../utils/useApiRequest";
|
||||
import { useToast } from "../../hooks/useToast";
|
||||
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||
|
||||
const schema = z.object({
|
||||
organization: zValidateAutoComplete("سازمان"),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
type ParentDistItem = {
|
||||
id: number;
|
||||
dist_identity?: number;
|
||||
batch_identity: string | number | null;
|
||||
species_code: number;
|
||||
maxCount: number;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const DistributeFromDistribution = ({
|
||||
item,
|
||||
getData,
|
||||
isEdit,
|
||||
parentDistributions,
|
||||
}: any) => {
|
||||
const showToast = useToast();
|
||||
const { closeModal } = useModalStore();
|
||||
|
||||
const [dists, setDists] = useState<ParentDistItem[]>([]);
|
||||
const [selectedSpeciesKeys, setSelectedSpeciesKeys] = useState<
|
||||
(string | number)[]
|
||||
>([]);
|
||||
const [counts, setCounts] = useState<Record<number, number | "">>({});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
trigger,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
organization: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { data: batchDetail } = useApiRequest({
|
||||
api: `/tag/web/api/v1/tag_distribution_batch/${item?.id}/`,
|
||||
method: "get",
|
||||
queryKey: ["tagDistributionBatchDetail", item?.id],
|
||||
enabled: !!item?.id && !isEdit,
|
||||
});
|
||||
|
||||
const mutation = useApiMutation({
|
||||
api: `/tag/web/api/v1/tag_distribution/${item?.id}/${isEdit ? "edit_" : ""}distribute_distribution/`,
|
||||
method: isEdit ? "put" : "post",
|
||||
});
|
||||
|
||||
const { data: speciesData } = useApiRequest({
|
||||
api: "/livestock/web/api/v1/livestock_species",
|
||||
method: "get",
|
||||
params: { page: 1, pageSize: 1000 },
|
||||
queryKey: ["species"],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const sourceDistributions = isEdit
|
||||
? parentDistributions
|
||||
: item?.distributions?.length
|
||||
? item.distributions
|
||||
: batchDetail?.distributions;
|
||||
|
||||
if (!sourceDistributions?.length) {
|
||||
setDists([]);
|
||||
setCounts({});
|
||||
return;
|
||||
}
|
||||
|
||||
const parentDists: ParentDistItem[] = sourceDistributions.map((d: any) => {
|
||||
const maxCount = d?.remaining_number || 0;
|
||||
return {
|
||||
id: d?.id ?? d?.dist_identity,
|
||||
dist_identity: d?.dist_identity,
|
||||
batch_identity: d?.batch_identity ?? null,
|
||||
species_code: d?.species_code,
|
||||
maxCount: Number(maxCount) || 0,
|
||||
label:
|
||||
d?.serial_from != null && d?.serial_to != null
|
||||
? `از ${d.serial_from} تا ${d.serial_to}`
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
setDists(parentDists);
|
||||
|
||||
if (isEdit && item?.distributions?.length) {
|
||||
const defaultCounts: Record<number, number | ""> = {};
|
||||
const defaultSpeciesKeys: (string | number)[] = [];
|
||||
|
||||
parentDists.forEach((pd) => {
|
||||
const childDist = item.distributions.find(
|
||||
(cd: any) =>
|
||||
cd.parent_tag_distribution === pd.id ||
|
||||
cd.species_code === pd.species_code,
|
||||
);
|
||||
if (childDist) {
|
||||
defaultCounts[pd.id] = childDist.total_tag_count || 0;
|
||||
if (!defaultSpeciesKeys.includes(pd.species_code)) {
|
||||
defaultSpeciesKeys.push(pd.species_code);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setCounts(defaultCounts);
|
||||
setSelectedSpeciesKeys(defaultSpeciesKeys);
|
||||
} else {
|
||||
setCounts({});
|
||||
setSelectedSpeciesKeys([]);
|
||||
}
|
||||
}, [item?.distributions, batchDetail?.distributions, parentDistributions]);
|
||||
|
||||
const speciesOptions = () =>
|
||||
speciesData?.results?.map((opt: any) => ({
|
||||
key: opt?.value,
|
||||
value: opt?.name,
|
||||
})) ?? [];
|
||||
|
||||
const getSpeciesName = (speciesCode: number) =>
|
||||
speciesOptions().find((s: any) => s.key === speciesCode)?.value ?? "نامشخص";
|
||||
|
||||
const visibleDists = dists.filter((d) =>
|
||||
selectedSpeciesKeys.includes(d.species_code),
|
||||
);
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const distsPayload = visibleDists
|
||||
.filter((d) => {
|
||||
const c = counts[d.id];
|
||||
return c !== "" && c !== undefined && c !== null && Number(c) > 0;
|
||||
})
|
||||
.map((d) => {
|
||||
const fromItem = item?.distributions?.find(
|
||||
(x: any) => x.id === d.id || x.dist_identity === d.id,
|
||||
);
|
||||
const batch_identity =
|
||||
fromItem != null ? fromItem.batch_identity : d.batch_identity;
|
||||
return {
|
||||
parent_tag_distribution: d.id,
|
||||
batch_identity: batch_identity != null ? batch_identity : null,
|
||||
species_code: d.species_code,
|
||||
count: Number(counts[d.id] ?? 0),
|
||||
};
|
||||
});
|
||||
|
||||
if (distsPayload.length === 0) {
|
||||
showToast("حداقل یک گونه با تعداد معتبر وارد کنید", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await mutation.mutateAsync({
|
||||
assigned_org: data.organization[0],
|
||||
parent_distribution_batch: item.id,
|
||||
dists: distsPayload,
|
||||
});
|
||||
|
||||
showToast(
|
||||
isEdit
|
||||
? "ویرایش توزیع با موفقیت انجام شد"
|
||||
: "توزیع از توزیع با موفقیت انجام شد",
|
||||
"success",
|
||||
);
|
||||
getData();
|
||||
closeModal();
|
||||
} catch (error: any) {
|
||||
showToast(
|
||||
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCountChange = (distId: number, value: number | "") => {
|
||||
setCounts((prev) => ({ ...prev, [distId]: value }));
|
||||
};
|
||||
|
||||
const isValidCount = (dist: ParentDistItem) => {
|
||||
const c = counts[dist.id];
|
||||
if (c === "" || c === undefined || c === null) return false;
|
||||
const num = Number(c);
|
||||
return num > 0 && num <= dist.maxCount;
|
||||
};
|
||||
|
||||
const speciesOptionsFromParent = () => {
|
||||
const uniqueSpecies = Array.from(
|
||||
new Map(dists.map((d) => [d.species_code, d])).values(),
|
||||
);
|
||||
return uniqueSpecies.map((d) => ({
|
||||
key: d.species_code,
|
||||
value: getSpeciesName(d.species_code),
|
||||
}));
|
||||
};
|
||||
|
||||
const hasValidDists =
|
||||
visibleDists.length > 0 && visibleDists.every((d) => isValidCount(d));
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Grid container column className="gap-3">
|
||||
<Controller
|
||||
name="organization"
|
||||
control={control}
|
||||
render={() => (
|
||||
<FormApiBasedAutoComplete
|
||||
defaultKey={item?.assigned_org?.id}
|
||||
title="انتخاب سازمان (دریافتکننده)"
|
||||
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
|
||||
keyField="id"
|
||||
valueField="name"
|
||||
error={!!errors.organization}
|
||||
errorMessage={errors.organization?.message}
|
||||
onChange={(r) => {
|
||||
setValue("organization", [r]);
|
||||
trigger("organization");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{dists.length > 0 && speciesData?.results && (
|
||||
<>
|
||||
<AutoComplete
|
||||
data={speciesOptionsFromParent()}
|
||||
multiselect
|
||||
selectedKeys={selectedSpeciesKeys}
|
||||
onChange={(keys: (string | number)[]) => {
|
||||
setSelectedSpeciesKeys(keys);
|
||||
}}
|
||||
title="گونه"
|
||||
/>
|
||||
{visibleDists.map((dist) => {
|
||||
const countVal = counts[dist.id];
|
||||
const numCount =
|
||||
countVal !== "" && countVal !== undefined && countVal !== null
|
||||
? Number(countVal)
|
||||
: null;
|
||||
const isOverMax = numCount !== null && numCount > dist.maxCount;
|
||||
const isEmpty = countVal === "" || countVal === undefined;
|
||||
const helperText = isOverMax
|
||||
? `تعداد نباید بیشتر از ${dist.maxCount.toLocaleString()} باشد`
|
||||
: isEmpty
|
||||
? "لطفا تعداد را وارد کنید"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Textfield
|
||||
key={dist.id}
|
||||
fullWidth
|
||||
formattedNumber
|
||||
placeholder={`تعداد ${getSpeciesName(dist.species_code)}${dist.label ? ` (${dist.label})` : ""} — حداکثر: ${dist.maxCount.toLocaleString()}`}
|
||||
value={counts[dist.id] ?? ""}
|
||||
onChange={(e) =>
|
||||
handleCountChange(dist.id, Number(e.target.value))
|
||||
}
|
||||
error={isOverMax}
|
||||
helperText={helperText}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button disabled={!hasValidDists} type="submit">
|
||||
ثبت
|
||||
</Button>
|
||||
</Grid>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
54
src/partials/tagging/DistributionSpeciesModal.tsx
Normal file
54
src/partials/tagging/DistributionSpeciesModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
export const DistributionSpeciesModal = ({ items }: { items: any[] }) => {
|
||||
const speciesMap: Record<number, string> = {
|
||||
1: "گاو",
|
||||
2: "گاومیش",
|
||||
3: "شتر",
|
||||
4: "گوسفند",
|
||||
5: "بز",
|
||||
};
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{items?.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 p-4
|
||||
bg-white dark:bg-gray-800
|
||||
hover:shadow-sm transition"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
گونه
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-100">
|
||||
{speciesMap[item?.species_code] ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-gray-100 dark:bg-gray-700 my-2" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد توزیع
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{item?.dist_count?.toLocaleString() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
تعداد پلاک
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{item?.tag_count?.toLocaleString() ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -45,8 +45,6 @@ export const SubmitNewTags = ({ getData, item }: SubmitNewTagsTypeProps) => {
|
||||
|
||||
const { profile } = useUserProfileStore();
|
||||
|
||||
console.log(item);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
@@ -58,13 +56,15 @@ export const SubmitNewTags = ({ getData, item }: SubmitNewTagsTypeProps) => {
|
||||
defaultValues: {
|
||||
country_code: 364,
|
||||
static_code: 0,
|
||||
species_code: 1,
|
||||
serial_start: item?.serial_from || "",
|
||||
serial_end: item?.serial_to || "",
|
||||
species_code: item?.species_code || 1,
|
||||
},
|
||||
});
|
||||
|
||||
const mutation = useApiMutation({
|
||||
api: "/tag/web/api/v1/tag/",
|
||||
method: "post",
|
||||
api: `/tag/web/api/v1/tag/${item?.id ? item.id : ""}`,
|
||||
method: item ? "put" : "post",
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
|
||||
284
src/partials/tagging/SubmitTagDistribution.tsx
Normal file
284
src/partials/tagging/SubmitTagDistribution.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import { z } from "zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Grid } from "../../components/Grid/Grid";
|
||||
import Button from "../../components/Button/Button";
|
||||
import Textfield from "../../components/Textfeild/Textfeild";
|
||||
import { RadioGroup } from "../../components/RadioButton/RadioGroup";
|
||||
import { FormApiBasedAutoComplete } from "../../components/FormItems/FormApiBasedAutoComplete";
|
||||
import AutoComplete from "../../components/AutoComplete/AutoComplete";
|
||||
import { zValidateAutoComplete } from "../../data/getFormTypeErrors";
|
||||
import { useApiMutation, useApiRequest } from "../../utils/useApiRequest";
|
||||
import { useToast } from "../../hooks/useToast";
|
||||
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||
|
||||
const distributionTypeOptions = [
|
||||
{ label: "توزیع گروهی", value: "group" },
|
||||
{ label: "توزیع تصادفی", value: "random" },
|
||||
];
|
||||
|
||||
const schema = z.object({
|
||||
organization: zValidateAutoComplete("سازمان"),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
type BatchItem = {
|
||||
batch_identity?: string | number;
|
||||
species_code?: number;
|
||||
count: number | "";
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export const SubmitTagDistribution = ({ item, getData }: any) => {
|
||||
const showToast = useToast();
|
||||
const { closeModal } = useModalStore();
|
||||
|
||||
const isEdit = Boolean(item?.id);
|
||||
|
||||
const [distributionType, setDistributionType] = useState<"group" | "random">(
|
||||
isEdit
|
||||
? item?.distribution_type === "random"
|
||||
? "random"
|
||||
: "group"
|
||||
: "group",
|
||||
);
|
||||
const [batches, setBatches] = useState<BatchItem[]>([]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
trigger,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
organization: [],
|
||||
},
|
||||
});
|
||||
|
||||
const mutation = useApiMutation({
|
||||
api: isEdit
|
||||
? `/tag/web/api/v1/tag_distribution/${item?.id}`
|
||||
: "/tag/web/api/v1/tag_distribution/",
|
||||
method: isEdit ? "put" : "post",
|
||||
});
|
||||
|
||||
const { data: speciesData } = useApiRequest({
|
||||
api: "/livestock/web/api/v1/livestock_species",
|
||||
method: "get",
|
||||
params: { page: 1, pageSize: 1000 },
|
||||
queryKey: ["species"],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!item) return;
|
||||
|
||||
setValue("organization", [item.assigned_org?.id]);
|
||||
trigger("organization");
|
||||
|
||||
const mappedBatches = item.distributions.map((d: any) => ({
|
||||
...(item.distribution_type === "batch" && {
|
||||
batch_identity: item.dist_batch_identity,
|
||||
}),
|
||||
species_code: d.species_code,
|
||||
count: d.distributed_number,
|
||||
label:
|
||||
item.distribution_type === "batch"
|
||||
? `از ${d.serial_from ?? "-"} تا ${d.serial_to ?? "-"}`
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
setBatches(mappedBatches);
|
||||
}, [item]);
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const dists =
|
||||
distributionType === "random"
|
||||
? batches.map((b) => ({
|
||||
species_code: b.species_code,
|
||||
count: b.count,
|
||||
}))
|
||||
: batches.map((b) => ({
|
||||
batch_identity: b.batch_identity,
|
||||
species_code: b.species_code,
|
||||
count: b.count,
|
||||
}));
|
||||
|
||||
try {
|
||||
await mutation.mutateAsync({
|
||||
assigner_org: item?.organization?.id,
|
||||
assigned_org: data.organization[0],
|
||||
dists,
|
||||
});
|
||||
|
||||
showToast(
|
||||
isEdit ? "ویرایش با موفقیت انجام شد" : "ثبت با موفقیت انجام شد",
|
||||
"success",
|
||||
);
|
||||
getData();
|
||||
closeModal();
|
||||
} catch (error: any) {
|
||||
showToast(
|
||||
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const speciesOptions = () => {
|
||||
return (
|
||||
speciesData?.results?.map((opt: any) => ({
|
||||
key: opt?.value,
|
||||
value: opt?.name,
|
||||
})) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Grid container column className="gap-3">
|
||||
<Controller
|
||||
name="organization"
|
||||
control={control}
|
||||
render={() => (
|
||||
<FormApiBasedAutoComplete
|
||||
defaultKey={item?.assigned_org?.id}
|
||||
title="انتخاب سازمان"
|
||||
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
|
||||
keyField="id"
|
||||
valueField="name"
|
||||
error={!!errors.organization}
|
||||
errorMessage={errors.organization?.message}
|
||||
onChange={(r) => {
|
||||
setValue("organization", [r]);
|
||||
trigger("organization");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<RadioGroup
|
||||
direction="row"
|
||||
options={distributionTypeOptions}
|
||||
value={distributionType}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value as "group" | "random";
|
||||
setDistributionType(val);
|
||||
setBatches([]);
|
||||
}}
|
||||
/>
|
||||
|
||||
{distributionType === "group" && (
|
||||
<FormApiBasedAutoComplete
|
||||
title="گروه پلاک"
|
||||
api="/tag/web/api/v1/tag_batch/"
|
||||
keyField="batch_identity"
|
||||
secondaryKey="species_code"
|
||||
valueTemplate="از v1 تا v2"
|
||||
valueField={["serial_from"]}
|
||||
valueField2={["serial_to"]}
|
||||
groupBy="species_code"
|
||||
defaultKey={
|
||||
item?.distributions?.map((d: any) => d.batch_identity) || []
|
||||
}
|
||||
groupFunction={(item) =>
|
||||
speciesOptions().find((s: any) => s.key === item)?.value ||
|
||||
"نامشخص"
|
||||
}
|
||||
valueTemplateProps={[{ v1: "string" }, { v2: "string" }]}
|
||||
multiple
|
||||
onChange={(items) => {
|
||||
setBatches(
|
||||
items?.map((r: any) => {
|
||||
const existing = batches.find(
|
||||
(b) =>
|
||||
b.batch_identity === r.key1 && b.species_code === r.key2,
|
||||
);
|
||||
return {
|
||||
batch_identity: r.key1,
|
||||
species_code: r.key2,
|
||||
count: existing?.count ?? "",
|
||||
};
|
||||
}) || [],
|
||||
);
|
||||
}}
|
||||
onChangeValue={(labels) => {
|
||||
setBatches((prev) =>
|
||||
prev.map((item, index) => ({
|
||||
...item,
|
||||
label: labels[index],
|
||||
})),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{distributionType === "random" && speciesData?.results && (
|
||||
<AutoComplete
|
||||
data={speciesOptions()}
|
||||
multiselect
|
||||
selectedKeys={batches.map((b) => b.species_code)}
|
||||
onChange={(keys: (string | number)[]) => {
|
||||
setBatches(
|
||||
keys.map((k) => {
|
||||
const prev = batches.find((b) => b.species_code === k);
|
||||
return {
|
||||
species_code: k as number,
|
||||
count: prev?.count ?? "",
|
||||
};
|
||||
}),
|
||||
);
|
||||
}}
|
||||
title="گونه"
|
||||
/>
|
||||
)}
|
||||
|
||||
{batches.map((batch, index) => (
|
||||
<Textfield
|
||||
key={index}
|
||||
fullWidth
|
||||
formattedNumber
|
||||
placeholder={
|
||||
distributionType === "group"
|
||||
? `تعداد ${
|
||||
speciesOptions().find(
|
||||
(s: any) => s.key === batch.species_code,
|
||||
)?.value
|
||||
} (${batch.label}) `
|
||||
: `تعداد ${
|
||||
speciesOptions().find(
|
||||
(s: any) => s.key === batch.species_code,
|
||||
)?.value
|
||||
}`
|
||||
}
|
||||
value={batch.count}
|
||||
onChange={(e) => {
|
||||
const next = [...batches];
|
||||
next[index].count = Number(e.target.value);
|
||||
setBatches(next);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
disabled={
|
||||
batches.length === 0 ||
|
||||
batches.some(
|
||||
(b) =>
|
||||
b.count === "" ||
|
||||
b.count === undefined ||
|
||||
b.count === null ||
|
||||
Number(b.count) <= 0,
|
||||
)
|
||||
}
|
||||
type="submit"
|
||||
>
|
||||
{isEdit ? "ویرایش" : "ثبت"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
399
src/partials/tagging/TagActiveDistributions.tsx
Normal file
399
src/partials/tagging/TagActiveDistributions.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Bars3Icon,
|
||||
CubeIcon,
|
||||
SparklesIcon,
|
||||
StopCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||
import { useApiRequest } from "../../utils/useApiRequest";
|
||||
import { formatJustDate, formatJustTime } from "../../utils/formatTime";
|
||||
import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo";
|
||||
import { Grid } from "../../components/Grid/Grid";
|
||||
import Typography from "../../components/Typography/Typography";
|
||||
import { Popover } from "../../components/PopOver/PopOver";
|
||||
import Button from "../../components/Button/Button";
|
||||
import { Tooltip } from "../../components/Tooltip/Tooltip";
|
||||
import { DeleteButtonForPopOver } from "../../components/PopOverButtons/PopOverButtons";
|
||||
import { SubmitTagDistribution } from "./SubmitTagDistribution";
|
||||
import { DistributeFromDistribution } from "./DistributeFromDistribution";
|
||||
import Table from "../../components/Table/Table";
|
||||
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion";
|
||||
import { TableButton } from "../../components/TableButton/TableButton";
|
||||
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { TAG_DISTRIBUTION } from "../../routes/paths";
|
||||
import { DocumentOperation } from "../../components/DocumentOperation/DocumentOperation";
|
||||
import { DocumentDownloader } from "../../components/DocumentDownloader/DocumentDownloader";
|
||||
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||
|
||||
export default function TagActiveDistributions() {
|
||||
const { openModal } = useModalStore();
|
||||
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
|
||||
const [tagsTableData, setTagsTableData] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: tagsData, refetch } = useApiRequest({
|
||||
api: "/tag/web/api/v1/tag_distribution_batch",
|
||||
method: "get",
|
||||
queryKey: ["tagsList", tableInfo],
|
||||
params: {
|
||||
...tableInfo,
|
||||
},
|
||||
});
|
||||
|
||||
const { profile } = useUserProfileStore();
|
||||
|
||||
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
|
||||
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false",
|
||||
method: "get",
|
||||
queryKey: ["tagDistributionActivesDashboard"],
|
||||
});
|
||||
|
||||
const showAssignDocColumn =
|
||||
(profile?.role?.type?.key === "ADM" ||
|
||||
tagsData?.results?.some(
|
||||
(item: any) => profile?.organization?.id === item?.assigned_org?.id,
|
||||
)) ??
|
||||
false;
|
||||
|
||||
const AbleToSeeAssignDoc = (item: any) => {
|
||||
if (
|
||||
profile?.role?.type?.key === "ADM" ||
|
||||
profile?.organization?.id === item?.assigned_org?.id
|
||||
) {
|
||||
return (
|
||||
<DocumentOperation
|
||||
key={item?.id}
|
||||
downloadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/distribution_pdf_view/`}
|
||||
payloadKey="dist_exit_document"
|
||||
// validFiles={["pdf"]}
|
||||
page="tag_distribution"
|
||||
access="Upload-Assign-Document"
|
||||
uploadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/assign_document/`}
|
||||
onUploadSuccess={handleUpdate}
|
||||
limitSize={3}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetch();
|
||||
updateDashboard();
|
||||
};
|
||||
const speciesMap: Record<number, string> = {
|
||||
1: "گاو",
|
||||
2: "گاومیش",
|
||||
3: "شتر",
|
||||
4: "گوسفند",
|
||||
5: "بز",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tagsData?.results) {
|
||||
const formattedData = tagsData.results.map((item: any, index: number) => {
|
||||
const dist = item?.distributions;
|
||||
|
||||
return [
|
||||
tableInfo.page === 1
|
||||
? index + 1
|
||||
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
|
||||
item?.dist_batch_identity,
|
||||
`${formatJustDate(item?.create_date)} (${
|
||||
formatJustDate(item?.create_date)
|
||||
? formatJustTime(item?.create_date)
|
||||
: "-"
|
||||
})`,
|
||||
item?.assigner_org?.name,
|
||||
item?.assigned_org?.name,
|
||||
item?.total_tag_count,
|
||||
item?.total_distributed_tag_count,
|
||||
item?.remaining_tag_count,
|
||||
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
|
||||
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
|
||||
<Grid container column className="gap-4 w-full">
|
||||
{dist?.map((opt: any, index: number) => (
|
||||
<Grid
|
||||
key={index}
|
||||
container
|
||||
column
|
||||
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
|
||||
>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
گونه:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{speciesMap[opt?.species_code] ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{item?.distribution_type === "batch" && opt?.serial_from && (
|
||||
<Grid container className="gap-2 items-center">
|
||||
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
بازه سریال:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
از {opt?.serial_from ?? "-"} تا {opt?.serial_to ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
تعداد پلاک:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.total_tag_count?.toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
پلاک های توزیع شده:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.distributed_number?.toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
پلاک های باقیمانده:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.remaining_number?.toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</ShowMoreInfo>,
|
||||
...(showAssignDocColumn ? [AbleToSeeAssignDoc(item)] : []),
|
||||
<DocumentDownloader
|
||||
key={index}
|
||||
link={item?.warehouse_exit_doc}
|
||||
title="دانلود"
|
||||
/>,
|
||||
item?.exit_doc_status ? (
|
||||
"تایید شده"
|
||||
) : (
|
||||
<Button
|
||||
page="tag_distribution"
|
||||
access="Accept-Assign-Document"
|
||||
size="small"
|
||||
disabled={item?.exit_doc_status}
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "تایید سند خروج",
|
||||
content: (
|
||||
<BooleanQuestion
|
||||
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/accept_exit_doc/`}
|
||||
method="post"
|
||||
getData={handleUpdate}
|
||||
title="آیا از تایید سند خروج مطمئنید؟"
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
تایید سند خروج
|
||||
</Button>
|
||||
),
|
||||
<Popover key={index}>
|
||||
<Tooltip title="جزئیات توزیع" position="right">
|
||||
<Button
|
||||
variant="detail"
|
||||
page="tag_distribution_detail"
|
||||
access="Show-Tag-Distribution-Detail"
|
||||
onClick={() => {
|
||||
const path =
|
||||
TAG_DISTRIBUTION +
|
||||
"/" +
|
||||
item?.dist_batch_identity +
|
||||
"/" +
|
||||
item?.id;
|
||||
navigate({ to: path });
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="ویرایش توزیع" position="right">
|
||||
<Button
|
||||
variant="edit"
|
||||
page="tag_distribution"
|
||||
access="Submit-Tag-Distribution"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "ویرایش توزیع پلاک",
|
||||
content: (
|
||||
<SubmitTagDistribution
|
||||
getData={handleUpdate}
|
||||
item={item}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="توزیع مجدد" position="right">
|
||||
<Button
|
||||
variant="share"
|
||||
page="tag_distribution"
|
||||
access="Distribute-From-Distribution"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "توزیع مجدد",
|
||||
content: (
|
||||
<DistributeFromDistribution
|
||||
getData={handleUpdate}
|
||||
item={item}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={"لغو توزیع"} position="right">
|
||||
<Button
|
||||
page="tag_distribution"
|
||||
access="Cancel-Tag-Distribution"
|
||||
icon={<StopCircleIcon className="w-5 h-5 text-red-400" />}
|
||||
variant="set"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "لغو توزیع پلاک",
|
||||
content: (
|
||||
<BooleanQuestion
|
||||
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/close_dist_batch/`}
|
||||
method="post"
|
||||
getData={handleUpdate}
|
||||
title="آیا از لغو توزیع پلاک مطمئنید؟"
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="tag_distribution"
|
||||
access="Delete-Tag-Distribution"
|
||||
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
|
||||
getData={refetch}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
|
||||
setTagsTableData(formattedData);
|
||||
} else {
|
||||
setTagsTableData([]);
|
||||
}
|
||||
}, [tagsData, tableInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4 mt-2">
|
||||
<Grid>
|
||||
<Button
|
||||
size="small"
|
||||
variant="submit"
|
||||
page="tag_distribution"
|
||||
access="Submit-Tag-Distribution"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "توزیع پلاک",
|
||||
content: <SubmitTagDistribution getData={handleUpdate} />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
توزیع پلاک
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
title="خلاصه اطلاعات"
|
||||
noPagination
|
||||
noSearch
|
||||
columns={[
|
||||
"تعداد توزیع",
|
||||
"پلاک های ارسالی",
|
||||
"پلاک های دریافتی",
|
||||
"توزیع های دریافتی",
|
||||
"توزیع های ارسالی",
|
||||
"جزئیات",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
tagDashboardData?.count?.toLocaleString() || 0,
|
||||
tagDashboardData?.total_sent_tag_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.total_recieved_tag_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
|
||||
0,
|
||||
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
|
||||
<TableButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات",
|
||||
content: (
|
||||
<DistributionSpeciesModal
|
||||
items={tagDashboardData?.items}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setTableInfo}
|
||||
count={tagsData?.count || 0}
|
||||
isPaginated
|
||||
title="توزیع پلاک"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"شناسه توزیع",
|
||||
"تاریخ ثبت",
|
||||
"توزیع کننده",
|
||||
"دریافت کننده",
|
||||
"تعداد کل پلاک",
|
||||
"پلاک های توزیع شده",
|
||||
"پلاک های باقیمانده",
|
||||
"نوع توزیع",
|
||||
"جزئیات توزیع",
|
||||
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
|
||||
"سند خروج از انبار",
|
||||
"تایید سند خروج",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={tagsTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
228
src/partials/tagging/TagCanceledDistributions.tsx
Normal file
228
src/partials/tagging/TagCanceledDistributions.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
BackwardIcon,
|
||||
Bars3Icon,
|
||||
CubeIcon,
|
||||
SparklesIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useModalStore } from "../../context/zustand-store/appStore";
|
||||
import { useApiRequest } from "../../utils/useApiRequest";
|
||||
import { formatJustDate, formatJustTime } from "../../utils/formatTime";
|
||||
import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo";
|
||||
import { Grid } from "../../components/Grid/Grid";
|
||||
import Typography from "../../components/Typography/Typography";
|
||||
import { Popover } from "../../components/PopOver/PopOver";
|
||||
import Button from "../../components/Button/Button";
|
||||
import { Tooltip } from "../../components/Tooltip/Tooltip";
|
||||
import { DeleteButtonForPopOver } from "../../components/PopOverButtons/PopOverButtons";
|
||||
|
||||
import Table from "../../components/Table/Table";
|
||||
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion";
|
||||
import { TableButton } from "../../components/TableButton/TableButton";
|
||||
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
|
||||
|
||||
export default function TagCanceledDistributions() {
|
||||
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_distribution_batch/closed_tag_dist_batch_list",
|
||||
method: "get",
|
||||
queryKey: ["tagsList", tableInfo],
|
||||
params: {
|
||||
...tableInfo,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
|
||||
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=true",
|
||||
method: "get",
|
||||
queryKey: ["tagDistributionCanceledDashboard"],
|
||||
});
|
||||
|
||||
const handleUpdate = () => {
|
||||
refetch();
|
||||
updateDashboard();
|
||||
};
|
||||
const speciesMap: Record<number, string> = {
|
||||
1: "گاو",
|
||||
2: "گاومیش",
|
||||
3: "شتر",
|
||||
4: "گوسفند",
|
||||
5: "بز",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tagsData?.results) {
|
||||
const formattedData = tagsData.results.map((item: any, index: number) => {
|
||||
const dist = item?.distributions;
|
||||
|
||||
return [
|
||||
tableInfo.page === 1
|
||||
? index + 1
|
||||
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
|
||||
item?.dist_batch_identity,
|
||||
`${formatJustDate(item?.create_date)} (${
|
||||
formatJustDate(item?.create_date)
|
||||
? formatJustTime(item?.create_date)
|
||||
: "-"
|
||||
})`,
|
||||
item?.assigner_org?.name,
|
||||
item?.assigned_org?.name,
|
||||
item?.total_tag_count,
|
||||
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
|
||||
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
|
||||
<Grid container column className="gap-4 w-full">
|
||||
{dist?.map((opt: any, index: number) => (
|
||||
<Grid
|
||||
key={index}
|
||||
container
|
||||
column
|
||||
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
|
||||
>
|
||||
{item?.distribution_type === "batch" && opt?.serial_from && (
|
||||
<Grid container className="gap-2 items-center">
|
||||
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
بازه سریال:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
از {opt?.serial_from ?? "-"} تا {opt?.serial_to ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid container className="gap-2 items-center">
|
||||
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
تعداد پلاک:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{opt?.distributed_number?.toLocaleString()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid container className="gap-2 items-center">
|
||||
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
|
||||
<Typography variant="body2" className="font-medium">
|
||||
گونه:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{speciesMap[opt?.species_code] ?? "-"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</ShowMoreInfo>,
|
||||
<Popover key={index}>
|
||||
<Tooltip title={"برگشت توزیع"} position="right">
|
||||
<Button
|
||||
page="tag_distribution"
|
||||
access="Cancel-Tag-Distribution"
|
||||
icon={<BackwardIcon className="w-5 h-5 text-red-400" />}
|
||||
variant="set"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "برگشت توزیع لغو شده",
|
||||
content: (
|
||||
<BooleanQuestion
|
||||
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/reactivate_tag_dist_batch/`}
|
||||
method="post"
|
||||
getData={handleUpdate}
|
||||
title="آیا از برگشت توزیع پلاک لغو شده مطمئنید؟"
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DeleteButtonForPopOver
|
||||
page="tag_distribution"
|
||||
access="Delete-Tag-Distribution"
|
||||
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
|
||||
getData={handleUpdate}
|
||||
/>
|
||||
</Popover>,
|
||||
];
|
||||
});
|
||||
|
||||
setTagsTableData(formattedData);
|
||||
} else {
|
||||
setTagsTableData([]);
|
||||
}
|
||||
}, [tagsData, tableInfo]);
|
||||
|
||||
return (
|
||||
<Grid container column className="gap-4 mt-2">
|
||||
<Grid isDashboard>
|
||||
<Table
|
||||
isDashboard
|
||||
title="خلاصه اطلاعات"
|
||||
noPagination
|
||||
noSearch
|
||||
columns={[
|
||||
"تعداد توزیع",
|
||||
"پلاک های ارسالی",
|
||||
"پلاک های دریافتی",
|
||||
"توزیع های دریافتی",
|
||||
"توزیع های ارسالی",
|
||||
"جزئیات",
|
||||
]}
|
||||
rows={[
|
||||
[
|
||||
tagDashboardData?.count?.toLocaleString() || 0,
|
||||
tagDashboardData?.total_sent_tag_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.total_recieved_tag_count?.toLocaleString() || 0,
|
||||
tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
|
||||
0,
|
||||
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
|
||||
<TableButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
openModal({
|
||||
title: "جزئیات",
|
||||
content: (
|
||||
<DistributionSpeciesModal
|
||||
items={tagDashboardData?.items}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</Grid>
|
||||
<Table
|
||||
className="mt-2"
|
||||
onChange={setTableInfo}
|
||||
count={tagsData?.count || 0}
|
||||
isPaginated
|
||||
title="توزیع های لغو شده"
|
||||
columns={[
|
||||
"ردیف",
|
||||
"شناسه توزیع",
|
||||
"تاریخ ثبت",
|
||||
"توزیع کننده",
|
||||
"دریافت کننده",
|
||||
"تعداد کل پلاک",
|
||||
"نوع توزیع",
|
||||
"جزئیات توزیع",
|
||||
"عملیات",
|
||||
]}
|
||||
rows={tagsTableData}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -52,3 +52,7 @@ export const UNITS_SETTINGS = "/unit-settings";
|
||||
|
||||
//TAGGING
|
||||
export const TAGGING = "/tagging";
|
||||
export const TAGS = "/tags";
|
||||
export const TAG_DISTRIBUTION = "/tag-distribution";
|
||||
export const TAG_DISTRIBUTION_DETAIL = "/tag-distribution/$identity/$id";
|
||||
export const TAGS_BATCH = "/tags/$id/$from/$to";
|
||||
|
||||
@@ -35,7 +35,9 @@ export const formatJustDate = (time: any) => {
|
||||
};
|
||||
|
||||
export const formatJustTime = (time: any) => {
|
||||
return format(new Date(time), "HH:mm");
|
||||
if (time) {
|
||||
return format(new Date(time), "HH:mm");
|
||||
} else return "";
|
||||
};
|
||||
|
||||
export function formatStampDate(timestamp: number) {
|
||||
|
||||
@@ -23,6 +23,9 @@ import Cooperatives from "../Pages/Cooperatives";
|
||||
import CooperativeRanchers from "../Pages/CooperativeRanchers";
|
||||
import SettingsOfUnits from "../Pages/SettingsOfUnits";
|
||||
import Tagging from "../Pages/Tagging";
|
||||
import Tags from "../Pages/Tags";
|
||||
import TagDistribtution from "../Pages/TagDistribution";
|
||||
import TagDistribtutionDetails from "../Pages/TagDistributionDetails";
|
||||
|
||||
export const managementCategoryItems = [
|
||||
{
|
||||
@@ -171,6 +174,26 @@ export const taggingCategoryItems = [
|
||||
path: R.TAGGING,
|
||||
component: Tagging,
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
path: R.TAGS,
|
||||
component: Tags,
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
path: R.TAGS_BATCH,
|
||||
component: Tags,
|
||||
},
|
||||
{
|
||||
name: "tag_distribution",
|
||||
path: R.TAG_DISTRIBUTION,
|
||||
component: TagDistribtution,
|
||||
},
|
||||
{
|
||||
name: "tag_distribution_detail",
|
||||
path: R.TAG_DISTRIBUTION_DETAIL,
|
||||
component: TagDistribtutionDetails,
|
||||
},
|
||||
];
|
||||
|
||||
export const posCategoryItems = [
|
||||
|
||||
@@ -97,6 +97,15 @@ export function getFaPermissions(permission: string) {
|
||||
case "tagging":
|
||||
faPermission = "پلاک کوبی";
|
||||
break;
|
||||
case "tags":
|
||||
faPermission = "پلاک ها";
|
||||
break;
|
||||
case "tag_distribution":
|
||||
faPermission = "توزیع پلاک";
|
||||
break;
|
||||
case "tag_distribution_detail":
|
||||
faPermission = "جزئیات توزیع پلاک";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
02.52
|
||||
02.66
|
||||
|
||||
Reference in New Issue
Block a user