feat: tag distributions

This commit is contained in:
2026-01-24 16:21:37 +03:30
parent 576fc434dc
commit e0633245cd
4 changed files with 500 additions and 222 deletions

View File

@@ -1,220 +1,38 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../utils/useApiRequest";
import { useState } from "react";
import { Grid } from "../components/Grid/Grid";
import Table from "../components/Table/Table";
import Button from "../components/Button/Button";
import { useModalStore } from "../context/zustand-store/appStore";
import { SubmitTagDistribution } from "../partials/tagging/SubmitTagDistribution";
import Typography from "../components/Typography/Typography";
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
import { formatJustDate } from "../utils/formatTime";
import { Bars3Icon, CubeIcon, SparklesIcon } from "@heroicons/react/24/outline";
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
import { Tooltip } from "../components/Tooltip/Tooltip";
import { Popover } from "../components/PopOver/PopOver";
import Tabs from "../components/Tab/Tab";
import TagActiveDistributions from "../partials/tagging/TagActiveDistributions";
import TagCanceledDistributions from "../partials/tagging/TagCanceledDistributions";
export default function TagDistribtution() {
const { openModal } = useModalStore();
const [tableInfo, setTableInfo] = useState({ page: 1, page_size: 10 });
const [tagsTableData, setTagsTableData] = useState([]);
const [selectedTab, setSelectedTab] = useState<number>(0);
const handleTabChange = (index: number) => {
setSelectedTab(index);
};
const { data: tagsData, refetch } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch",
method: "get",
queryKey: ["tagsList", tableInfo],
params: {
...tableInfo,
const tabItems = [
{
label: "توزیع فعال",
page: "tag_distribution",
access: "Tag-Distribution-Actives",
},
});
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag/tag_dashboard/",
method: "get",
queryKey: ["tagDashboard"],
});
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),
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
variant="edit"
page="tagging"
access="Create-Tag"
onClick={() => {
openModal({
title: "ایجاد پلاک جدید",
content: (
<SubmitTagDistribution
getData={handleUpdate}
item={item}
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
api={`livestock/web/api/v1/livestock/${item?.id}/`}
getData={refetch}
/>
</Popover>,
];
});
setTagsTableData(formattedData);
} else {
setTagsTableData([]);
}
}, [tagsData, tableInfo]);
{
label: "توزیع های لغو شده",
page: "tag_distribution",
access: "Tag-Distribution-Cancels",
},
];
return (
<Grid container column className="gap-4 mt-2">
<Grid>
<Button
size="small"
variant="submit"
page="tagging"
access="Create-Tag"
onClick={() => {
openModal({
title: "ایجاد پلاک جدید",
content: <SubmitTagDistribution getData={handleUpdate} />,
});
}}
>
توزیع پلاک
</Button>
<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 isDashboard>
<Table
isDashboard
title="خلاصه اطلاعات"
noPagination
noSearch
columns={[
"تعداد کل",
"تعداد پلاک های آزاد",
"تعداد پلاک شده",
"گاو",
"گاومیش",
"شتر",
"گوسفند",
"بز",
]}
rows={[
[
tagDashboardData?.count?.toLocaleString() || 0,
tagDashboardData?.free_count?.toLocaleString() || 0,
tagDashboardData?.assign_count?.toLocaleString() || 0,
tagDashboardData?.cow_count?.toLocaleString() || 0,
tagDashboardData?.buffalo_count?.toLocaleString() || 0,
tagDashboardData?.camel_count?.toLocaleString() || 0,
tagDashboardData?.sheep_count?.toLocaleString() || 0,
tagDashboardData?.goat_count?.toLocaleString() || 0,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="توزیع پلاک"
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"نوع توزیع",
"جزئیات توزیع",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}

View File

@@ -9,18 +9,10 @@ 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 } from "../../utils/useApiRequest";
import { useApiMutation, useApiRequest } from "../../utils/useApiRequest";
import { useToast } from "../../hooks/useToast";
import { useModalStore } from "../../context/zustand-store/appStore";
const speciesOptions = [
{ key: 1, value: "گاو" },
{ key: 2, value: "گاومیش" },
{ key: 3, value: "شتر" },
{ key: 4, value: "گوسفند" },
{ key: 5, value: "بز" },
];
const distributionTypeOptions = [
{ label: "توزیع گروهی", value: "group" },
{ label: "توزیع تصادفی", value: "random" },
@@ -74,6 +66,13 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
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;
@@ -129,6 +128,15 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
}
};
const speciesOptions = () => {
return speciesData?.results?.map((opt: any) => {
return {
key: opt?.value,
value: opt?.name,
};
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-3">
@@ -177,7 +185,7 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
item?.distributions?.map((d: any) => d.batch_identity) || []
}
groupFunction={(item) =>
speciesOptions.find((s) => s.key === item)?.value || "نامشخص"
speciesOptions().find((s) => s.key === item)?.value || "نامشخص"
}
valueTemplateProps={[{ v1: "string" }, { v2: "string" }]}
multiple
@@ -207,9 +215,9 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
/>
)}
{distributionType === "random" && (
{distributionType === "random" && speciesData?.results && (
<AutoComplete
data={speciesOptions}
data={speciesOptions()}
multiselect
selectedKeys={batches.map((b) => b.species_code)}
onChange={(keys: (string | number)[]) => {
@@ -236,7 +244,7 @@ export const SubmitTagDistribution = ({ item, getData }: any) => {
distributionType === "group"
? `تعداد (${batch.label})`
: `تعداد ${
speciesOptions.find((s) => s.key === batch.species_code)
speciesOptions().find((s) => s.key === batch.species_code)
?.value
}`
}

View File

@@ -0,0 +1,244 @@
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 } 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 Table from "../../components/Table/Table";
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion";
export default function TagActiveDistributions() {
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",
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),
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
variant="edit"
page="tag_distribution"
access="Submit-Tag-Distribution"
onClick={() => {
openModal({
title: "ویرایش توزیع پلاک",
content: (
<SubmitTagDistribution
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,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="توزیع پلاک"
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"نوع توزیع",
"جزئیات توزیع",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}

View File

@@ -0,0 +1,208 @@
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 } 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";
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),
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,
],
]}
/>
</Grid>
<Table
className="mt-2"
onChange={setTableInfo}
count={tagsData?.count || 0}
isPaginated
title="توزیع های لغو شده"
columns={[
"ردیف",
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"نوع توزیع",
"جزئیات توزیع",
"عملیات",
]}
rows={tagsTableData}
/>
</Grid>
);
}