Compare commits

...

32 Commits

Author SHA1 Message Date
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
bb1b22152a version changed to 02.54 2026-01-24 16:21:55 +03:30
de16f203d4 feat: tag distributions 2026-01-24 16:21:46 +03:30
e0633245cd feat: tag distributions 2026-01-24 16:21:37 +03:30
576fc434dc version changed to 02.53 2026-01-24 12:02:19 +03:30
cfc4b8cc53 add: submit tag distribution numbers validation 2026-01-24 12:01:56 +03:30
3550e1fec7 feat: initiate tag distributions 2026-01-21 20:00:06 +03:30
b573a16e89 add : tag batch update and delete 2026-01-19 20:04:14 +03:30
19 changed files with 1881 additions and 90 deletions

View File

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

View File

@@ -0,0 +1,38 @@
import { useState } from "react";
import { Grid } from "../components/Grid/Grid";
import Tabs from "../components/Tab/Tab";
import TagActiveDistributions from "../partials/tagging/TagActiveDistributions";
import TagCanceledDistributions from "../partials/tagging/TagCanceledDistributions";
export default function TagDistribtution() {
const [selectedTab, setSelectedTab] = useState<number>(0);
const handleTabChange = (index: number) => {
setSelectedTab(index);
};
const tabItems = [
{
label: "توزیع فعال",
page: "tag_distribution",
access: "Tag-Distribution-Actives",
},
{
label: "توزیع های لغو شده",
page: "tag_distribution",
access: "Tag-Distribution-Cancels",
},
];
return (
<Grid container column className="justify-center mt-2">
<Tabs tabs={tabItems} onChange={handleTabChange} size="medium" />
<Grid container column className="mt-2">
{selectedTab === 0 ? (
<TagActiveDistributions />
) : (
<TagCanceledDistributions />
)}
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,150 @@
import { useParams } from "@tanstack/react-router";
import { Bars3Icon, CubeIcon, SparklesIcon } from "@heroicons/react/24/outline";
import { useApiRequest } from "../utils/useApiRequest";
import { formatJustDate, formatJustTime } from "../utils/formatTime";
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../components/Grid/Grid";
import Typography from "../components/Typography/Typography";
import Table from "../components/Table/Table";
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
export default function TagDistribtutionDetails() {
const { id } = useParams({ strict: false });
const { data } = useApiRequest({
api: `/tag/web/api/v1/tag_distribution_batch/${id}/`,
method: "get",
queryKey: ["tagBatchInnerDashboard", id],
enabled: !!id,
});
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>
</Grid>
);
}

View File

@@ -10,24 +10,38 @@ import { SubmitNewTags } from "../partials/tagging/SubmitNewTags";
import { useNavigate } from "@tanstack/react-router";
import { TAGS } from "../routes/paths";
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
import { TableButton } from "../components/TableButton/TableButton";
import AutoComplete from "../components/AutoComplete/AutoComplete";
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
export default function Tagging() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const [tagsTableData, setTagsTableData] = useState<any[]>([]);
const [selectedSpecie, setSelectedSpecie] = useState<
(string | number)[] | any
>([]);
const navigate = useNavigate();
const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_batch/",
api: `/tag/web/api/v1/tag_batch/?species_code=${
selectedSpecie.length ? selectedSpecie[0] : ""
}`,
method: "get",
queryKey: ["tagsList", tableInfo],
params: {
...tableInfo,
},
queryKey: ["tagsList", tableInfo, selectedSpecie],
params: { ...tableInfo },
});
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag/tag_dashboard/",
api: "/tag/web/api/v1/tag_batch/main_dashboard/",
method: "get",
queryKey: ["tagDashboard"],
});
@@ -48,16 +62,18 @@ export default function Tagging() {
item?.species_code === 1
? "گاو"
: item?.species_code === 2
? "گاومیش"
: item?.species_code === 3
? "شتر"
: item?.species_code === 4
? "گوسفند"
: item?.species_code === 5
? "بز"
: "نامشخص",
? "گاومیش"
: item?.species_code === 3
? "شتر"
: item?.species_code === 4
? "گوسفند"
: item?.species_code === 5
? "بز"
: "نامشخص",
item?.serial_from || "-",
item?.serial_to || "-",
item?.total_distributed_tags || 0,
item?.total_remaining_tags || 0,
<Popover key={item.id}>
<Tooltip title="مشاهده پلاک ها" position="right">
<Button
@@ -77,6 +93,21 @@ export default function Tagging() {
}}
/>
</Tooltip>
<Tooltip title="ویرایش" position="right">
<Button
variant="edit"
page="livestock_farmers"
access="Edit-Rancher"
onClick={() => {
openModal({
title: "ایجاد پلاک جدید",
content: (
<SubmitNewTags getData={handleUpdate} item={item} />
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tagging"
access="Delete-Tag"
@@ -90,10 +121,26 @@ export default function Tagging() {
} else {
setTagsTableData([]);
}
}, [tagsData]);
}, [tagsData, tableInfo.page, tableInfo.page_size, refetch]);
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_species",
method: "get",
params: { page: 1, pageSize: 1000 },
queryKey: ["species"],
});
const speciesOptions = () => {
return speciesData?.results?.map((opt: any) => {
return {
key: opt?.value,
value: opt?.name,
};
});
};
return (
<Grid container column className="gap-4 mt-2">
<Grid container column className="gap-4 mt-2 rtl">
<Grid>
<Button
size="small"
@@ -118,30 +165,112 @@ export default function Tagging() {
noPagination
noSearch
columns={[
"تعداد کل",
"تعداد پلاک های آزاد",
"تعداد پلاک شده",
"گاو",
"گاومیش",
"شتر",
"گوسفند",
"بز",
"تعداد گروه پلاک",
"پلاکهای تولیدشده",
"گروه پلاک های دارای توزیع",
"پلاک توزیع شده",
"پلاک باقی‌مانده",
"جزئیات گونه ها",
]}
rows={[
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.free_count?.toLocaleString() || 0,
tagDashboardData?.assign_count?.toLocaleString() || 0,
tagDashboardData?.cow_count?.toLocaleString() || 0,
tagDashboardData?.buffalo_count?.toLocaleString() || 0,
tagDashboardData?.camel_count?.toLocaleString() || 0,
tagDashboardData?.sheep_count?.toLocaleString() || 0,
tagDashboardData?.goat_count?.toLocaleString() || 0,
tagDashboardData?.batch_count?.toLocaleString() || 0,
tagDashboardData?.tag_count_created_by_batch?.toLocaleString() ||
0,
tagDashboardData?.has_distributed_batches_number?.toLocaleString() ||
0,
tagDashboardData?.total_distributed_tags?.toLocaleString() || 0,
tagDashboardData?.total_remaining_tags?.toLocaleString() || 0,
<TableButton
key="species-stats"
size="small"
onClick={() =>
openModal({
title: "آمار گونه‌ای",
isFullSize: true,
content: (
<BatchBySpeciesModal
batchData={
tagDashboardData?.batch_data_by_species || []
}
onRowAction={(row) => {
openModal({
title: `جزئیات ${
speciesMap[row?.species_code] ?? "-"
}`,
content: (
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
تعداد بچ
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.batch_count?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک تولید شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.tag_count_created_by_batch?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک توزیع شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_distributed_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک باقیمانده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_remaining_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
بچهای توزیعشده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.has_distributed_batches_number?.toLocaleString?.() ??
0}
</span>
</div>
</div>
),
});
}}
/>
),
})
}
>
مشاهده
</TableButton>,
],
]}
/>
</Grid>
<Grid container className="items-center gap-2">
<Grid>
{speciesOptions() && (
<AutoComplete
data={speciesOptions()}
selectedKeys={selectedSpecie}
onChange={setSelectedSpecie}
title="گونه"
/>
)}
</Grid>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
@@ -151,9 +280,11 @@ export default function Tagging() {
columns={[
"ردیف",
"سازمان ثبت کننده",
"کد گونه",
"گونه",
"از بازه سریال",
"تا بازه سریال",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"عملیات",
]}
rows={tagsTableData}
@@ -161,3 +292,78 @@ export default function Tagging() {
</Grid>
);
}
function BatchBySpeciesModal({
batchData = [],
}: {
batchData: Array<any>;
onRowAction?: (row: any, index: number) => void;
}) {
return (
<div className="w-full">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{batchData?.map((row, idx) => {
const speciesName = speciesMap[row?.species_code] ?? "-";
return (
<div
key={idx}
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm p-4 flex flex-col"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-primary/70"></div>
<span className="text-sm font-bold text-gray-900 dark:text-gray-100">
{speciesName}
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
تعداد گروه پلاک
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.batch_count?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
گروه پلاک های توزیعشده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.has_distributed_batches_number?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک تولید شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.tag_count_created_by_batch?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک توزیع شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_distributed_tags?.toLocaleString?.() ?? 0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک باقیمانده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_remaining_tags?.toLocaleString?.() ?? 0}
</span>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -9,23 +9,44 @@ import { Tooltip } from "../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
import { TagDetails } from "../partials/tagging/TagDetails";
import { useParams } from "@tanstack/react-router";
import { TableButton } from "../components/TableButton/TableButton";
import AutoComplete from "../components/AutoComplete/AutoComplete";
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
const statusOptions = [
{ key: "F", value: "آزاد" },
{ key: "A", value: "پلاک شده" },
{ key: "R", value: "رزرو" },
];
export default function Tags() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const { id, from, to } = useParams({ strict: false });
const [selectedStatus, setSelectedStatus] = useState<(string | number)[]>([]);
const { data: tagsData, refetch } = useApiRequest({
api: `/tag/web/api/v1/tag/${id ? id + "/tags_by_batch" : ""}`,
method: "get",
queryKey: ["tagsList", tableInfo],
queryKey: ["tagsList", tableInfo, selectedStatus],
params: {
...tableInfo,
status: selectedStatus.length ? selectedStatus[0] : undefined,
},
});
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",
queryKey: ["tagDashboard"],
});
@@ -89,35 +110,150 @@ export default function Tags() {
return (
<Grid container column className="gap-4 mt-2">
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد کل",
"تعداد پلاک های آزاد",
"تعداد پلاک شده",
"گاو",
"گاومیش",
"شتر",
"گوسفند",
"بز",
]}
rows={[
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.free_count?.toLocaleString() || 0,
tagDashboardData?.assign_count?.toLocaleString() || 0,
tagDashboardData?.cow_count?.toLocaleString() || 0,
tagDashboardData?.buffalo_count?.toLocaleString() || 0,
tagDashboardData?.camel_count?.toLocaleString() || 0,
tagDashboardData?.sheep_count?.toLocaleString() || 0,
tagDashboardData?.goat_count?.toLocaleString() || 0,
],
]}
/>
{tagDashboardData && (
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={
id
? [
"تعداد پلاک",
"پلاک های توزیع شده",
"تعداد پلاک های گروه پلاک",
"تعداد پلاک های توزیع شده",
"تعداد پلاک های باقیمانده",
"آمار گونه ای",
]
: [
"تعداد کل",
"تعداد پلاک های آزاد",
"تعداد پلاک شده",
"گاو",
"گاومیش",
"شتر",
"گوسفند",
"بز",
]
}
rows={
id
? [
[
tagDashboardData?.batch_count?.toLocaleString() || 0,
tagDashboardData?.has_distributed_batches_number?.toLocaleString() ||
0,
tagDashboardData?.tag_count_created_by_batch?.toLocaleString() ||
0,
tagDashboardData?.total_distributed_tags?.toLocaleString() ||
0,
tagDashboardData?.total_remaining_tags?.toLocaleString() ||
0,
<TableButton
key="species-stats"
size="small"
onClick={() =>
openModal({
title: "آمار گونه‌ای",
isFullSize: true,
content: (
<BatchBySpeciesModal
batchData={
tagDashboardData?.batch_data_by_species || []
}
onRowAction={(row) => {
openModal({
title: `جزئیات ${
speciesMap[row?.species_code] ?? "-"
}`,
content: (
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
تعداد بچ
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.batch_count?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک تولید شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.tag_count_created_by_batch?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک توزیع شده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_distributed_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
پلاک باقیمانده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.total_remaining_tags?.toLocaleString?.() ??
0}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-300">
بچهای توزیعشده
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{row?.has_distributed_batches_number?.toLocaleString?.() ??
0}
</span>
</div>
</div>
),
});
}}
/>
),
})
}
>
مشاهده
</TableButton>,
],
]
: [
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.free_count?.toLocaleString() || 0,
tagDashboardData?.assign_count?.toLocaleString() || 0,
tagDashboardData?.cow_count?.toLocaleString() || 0,
tagDashboardData?.buffalo_count?.toLocaleString() || 0,
tagDashboardData?.camel_count?.toLocaleString() || 0,
tagDashboardData?.sheep_count?.toLocaleString() || 0,
tagDashboardData?.goat_count?.toLocaleString() || 0,
],
]
}
/>
</Grid>
)}
<Grid container className="items-center gap-2">
<Grid>
<AutoComplete
data={statusOptions}
selectedKeys={selectedStatus}
onChange={setSelectedStatus}
title="فیلتر پلاک ها"
/>
</Grid>
</Grid>
<Table
@@ -140,3 +276,78 @@ export default function Tags() {
</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

@@ -27,7 +27,7 @@ interface AutoCompleteProps {
multiselect?: boolean;
inPage?: boolean;
disabled?: boolean;
selectedKeys: (number | string)[];
selectedKeys: (number | string)[] | any;
onChange: (keys: (number | string)[]) => void | [];
width?: string;
buttonHeight?: number | string;

View File

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

View File

@@ -226,17 +226,25 @@ export const FormApiBasedAutoComplete = ({
setSelectedKeys(defaultItems.map((item: any) => item.key));
if (onChange) {
if (secondaryKey) {
onChange({
key1: defaultItems.map((item: any) => item.key),
key2: defaultItems.map((item: any) => item.secondaryKey),
...(tertiaryKey
? {
key3: defaultItems.map(
(item: any) => item.tertiaryKey
),
}
: {}),
});
console.log("dksk0", defaultItems);
onChange(
defaultItems.map((item: any) => {
return {
key: item?.key,
key2: item?.secondaryKey,
...(tertiaryKey
? {
key3: item?.tertiaryKey,
}
: {}),
};
})
);
if (onChangeValue) {
onChangeValue(
defaultItems.map((item: any) => item.value.trim())
);
}
} else {
onChange(defaultItems.map((item: any) => item.key));
}

View File

@@ -0,0 +1,254 @@
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 }: 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,
});
const mutation = useApiMutation({
api: `/tag/web/api/v1/tag_distribution/${item?.id}/distribute_distribution/`,
method: "post",
});
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_species",
method: "get",
params: { page: 1, pageSize: 1000 },
queryKey: ["species"],
});
useEffect(() => {
const sourceDistributions = 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);
setCounts({});
setSelectedSpeciesKeys([]);
}, [item?.distributions, batchDetail?.distributions]);
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("توزیع از توزیع با موفقیت انجام شد", "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
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

@@ -45,8 +45,6 @@ export const SubmitNewTags = ({ getData, item }: SubmitNewTagsTypeProps) => {
const { profile } = useUserProfileStore();
console.log(item);
const {
control,
handleSubmit,
@@ -58,13 +56,15 @@ export const SubmitNewTags = ({ getData, item }: SubmitNewTagsTypeProps) => {
defaultValues: {
country_code: 364,
static_code: 0,
species_code: 1,
serial_start: item?.serial_from || "",
serial_end: item?.serial_to || "",
species_code: item?.species_code || 1,
},
});
const mutation = useApiMutation({
api: "/tag/web/api/v1/tag/",
method: "post",
api: `/tag/web/api/v1/tag/${item?.id ? item.id : ""}`,
method: item ? "put" : "post",
});
const onSubmit = async (data: FormValues) => {

View File

@@ -0,0 +1,284 @@
import { z } from "zod";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Grid } from "../../components/Grid/Grid";
import Button from "../../components/Button/Button";
import Textfield from "../../components/Textfeild/Textfeild";
import { RadioGroup } from "../../components/RadioButton/RadioGroup";
import { FormApiBasedAutoComplete } from "../../components/FormItems/FormApiBasedAutoComplete";
import AutoComplete from "../../components/AutoComplete/AutoComplete";
import { zValidateAutoComplete } from "../../data/getFormTypeErrors";
import { useApiMutation, useApiRequest } from "../../utils/useApiRequest";
import { useToast } from "../../hooks/useToast";
import { useModalStore } from "../../context/zustand-store/appStore";
const distributionTypeOptions = [
{ label: "توزیع گروهی", value: "group" },
{ label: "توزیع تصادفی", value: "random" },
];
const schema = z.object({
organization: zValidateAutoComplete("سازمان"),
});
type FormValues = z.infer<typeof schema>;
type BatchItem = {
batch_identity?: string | number;
species_code?: number;
count: number | "";
label?: string;
};
export const SubmitTagDistribution = ({ item, getData }: any) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const isEdit = Boolean(item?.id);
const [distributionType, setDistributionType] = useState<"group" | "random">(
isEdit
? item?.distribution_type === "random"
? "random"
: "group"
: "group",
);
const [batches, setBatches] = useState<BatchItem[]>([]);
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
organization: [],
},
});
const mutation = useApiMutation({
api: isEdit
? `/tag/web/api/v1/tag_distribution/${item?.id}`
: "/tag/web/api/v1/tag_distribution/",
method: isEdit ? "put" : "post",
});
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_species",
method: "get",
params: { page: 1, pageSize: 1000 },
queryKey: ["species"],
});
useEffect(() => {
if (!item) return;
setValue("organization", [item.assigned_org?.id]);
trigger("organization");
const mappedBatches = item.distributions.map((d: any) => ({
...(item.distribution_type === "batch" && {
batch_identity: item.dist_batch_identity,
}),
species_code: d.species_code,
count: d.distributed_number,
label:
item.distribution_type === "batch"
? `از ${d.serial_from ?? "-"} تا ${d.serial_to ?? "-"}`
: undefined,
}));
setBatches(mappedBatches);
}, [item]);
const onSubmit = async (data: FormValues) => {
const dists =
distributionType === "random"
? batches.map((b) => ({
species_code: b.species_code,
count: b.count,
}))
: batches.map((b) => ({
batch_identity: b.batch_identity,
species_code: b.species_code,
count: b.count,
}));
try {
await mutation.mutateAsync({
assigner_org: item?.organization?.id,
assigned_org: data.organization[0],
dists,
});
showToast(
isEdit ? "ویرایش با موفقیت انجام شد" : "ثبت با موفقیت انجام شد",
"success",
);
getData();
closeModal();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
const speciesOptions = () => {
return (
speciesData?.results?.map((opt: any) => ({
key: opt?.value,
value: opt?.name,
})) ?? []
);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-3">
<Controller
name="organization"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.assigned_org?.id}
title="انتخاب سازمان"
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
trigger("organization");
}}
/>
)}
/>
<RadioGroup
direction="row"
options={distributionTypeOptions}
value={distributionType}
onChange={(e) => {
const val = e.target.value as "group" | "random";
setDistributionType(val);
setBatches([]);
}}
/>
{distributionType === "group" && (
<FormApiBasedAutoComplete
title="گروه پلاک"
api="/tag/web/api/v1/tag_batch/"
keyField="batch_identity"
secondaryKey="species_code"
valueTemplate="از v1 تا v2"
valueField={["serial_from"]}
valueField2={["serial_to"]}
groupBy="species_code"
defaultKey={
item?.distributions?.map((d: any) => d.batch_identity) || []
}
groupFunction={(item) =>
speciesOptions().find((s: any) => s.key === item)?.value ||
"نامشخص"
}
valueTemplateProps={[{ v1: "string" }, { v2: "string" }]}
multiple
onChange={(items) => {
setBatches(
items?.map((r: any) => {
const existing = batches.find(
(b) =>
b.batch_identity === r.key1 && b.species_code === r.key2,
);
return {
batch_identity: r.key1,
species_code: r.key2,
count: existing?.count ?? "",
};
}) || [],
);
}}
onChangeValue={(labels) => {
setBatches((prev) =>
prev.map((item, index) => ({
...item,
label: labels[index],
})),
);
}}
/>
)}
{distributionType === "random" && speciesData?.results && (
<AutoComplete
data={speciesOptions()}
multiselect
selectedKeys={batches.map((b) => b.species_code)}
onChange={(keys: (string | number)[]) => {
setBatches(
keys.map((k) => {
const prev = batches.find((b) => b.species_code === k);
return {
species_code: k as number,
count: prev?.count ?? "",
};
}),
);
}}
title="گونه"
/>
)}
{batches.map((batch, index) => (
<Textfield
key={index}
fullWidth
formattedNumber
placeholder={
distributionType === "group"
? `تعداد ${
speciesOptions().find(
(s: any) => s.key === batch.species_code,
)?.value
} (${batch.label}) `
: `تعداد ${
speciesOptions().find(
(s: any) => s.key === batch.species_code,
)?.value
}`
}
value={batch.count}
onChange={(e) => {
const next = [...batches];
next[index].count = Number(e.target.value);
setBatches(next);
}}
/>
))}
<Button
disabled={
batches.length === 0 ||
batches.some(
(b) =>
b.count === "" ||
b.count === undefined ||
b.count === null ||
Number(b.count) <= 0,
)
}
type="submit"
>
{isEdit ? "ویرایش" : "ثبت"}
</Button>
</Grid>
</form>
);
};

