Compare commits

...

43 Commits

Author SHA1 Message Date
468ad9079e version changed to 02.67 2026-02-10 16:50:13 +03:30
28f7d5c991 feat: inquiry unique id 2026-02-10 16:50:04 +03:30
763d5b79d3 add: quaternaryKey 2026-02-10 16:49:48 +03:30
f8e8e7f1f1 fix: unique unit id validation 2026-02-10 15:16:53 +03:30
63aed5572c fix: AutoComplete click event on open icon 2026-02-10 15:11:45 +03:30
59ae3b76d9 update: add address text color 2026-02-10 14:52:23 +03:30
93867ce7ee version changed to 02.66 2026-02-09 16:23:19 +03:30
4210dbd7e2 add: organization type filter 2026-02-09 14:38:32 +03:30
3624b3bc70 add: size prop 2026-02-09 13:53:43 +03:30
4d00b0d492 add: unique_unit_identity 2026-02-09 12:34:56 +03:30
ed7b257ed8 add: limit size for upload doc and removed validFiles limit 2026-02-09 10:23:35 +03:30
03136f5f30 add: limit size 2026-02-09 10:22:34 +03:30
908a69ce0e version changed to 02.65 2026-02-08 16:53:18 +03:30
a818683247 add: multiple addresses 2026-02-08 16:52:55 +03:30
95780cfbc9 add: document upload and sign 2026-02-08 16:52:26 +03:30
071c3e159b feat: document operation 2026-02-08 16:52:00 +03:30
90f51c6899 feat: edit distribution from distribution 2026-02-08 11:20:10 +03:30
e967329108 chore: fix grammar mistake 2026-02-03 10:24:28 +03:30
f8d2da4f28 version changed to 02.64 2026-02-02 16:34:27 +03:30
bb1d5b3315 update: tag distribution details 2026-02-02 16:34:21 +03:30
d8d415a8f5 fix: remaining number 2026-02-02 14:50:44 +03:30
f58c8e6c58 add: new keys 2026-02-02 14:49:12 +03:30
3a17fcb448 version changed to 02.63 2026-02-02 12:14:52 +03:30
6e219aca1a feat: distribute from distribution 2026-02-02 12:14:48 +03:30
9b74be078f version changed to 02.62 2026-02-02 11:03:09 +03:30
5fd55c4b10 fix: filter error 2026-02-02 11:03:01 +03:30
6b5276ce36 add: new tagging key 2026-02-02 08:40:06 +03:30
e465843eb9 version changed to 02.61 2026-01-28 17:42:11 +03:30
e5402f9037 add: tag dist detail 2026-01-28 17:41:53 +03:30
e342a7cdd5 version changed to 02.60 2026-01-27 11:20:42 +03:30
f5de2f68b5 version changed to 02.59 2026-01-27 11:19:10 +03:30
44ea5974eb add: tags filter 2026-01-27 11:19:03 +03:30
cb87251d62 version changed to 02.58 2026-01-26 16:33:34 +03:30
a1b430ad8e add: tags filters 2026-01-26 16:33:29 +03:30
7b88a664b0 add: specie filter 2026-01-26 15:00:36 +03:30
2a6d978dba fix : time error 2026-01-26 14:34:56 +03:30
b9f9e6cd06 version changed to 02.57 2026-01-25 14:36:12 +03:30
de31fa9e6d update: dashboard 2026-01-25 14:36:07 +03:30
a705d0360b add: distribution species modal 2026-01-25 11:36:41 +03:30
f1ba276c6c version changed to 02.56 2026-01-25 09:25:35 +03:30
983a072487 update: dockerfile mirror 2026-01-25 09:25:29 +03:30
0c951f7b4c version changed to 02.55 2026-01-25 08:47:14 +03:30
4a719c9d1c add: tag dist time 2026-01-25 08:47:02 +03:30
21 changed files with 1993 additions and 174 deletions

View File

@@ -1,4 +1,4 @@
FROM node:18-alpine FROM registry.hamdocker.ir/seniorkian/node:18-alpine
WORKDIR /app WORKDIR /app

View File

