Compare commits

...

18 Commits

Author SHA1 Message Date
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
16 changed files with 1360 additions and 78 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

@@ -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"],
});
@@ -58,6 +72,8 @@ export default function Tagging() {
: "نامشخص",
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,109 @@ 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?.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 +277,11 @@ export default function Tagging() {
columns={[
"ردیف",
"سازمان ثبت کننده",
"کد گونه",
"گونه",
"از بازه سریال",
"تا بازه سریال",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"عملیات",
]}
rows={tagsTableData}
@@ -161,3 +289,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,12 +9,22 @@ 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";
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
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 { data: tagsData, refetch } = useApiRequest({
api: `/tag/web/api/v1/tag/${id ? id + "/tags_by_batch" : ""}`,
method: "get",
@@ -25,7 +35,9 @@ export default function Tags() {
});
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,36 +101,140 @@ 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,
],
]}
/>
</Grid>
{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>
)}
<Table
className="mt-2"
@@ -140,3 +256,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

@@ -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,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) => {
return {
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,264 @@
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 Table from "../../components/Table/Table";
import { BooleanQuestion } from "../../components/BooleanQuestion/BooleanQuestion";
import { TableButton } from "../../components/TableButton/TableButton";
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
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)} (${
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
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,
<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,5 @@ export const UNITS_SETTINGS = "/unit-settings";
//TAGGING
export const TAGGING = "/tagging";
export const TAGS = "/tags";
export const TAG_DISTRIBUTION = "/tag-distribution";
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,7 @@ 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";
export const managementCategoryItems = [
{
@@ -182,6 +183,11 @@ export const taggingCategoryItems = [
path: R.TAGS_BATCH,
component: Tags,
},
{
name: "tag_distribution",
path: R.TAG_DISTRIBUTION,
component: TagDistribtution,
},
];
export const posCategoryItems = [

View File

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

View File

@@ -1 +1 @@
02.52
02.58