Compare commits

...

18 Commits

Author SHA1 Message Date
93867ce7ee version changed to 02.66 2026-02-09 16:23:19 +03:30
4210dbd7e2 add: organization type filter 2026-02-09 14:38:32 +03:30
3624b3bc70 add: size prop 2026-02-09 13:53:43 +03:30
4d00b0d492 add: unique_unit_identity 2026-02-09 12:34:56 +03:30
ed7b257ed8 add: limit size for upload doc and removed validFiles limit 2026-02-09 10:23:35 +03:30
03136f5f30 add: limit size 2026-02-09 10:22:34 +03:30
908a69ce0e version changed to 02.65 2026-02-08 16:53:18 +03:30
a818683247 add: multiple addresses 2026-02-08 16:52:55 +03:30
95780cfbc9 add: document upload and sign 2026-02-08 16:52:26 +03:30
071c3e159b feat: document operation 2026-02-08 16:52:00 +03:30
90f51c6899 feat: edit distribution from distribution 2026-02-08 11:20:10 +03:30
e967329108 chore: fix grammar mistake 2026-02-03 10:24:28 +03:30
f8d2da4f28 version changed to 02.64 2026-02-02 16:34:27 +03:30
bb1d5b3315 update: tag distribution details 2026-02-02 16:34:21 +03:30
d8d415a8f5 fix: remaining number 2026-02-02 14:50:44 +03:30
f58c8e6c58 add: new keys 2026-02-02 14:49:12 +03:30
3a17fcb448 version changed to 02.63 2026-02-02 12:14:52 +03:30
6e219aca1a feat: distribute from distribution 2026-02-02 12:14:48 +03:30
10 changed files with 1215 additions and 97 deletions

View File

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

View File

