feat: initiate tag distributions

This commit is contained in:
2026-01-21 20:00:06 +03:30
parent b573a16e89
commit 3550e1fec7
7 changed files with 506 additions and 12 deletions

View File

@@ -0,0 +1,220 @@
import { useEffect, useState } from "react";
import { useApiRequest } from "../utils/useApiRequest";
import { Grid } from "../components/Grid/Grid";
import Table from "../components/Table/Table";
import Button from "../components/Button/Button";
import { useModalStore } from "../context/zustand-store/appStore";
import { 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";
export default function TagDistribtution() {
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/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]);
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>
<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

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

View File

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

View File

@@ -0,0 +1,256 @@
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 } 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" },
];
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",
});
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"
);
}
};
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) => 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" && (
<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"
? `تعداد (${batch.label})`
: `تعداد ${
speciesOptions.find((s) => s.key === batch.species_code)
?.value
}`
}
value={batch.count}
onChange={(e) => {
const next = [...batches];
next[index].count = Number(e.target.value);
setBatches(next);
}}
/>
))}
<Button type="submit">{isEdit ? "ویرایش" : "ثبت"}</Button>
</Grid>
</form>
);
};

View File

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

View File

@@ -24,6 +24,7 @@ import CooperativeRanchers from "../Pages/CooperativeRanchers";
import SettingsOfUnits from "../Pages/SettingsOfUnits"; 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";
export const managementCategoryItems = [ export const managementCategoryItems = [
{ {
@@ -182,6 +183,11 @@ export const taggingCategoryItems = [
path: R.TAGS_BATCH, path: R.TAGS_BATCH,
component: Tags, component: Tags,
}, },
{
name: "tag_distribution",
path: R.TAG_DISTRIBUTION,
component: TagDistribtution,
},
]; ];
export const posCategoryItems = [ export const posCategoryItems = [

View File

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