@@ -6,12 +6,12 @@ import { OrganizationsTypes } from "../partials/management/OrganizationsTypes";
export default function Organizations() { export default function Organizations() {
const tabItems = [ const tabItems = [
{ label: "سازمان ها" },
{ {
label: "نهاد", label: "نهاد",
page: "organizations", page: "organizations",
access: "Show-Organization-Type", access: "Show-Organization-Type",
}, },
{ label: "سازمان ها" },
]; ];
const [selectedTab, setSelectedTab] = useState<number>(0); const [selectedTab, setSelectedTab] = useState<number>(0);
@@ -22,7 +22,7 @@ export default function Organizations() {
return ( return (
<Grid container column className="gap-2"> <Grid container column className="gap-2">
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" /> <Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
{selectedTab === 0 ? <OrganizationsList /> : <OrganizationsTypes />} {selectedTab === 0 ? <OrganizationsTypes /> : <OrganizationsList />}
</Grid> </Grid>
); );
} }

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

View File

@@ -10,24 +10,38 @@ import { SubmitNewTags } from "../partials/tagging/SubmitNewTags";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { TAGS } from "../routes/paths"; import { TAGS } from "../routes/paths";
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons"; 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() { export default function Tagging() {
const { openModal } = useModalStore(); const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 }); 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 navigate = useNavigate();
const { data: tagsData, refetch } = useApiRequest({ const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_batch/", api: `/tag/web/api/v1/tag_batch/?species_code=${
selectedSpecie.length ? selectedSpecie[0] : ""
}`,
method: "get", method: "get",
queryKey: ["tagsList", tableInfo], queryKey: ["tagsList", tableInfo, selectedSpecie],
params: { params: { ...tableInfo },
...tableInfo,
},
}); });
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({ 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", method: "get",
queryKey: ["tagDashboard"], queryKey: ["tagDashboard"],
}); });
@@ -48,16 +62,18 @@ export default function Tagging() {
item?.species_code === 1 item?.species_code === 1
? "گاو" ? "گاو"
: item?.species_code === 2 : item?.species_code === 2
? "گاومیش" ? "گاومیش"
: item?.species_code === 3 : item?.species_code === 3
? "شتر" ? "شتر"
: item?.species_code === 4 : item?.species_code === 4
? "گوسفند" ? "گوسفند"
: item?.species_code === 5 : item?.species_code === 5
? "بز" ? "بز"
: "نامشخص", : "نامشخص",
item?.serial_from || "-", item?.serial_from || "-",
item?.serial_to || "-", item?.serial_to || "-",
item?.total_distributed_tags || 0,
item?.total_remaining_tags || 0,
<Popover key={item.id}> <Popover key={item.id}>
<Tooltip title="مشاهده پلاک ها" position="right"> <Tooltip title="مشاهده پلاک ها" position="right">
<Button <Button
@@ -92,7 +108,6 @@ export default function Tagging() {
}} }}
/> />
</Tooltip> </Tooltip>
<DeleteButtonForPopOver <DeleteButtonForPopOver
page="tagging" page="tagging"
access="Delete-Tag" access="Delete-Tag"
@@ -106,10 +121,26 @@ export default function Tagging() {
} else { } else {
setTagsTableData([]); 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 ( return (
<Grid container column className="gap-4 mt-2"> <Grid container column className="gap-4 mt-2 rtl">
<Grid> <Grid>
<Button <Button
size="small" size="small"
@@ -134,30 +165,112 @@ export default function Tagging() {
noPagination noPagination
noSearch noSearch
columns={[ columns={[
"تعداد کل", "تعداد گروه پلاک",
"تعداد پلاک های آزاد", "پلاکهای تولیدشده",
"تعداد پلاک شده", "گروه پلاک های دارای توزیع",
"گاو", "پلاک توزیع شده",
"گاومیش", "پلاک باقی‌مانده",
"شتر", "جزئیات گونه ها",
"گوسفند",
"بز",
]} ]}
rows={[ rows={[
[ [
tagDashboardData?.count?.toLocaleString() || 0, tagDashboardData?.batch_count?.toLocaleString() || 0,
tagDashboardData?.free_count?.toLocaleString() || 0, tagDashboardData?.tag_count_created_by_batch?.toLocaleString() ||
tagDashboardData?.assign_count?.toLocaleString() || 0, 0,
tagDashboardData?.cow_count?.toLocaleString() || 0, tagDashboardData?.has_distributed_batches_number?.toLocaleString() ||
tagDashboardData?.buffalo_count?.toLocaleString() || 0, 0,
tagDashboardData?.camel_count?.toLocaleString() || 0, tagDashboardData?.total_distributed_tags?.toLocaleString() || 0,
tagDashboardData?.sheep_count?.toLocaleString() || 0, tagDashboardData?.total_remaining_tags?.toLocaleString() || 0,
tagDashboardData?.goat_count?.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>
<Grid container className="items-center gap-2">
<Grid>
{speciesOptions() && (
<AutoComplete
data={speciesOptions()}
selectedKeys={selectedSpecie}
onChange={setSelectedSpecie}
title="گونه"
/>
)}
</Grid>
</Grid>
<Table <Table
className="mt-2" className="mt-2"
onChange={setTableInfo} onChange={setTableInfo}
@@ -167,9 +280,11 @@ export default function Tagging() {
columns={[ columns={[
"ردیف", "ردیف",
"سازمان ثبت کننده", "سازمان ثبت کننده",
"کد گونه", "گونه",
"از بازه سریال", "از بازه سریال",
"تا بازه سریال", "تا بازه سریال",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"عملیات", "عملیات",
]} ]}
rows={tagsTableData} rows={tagsTableData}
@@ -177,3 +292,78 @@ export default function Tagging() {
</Grid> </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>
);
}

View File

@@ -9,23 +9,44 @@ import { Tooltip } from "../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons"; import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
import { TagDetails } from "../partials/tagging/TagDetails"; import { TagDetails } from "../partials/tagging/TagDetails";
import { useParams } from "@tanstack/react-router"; 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() { export default function Tags() {
const { openModal } = useModalStore(); const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 }); const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]); const [tagsTableData, setTagsTableData] = useState([]);
const { id, from, to } = useParams({ strict: false }); const { id, from, to } = useParams({ strict: false });
const [selectedStatus, setSelectedStatus] = useState<(string | number)[]>([]);
const { data: tagsData, refetch } = useApiRequest({ const { data: tagsData, refetch } = useApiRequest({
api: `/tag/web/api/v1/tag/${id ? id + "/tags_by_batch" : ""}`, api: `/tag/web/api/v1/tag/${id ? id + "/tags_by_batch" : ""}`,
method: "get", method: "get",
queryKey: ["tagsList", tableInfo], queryKey: ["tagsList", tableInfo, selectedStatus],
params: { params: {
...tableInfo, ...tableInfo,
status: selectedStatus.length ? selectedStatus[0] : undefined,
}, },
}); });
const { data: tagDashboardData } = useApiRequest({ const { data: tagDashboardData } = useApiRequest({
api: "/tag/web/api/v1/tag/tag_dashboard/", api: id
? `/tag/web/api/v1/tag_batch/${id}/inner_dashboard`
: "/tag/web/api/v1/tag/tag_dashboard/",
method: "get", method: "get",
queryKey: ["tagDashboard"], queryKey: ["tagDashboard"],
}); });
@@ -89,35 +110,150 @@ export default function Tags() {
return ( return (
<Grid container column className="gap-4 mt-2"> <Grid container column className="gap-4 mt-2">
<Grid isDashboard> {tagDashboardData && (
<Table <Grid isDashboard>
isDashboard <Table
title="خلاصه اطلاعات" isDashboard
noPagination title="خلاصه اطلاعات"
noSearch noPagination
columns={[ noSearch
"تعداد کل", columns={
"تعداد پلاک های آزاد", id
"تعداد پلاک شده", ? [
"گاو", "تعداد پلاک",
"گاومیش", "پلاک های توزیع شده",
"شتر", "تعداد پلاک های گروه پلاک",
"گوسفند", "تعداد پلاک های توزیع شده",
"بز", "تعداد پلاک های باقیمانده",
]} "آمار گونه ای",
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, "بز",
], ]
]} }
/> 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> </Grid>
<Table <Table
@@ -140,3 +276,78 @@ export default function Tags() {
</Grid> </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>
);
}

View File

@@ -196,11 +196,11 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
if (window.visualViewport) { if (window.visualViewport) {
window.visualViewport.addEventListener( window.visualViewport.addEventListener(
"resize", "resize",
handleVisualViewportResize handleVisualViewportResize,
); );
window.visualViewport.addEventListener( window.visualViewport.addEventListener(
"scroll", "scroll",
handleVisualViewportScroll handleVisualViewportScroll,
); );
} }
const inputElement = inputRef.current; const inputElement = inputRef.current;
@@ -222,11 +222,11 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
if (window.visualViewport) { if (window.visualViewport) {
window.visualViewport.removeEventListener( window.visualViewport.removeEventListener(
"resize", "resize",
handleVisualViewportResize handleVisualViewportResize,
); );
window.visualViewport.removeEventListener( window.visualViewport.removeEventListener(
"scroll", "scroll",
handleVisualViewportScroll handleVisualViewportScroll,
); );
} }
const inputElement = inputRef.current; const inputElement = inputRef.current;
@@ -247,7 +247,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
target.closest(".select-group") && !clickedInsideCurrent; target.closest(".select-group") && !clickedInsideCurrent;
const clickedOnPortalDropdown = target.closest( const clickedOnPortalDropdown = target.closest(
`[data-autocomplete-portal="${uniqueId}"]` `[data-autocomplete-portal="${uniqueId}"]`,
); );
if (clickedOnAnotherAutocomplete) { if (clickedOnAnotherAutocomplete) {
@@ -318,7 +318,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
const preventTouchMove = (e: TouchEvent) => { const preventTouchMove = (e: TouchEvent) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
const dropdown = document.querySelector( const dropdown = document.querySelector(
`[data-autocomplete-portal="${uniqueId}"]` `[data-autocomplete-portal="${uniqueId}"]`,
); );
if (dropdown) { if (dropdown) {
@@ -326,7 +326,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
if (touch) { if (touch) {
const elementAtPoint = document.elementFromPoint( const elementAtPoint = document.elementFromPoint(
touch.clientX, touch.clientX,
touch.clientY touch.clientY,
); );
if ( if (
elementAtPoint && elementAtPoint &&
@@ -375,12 +375,12 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
setLocalInputValue(value); setLocalInputValue(value);
setIsTyping(true); setIsTyping(true);
const filtered = data.filter((item) => const filtered = data.filter((item) =>
item.value.toLowerCase().includes(value.toLowerCase()) item.value.toLowerCase().includes(value.toLowerCase()),
); );
setFilteredData(filtered); setFilteredData(filtered);
setShowOptions(true); setShowOptions(true);
}, },
[data] [data],
); );
const handleChange = useCallback( const handleChange = useCallback(
@@ -390,7 +390,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
if (onChangeValue && newSelectedKeys.length > 0) { if (onChangeValue && newSelectedKeys.length > 0) {
const selectedItem = data.find( const selectedItem = data.find(
(item) => item.key === newSelectedKeys[0] (item) => item.key === newSelectedKeys[0],
); );
if (selectedItem) { if (selectedItem) {
onChangeValue({ onChangeValue({
@@ -400,7 +400,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
} }
} }
}, },
[onChange, onChangeValue, data] [onChange, onChangeValue, data],
); );
const handleOptionClick = useCallback( const handleOptionClick = useCallback(
@@ -430,7 +430,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
setShowOptions(false); setShowOptions(false);
} }
}, },
[multiselect, handleChange] [multiselect, handleChange],
); );
const handleInputClick = useCallback(() => { const handleInputClick = useCallback(() => {
@@ -507,8 +507,8 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
item.originalGroupKey !== undefined item.originalGroupKey !== undefined
? item.originalGroupKey ? item.originalGroupKey
: String(item.key).startsWith("__group__") : String(item.key).startsWith("__group__")
? String(item.key).slice(11) ? String(item.key).slice(11)
: item.key; : item.key;
onGroupHeaderClick(groupKey); onGroupHeaderClick(groupKey);
} else if (!item.disabled) { } else if (!item.disabled) {
handleOptionClick(item.key); handleOptionClick(item.key);
@@ -523,8 +523,8 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
isGroupHeader && onGroupHeaderClick isGroupHeader && onGroupHeaderClick
? "cursor-pointer opacity-55 hover:bg-gray-100 text-dark-800 dark:text-dark-100 dark:hover:bg-primary-900/90 font-semibold bg-gray-200 dark:bg-primary-900" ? "cursor-pointer opacity-55 hover:bg-gray-100 text-dark-800 dark:text-dark-100 dark:hover:bg-primary-900/90 font-semibold bg-gray-200 dark:bg-primary-900"
: item.disabled : item.disabled
? "text-gray-400 dark:text-dark-500 cursor-not-allowed" ? "text-gray-400 dark:text-dark-500 cursor-not-allowed"
: "cursor-pointer hover:bg-primary-100 text-dark-800 dark:text-dark-100 dark:hover:bg-dark-700" : "cursor-pointer hover:bg-primary-100 text-dark-800 dark:text-dark-100 dark:hover:bg-dark-700"
} }
${ ${
isSelected && !isGroupHeader isSelected && !isGroupHeader
@@ -602,7 +602,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
> >
{dropdownOptions} {dropdownOptions}
</motion.ul>, </motion.ul>,
document.body document.body,
); );
}, [ }, [
showOptions, showOptions,
@@ -638,6 +638,7 @@ const AutoComplete: React.FC<AutoCompleteProps> = ({
placeholder={title || "انتخاب کنید..."} placeholder={title || "انتخاب کنید..."}
/> />
<ChevronDownIcon <ChevronDownIcon
onClick={handleInputClick}
className={`absolute left-3 text-dark-400 dark:text-dark-100 transition-transform duration-200 ${ className={`absolute left-3 text-dark-400 dark:text-dark-100 transition-transform duration-200 ${
showOptions ? "transform rotate-180" : "" showOptions ? "transform rotate-180" : ""
} ${getSizeStyles(size).icon}`} } ${getSizeStyles(size).icon}`}

View File

@@ -7,6 +7,7 @@ import React, {
} from "react"; } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { import {
ArrowUpCircleIcon,
ChartBarIcon, ChartBarIcon,
DocumentChartBarIcon, DocumentChartBarIcon,
EyeIcon, EyeIcon,
@@ -56,7 +57,8 @@ type ButtonProps = {
| "view" | "view"
| "info" | "info"
| "chart" | "chart"
| "set"; | "set"
| "share";
page?: string; page?: string;
access?: string; access?: string;
height?: string | number; height?: string | number;
@@ -161,6 +163,10 @@ const Button: React.FC<ButtonProps> = ({
return ( return (
<WrenchIcon className="w-5 h-5 text-purple-400 dark:text-purple-100" /> <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: default:
return null; return null;
} }
@@ -181,7 +187,7 @@ const Button: React.FC<ButtonProps> = ({
return true; return true;
} else { } else {
const finded = profile?.permissions?.find( const finded = profile?.permissions?.find(
(item: any) => item.page_name === page (item: any) => item.page_name === page,
); );
if (finded && finded.page_access.includes(access)) { if (finded && finded.page_access.includes(access)) {
return true; return true;
@@ -237,7 +243,7 @@ const Button: React.FC<ButtonProps> = ({
sizeStyles.padding, sizeStyles.padding,
sizeStyles.text, sizeStyles.text,
className, className,
checkIsMobile() && !icon && !variant && children && mobileBorders checkIsMobile() && !icon && !variant && children && mobileBorders,
)} )}
style={{ height }} style={{ height }}
> >
@@ -256,7 +262,7 @@ const Button: React.FC<ButtonProps> = ({
.then((response) => { .then((response) => {
closeBackdrop(); closeBackdrop();
const url = window.URL.createObjectURL( const url = window.URL.createObjectURL(
new Blob([response.data]) new Blob([response.data]),
); );
const link = document.createElement("a"); const link = document.createElement("a");

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

View File

@@ -7,6 +7,7 @@ import { getNestedValue } from "../../utils/getNestedValue";
type FormEnterLocationsProps = { type FormEnterLocationsProps = {
title: string; title: string;
api: string; api: string;
size?: "small" | "medium" | "large";
error?: boolean; error?: boolean;
errorMessage?: any; errorMessage?: any;
multiple?: boolean; multiple?: boolean;
@@ -15,6 +16,7 @@ type FormEnterLocationsProps = {
keyField?: string; keyField?: string;
secondaryKey?: string | string[]; secondaryKey?: string | string[];
tertiaryKey?: string | string[]; tertiaryKey?: string | string[];
quaternaryKey?: string | string[];
valueField?: string | string[]; valueField?: string | string[];
valueField2?: string | string[]; valueField2?: string | string[];
valueField3?: string | string[]; valueField3?: string | string[];
@@ -31,6 +33,7 @@ type FormEnterLocationsProps = {
export const FormApiBasedAutoComplete = ({ export const FormApiBasedAutoComplete = ({
title, title,
api, api,
size,
error, error,
errorMessage, errorMessage,
onChange, onChange,
@@ -38,6 +41,7 @@ export const FormApiBasedAutoComplete = ({
keyField = "id", keyField = "id",
secondaryKey, secondaryKey,
tertiaryKey, tertiaryKey,
quaternaryKey,
valueField = "name", valueField = "name",
valueField2, valueField2,
valueField3, valueField3,
@@ -94,7 +98,7 @@ export const FormApiBasedAutoComplete = ({
if (filterAddress && filterValue) { if (filterAddress && filterValue) {
data = apiData.results?.filter( data = apiData.results?.filter(
(item: any) => (item: any) =>
!filterValue.includes(getNestedValue(item, filterAddress)) !filterValue.includes(getNestedValue(item, filterAddress)),
); );
} else { } else {
data = apiData.results; data = apiData.results;
@@ -111,6 +115,11 @@ export const FormApiBasedAutoComplete = ({
? option[tertiaryKey] ? option[tertiaryKey]
: getNestedValue(option, tertiaryKey) : getNestedValue(option, tertiaryKey)
: undefined, : undefined,
quaternaryKey: quaternaryKey
? typeof quaternaryKey === "string"
? option[quaternaryKey]
: getNestedValue(option, quaternaryKey)
: undefined,
value: valueTemplate value: valueTemplate
? valueTemplate ? valueTemplate
.replace( .replace(
@@ -119,10 +128,10 @@ export const FormApiBasedAutoComplete = ({
valueField === "page" valueField === "page"
? getFaPermissions(option[valueField]) ? getFaPermissions(option[valueField])
: typeof valueField === "string" : typeof valueField === "string"
? option[valueField] ? option[valueField]
: getNestedValue(option, valueField), : getNestedValue(option, valueField),
"v1" "v1",
) ),
) )
.replace( .replace(
/v2/g, /v2/g,
@@ -132,8 +141,8 @@ export const FormApiBasedAutoComplete = ({
? option[valueField2] ? option[valueField2]
: getNestedValue(option, valueField2) : getNestedValue(option, valueField2)
: "", : "",
"v2" "v2",
) ),
) )
.replace( .replace(
/v3/g, /v3/g,
@@ -143,15 +152,15 @@ export const FormApiBasedAutoComplete = ({
? option[valueField3] ? option[valueField3]
: getNestedValue(option, valueField3) : getNestedValue(option, valueField3)
: "", : "",
"v3" "v3",
) ),
) )
: `${ : `${
valueField === "page" valueField === "page"
? getFaPermissions(option[valueField]) ? getFaPermissions(option[valueField])
: typeof valueField === "string" : typeof valueField === "string"
? option[valueField] ? option[valueField]
: getNestedValue(option, valueField) : getNestedValue(option, valueField)
} ${ } ${
valueField2 valueField2
? " - " + ? " - " +
@@ -208,7 +217,7 @@ export const FormApiBasedAutoComplete = ({
setData(finalData); setData(finalData);
const actualDataItems = finalData.filter( const actualDataItems = finalData.filter(
(item: any) => !item.isGroupHeader && !item.disabled (item: any) => !item.isGroupHeader && !item.disabled,
); );
if (defaultKey !== undefined && defaultKey !== null) { if (defaultKey !== undefined && defaultKey !== null) {
@@ -218,10 +227,10 @@ export const FormApiBasedAutoComplete = ({
setSelectedKeys([]); setSelectedKeys([]);
} else { } else {
const defaultIds = defaultKey.map((item: any) => const defaultIds = defaultKey.map((item: any) =>
typeof item === "object" ? item[keyField] : item typeof item === "object" ? item[keyField] : item,
); );
const defaultItems = actualDataItems.filter((item: any) => const defaultItems = actualDataItems.filter((item: any) =>
defaultIds.includes(item.key) defaultIds.includes(item.key),
); );
setSelectedKeys(defaultItems.map((item: any) => item.key)); setSelectedKeys(defaultItems.map((item: any) => item.key));
if (onChange) { if (onChange) {
@@ -237,12 +246,17 @@ export const FormApiBasedAutoComplete = ({
key3: item?.tertiaryKey, key3: item?.tertiaryKey,
} }
: {}), : {}),
...(quaternaryKey
? {
key4: item?.quaternaryKey,
}
: {}),
}; };
}) }),
); );
if (onChangeValue) { if (onChangeValue) {
onChangeValue( onChangeValue(
defaultItems.map((item: any) => item.value.trim()) defaultItems.map((item: any) => item.value.trim()),
); );
} }
} else { } else {
@@ -258,7 +272,7 @@ export const FormApiBasedAutoComplete = ({
? defaultKey[keyField] ? defaultKey[keyField]
: defaultKey; : defaultKey;
const defaultItem = actualDataItems.find( const defaultItem = actualDataItems.find(
(item: any) => item.key === keyToFind (item: any) => item.key === keyToFind,
); );
if (defaultItem) { if (defaultItem) {
setSelectedKeys([keyToFind]); setSelectedKeys([keyToFind]);
@@ -268,6 +282,9 @@ export const FormApiBasedAutoComplete = ({
key1: defaultItem.key, key1: defaultItem.key,
key2: defaultItem.secondaryKey, key2: defaultItem.secondaryKey,
...(tertiaryKey ? { key3: defaultItem.tertiaryKey } : {}), ...(tertiaryKey ? { key3: defaultItem.tertiaryKey } : {}),
...(quaternaryKey
? { key4: defaultItem.quaternaryKey }
: {}),
}); });
} else { } else {
onChange(keyToFind); onChange(keyToFind);
@@ -278,6 +295,7 @@ export const FormApiBasedAutoComplete = ({
key1: defaultItem.key, key1: defaultItem.key,
key2: defaultItem.secondaryKey, key2: defaultItem.secondaryKey,
...(tertiaryKey ? { key3: defaultItem.tertiaryKey } : {}), ...(tertiaryKey ? { key3: defaultItem.tertiaryKey } : {}),
...(quaternaryKey ? { key4: defaultItem.quaternaryKey } : {}),
value: defaultItem.value.trim(), value: defaultItem.value.trim(),
}); });
} }
@@ -296,18 +314,18 @@ export const FormApiBasedAutoComplete = ({
const groupItemKeys = groupItems.map((item: any) => item.key); const groupItemKeys = groupItems.map((item: any) => item.key);
const allGroupItemsSelected = groupItemKeys.every((key) => const allGroupItemsSelected = groupItemKeys.every((key) =>
selectedKeys.includes(key) selectedKeys.includes(key),
); );
let newSelectedKeys: (string | number)[]; let newSelectedKeys: (string | number)[];
if (allGroupItemsSelected) { if (allGroupItemsSelected) {
newSelectedKeys = selectedKeys.filter( newSelectedKeys = selectedKeys.filter(
(key) => !groupItemKeys.includes(key) (key) => !groupItemKeys.includes(key),
); );
} else { } else {
const newKeys = groupItemKeys.filter( const newKeys = groupItemKeys.filter(
(key) => !selectedKeys.includes(key) (key) => !selectedKeys.includes(key),
); );
newSelectedKeys = [...selectedKeys, ...newKeys]; newSelectedKeys = [...selectedKeys, ...newKeys];
} }
@@ -320,7 +338,7 @@ export const FormApiBasedAutoComplete = ({
(item: any) => (item: any) =>
newSelectedKeys.includes(item.key) && newSelectedKeys.includes(item.key) &&
!item.isGroupHeader && !item.isGroupHeader &&
!item.disabled !item.disabled,
); );
if (secondaryKey) { if (secondaryKey) {
@@ -329,7 +347,8 @@ export const FormApiBasedAutoComplete = ({
key1: item.key, key1: item.key,
key2: item.secondaryKey, key2: item.secondaryKey,
...(tertiaryKey ? { key3: item.tertiaryKey } : {}), ...(tertiaryKey ? { key3: item.tertiaryKey } : {}),
})) ...(quaternaryKey ? { key4: item.quaternaryKey } : {}),
})),
); );
if (onChangeValue) { if (onChangeValue) {
onChangeValue(selectedItems.map((item: any) => item.value.trim())); onChangeValue(selectedItems.map((item: any) => item.value.trim()));
@@ -344,6 +363,7 @@ export const FormApiBasedAutoComplete = ({
<AutoComplete <AutoComplete
multiselect={multiple} multiselect={multiple}
selectField={selectField} selectField={selectField}
size={size}
data={data} data={data}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
onChange={(newSelectedKeys) => { onChange={(newSelectedKeys) => {
@@ -357,7 +377,7 @@ export const FormApiBasedAutoComplete = ({
(item: any) => (item: any) =>
newSelectedKeys.includes(item.key) && newSelectedKeys.includes(item.key) &&
!item.isGroupHeader && !item.isGroupHeader &&
!item.disabled !item.disabled,
); );
onChange( onChange(
@@ -365,16 +385,17 @@ export const FormApiBasedAutoComplete = ({
key1: item.key, key1: item.key,
key2: item.secondaryKey, key2: item.secondaryKey,
...(tertiaryKey ? { key3: item.tertiaryKey } : {}), ...(tertiaryKey ? { key3: item.tertiaryKey } : {}),
})) ...(quaternaryKey ? { key4: item.quaternaryKey } : {}),
})),
); );
if (onChangeValue) { if (onChangeValue) {
onChangeValue( onChangeValue(
selectedItems.map((item: any) => item.value.trim()) selectedItems.map((item: any) => item.value.trim()),
); );
} }
} else { } else {
const validKeys = newSelectedKeys.filter( const validKeys = newSelectedKeys.filter(
(key) => !String(key).startsWith("__group__") (key) => !String(key).startsWith("__group__"),
); );
onChange(validKeys); onChange(validKeys);
} }
@@ -384,7 +405,7 @@ export const FormApiBasedAutoComplete = ({
(item: any) => (item: any) =>
item.key === newSelectedKeys[0] && item.key === newSelectedKeys[0] &&
!item.isGroupHeader && !item.isGroupHeader &&
!item.disabled !item.disabled,
); );
if (onChangeValue) { if (onChangeValue) {
onChangeValue({ onChangeValue({
@@ -394,6 +415,9 @@ export const FormApiBasedAutoComplete = ({
...(tertiaryKey ...(tertiaryKey
? { key3: selectedItem?.tertiaryKey ?? "" } ? { key3: selectedItem?.tertiaryKey ?? "" }
: {}), : {}),
...(quaternaryKey
? { key4: selectedItem?.quaternaryKey ?? "" }
: {}),
}); });
} }
@@ -402,6 +426,9 @@ export const FormApiBasedAutoComplete = ({
key1: selectedItem.key, key1: selectedItem.key,
key2: selectedItem.secondaryKey, key2: selectedItem.secondaryKey,
...(tertiaryKey ? { key3: selectedItem.tertiaryKey } : {}), ...(tertiaryKey ? { key3: selectedItem.tertiaryKey } : {}),
...(quaternaryKey
? { key4: selectedItem.quaternaryKey }
: {}),
}); });
} }
} else { } else {

View File

@@ -8,7 +8,6 @@ import {
zValidateNumber, zValidateNumber,
zValidateNumberOptional, zValidateNumberOptional,
zValidateString, zValidateString,
zValidateStringOptional,
} from "../../data/getFormTypeErrors"; } from "../../data/getFormTypeErrors";
import { z } from "zod"; import { z } from "zod";
import { useApiMutation } from "../../utils/useApiRequest"; import { useApiMutation } from "../../utils/useApiRequest";
@@ -21,16 +20,23 @@ import { getToastResponse } from "../../data/getToastResponse";
import { useUserProfileStore } from "../../context/zustand-store/userStore"; import { useUserProfileStore } from "../../context/zustand-store/userStore";
import { useState } from "react"; import { useState } from "react";
import Checkbox from "../../components/CheckBox/CheckBox"; import Checkbox from "../../components/CheckBox/CheckBox";
import {
ArrowPathIcon,
CheckBadgeIcon,
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import axios from "axios";
const schema = z.object({ const schema = z.object({
name: zValidateString("نام سازمان"), name: zValidateString("نام سازمان"),
national_unique_id: zValidateString("شناسه کشوری"), national_unique_id: zValidateString("شناسه کشوری"),
address: zValidateStringOptional("آدرس"),
field_of_activity: zValidateAutoComplete("حوزه فعالیت"), field_of_activity: zValidateAutoComplete("حوزه فعالیت"),
province: zValidateNumber("استان"), province: zValidateNumber("استان"),
city: zValidateNumber("شهر"), city: zValidateNumber("شهر"),
organization: zValidateNumberOptional("سازمان"), organization: zValidateNumberOptional("سازمان"),
organizationType: zValidateNumber("سازمان"), organizationType: zValidateNumber("سازمان"),
unique_unit_identity: zValidateNumberOptional("شناسه یکتا واحد"),
is_repeatable: z.boolean(), is_repeatable: z.boolean(),
free_visibility_by_scope: z.boolean(), free_visibility_by_scope: z.boolean(),
}); });
@@ -75,8 +81,8 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
name: item?.name || "", name: item?.name || "",
address: item?.address || "",
national_unique_id: item?.national_unique_id || "", national_unique_id: item?.national_unique_id || "",
unique_unit_identity: item?.unique_unit_identity || "",
free_visibility_by_scope: item?.free_visibility_by_scope || false, free_visibility_by_scope: item?.free_visibility_by_scope || false,
field_of_activity: field_of_activity:
item && item?.field_of_activity !== "EM" item && item?.field_of_activity !== "EM"
@@ -95,22 +101,94 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
city: string | any; city: string | any;
}>({ province: "", city: "" }); }>({ 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 [isInquiryRequired, setIsInquiryRequired] = useState(false);
const [inquiryPassed, setInquiryPassed] = useState(false);
const [inquiryLoading, setInquiryLoading] = useState(false);
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 handleInquiry = async () => {
const code = getValues("unique_unit_identity");
if (!code) {
showToast("لطفاً شناسه یکتا واحد را وارد کنید!", "error");
return;
}
setInquiryLoading(true);
try {
await axios.get(
`https://rsibackend.rasadyar.com/app/has_code_in_db/?code=${code}`,
);
setInquiryPassed(true);
showToast("استعلام با موفقیت انجام شد!", "success");
} catch (error: any) {
if (error?.response?.status === 404) {
setInquiryPassed(false);
showToast("شناسه موجود نیست!", "error");
} else {
showToast("خطا در استعلام!", "error");
}
} finally {
setInquiryLoading(false);
}
};
const onSubmit = async (data: FormValues) => { const onSubmit = async (data: FormValues) => {
if (isInquiryRequired && !data.unique_unit_identity) {
showToast("شناسه یکتا واحد الزامی است!", "error");
return;
}
if (isInquiryRequired && !inquiryPassed) {
showToast("لطفاً ابتدا استعلام شناسه یکتا واحد را انجام دهید!", "error");
return;
}
try { try {
await mutation.mutateAsync({ await mutation.mutateAsync({
addresses: addresses.filter(
(a) => a.postal_code.trim() || a.address.trim(),
),
organization: { organization: {
name: `${data?.name} ${ name: `${data?.name} ${
data?.is_repeatable data?.is_repeatable
? "" ? ""
: data.field_of_activity[0] === "CI" : data.field_of_activity[0] === "CI"
? LocationValues.city ? LocationValues.city
: LocationValues.province : LocationValues.province
}`, }`,
...(data.organizationType !== undefined && { ...(data.organizationType !== undefined && {
type: data.organizationType, type: data.organizationType,
}), }),
national_unique_id: data?.national_unique_id, national_unique_id: data?.national_unique_id,
...(data?.unique_unit_identity && {
unique_unit_identity: data.unique_unit_identity,
}),
province: data?.province, province: data?.province,
city: data?.city, city: data?.city,
...(data.organization && { ...(data.organization && {
@@ -118,7 +196,6 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
}), }),
field_of_activity: data.field_of_activity[0], field_of_activity: data.field_of_activity[0],
free_visibility_by_scope: data.free_visibility_by_scope, free_visibility_by_scope: data.free_visibility_by_scope,
address: data.address,
}, },
}); });
showToast(getToastResponse(item, "سازمان"), "success"); showToast(getToastResponse(item, "سازمان"), "success");
@@ -128,12 +205,12 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
if (error?.status === 403) { if (error?.status === 403) {
showToast( showToast(
error?.response?.data?.message || "این سازمان تکراری است!", error?.response?.data?.message || "این سازمان تکراری است!",
"error" "error",
); );
} else { } else {
showToast( showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!", error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error" "error",
); );
} }
} }
@@ -153,6 +230,7 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
keyField="id" keyField="id"
secondaryKey="is_repeatable" secondaryKey="is_repeatable"
tertiaryKey="org_type_field" tertiaryKey="org_type_field"
quaternaryKey="key"
valueField="name" valueField="name"
error={!!errors.organizationType} error={!!errors.organizationType}
errorMessage={errors.organizationType?.message} errorMessage={errors.organizationType?.message}
@@ -163,6 +241,12 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
trigger(["organizationType"]); trigger(["organizationType"]);
}} }}
onChangeValue={(r) => { onChangeValue={(r) => {
if (r.key4 === "U" || r.key4 === "CO") {
setIsInquiryRequired(true);
} else {
setIsInquiryRequired(false);
setInquiryPassed(false);
}
if (!r.key2) { if (!r.key2) {
setValue("name", r.value); setValue("name", r.value);
} else { } else {
@@ -223,6 +307,62 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
)} )}
/> />
<Controller
name="unique_unit_identity"
control={control}
render={({ field }) => (
<div className="flex items-start gap-2 w-full">
<Textfield
fullWidth
placeholder={
isInquiryRequired
? "شناسه یکتا واحد"
: "شناسه یکتا واحد (اختیاری)"
}
value={field.value}
onChange={(e) => {
field.onChange(e);
setInquiryPassed(false);
}}
error={
!!errors.unique_unit_identity ||
(isInquiryRequired && !inquiryPassed && !!field.value)
}
helperText={
errors.unique_unit_identity?.message ||
(isInquiryRequired && inquiryPassed
? "استعلام تایید شده"
: undefined)
}
/>
{isInquiryRequired && (
<button
type="button"
onClick={handleInquiry}
disabled={inquiryLoading || !field.value}
className={`shrink-0 flex items-center gap-1 mt-[2px] px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
inquiryPassed
? "bg-green-500 text-white hover:bg-green-600"
: "bg-blue-500 text-white hover:bg-blue-600"
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
{inquiryLoading
? "..."
: inquiryPassed
? "تایید شده"
: "استعلام"}
{inquiryPassed && !inquiryLoading ? (
<CheckBadgeIcon className="w-4 h-4 text-white" />
) : (
<ArrowPathIcon className="w-4 h-4 text-white" />
)}
</button>
)}
</div>
)}
/>
<Controller <Controller
name="province" name="province"
control={control} control={control}
@@ -258,7 +398,7 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
defaultKey={item?.parent_organization?.id} defaultKey={item?.parent_organization?.id}
title="سازمان والد (اختیاری)" title="سازمان والد (اختیاری)"
api={`auth/api/v1/organization/organizations_by_province?province=${getValues( api={`auth/api/v1/organization/organizations_by_province?province=${getValues(
"province" "province",
)}`} )}`}
keyField="id" keyField="id"
valueField="name" valueField="name"
@@ -273,20 +413,53 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
)} )}
/> />
<Controller <div className="flex flex-col gap-2 w-full">
name="address" <div className="flex items-center justify-between">
control={control} <span className="text-sm font-medium text-gray-700">آدرسها</span>
render={({ field }) => ( <button
<Textfield type="button"
fullWidth onClick={handleAddAddress}
placeholder="آدرس (اختیاری)" className="flex items-center gap-1 text-sm text-blue-500 dark:text-blue-300 hover:text-blue-800 transition-colors"
value={field.value} >
onChange={field.onChange} <PlusIcon className="w-4 h-4" />
error={!!errors.address} افزودن آدرس
helperText={errors.address?.message} </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 <Controller
name="free_visibility_by_scope" name="free_visibility_by_scope"

View File

@@ -3,6 +3,7 @@ import { Grid } from "../../components/Grid/Grid";
import Button from "../../components/Button/Button"; import Button from "../../components/Button/Button";
import { AddOrganization } from "./AddOrganization"; import { AddOrganization } from "./AddOrganization";
import AutoComplete from "../../components/AutoComplete/AutoComplete"; import AutoComplete from "../../components/AutoComplete/AutoComplete";
import { FormApiBasedAutoComplete } from "../../components/FormItems/FormApiBasedAutoComplete";
import Table from "../../components/Table/Table"; import Table from "../../components/Table/Table";
import { useModalStore } from "../../context/zustand-store/appStore"; import { useModalStore } from "../../context/zustand-store/appStore";
import { useApiRequest } from "../../utils/useApiRequest"; import { useApiRequest } from "../../utils/useApiRequest";
@@ -19,6 +20,9 @@ export const OrganizationsList = () => {
const [selectedProvinceKeys, setSelectedProvinceKeys] = useState< const [selectedProvinceKeys, setSelectedProvinceKeys] = useState<
(string | number)[] (string | number)[]
>([]); >([]);
const [selectedOrganizationType, setSelectedOrganizationType] = useState<
string | number
>("");
const [params, setParams] = useState({ page: 1, page_size: 10 }); const [params, setParams] = useState({ page: 1, page_size: 10 });
const [tableData, setTableData] = useState([]); const [tableData, setTableData] = useState([]);
const { profile } = useUserProfileStore(); const { profile } = useUserProfileStore();
@@ -32,11 +36,16 @@ export const OrganizationsList = () => {
const { data: apiData, refetch } = useApiRequest({ const { data: apiData, refetch } = useApiRequest({
api: selectedProvinceKeys?.length api: selectedProvinceKeys?.length
? `/auth/api/v1/organization/organizations_by_province?province=${selectedProvinceKeys[0]}` ? `/auth/api/v1/organization/organizations_by_province?province=${selectedProvinceKeys[0]}${selectedOrganizationType ? `&org_type=${selectedOrganizationType}` : ""}`
: "/auth/api/v1/organization/", : `/auth/api/v1/organization/${selectedOrganizationType ? `?org_type=${selectedOrganizationType}` : ""}`,
method: "get", method: "get",
params: params, params: params,
queryKey: ["organizations", params, selectedProvinceKeys], queryKey: [
"organizations",
params,
selectedProvinceKeys,
selectedOrganizationType,
],
}); });
useEffect(() => { useEffect(() => {
@@ -50,16 +59,24 @@ export const OrganizationsList = () => {
`${item?.type?.name}`, `${item?.type?.name}`,
item?.parent_organization?.name, item?.parent_organization?.name,
item?.national_unique_id, item?.national_unique_id,
item?.unique_unit_identity || "-",
item?.field_of_activity === "CO" item?.field_of_activity === "CO"
? "کشور" ? "کشور"
: item?.field_of_activity === "PR" : item?.field_of_activity === "PR"
? "استان" ? "استان"
: item?.field_of_activity === "CI" : item?.field_of_activity === "CI"
? "شهرستان" ? "شهرستان"
: "نامشخص", : "نامشخص",
item?.province?.name, item?.province?.name,
item?.city?.name, item?.city?.name,
item?.address || "-", <ShowMoreInfo
key={`address-${i}`}
title="آدرس‌ها"
disabled={!item?.addresses?.length}
data={item?.addresses}
columns={["کد پستی", "آدرس"]}
accessKeys={[["postal_code"], ["address"]]}
/>,
<ShowMoreInfo <ShowMoreInfo
key={i} key={i}
title="اطلاعات حساب" title="اطلاعات حساب"
@@ -151,6 +168,16 @@ export const OrganizationsList = () => {
ایجاد سازمان ایجاد سازمان
</Button> </Button>
</Grid> </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" && ( {profile?.organization?.type?.org_type_field === "CO" && (
<Grid> <Grid>
<AutoComplete <AutoComplete
@@ -177,6 +204,7 @@ export const OrganizationsList = () => {
"نهاد", "نهاد",
"سازمان والد", "سازمان والد",
"شناسه کشوری", "شناسه کشوری",
"شناسه یکتا واحد",
"حوزه فعالیت", "حوزه فعالیت",
"استان", "استان",
"شهر", "شهر",

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

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

View File

@@ -42,7 +42,7 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
? item?.distribution_type === "random" ? item?.distribution_type === "random"
? "random" ? "random"
: "group" : "group"
: "group" : "group",
); );
const [batches, setBatches] = useState<BatchItem[]>([]); const [batches, setBatches] = useState<BatchItem[]>([]);
@@ -116,25 +116,25 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
showToast( showToast(
isEdit ? "ویرایش با موفقیت انجام شد" : "ثبت با موفقیت انجام شد", isEdit ? "ویرایش با موفقیت انجام شد" : "ثبت با موفقیت انجام شد",
"success" "success",
); );
getData(); getData();
closeModal(); closeModal();
} catch (error: any) { } catch (error: any) {
showToast( showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!", error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error" "error",
); );
} }
}; };
const speciesOptions = () => { const speciesOptions = () => {
return speciesData?.results?.map((opt: any) => { return (
return { speciesData?.results?.map((opt: any) => ({
key: opt?.value, key: opt?.value,
value: opt?.name, value: opt?.name,
}; })) ?? []
}); );
}; };
return ( return (
@@ -195,14 +195,14 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
items?.map((r: any) => { items?.map((r: any) => {
const existing = batches.find( const existing = batches.find(
(b) => (b) =>
b.batch_identity === r.key1 && b.species_code === r.key2 b.batch_identity === r.key1 && b.species_code === r.key2,
); );
return { return {
batch_identity: r.key1, batch_identity: r.key1,
species_code: r.key2, species_code: r.key2,
count: existing?.count ?? "", count: existing?.count ?? "",
}; };
}) || [] }) || [],
); );
}} }}
onChangeValue={(labels) => { onChangeValue={(labels) => {
@@ -210,7 +210,7 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
prev.map((item, index) => ({ prev.map((item, index) => ({
...item, ...item,
label: labels[index], label: labels[index],
})) })),
); );
}} }}
/> />
@@ -229,7 +229,7 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
species_code: k as number, species_code: k as number,
count: prev?.count ?? "", count: prev?.count ?? "",
}; };
}) }),
); );
}} }}
title="گونه" title="گونه"
@@ -243,10 +243,14 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
formattedNumber formattedNumber
placeholder={ placeholder={
distributionType === "group" distributionType === "group"
? `تعداد (${batch.label})` ? `تعداد ${
speciesOptions().find(
(s: any) => s.key === batch.species_code,
)?.value
} (${batch.label}) `
: `تعداد ${ : `تعداد ${
speciesOptions().find( speciesOptions().find(
(s: any) => s.key === batch.species_code (s: any) => s.key === batch.species_code,
)?.value )?.value
}` }`
} }
@@ -267,7 +271,7 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
b.count === "" || b.count === "" ||
b.count === undefined || b.count === undefined ||
b.count === null || b.count === null ||
Number(b.count) <= 0 Number(b.count) <= 0,
) )
} }
type="submit" type="submit"

View File

@@ -7,7 +7,7 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { useModalStore } from "../../context/zustand-store/appStore"; import { useModalStore } from "../../context/zustand-store/appStore";
import { useApiRequest } from "../../utils/useApiRequest"; import { useApiRequest } from "../../utils/useApiRequest";
import { formatJustDate } from "../../utils/formatTime"; import { formatJustDate, formatJustTime } from "../../utils/formatTime";
import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo"; import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../../components/Grid/Grid"; import { Grid } from "../../components/Grid/Grid";
import Typography from "../../components/Typography/Typography"; import Typography from "../../components/Typography/Typography";
@@ -16,13 +16,22 @@ import Button from "../../components/Button/Button";
import { Tooltip } from "../../components/Tooltip/Tooltip"; import { Tooltip } from "../../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../../components/PopOverButtons/PopOverButtons"; import { DeleteButtonForPopOver } from "../../components/PopOverButtons/PopOverButtons";
import { SubmitTagDistribution } from "./SubmitTagDistribution"; import { SubmitTagDistribution } from "./SubmitTagDistribution";
import { DistributeFromDistribution } from "./DistributeFromDistribution";
import Table from "../../components/Table/Table"; import Table from "../../components/Table/Table";
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion"; 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() { export default function TagActiveDistributions() {
const { openModal } = useModalStore(); const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 }); const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]); const [tagsTableData, setTagsTableData] = useState([]);
const navigate = useNavigate();
const { data: tagsData, refetch } = useApiRequest({ const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch", api: "/tag/web/api/v1/tag_distribution_batch",
@@ -33,12 +42,44 @@ export default function TagActiveDistributions() {
}, },
}); });
const { profile } = useUserProfileStore();
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({ const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false", api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false",
method: "get", method: "get",
queryKey: ["tagDistributionActivesDashboard"], 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 = () => { const handleUpdate = () => {
refetch(); refetch();
updateDashboard(); updateDashboard();
@@ -61,10 +102,16 @@ export default function TagActiveDistributions() {
? index + 1 ? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1, : index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.dist_batch_identity, item?.dist_batch_identity,
formatJustDate(item?.create_date), `${formatJustDate(item?.create_date)} (${
formatJustDate(item?.create_date)
? formatJustTime(item?.create_date)
: "-"
})`,
item?.assigner_org?.name, item?.assigner_org?.name,
item?.assigned_org?.name, item?.assigned_org?.name,
item?.total_tag_count, item?.total_tag_count,
item?.total_distributed_tag_count,
item?.remaining_tag_count,
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی", item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
<ShowMoreInfo key={item?.id} title="جزئیات توزیع"> <ShowMoreInfo key={item?.id} title="جزئیات توزیع">
<Grid container column className="gap-4 w-full"> <Grid container column className="gap-4 w-full">
@@ -75,6 +122,19 @@ export default function TagActiveDistributions() {
column column
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4" 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 && ( {item?.distribution_type === "batch" && opt?.serial_from && (
<Grid container className="gap-2 items-center"> <Grid container className="gap-2 items-center">
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" /> <Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
@@ -99,27 +159,85 @@ export default function TagActiveDistributions() {
variant="body2" variant="body2"
className="text-gray-700 dark:text-gray-300" className="text-gray-700 dark:text-gray-300"
> >
{opt?.distributed_number?.toLocaleString()} {opt?.total_tag_count?.toLocaleString()}
</Typography> </Typography>
</Grid> </Grid>
<Grid container className="gap-2 items-center"> <Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" /> <CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium"> <Typography variant="body2" className="font-medium">
گونه: پلاک های توزیع شده:
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
className="text-gray-700 dark:text-gray-300" className="text-gray-700 dark:text-gray-300"
> >
{speciesMap[opt?.species_code] ?? "-"} {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> </Typography>
</Grid> </Grid>
</Grid> </Grid>
))} ))}
</Grid> </Grid>
</ShowMoreInfo>, </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}> <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"> <Tooltip title="ویرایش توزیع" position="right">
<Button <Button
variant="edit" variant="edit"
@@ -138,6 +256,24 @@ export default function TagActiveDistributions() {
}} }}
/> />
</Tooltip> </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"> <Tooltip title={"لغو توزیع"} position="right">
<Button <Button
page="tag_distribution" page="tag_distribution"
@@ -206,6 +342,7 @@ export default function TagActiveDistributions() {
"پلاک های دریافتی", "پلاک های دریافتی",
"توزیع های دریافتی", "توزیع های دریافتی",
"توزیع های ارسالی", "توزیع های ارسالی",
"جزئیات",
]} ]}
rows={[ rows={[
[ [
@@ -215,6 +352,19 @@ export default function TagActiveDistributions() {
tagDashboardData?.total_recieved_distributions?.toLocaleString() || tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
0, 0,
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0, tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
<TableButton
size="small"
onClick={() => {
openModal({
title: "جزئیات",
content: (
<DistributionSpeciesModal
items={tagDashboardData?.items}
/>
),
});
}}
/>,
], ],
]} ]}
/> />
@@ -233,8 +383,13 @@ export default function TagActiveDistributions() {
"توزیع کننده", "توزیع کننده",
"دریافت کننده", "دریافت کننده",
"تعداد کل پلاک", "تعداد کل پلاک",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"نوع توزیع", "نوع توزیع",
"جزئیات توزیع", "جزئیات توزیع",
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
"سند خروج از انبار",
"تایید سند خروج",
"عملیات", "عملیات",
]} ]}
rows={tagsTableData} rows={tagsTableData}

View File

@@ -7,7 +7,7 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { useModalStore } from "../../context/zustand-store/appStore"; import { useModalStore } from "../../context/zustand-store/appStore";
import { useApiRequest } from "../../utils/useApiRequest"; import { useApiRequest } from "../../utils/useApiRequest";
import { formatJustDate } from "../../utils/formatTime"; import { formatJustDate, formatJustTime } from "../../utils/formatTime";
import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo"; import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../../components/Grid/Grid"; import { Grid } from "../../components/Grid/Grid";
import Typography from "../../components/Typography/Typography"; import Typography from "../../components/Typography/Typography";
@@ -18,6 +18,8 @@ import { DeleteButtonForPopOver } from "../../components/PopOverButtons/PopOverB
import Table from "../../components/Table/Table"; import Table from "../../components/Table/Table";
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion"; import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion";
import { TableButton } from "../../components/TableButton/TableButton";
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
export default function TagCanceledDistributions() { export default function TagCanceledDistributions() {
const { openModal } = useModalStore(); const { openModal } = useModalStore();
@@ -61,7 +63,11 @@ export default function TagCanceledDistributions() {
? index + 1 ? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1, : index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.dist_batch_identity, item?.dist_batch_identity,
formatJustDate(item?.create_date), `${formatJustDate(item?.create_date)} (${
formatJustDate(item?.create_date)
? formatJustTime(item?.create_date)
: "-"
})`,
item?.assigner_org?.name, item?.assigner_org?.name,
item?.assigned_org?.name, item?.assigned_org?.name,
item?.total_tag_count, item?.total_tag_count,
@@ -171,6 +177,7 @@ export default function TagCanceledDistributions() {
"پلاک های دریافتی", "پلاک های دریافتی",
"توزیع های دریافتی", "توزیع های دریافتی",
"توزیع های ارسالی", "توزیع های ارسالی",
"جزئیات",
]} ]}
rows={[ rows={[
[ [
@@ -180,6 +187,19 @@ export default function TagCanceledDistributions() {
tagDashboardData?.total_recieved_distributions?.toLocaleString() || tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
0, 0,
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0, tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
<TableButton
size="small"
onClick={() => {
openModal({
title: "جزئیات",
content: (
<DistributionSpeciesModal
items={tagDashboardData?.items}
/>
),
});
}}
/>,
], ],
]} ]}
/> />

View File

@@ -54,4 +54,5 @@ export const UNITS_SETTINGS = "/unit-settings";
export const TAGGING = "/tagging"; export const TAGGING = "/tagging";
export const TAGS = "/tags"; export const TAGS = "/tags";
export const TAG_DISTRIBUTION = "/tag-distribution"; export const TAG_DISTRIBUTION = "/tag-distribution";
export const TAG_DISTRIBUTION_DETAIL = "/tag-distribution/$identity/$id";
export const TAGS_BATCH = "/tags/$id/$from/$to"; export const TAGS_BATCH = "/tags/$id/$from/$to";

View File

@@ -35,7 +35,9 @@ export const formatJustDate = (time: any) => {
}; };
export const formatJustTime = (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) { export function formatStampDate(timestamp: number) {

View File

@@ -25,6 +25,7 @@ import SettingsOfUnits from "../Pages/SettingsOfUnits";
import Tagging from "../Pages/Tagging"; import Tagging from "../Pages/Tagging";
import Tags from "../Pages/Tags"; import Tags from "../Pages/Tags";
import TagDistribtution from "../Pages/TagDistribution"; import TagDistribtution from "../Pages/TagDistribution";
import TagDistribtutionDetails from "../Pages/TagDistributionDetails";
export const managementCategoryItems = [ export const managementCategoryItems = [
{ {
@@ -188,6 +189,11 @@ export const taggingCategoryItems = [
path: R.TAG_DISTRIBUTION, path: R.TAG_DISTRIBUTION,
component: TagDistribtution, component: TagDistribtution,
}, },
{
name: "tag_distribution_detail",
path: R.TAG_DISTRIBUTION_DETAIL,
component: TagDistribtutionDetails,
},
]; ];
export const posCategoryItems = [ export const posCategoryItems = [

View File

@@ -103,6 +103,9 @@ export function getFaPermissions(permission: string) {
case "tag_distribution": case "tag_distribution":
faPermission = "توزیع پلاک"; faPermission = "توزیع پلاک";
break; break;
case "tag_distribution_detail":
faPermission = "جزئیات توزیع پلاک";
break;
default: default:
break; break;
} }

View File

@@ -1 +1 @@
02.54 02.67