@@ -1,55 +1,411 @@
import { useEffect, useState } from "react";
import { useParams } from "@tanstack/react-router";
import { Bars3Icon, CubeIcon, SparklesIcon } from "@heroicons/react/24/outline";
import { useApiRequest } from "../utils/useApiRequest";
import { useModalStore } from "../context/zustand-store/appStore";
import { formatJustDate, formatJustTime } from "../utils/formatTime";
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
import { Grid } from "../components/Grid/Grid";
import Typography from "../components/Typography/Typography";
import Table from "../components/Table/Table";
import { Popover } from "../components/PopOver/PopOver";
import Button from "../components/Button/Button";
import { Tooltip } from "../components/Tooltip/Tooltip";
import { DistributeFromDistribution } from "../partials/tagging/DistributeFromDistribution";
import { DocumentOperation } from "../components/DocumentOperation/DocumentOperation";
import { DocumentDownloader } from "../components/DocumentDownloader/DocumentDownloader";
import { BooleanQuestion } from "../components/BooleanQuestion/BooleanQuestion";
import { useUserProfileStore } from "../context/zustand-store/userStore";
import { DeleteButtonForPopOver } from "../components/PopOverButtons/PopOverButtons";
const speciesMap: Record<number, string> = {
1: "گاو",
2: "گاومیش",
3: "شتر",
4: "گوسفند",
5: "بز",
};
export default function TagDistribtutionDetails() {
const { id } = useParams({ strict: false });
const [tableData, setTableData] = useState([]);
const { openModal } = useModalStore();
const [childTableInfo, setChildTableInfo] = useState({
page: 1,
page_size: 10,
});
const [childTableData, setChildTableData] = useState([]);
const { data } = useApiRequest({
const { data, refetch: refetchData } = useApiRequest({
api: `/tag/web/api/v1/tag_distribution_batch/${id}/`,
method: "get",
queryKey: ["tagBatchInnerDashboard", id],
enabled: !!id,
});
useEffect(() => {
if (data?.distributions) {
const rows = data.distributions.map((item: any, index: number) => [
index + 1,
item?.dist_identity,
item?.batch_identity,
item?.distribution_type === "batch" ? "توزیع گروهی" : "توزیع تصادفی",
item?.species_code,
item?.total_tag_count,
item?.distributed_number,
item?.remaining_number,
`از ${item?.serial_from} تا ${item?.serial_to}`,
]);
setTableData(rows);
const { data: childData, refetch: refetchChildList } = useApiRequest({
api: `/tag/web/api/v1/tag_distribution_batch/${id}/child_list/`,
method: "get",
queryKey: ["tagDistributionChildList", id, childTableInfo],
params: {
...childTableInfo,
},
enabled: !!id,
});
const { profile } = useUserProfileStore();
const showAssignDocColumn =
childData?.results?.some(
(item: any) =>
profile?.role?.type?.key === "ADM" ||
profile?.organization?.id === item?.assigned_org?.id,
) ?? false;
const AbleToSeeAssignDoc = (item: any) => {
if (
profile?.role?.type?.key === "ADM" ||
profile?.organization?.id === item?.assigned_org?.id
) {
return (
<DocumentOperation
key={item?.id}
downloadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/distribution_pdf_view/`}
payloadKey="dist_exit_document"
validFiles={["pdf"]}
page="tag_distribution"
access="Upload-Assign-Document"
uploadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/assign_document/`}
onUploadSuccess={handleUpdate}
/>
);
} else {
return "-";
}
}, [data]);
};
const handleUpdate = () => {
refetchData();
refetchChildList();
};
useEffect(() => {
if (childData?.results) {
const formattedData = childData.results.map(
(item: any, index: number) => {
const dist = item?.distributions;
return [
childTableInfo.page === 1
? index + 1
: index +
childTableInfo.page_size * (childTableInfo.page - 1) +
1,
item?.dist_batch_identity ?? "-",
`${formatJustDate(item?.create_date) ?? "-"} (${
formatJustDate(item?.create_date)
? (formatJustTime(item?.create_date) ?? "-")
: "-"
})`,
item?.assigner_org?.name ?? "-",
item?.assigned_org?.name ?? "-",
item?.total_tag_count?.toLocaleString() ?? "-",
item?.total_distributed_tag_count?.toLocaleString() ?? "-",
item?.remaining_tag_count?.toLocaleString() ?? "-",
item?.distribution_type === "batch"
? "توزیع گروهی"
: "توزیع تصادفی",
<ShowMoreInfo key={item?.id} title="جزئیات توزیع">
<Grid container column className="gap-4 w-full">
{dist?.map((opt: any, idx: number) => (
<Grid
key={idx}
container
column
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
>
<Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
گونه:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{speciesMap[opt?.species_code] ?? "-"}
</Typography>
</Grid>
{item?.distribution_type === "batch" &&
opt?.serial_from != null && (
<Grid container className="gap-2 items-center">
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
بازه سریال:
</Typography>
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
از {opt?.serial_from ?? "-"} تا{" "}
{opt?.serial_to ?? "-"}
</Typography>
</Grid>
)}
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
تعداد پلاک:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.total_tag_count?.toLocaleString() ?? "-"}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های توزیع شده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.distributed_number?.toLocaleString() ?? "-"}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های باقیمانده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.remaining_number?.toLocaleString() ?? "-"}
</Typography>
</Grid>
</Grid>
))}
</Grid>
</ShowMoreInfo>,
...(showAssignDocColumn ? [AbleToSeeAssignDoc(item)] : []),
<DocumentDownloader
key={`doc-${item?.id}`}
link={item?.warehouse_exit_doc}
title="دانلود"
/>,
item?.exit_doc_status ? (
"تایید شده"
) : (
<Button
key={`btn-${item?.id}`}
page="tag_distribution"
access="Accept-Assign-Document"
size="small"
disabled={item?.exit_doc_status}
onClick={() => {
openModal({
title: "تایید سند خروج",
content: (
<BooleanQuestion
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/accept_exit_doc/`}
method="post"
getData={handleUpdate}
title="آیا از تایید سند خروج مطمئنید؟"
/>
),
});
}}
>
تایید سند خروج
</Button>
),
<Popover key={`popover-${item?.id}`}>
<Tooltip title="ویرایش توزیع" position="right">
<Button
variant="edit"
page="tag_distribution"
access="Submit-Tag-Distribution"
onClick={() => {
openModal({
title: "ویرایش توزیع",
content: (
<DistributeFromDistribution
getData={handleUpdate}
item={item}
isEdit
parentDistributions={data?.distributions}
/>
),
});
}}
/>
</Tooltip>
<DeleteButtonForPopOver
page="tag_distribution"
access="Delete-Tag-Distribution"
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/`}
getData={handleUpdate}
/>
</Popover>,
];
},
);
setChildTableData(formattedData);
} else {
setChildTableData([]);
}
}, [childData, childTableInfo]);
const dist = data?.distributions;
return (
<Grid container column className="gap-4">
<Grid container column className="gap-4 mt-2">
<Grid isDashboard>
<Table
isDashboard
title="مشخصات توزیع پلاک"
noSearch
noPagination
columns={[
"شناسه توزیع",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"نوع توزیع",
"جزئیات توزیع",
]}
rows={[
[
data?.dist_batch_identity ?? "-",
`${formatJustDate(data?.create_date) ?? "-"} (${
formatJustDate(data?.create_date)
? (formatJustTime(data?.create_date) ?? "-")
: "-"
})`,
data?.assigner_org?.name ?? "-",
data?.assigned_org?.name ?? "-",
data?.total_tag_count?.toLocaleString() ?? "-",
data?.total_distributed_tag_count?.toLocaleString() ?? "-",
data?.remaining_tag_count?.toLocaleString() ?? "-",
data?.distribution_type === "batch"
? "توزیع گروهی"
: "توزیع تصادفی",
<ShowMoreInfo key={data?.id} title="جزئیات توزیع">
<Grid container column className="gap-4 w-full">
{dist?.map((opt: any, index: number) => (
<Grid
key={index}
container
column
className="gap-3 w-full rounded-xl border border-gray-200 dark:border-gray-700 p-4"
>
<Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
گونه:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{speciesMap[opt?.species_code] ?? "-"}
</Typography>
</Grid>
{data?.distribution_type === "batch" &&
opt?.serial_from != null && (
<Grid container className="gap-2 items-center">
<Bars3Icon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
بازه سریال:
</Typography>
<Typography
variant="body2"
className="text-gray-600 dark:text-gray-400"
>
از {opt?.serial_from ?? "-"} تا{" "}
{opt?.serial_to ?? "-"}
</Typography>
</Grid>
)}
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
تعداد پلاک:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.total_tag_count?.toLocaleString() ?? "-"}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های توزیع شده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.distributed_number?.toLocaleString() ?? "-"}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های باقیمانده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.remaining_number?.toLocaleString() ?? "-"}
</Typography>
</Grid>
</Grid>
))}
</Grid>
</ShowMoreInfo>,
],
]}
/>
</Grid>
<Table
title="جزئیات توزیع پلاک"
noSearch
noPagination
className="mt-2"
onChange={setChildTableInfo}
count={childData?.count || 0}
isPaginated
title="توزیع های مجدد"
columns={[
"ردیف",
"شناسه توزیع",
"شناسه پلاک",
"تاریخ ثبت",
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"نوع توزیع",
"کد گونه",
"تعداد کل پلاک ها",
"تعداد توزیع شده",
عداد باقیمانده",
"بازه سریال",
"جزئیات توزیع",
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
"سند خروج از انبار",
ایید سند خروج",
"عملیات",
]}
rows={tableData}
rows={childTableData}
/>
</Grid>
);

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

@@ -0,0 +1,236 @@
import React, { useRef, useState, useEffect, ChangeEvent } from "react";
import {
ArrowDownTrayIcon,
ArrowUpTrayIcon,
CheckCircleIcon,
} from "@heroicons/react/24/outline";
import api from "../../utils/axios";
import { useBackdropStore } from "../../context/zustand-store/appStore";
import { useToast } from "../../hooks/useToast";
import { useUserProfileStore } from "../../context/zustand-store/userStore";
import { RolesContextMenu } from "../Button/RolesContextMenu";
interface DocumentOperationProps {
downloadLink: string;
uploadLink: string;
validFiles?: string[];
payloadKey: string;
onUploadSuccess?: () => void;
page?: string;
access?: string;
limitSize?: number;
}
const buildAcceptString = (extensions: string[]): string => {
const mimeTypes: string[] = [];
extensions.forEach((ext) => {
const lower = ext.toLowerCase().replace(".", "");
if (lower === "img" || lower === "image") {
mimeTypes.push("image/*");
} else {
mimeTypes.push(`.${lower}`);
}
});
return mimeTypes.join(",");
};
export const DocumentOperation = ({
downloadLink,
uploadLink,
validFiles = [],
payloadKey,
onUploadSuccess,
page = "",
access = "",
limitSize,
}: DocumentOperationProps) => {
const { openBackdrop, closeBackdrop } = useBackdropStore();
const showToast = useToast();
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploadedFileName, setUploadedFileName] = useState<string>("");
const { profile } = useUserProfileStore();
const [contextMenu, setContextMenu] = useState<{
x: number;
y: number;
} | null>(null);
const isAdmin = profile?.role?.type?.key === "ADM";
const ableToSee = () => {
if (!access || !page) {
return true;
}
const found = profile?.permissions?.find(
(item: any) => item.page_name === page,
);
if (found && found.page_access.includes(access)) {
return true;
}
return false;
};
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
if (isAdmin && page && access) {
e.preventDefault();
e.stopPropagation();
setContextMenu({
x: e.clientX,
y: e.clientY,
});
}
};
useEffect(() => {
const handleClick = () => {
if (contextMenu) {
setContextMenu(null);
}
};
if (contextMenu) {
document.addEventListener("click", handleClick);
}
return () => {
document.removeEventListener("click", handleClick);
};
}, [contextMenu]);
const handleDownload = async () => {
if (!downloadLink) return;
openBackdrop();
try {
const response = await api.get(downloadLink, {
responseType: "blob",
});
const contentDisposition = response.headers["content-disposition"];
let fileName = "document";
if (contentDisposition) {
const match = contentDisposition.match(
/filename\*?=(?:UTF-8''|"?)([^";]+)/i,
);
if (match?.[1]) {
fileName = decodeURIComponent(match[1].replace(/"/g, ""));
}
} else {
const urlParts = downloadLink.split("/").filter(Boolean);
const lastPart = urlParts[urlParts.length - 1];
if (lastPart && lastPart.includes(".")) {
fileName = lastPart;
}
}
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
showToast("فایل با موفقیت دانلود شد", "success");
} catch {
showToast("خطا در دانلود فایل", "error");
} finally {
closeBackdrop();
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
if (limitSize && file.size > limitSize * 1024 * 1024) {
showToast(`حداکثر حجم فایل ${limitSize} مگابایت است`, "error");
return;
}
openBackdrop();
try {
const formData = new FormData();
formData.append(payloadKey, file);
await api.post(uploadLink, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
setUploadedFileName(file.name);
showToast("فایل با موفقیت آپلود شد", "success");
onUploadSuccess?.();
} catch {
showToast("خطا در آپلود فایل", "error");
} finally {
closeBackdrop();
}
};
const acceptString =
validFiles.length > 0 ? buildAcceptString(validFiles) : undefined;
return (
<>
<div
className="inline-flex items-center"
onContextMenu={handleContextMenu}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept={acceptString}
/>
<button
type="button"
onClick={handleDownload}
disabled={!downloadLink || !ableToSee()}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-r-lg border border-l-0 border-primary-300 dark:border-dark-400 bg-primary-50 dark:bg-dark-600 text-primary-700 dark:text-primary-200 hover:bg-primary-100 dark:hover:bg-dark-500 transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<ArrowDownTrayIcon className="w-4 h-4" />
<span>دانلود</span>
</button>
<button
type="button"
onClick={handleUploadClick}
disabled={!uploadLink || !ableToSee()}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-l-lg border border-primary-300 dark:border-dark-400 bg-primary-600 dark:bg-primary-700 text-white hover:bg-primary-500 dark:hover:bg-primary-800 transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
{uploadedFileName ? (
<CheckCircleIcon className="w-4 h-4 text-green-300" />
) : (
<ArrowUpTrayIcon className="w-4 h-4" />
)}
<span>آپلود</span>
</button>
</div>
{contextMenu && page && access && (
<RolesContextMenu
page={page}
access={access}
position={contextMenu}
onClose={() => setContextMenu(null)}
/>
)}
</>
);
};

View File

@@ -7,6 +7,7 @@ import { getNestedValue } from "../../utils/getNestedValue";
type FormEnterLocationsProps = {
title: string;
api: string;
size?: "small" | "medium" | "large";
error?: boolean;
errorMessage?: any;
multiple?: boolean;
@@ -31,6 +32,7 @@ type FormEnterLocationsProps = {
export const FormApiBasedAutoComplete = ({
title,
api,
size,
error,
errorMessage,
onChange,
@@ -94,7 +96,7 @@ export const FormApiBasedAutoComplete = ({
if (filterAddress && filterValue) {
data = apiData.results?.filter(
(item: any) =>
!filterValue.includes(getNestedValue(item, filterAddress))
!filterValue.includes(getNestedValue(item, filterAddress)),
);
} else {
data = apiData.results;
@@ -119,10 +121,10 @@ export const FormApiBasedAutoComplete = ({
valueField === "page"
? getFaPermissions(option[valueField])
: typeof valueField === "string"
? option[valueField]
: getNestedValue(option, valueField),
"v1"
)
? option[valueField]
: getNestedValue(option, valueField),
"v1",
),
)
.replace(
/v2/g,
@@ -132,8 +134,8 @@ export const FormApiBasedAutoComplete = ({
? option[valueField2]
: getNestedValue(option, valueField2)
: "",
"v2"
)
"v2",
),
)
.replace(
/v3/g,
@@ -143,15 +145,15 @@ export const FormApiBasedAutoComplete = ({
? option[valueField3]
: getNestedValue(option, valueField3)
: "",
"v3"
)
"v3",
),
)
: `${
valueField === "page"
? getFaPermissions(option[valueField])
: typeof valueField === "string"
? option[valueField]
: getNestedValue(option, valueField)
? option[valueField]
: getNestedValue(option, valueField)
} ${
valueField2
? " - " +
@@ -208,7 +210,7 @@ export const FormApiBasedAutoComplete = ({
setData(finalData);
const actualDataItems = finalData.filter(
(item: any) => !item.isGroupHeader && !item.disabled
(item: any) => !item.isGroupHeader && !item.disabled,
);
if (defaultKey !== undefined && defaultKey !== null) {
@@ -218,10 +220,10 @@ export const FormApiBasedAutoComplete = ({
setSelectedKeys([]);
} else {
const defaultIds = defaultKey.map((item: any) =>
typeof item === "object" ? item[keyField] : item
typeof item === "object" ? item[keyField] : item,
);
const defaultItems = actualDataItems.filter((item: any) =>
defaultIds.includes(item.key)
defaultIds.includes(item.key),
);
setSelectedKeys(defaultItems.map((item: any) => item.key));
if (onChange) {
@@ -238,11 +240,11 @@ export const FormApiBasedAutoComplete = ({
}
: {}),
};
})
}),
);
if (onChangeValue) {
onChangeValue(
defaultItems.map((item: any) => item.value.trim())
defaultItems.map((item: any) => item.value.trim()),
);
}
} else {
@@ -258,7 +260,7 @@ export const FormApiBasedAutoComplete = ({
? defaultKey[keyField]
: defaultKey;
const defaultItem = actualDataItems.find(
(item: any) => item.key === keyToFind
(item: any) => item.key === keyToFind,
);
if (defaultItem) {
setSelectedKeys([keyToFind]);
@@ -296,18 +298,18 @@ export const FormApiBasedAutoComplete = ({
const groupItemKeys = groupItems.map((item: any) => item.key);
const allGroupItemsSelected = groupItemKeys.every((key) =>
selectedKeys.includes(key)
selectedKeys.includes(key),
);
let newSelectedKeys: (string | number)[];
if (allGroupItemsSelected) {
newSelectedKeys = selectedKeys.filter(
(key) => !groupItemKeys.includes(key)
(key) => !groupItemKeys.includes(key),
);
} else {
const newKeys = groupItemKeys.filter(
(key) => !selectedKeys.includes(key)
(key) => !selectedKeys.includes(key),
);
newSelectedKeys = [...selectedKeys, ...newKeys];
}
@@ -320,7 +322,7 @@ export const FormApiBasedAutoComplete = ({
(item: any) =>
newSelectedKeys.includes(item.key) &&
!item.isGroupHeader &&
!item.disabled
!item.disabled,
);
if (secondaryKey) {
@@ -329,7 +331,7 @@ export const FormApiBasedAutoComplete = ({
key1: item.key,
key2: item.secondaryKey,
...(tertiaryKey ? { key3: item.tertiaryKey } : {}),
}))
})),
);
if (onChangeValue) {
onChangeValue(selectedItems.map((item: any) => item.value.trim()));
@@ -344,6 +346,7 @@ export const FormApiBasedAutoComplete = ({
<AutoComplete
multiselect={multiple}
selectField={selectField}
size={size}
data={data}
selectedKeys={selectedKeys}
onChange={(newSelectedKeys) => {
@@ -357,7 +360,7 @@ export const FormApiBasedAutoComplete = ({
(item: any) =>
newSelectedKeys.includes(item.key) &&
!item.isGroupHeader &&
!item.disabled
!item.disabled,
);
onChange(
@@ -365,16 +368,16 @@ export const FormApiBasedAutoComplete = ({
key1: item.key,
key2: item.secondaryKey,
...(tertiaryKey ? { key3: item.tertiaryKey } : {}),
}))
})),
);
if (onChangeValue) {
onChangeValue(
selectedItems.map((item: any) => item.value.trim())
selectedItems.map((item: any) => item.value.trim()),
);
}
} else {
const validKeys = newSelectedKeys.filter(
(key) => !String(key).startsWith("__group__")
(key) => !String(key).startsWith("__group__"),
);
onChange(validKeys);
}
@@ -384,7 +387,7 @@ export const FormApiBasedAutoComplete = ({
(item: any) =>
item.key === newSelectedKeys[0] &&
!item.isGroupHeader &&
!item.disabled
!item.disabled,
);
if (onChangeValue) {
onChangeValue({

View File

@@ -21,16 +21,17 @@ import { getToastResponse } from "../../data/getToastResponse";
import { useUserProfileStore } from "../../context/zustand-store/userStore";
import { useState } from "react";
import Checkbox from "../../components/CheckBox/CheckBox";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
const schema = z.object({
name: zValidateString("نام سازمان"),
national_unique_id: zValidateString("شناسه کشوری"),
address: zValidateStringOptional("آدرس"),
field_of_activity: zValidateAutoComplete("حوزه فعالیت"),
province: zValidateNumber("استان"),
city: zValidateNumber("شهر"),
organization: zValidateNumberOptional("سازمان"),
organizationType: zValidateNumber("سازمان"),
unique_unit_identity: zValidateStringOptional("شناسه یکتا واحد"),
is_repeatable: z.boolean(),
free_visibility_by_scope: z.boolean(),
});
@@ -75,8 +76,8 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
resolver: zodResolver(schema),
defaultValues: {
name: item?.name || "",
address: item?.address || "",
national_unique_id: item?.national_unique_id || "",
unique_unit_identity: item?.unique_unit_identity || "",
free_visibility_by_scope: item?.free_visibility_by_scope || false,
field_of_activity:
item && item?.field_of_activity !== "EM"
@@ -95,22 +96,57 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
city: string | any;
}>({ province: "", city: "" });
const [addresses, setAddresses] = useState<
{ postal_code: string; address: string }[]
>(
item?.addresses?.length
? item.addresses.map((a: any) => ({
postal_code: a.postal_code || "",
address: a.address || "",
}))
: [{ postal_code: "", address: "" }],
);
const handleAddAddress = () => {
setAddresses((prev) => [...prev, { postal_code: "", address: "" }]);
};
const handleRemoveAddress = (index: number) => {
setAddresses((prev) => prev.filter((_, i) => i !== index));
};
const handleAddressChange = (
index: number,
field: "postal_code" | "address",
value: string,
) => {
setAddresses((prev) =>
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
);
};
const onSubmit = async (data: FormValues) => {
try {
await mutation.mutateAsync({
addresses: addresses.filter(
(a) => a.postal_code.trim() || a.address.trim(),
),
organization: {
name: `${data?.name} ${
data?.is_repeatable
? ""
: data.field_of_activity[0] === "CI"
? LocationValues.city
: LocationValues.province
? LocationValues.city
: LocationValues.province
}`,
...(data.organizationType !== undefined && {
type: data.organizationType,
}),
national_unique_id: data?.national_unique_id,
...(data?.unique_unit_identity && {
unique_unit_identity: data.unique_unit_identity,
}),
province: data?.province,
city: data?.city,
...(data.organization && {
@@ -118,7 +154,6 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
}),
field_of_activity: data.field_of_activity[0],
free_visibility_by_scope: data.free_visibility_by_scope,
address: data.address,
},
});
showToast(getToastResponse(item, "سازمان"), "success");
@@ -128,12 +163,12 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
if (error?.status === 403) {
showToast(
error?.response?.data?.message || "این سازمان تکراری است!",
"error"
"error",
);
} else {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error"
"error",
);
}
}
@@ -223,6 +258,21 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
)}
/>
<Controller
name="unique_unit_identity"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="شناسه یکتا واحد (اختیاری)"
value={field.value}
onChange={field.onChange}
error={!!errors.unique_unit_identity}
helperText={errors.unique_unit_identity?.message}
/>
)}
/>
<Controller
name="province"
control={control}
@@ -258,7 +308,7 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
defaultKey={item?.parent_organization?.id}
title="سازمان والد (اختیاری)"
api={`auth/api/v1/organization/organizations_by_province?province=${getValues(
"province"
"province",
)}`}
keyField="id"
valueField="name"
@@ -273,20 +323,53 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
)}
/>
<Controller
name="address"
control={control}
render={({ field }) => (
<Textfield
fullWidth
placeholder="آدرس (اختیاری)"
value={field.value}
onChange={field.onChange}
error={!!errors.address}
helperText={errors.address?.message}
/>
)}
/>
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">آدرسها</span>
<button
type="button"
onClick={handleAddAddress}
className="flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 transition-colors"
>
<PlusIcon className="w-4 h-4" />
افزودن آدرس
</button>
</div>
{addresses.map((addr, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 border border-gray-200 rounded-lg"
>
<div className="flex flex-col gap-2 flex-1">
<Textfield
fullWidth
placeholder="کد پستی"
value={addr.postal_code}
onChange={(e) =>
handleAddressChange(index, "postal_code", e.target.value)
}
/>
<Textfield
fullWidth
placeholder="آدرس"
value={addr.address}
onChange={(e) =>
handleAddressChange(index, "address", e.target.value)
}
/>
</div>
{addresses.length > 1 && (
<button
type="button"
onClick={() => handleRemoveAddress(index)}
className="mt-2 text-red-500 hover:text-red-700 transition-colors shrink-0"
>
<TrashIcon className="w-5 h-5" />
</button>
)}
</div>
))}
</div>
<Controller
name="free_visibility_by_scope"

View File

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

View File

@@ -0,0 +1,290 @@
import { z } from "zod";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Grid } from "../../components/Grid/Grid";
import Button from "../../components/Button/Button";
import Textfield from "../../components/Textfeild/Textfeild";
import { FormApiBasedAutoComplete } from "../../components/FormItems/FormApiBasedAutoComplete";
import AutoComplete from "../../components/AutoComplete/AutoComplete";
import { zValidateAutoComplete } from "../../data/getFormTypeErrors";
import { useApiMutation, useApiRequest } from "../../utils/useApiRequest";
import { useToast } from "../../hooks/useToast";
import { useModalStore } from "../../context/zustand-store/appStore";
const schema = z.object({
organization: zValidateAutoComplete("سازمان"),
});
type FormValues = z.infer<typeof schema>;
type ParentDistItem = {
id: number;
dist_identity?: number;
batch_identity: string | number | null;
species_code: number;
maxCount: number;
label?: string;
};
export const DistributeFromDistribution = ({
item,
getData,
isEdit,
parentDistributions,
}: any) => {
const showToast = useToast();
const { closeModal } = useModalStore();
const [dists, setDists] = useState<ParentDistItem[]>([]);
const [selectedSpeciesKeys, setSelectedSpeciesKeys] = useState<
(string | number)[]
>([]);
const [counts, setCounts] = useState<Record<number, number | "">>({});
const {
control,
handleSubmit,
setValue,
trigger,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
organization: [],
},
});
const { data: batchDetail } = useApiRequest({
api: `/tag/web/api/v1/tag_distribution_batch/${item?.id}/`,
method: "get",
queryKey: ["tagDistributionBatchDetail", item?.id],
enabled: !!item?.id && !isEdit,
});
const mutation = useApiMutation({
api: `/tag/web/api/v1/tag_distribution/${item?.id}/${isEdit ? "edit_" : ""}distribute_distribution/`,
method: isEdit ? "put" : "post",
});
const { data: speciesData } = useApiRequest({
api: "/livestock/web/api/v1/livestock_species",
method: "get",
params: { page: 1, pageSize: 1000 },
queryKey: ["species"],
});
useEffect(() => {
const sourceDistributions = isEdit
? parentDistributions
: item?.distributions?.length
? item.distributions
: batchDetail?.distributions;
if (!sourceDistributions?.length) {
setDists([]);
setCounts({});
return;
}
const parentDists: ParentDistItem[] = sourceDistributions.map((d: any) => {
const maxCount = d?.remaining_number || 0;
return {
id: d?.id ?? d?.dist_identity,
dist_identity: d?.dist_identity,
batch_identity: d?.batch_identity ?? null,
species_code: d?.species_code,
maxCount: Number(maxCount) || 0,
label:
d?.serial_from != null && d?.serial_to != null
? `از ${d.serial_from} تا ${d.serial_to}`
: undefined,
};
});
setDists(parentDists);
if (isEdit && item?.distributions?.length) {
const defaultCounts: Record<number, number | ""> = {};
const defaultSpeciesKeys: (string | number)[] = [];
parentDists.forEach((pd) => {
const childDist = item.distributions.find(
(cd: any) =>
cd.parent_tag_distribution === pd.id ||
cd.species_code === pd.species_code,
);
if (childDist) {
defaultCounts[pd.id] = childDist.total_tag_count || 0;
if (!defaultSpeciesKeys.includes(pd.species_code)) {
defaultSpeciesKeys.push(pd.species_code);
}
}
});
setCounts(defaultCounts);
setSelectedSpeciesKeys(defaultSpeciesKeys);
} else {
setCounts({});
setSelectedSpeciesKeys([]);
}
}, [item?.distributions, batchDetail?.distributions, parentDistributions]);
const speciesOptions = () =>
speciesData?.results?.map((opt: any) => ({
key: opt?.value,
value: opt?.name,
})) ?? [];
const getSpeciesName = (speciesCode: number) =>
speciesOptions().find((s: any) => s.key === speciesCode)?.value ?? "نامشخص";
const visibleDists = dists.filter((d) =>
selectedSpeciesKeys.includes(d.species_code),
);
const onSubmit = async (data: FormValues) => {
const distsPayload = visibleDists
.filter((d) => {
const c = counts[d.id];
return c !== "" && c !== undefined && c !== null && Number(c) > 0;
})
.map((d) => {
const fromItem = item?.distributions?.find(
(x: any) => x.id === d.id || x.dist_identity === d.id,
);
const batch_identity =
fromItem != null ? fromItem.batch_identity : d.batch_identity;
return {
parent_tag_distribution: d.id,
batch_identity: batch_identity != null ? batch_identity : null,
species_code: d.species_code,
count: Number(counts[d.id] ?? 0),
};
});
if (distsPayload.length === 0) {
showToast("حداقل یک گونه با تعداد معتبر وارد کنید", "error");
return;
}
try {
await mutation.mutateAsync({
assigned_org: data.organization[0],
parent_distribution_batch: item.id,
dists: distsPayload,
});
showToast(
isEdit
? "ویرایش توزیع با موفقیت انجام شد"
: "توزیع از توزیع با موفقیت انجام شد",
"success",
);
getData();
closeModal();
} catch (error: any) {
showToast(
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
"error",
);
}
};
const handleCountChange = (distId: number, value: number | "") => {
setCounts((prev) => ({ ...prev, [distId]: value }));
};
const isValidCount = (dist: ParentDistItem) => {
const c = counts[dist.id];
if (c === "" || c === undefined || c === null) return false;
const num = Number(c);
return num > 0 && num <= dist.maxCount;
};
const speciesOptionsFromParent = () => {
const uniqueSpecies = Array.from(
new Map(dists.map((d) => [d.species_code, d])).values(),
);
return uniqueSpecies.map((d) => ({
key: d.species_code,
value: getSpeciesName(d.species_code),
}));
};
const hasValidDists =
visibleDists.length > 0 && visibleDists.every((d) => isValidCount(d));
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container column className="gap-3">
<Controller
name="organization"
control={control}
render={() => (
<FormApiBasedAutoComplete
defaultKey={item?.assigned_org?.id}
title="انتخاب سازمان (دریافت‌کننده)"
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
keyField="id"
valueField="name"
error={!!errors.organization}
errorMessage={errors.organization?.message}
onChange={(r) => {
setValue("organization", [r]);
trigger("organization");
}}
/>
)}
/>
{dists.length > 0 && speciesData?.results && (
<>
<AutoComplete
data={speciesOptionsFromParent()}
multiselect
selectedKeys={selectedSpeciesKeys}
onChange={(keys: (string | number)[]) => {
setSelectedSpeciesKeys(keys);
}}
title="گونه"
/>
{visibleDists.map((dist) => {
const countVal = counts[dist.id];
const numCount =
countVal !== "" && countVal !== undefined && countVal !== null
? Number(countVal)
: null;
const isOverMax = numCount !== null && numCount > dist.maxCount;
const isEmpty = countVal === "" || countVal === undefined;
const helperText = isOverMax
? `تعداد نباید بیشتر از ${dist.maxCount.toLocaleString()} باشد`
: isEmpty
? "لطفا تعداد را وارد کنید"
: undefined;
return (
<Textfield
key={dist.id}
fullWidth
formattedNumber
placeholder={`تعداد ${getSpeciesName(dist.species_code)}${dist.label ? ` (${dist.label})` : ""} — حداکثر: ${dist.maxCount.toLocaleString()}`}
value={counts[dist.id] ?? ""}
onChange={(e) =>
handleCountChange(dist.id, Number(e.target.value))
}
error={isOverMax}
helperText={helperText}
/>
);
})}
</>
)}
<Button disabled={!hasValidDists} type="submit">
ثبت
</Button>
</Grid>
</form>
);
};

View File

@@ -16,12 +16,16 @@ 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";
import { DocumentOperation } from "../../components/DocumentOperation/DocumentOperation";
import { DocumentDownloader } from "../../components/DocumentDownloader/DocumentDownloader";
import { useUserProfileStore } from "../../context/zustand-store/userStore";
export default function TagActiveDistributions() {
const { openModal } = useModalStore();
@@ -38,12 +42,44 @@ export default function TagActiveDistributions() {
},
});
const { profile } = useUserProfileStore();
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false",
method: "get",
queryKey: ["tagDistributionActivesDashboard"],
});
const showAssignDocColumn =
(profile?.role?.type?.key === "ADM" ||
tagsData?.results?.some(
(item: any) => profile?.organization?.id === item?.assigned_org?.id,
)) ??
false;
const AbleToSeeAssignDoc = (item: any) => {
if (
profile?.role?.type?.key === "ADM" ||
profile?.organization?.id === item?.assigned_org?.id
) {
return (
<DocumentOperation
key={item?.id}
downloadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/distribution_pdf_view/`}
payloadKey="dist_exit_document"
// validFiles={["pdf"]}
page="tag_distribution"
access="Upload-Assign-Document"
uploadLink={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/assign_document/`}
onUploadSuccess={handleUpdate}
limitSize={3}
/>
);
} else {
return "-";
}
};
const handleUpdate = () => {
refetch();
updateDashboard();
@@ -74,6 +110,8 @@ export default function TagActiveDistributions() {
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">
@@ -84,6 +122,19 @@ export default function TagActiveDistributions() {
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" />
@@ -111,25 +162,67 @@ export default function TagActiveDistributions() {
{opt?.total_tag_count?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<SparklesIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
گونه:
پلاک های توزیع شده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{speciesMap[opt?.species_code] ?? "-"}
{opt?.distributed_number?.toLocaleString()}
</Typography>
</Grid>
<Grid container className="gap-2 items-center">
<CubeIcon className="w-5 h-5 text-gray-500 dark:text-gray-300" />
<Typography variant="body2" className="font-medium">
پلاک های باقیمانده:
</Typography>
<Typography
variant="body2"
className="text-gray-700 dark:text-gray-300"
>
{opt?.remaining_number?.toLocaleString()}
</Typography>
</Grid>
</Grid>
))}
</Grid>
</ShowMoreInfo>,
...(showAssignDocColumn ? [AbleToSeeAssignDoc(item)] : []),
<DocumentDownloader
key={index}
link={item?.warehouse_exit_doc}
title="دانلود"
/>,
item?.exit_doc_status ? (
"تایید شده"
) : (
<Button
page="tag_distribution"
access="Accept-Assign-Document"
size="small"
disabled={item?.exit_doc_status}
onClick={() => {
openModal({
title: "تایید سند خروج",
content: (
<BooleanQuestion
api={`/tag/web/api/v1/tag_distribution_batch/${item?.id}/accept_exit_doc/`}
method="post"
getData={handleUpdate}
title="آیا از تایید سند خروج مطمئنید؟"
/>
),
});
}}
>
تایید سند خروج
</Button>
),
<Popover key={index}>
<Tooltip title="جزيٓیات توزیع" position="right">
<Tooltip title="جزئیات توزیع" position="right">
<Button
variant="detail"
page="tag_distribution_detail"
@@ -163,6 +256,24 @@ export default function TagActiveDistributions() {
}}
/>
</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"
@@ -272,8 +383,13 @@ export default function TagActiveDistributions() {
"توزیع کننده",
"دریافت کننده",
"تعداد کل پلاک",
"پلاک های توزیع شده",
"پلاک های باقیمانده",
"نوع توزیع",
"جزئیات توزیع",
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
"سند خروج از انبار",
"تایید سند خروج",
"عملیات",
]}
rows={tagsTableData}

View File

@@ -1 +1 @@
02.62
02.66