View File

@@ -0,0 +1,330 @@
import { useEffect, useState } from "react";
import {
Bars3Icon,
CubeIcon,
SparklesIcon,
StopCircleIcon,
} from "@heroicons/react/24/outline";
import { useModalStore } from "../../context/zustand-store/appStore";
import { useApiRequest } from "../../utils/useApiRequest";
import { formatJustDate, formatJustTime } from "../../utils/formatTime";
import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../../components/Grid/Grid";
import Typography from "../../components/Typography/Typography";
import { Popover } from "../../components/PopOver/PopOver";
import Button from "../../components/Button/Button";
import { Tooltip } from "../../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../../components/PopOverButtons/PopOverButtons";
import { SubmitTagDistribution } from "./SubmitTagDistribution";
import { DistributeFromDistribution } from "./DistributeFromDistribution";
import Table from "../../components/Table/Table";
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion";
import { TableButton } from "../../components/TableButton/TableButton";
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
import { useNavigate } from "@tanstack/react-router";
import { TAG_DISTRIBUTION } from "../../routes/paths";
export default function TagActiveDistributions() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const navigate = useNavigate();
const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch",
method: "get",
queryKey: ["tagsList", tableInfo],
params: {
...tableInfo,
},
});
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false",
method: "get",
queryKey: ["tagDistributionActivesDashboard"],
});
const handleUpdate = () => {
refetch();
updateDashboard();
};
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
useEffect(() => {
if (tagsData?.results) {
const formattedData = tagsData.results.map((item: any, index: number) => {
const dist = item?.distributions;
return [
tableInfo.page === 1
? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.dist_batch_identity,
`${formatJustDate(item?.create_date)} (${
formatJustDate(item?.create_date)
? formatJustTime(item?.create_date)
: "-"
})`,
item?.assigner_org?.name,
item?.assigned_org?.name,
item?.total_tag_count,
item?.total_distributed_tag_count,
item?.remaining_tag_count,
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
<Grid container column className="gap-4 w-full">
{dist?.map((opt: any, index: number) => (
<Grid
key={index}
container
column
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
>
<Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
گونه:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{speciesMap[opt?.species_code] ?? "-"}
</Typography>
</Grid>
{item?.distribution_type === "batch" && opt?.serial_from && (
<Grid container className="gap-2 items-center">
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
بازه سریال:
</Typography>
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
از {opt?.serial_from ?? "-"} تا {opt?.serial_to ?? "-"}
</Typography>
</Grid>
)}
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
تعداد پلاک:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.total_tag_count?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های توزیع شده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.distributed_number?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های باقیمانده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.remaining_number?.toLocaleString()}
</Typography>
</Grid>
</Grid>
))}
</Grid>
</ShowMoreInfo>,
<Popover key={index}>
<Tooltip title="جزيٓیات توزیع" position="right">
<Button
variant="detail"
page="tag_distribution_detail"
access="Show-Tag-Distribution-Detail"
onClick={() => {
const path =
TAG_DISTRIBUTION +
"/" +
item?.dist_batch_identity +
"/" +
item?.id;
navigate({ to: path });
}}
/>
</Tooltip>
<Tooltip title="ویرایش توزیع" position="right">
<Button
variant="edit"
page="tag_distribution"
access="Submit-Tag-Distribution"
onClick={() => {
openModal({
title: "ویرایش توزیع پلاک",
content: (
<SubmitTagDistribution
getData={handleUpdate}
item={item}
/>
),
});
}}
/>
</Tooltip>
<Tooltip title="توزیع مجدد" position="right">
<Button
variant="share"
page="tag_distribution"
access="Distribute-From-Distribution"
onClick={() => {
openModal({
title: "توزیع مجدد",
content: (
<DistributeFromDistribution
getData={handleUpdate}
item={item}
/>
),
});
}}
/>
</Tooltip>
<Tooltip title={"لغو توزیع"} position="right">
<Button
page="tag_distribution"
access="Cancel-Tag-Distribution"
icon={<StopCircleIcon className="w-5 h-5 text-red-400" />}
variant="set"
onClick={() => {
openModal({
title: "لغو توزیع پلاک",
content: (
<BooleanQuestion
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/close_dist_batch/`}
method="post"
getData={handleUpdate}
title="آیا از لغو توزیع پلاک مطمئنید؟"
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tag_distribution"
access="Delete-Tag-Distribution"
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTagsTableData(formattedData);
} else {
setTagsTableData([]);
}
}, [tagsData, tableInfo]);
return (
<Grid container column className="gap-4 mt-2">
<Grid>
<Button
size="small"
variant="submit"
page="tag_distribution"
access="Submit-Tag-Distribution"
onClick={() => {
openModal({
title: "توزیع پلاک",
content: <SubmitTagDistribution getData={handleUpdate} />,
});
}}
>
توزیع پلاک
</Button>
</Grid>
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد توزیع",
"پلاک های ارسالی",
"پلاک های دریافتی",
"توزیع های دریافتی",
"توزیع های ارسالی",
"جزئیات",
]}
rows={[
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.total_sent_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
0,
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
<TableButton
size="small"
onClick={() => {
openModal({
title: "جزئیات",
content: (
<DistributionSpeciesModal
items={tagDashboardData?.items}
/>
),
});
}}
/>,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="توزیع پلاک"
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"نوع توزیع",
"جزئیات توزیع",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}

View File

@@ -0,0 +1,228 @@
import { useEffect, useState } from "react";
import {
BackwardIcon,
Bars3Icon,
CubeIcon,
SparklesIcon,
} from "@heroicons/react/24/outline";
import { useModalStore } from "../../context/zustand-store/appStore";
import { useApiRequest } from "../../utils/useApiRequest";
import { formatJustDate, formatJustTime } from "../../utils/formatTime";
import ShowMoreInfo from "../../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../../components/Grid/Grid";
import Typography from "../../components/Typography/Typography";
import { Popover } from "../../components/PopOver/PopOver";
import Button from "../../components/Button/Button";
import { Tooltip } from "../../components/Tooltip/Tooltip";
import { DeleteButtonForPopOver } from "../../components/PopOverButtons/PopOverButtons";
import Table from "../../components/Table/Table";
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion";
import { TableButton } from "../../components/TableButton/TableButton";
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
export default function TagCanceledDistributions() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/closed_tag_dist_batch_list",
method: "get",
queryKey: ["tagsList", tableInfo],
params: {
...tableInfo,
},
});
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=true",
method: "get",
queryKey: ["tagDistributionCanceledDashboard"],
});
const handleUpdate = () => {
refetch();
updateDashboard();
};
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
useEffect(() => {
if (tagsData?.results) {
const formattedData = tagsData.results.map((item: any, index: number) => {
const dist = item?.distributions;
return [
tableInfo.page === 1
? index + 1
: index + tableInfo.page_size * (tableInfo.page - 1) + 1,
item?.dist_batch_identity,
`${formatJustDate(item?.create_date)} (${
formatJustDate(item?.create_date)
? formatJustTime(item?.create_date)
: "-"
})`,
item?.assigner_org?.name,
item?.assigned_org?.name,
item?.total_tag_count,
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
<Grid container column className="gap-4 w-full">
{dist?.map((opt: any, index: number) => (
<Grid
key={index}
container
column
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
>
{item?.distribution_type === "batch" && opt?.serial_from && (
<Grid container className="gap-2 items-center">
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
بازه سریال:
</Typography>
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
از {opt?.serial_from ?? "-"} تا {opt?.serial_to ?? "-"}
</Typography>
</Grid>
)}
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
تعداد پلاک:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.distributed_number?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
گونه:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{speciesMap[opt?.species_code] ?? "-"}
</Typography>
</Grid>
</Grid>
))}
</Grid>
</ShowMoreInfo>,
<Popover key={index}>
<Tooltip title={"برگشت توزیع"} position="right">
<Button
page="tag_distribution"
access="Cancel-Tag-Distribution"
icon={<BackwardIcon className="w-5 h-5 text-red-400" />}
variant="set"
onClick={() => {
openModal({
title: "برگشت توزیع لغو شده",
content: (
<BooleanQuestion
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/reactivate_tag_dist_batch/`}
method="post"
getData={handleUpdate}
title="آیا از برگشت توزیع پلاک لغو شده مطمئنید؟"
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tag_distribution"
access="Delete-Tag-Distribution"
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
getData={handleUpdate}
/>
</Popover>,
];
});
setTagsTableData(formattedData);
} else {
setTagsTableData([]);
}
}, [tagsData, tableInfo]);
return (
<Grid container column className="gap-4 mt-2">
<Grid isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد توزیع",
"پلاک های ارسالی",
"پلاک های دریافتی",
"توزیع های دریافتی",
"توزیع های ارسالی",
"جزئیات",
]}
rows={[
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.total_sent_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_tag_count?.toLocaleString() || 0,
tagDashboardData?.total_recieved_distributions?.toLocaleString() ||
0,
tagDashboardData?.total_sent_distributions?.toLocaleString() || 0,
<TableButton
size="small"
onClick={() => {
openModal({
title: "جزئیات",
content: (
<DistributionSpeciesModal
items={tagDashboardData?.items}
/>
),
});
}}
/>,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="توزیع های لغو شده"
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"نوع توزیع",
"جزئیات توزیع",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}

View File

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

View File

@@ -35,7 +35,9 @@ export const formatJustDate = (time: any) => {
};
export const formatJustTime = (time: any) => {
return format(new Date(time), "HH:mm");
if (time) {
return format(new Date(time), "HH:mm");
} else return "";
};
export function formatStampDate(timestamp: number) {

View File

@@ -24,6 +24,8 @@ import CooperativeRanchers from "../Pages/CooperativeRanchers";
import SettingsOfUnits from "../Pages/SettingsOfUnits";
import Tagging from "../Pages/Tagging";
import Tags from "../Pages/Tags";
import TagDistribtution from "../Pages/TagDistribution";
import TagDistribtutionDetails from "../Pages/TagDistributionDetails";
export const managementCategoryItems = [
{
@@ -182,6 +184,16 @@ export const taggingCategoryItems = [
path: R.TAGS_BATCH,
component: Tags,
},
{
name: "tag_distribution",
path: R.TAG_DISTRIBUTION,
component: TagDistribtution,
},
{
name: "tag_distribution_detail",
path: R.TAG_DISTRIBUTION_DETAIL,
component: TagDistribtutionDetails,
},
];
export const posCategoryItems = [

View File

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

View File

@@ -1 +1 @@
02.52
02.64