Compare commits
6 Commits
f8d2da4f28
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 908a69ce0e | |||
| a818683247 | |||
| 95780cfbc9 | |||
| 071c3e159b | |||
| 90f51c6899 | |||
| e967329108 |
@@ -1,11 +1,22 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "@tanstack/react-router";
|
import { useParams } from "@tanstack/react-router";
|
||||||
import { Bars3Icon, CubeIcon, SparklesIcon } from "@heroicons/react/24/outline";
|
import { Bars3Icon, CubeIcon, SparklesIcon } from "@heroicons/react/24/outline";
|
||||||
import { useApiRequest } from "../utils/useApiRequest";
|
import { useApiRequest } from "../utils/useApiRequest";
|
||||||
|
import { useModalStore } from "../context/zustand-store/appStore";
|
||||||
import { formatJustDate, formatJustTime } from "../utils/formatTime";
|
import { formatJustDate, formatJustTime } from "../utils/formatTime";
|
||||||
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
|
import ShowMoreInfo from "../components/ShowMoreInfo/ShowMoreInfo";
|
||||||
import { Grid } from "../components/Grid/Grid";
|
import { Grid } from "../components/Grid/Grid";
|
||||||
import Typography from "../components/Typography/Typography";
|
import Typography from "../components/Typography/Typography";
|
||||||
import Table from "../components/Table/Table";
|
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> = {
|
const speciesMap: Record<number, string> = {
|
||||||
1: "گاو",
|
1: "گاو",
|
||||||
@@ -17,14 +28,240 @@ const speciesMap: Record<number, string> = {
|
|||||||
|
|
||||||
export default function TagDistribtutionDetails() {
|
export default function TagDistribtutionDetails() {
|
||||||
const { id } = useParams({ strict: false });
|
const { id } = useParams({ strict: false });
|
||||||
|
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}/`,
|
api: `/tag/web/api/v1/tag_distribution_batch/${id}/`,
|
||||||
method: "get",
|
method: "get",
|
||||||
queryKey: ["tagBatchInnerDashboard", id],
|
queryKey: ["tagBatchInnerDashboard", id],
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 "-";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
const dist = data?.distributions;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -145,6 +382,31 @@ export default function TagDistribtutionDetails() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
className="mt-2"
|
||||||
|
onChange={setChildTableInfo}
|
||||||
|
count={childData?.count || 0}
|
||||||
|
isPaginated
|
||||||
|
title="توزیع های مجدد"
|
||||||
|
columns={[
|
||||||
|
"ردیف",
|
||||||
|
"شناسه توزیع",
|
||||||
|
"تاریخ ثبت",
|
||||||
|
"توزیع کننده",
|
||||||
|
"دریافت کننده",
|
||||||
|
"تعداد کل پلاک",
|
||||||
|
"پلاک های توزیع شده",
|
||||||
|
"پلاک های باقیمانده",
|
||||||
|
"نوع توزیع",
|
||||||
|
"جزئیات توزیع",
|
||||||
|
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
|
||||||
|
"سند خروج از انبار",
|
||||||
|
"تایید سند خروج",
|
||||||
|
"عملیات",
|
||||||
|
]}
|
||||||
|
rows={childTableData}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
229
src/components/DocumentOperation/DocumentOperation.tsx
Normal file
229
src/components/DocumentOperation/DocumentOperation.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = "",
|
||||||
|
}: 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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
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>{uploadedFileName ? "آپلود شده" : "آپلود"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{contextMenu && page && access && (
|
||||||
|
<RolesContextMenu
|
||||||
|
page={page}
|
||||||
|
access={access}
|
||||||
|
position={contextMenu}
|
||||||
|
onClose={() => setContextMenu(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
zValidateNumber,
|
zValidateNumber,
|
||||||
zValidateNumberOptional,
|
zValidateNumberOptional,
|
||||||
zValidateString,
|
zValidateString,
|
||||||
zValidateStringOptional,
|
|
||||||
} from "../../data/getFormTypeErrors";
|
} from "../../data/getFormTypeErrors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useApiMutation } from "../../utils/useApiRequest";
|
import { useApiMutation } from "../../utils/useApiRequest";
|
||||||
@@ -21,11 +20,11 @@ import { getToastResponse } from "../../data/getToastResponse";
|
|||||||
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
import { useUserProfileStore } from "../../context/zustand-store/userStore";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Checkbox from "../../components/CheckBox/CheckBox";
|
import Checkbox from "../../components/CheckBox/CheckBox";
|
||||||
|
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: zValidateString("نام سازمان"),
|
name: zValidateString("نام سازمان"),
|
||||||
national_unique_id: zValidateString("شناسه کشوری"),
|
national_unique_id: zValidateString("شناسه کشوری"),
|
||||||
address: zValidateStringOptional("آدرس"),
|
|
||||||
field_of_activity: zValidateAutoComplete("حوزه فعالیت"),
|
field_of_activity: zValidateAutoComplete("حوزه فعالیت"),
|
||||||
province: zValidateNumber("استان"),
|
province: zValidateNumber("استان"),
|
||||||
city: zValidateNumber("شهر"),
|
city: zValidateNumber("شهر"),
|
||||||
@@ -75,7 +74,6 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
|||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: item?.name || "",
|
name: item?.name || "",
|
||||||
address: item?.address || "",
|
|
||||||
national_unique_id: item?.national_unique_id || "",
|
national_unique_id: item?.national_unique_id || "",
|
||||||
free_visibility_by_scope: item?.free_visibility_by_scope || false,
|
free_visibility_by_scope: item?.free_visibility_by_scope || false,
|
||||||
field_of_activity:
|
field_of_activity:
|
||||||
@@ -95,9 +93,41 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
|||||||
city: string | any;
|
city: string | any;
|
||||||
}>({ province: "", city: "" });
|
}>({ 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) => {
|
const onSubmit = async (data: FormValues) => {
|
||||||
try {
|
try {
|
||||||
await mutation.mutateAsync({
|
await mutation.mutateAsync({
|
||||||
|
addresses: addresses.filter(
|
||||||
|
(a) => a.postal_code.trim() || a.address.trim(),
|
||||||
|
),
|
||||||
organization: {
|
organization: {
|
||||||
name: `${data?.name} ${
|
name: `${data?.name} ${
|
||||||
data?.is_repeatable
|
data?.is_repeatable
|
||||||
@@ -118,7 +148,6 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
|||||||
}),
|
}),
|
||||||
field_of_activity: data.field_of_activity[0],
|
field_of_activity: data.field_of_activity[0],
|
||||||
free_visibility_by_scope: data.free_visibility_by_scope,
|
free_visibility_by_scope: data.free_visibility_by_scope,
|
||||||
address: data.address,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
showToast(getToastResponse(item, "سازمان"), "success");
|
showToast(getToastResponse(item, "سازمان"), "success");
|
||||||
@@ -128,12 +157,12 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
|||||||
if (error?.status === 403) {
|
if (error?.status === 403) {
|
||||||
showToast(
|
showToast(
|
||||||
error?.response?.data?.message || "این سازمان تکراری است!",
|
error?.response?.data?.message || "این سازمان تکراری است!",
|
||||||
"error"
|
"error",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showToast(
|
showToast(
|
||||||
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
|
error?.response?.data?.message || "خطا در ثبت اطلاعات!",
|
||||||
"error"
|
"error",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +287,7 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
|||||||
defaultKey={item?.parent_organization?.id}
|
defaultKey={item?.parent_organization?.id}
|
||||||
title="سازمان والد (اختیاری)"
|
title="سازمان والد (اختیاری)"
|
||||||
api={`auth/api/v1/organization/organizations_by_province?province=${getValues(
|
api={`auth/api/v1/organization/organizations_by_province?province=${getValues(
|
||||||
"province"
|
"province",
|
||||||
)}`}
|
)}`}
|
||||||
keyField="id"
|
keyField="id"
|
||||||
valueField="name"
|
valueField="name"
|
||||||
@@ -273,20 +302,53 @@ export const AddOrganization = ({ getData, item }: AddPageProps) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<div className="flex flex-col gap-2 w-full">
|
||||||
name="address"
|
<div className="flex items-center justify-between">
|
||||||
control={control}
|
<span className="text-sm font-medium text-gray-700">آدرسها</span>
|
||||||
render={({ field }) => (
|
<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
|
<Textfield
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder="آدرس (اختیاری)"
|
placeholder="کد پستی"
|
||||||
value={field.value}
|
value={addr.postal_code}
|
||||||
onChange={field.onChange}
|
onChange={(e) =>
|
||||||
error={!!errors.address}
|
handleAddressChange(index, "postal_code", e.target.value)
|
||||||
helperText={errors.address?.message}
|
}
|
||||||
/>
|
/>
|
||||||
|
<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
|
<Controller
|
||||||
name="free_visibility_by_scope"
|
name="free_visibility_by_scope"
|
||||||
|
|||||||
@@ -59,7 +59,14 @@ export const OrganizationsList = () => {
|
|||||||
: "نامشخص",
|
: "نامشخص",
|
||||||
item?.province?.name,
|
item?.province?.name,
|
||||||
item?.city?.name,
|
item?.city?.name,
|
||||||
item?.address || "-",
|
<ShowMoreInfo
|
||||||
|
key={`address-${i}`}
|
||||||
|
title="آدرسها"
|
||||||
|
disabled={!item?.addresses?.length}
|
||||||
|
data={item?.addresses}
|
||||||
|
columns={["کد پستی", "آدرس"]}
|
||||||
|
accessKeys={[["postal_code"], ["address"]]}
|
||||||
|
/>,
|
||||||
<ShowMoreInfo
|
<ShowMoreInfo
|
||||||
key={i}
|
key={i}
|
||||||
title="اطلاعات حساب"
|
title="اطلاعات حساب"
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ type ParentDistItem = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DistributeFromDistribution = ({ item, getData }: any) => {
|
export const DistributeFromDistribution = ({
|
||||||
|
item,
|
||||||
|
getData,
|
||||||
|
isEdit,
|
||||||
|
parentDistributions,
|
||||||
|
}: any) => {
|
||||||
const showToast = useToast();
|
const showToast = useToast();
|
||||||
const { closeModal } = useModalStore();
|
const { closeModal } = useModalStore();
|
||||||
|
|
||||||
@@ -54,12 +59,12 @@ export const DistributeFromDistribution = ({ item, getData }: any) => {
|
|||||||
api: `/tag/web/api/v1/tag_distribution_batch/${item?.id}/`,
|
api: `/tag/web/api/v1/tag_distribution_batch/${item?.id}/`,
|
||||||
method: "get",
|
method: "get",
|
||||||
queryKey: ["tagDistributionBatchDetail", item?.id],
|
queryKey: ["tagDistributionBatchDetail", item?.id],
|
||||||
enabled: !!item?.id,
|
enabled: !!item?.id && !isEdit,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutation = useApiMutation({
|
const mutation = useApiMutation({
|
||||||
api: `/tag/web/api/v1/tag_distribution/${item?.id}/distribute_distribution/`,
|
api: `/tag/web/api/v1/tag_distribution/${item?.id}/${isEdit ? "edit_" : ""}distribute_distribution/`,
|
||||||
method: "post",
|
method: isEdit ? "put" : "post",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: speciesData } = useApiRequest({
|
const { data: speciesData } = useApiRequest({
|
||||||
@@ -70,7 +75,9 @@ export const DistributeFromDistribution = ({ item, getData }: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sourceDistributions = item?.distributions?.length
|
const sourceDistributions = isEdit
|
||||||
|
? parentDistributions
|
||||||
|
: item?.distributions?.length
|
||||||
? item.distributions
|
? item.distributions
|
||||||
: batchDetail?.distributions;
|
: batchDetail?.distributions;
|
||||||
|
|
||||||
@@ -96,9 +103,32 @@ export const DistributeFromDistribution = ({ item, getData }: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setDists(parentDists);
|
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({});
|
setCounts({});
|
||||||
setSelectedSpeciesKeys([]);
|
setSelectedSpeciesKeys([]);
|
||||||
}, [item?.distributions, batchDetail?.distributions]);
|
}
|
||||||
|
}, [item?.distributions, batchDetail?.distributions, parentDistributions]);
|
||||||
|
|
||||||
const speciesOptions = () =>
|
const speciesOptions = () =>
|
||||||
speciesData?.results?.map((opt: any) => ({
|
speciesData?.results?.map((opt: any) => ({
|
||||||
@@ -145,7 +175,12 @@ export const DistributeFromDistribution = ({ item, getData }: any) => {
|
|||||||
dists: distsPayload,
|
dists: distsPayload,
|
||||||
});
|
});
|
||||||
|
|
||||||
showToast("توزیع از توزیع با موفقیت انجام شد", "success");
|
showToast(
|
||||||
|
isEdit
|
||||||
|
? "ویرایش توزیع با موفقیت انجام شد"
|
||||||
|
: "توزیع از توزیع با موفقیت انجام شد",
|
||||||
|
"success",
|
||||||
|
);
|
||||||
getData();
|
getData();
|
||||||
closeModal();
|
closeModal();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -188,6 +223,7 @@ export const DistributeFromDistribution = ({ item, getData }: any) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<FormApiBasedAutoComplete
|
<FormApiBasedAutoComplete
|
||||||
|
defaultKey={item?.assigned_org?.id}
|
||||||
title="انتخاب سازمان (دریافتکننده)"
|
title="انتخاب سازمان (دریافتکننده)"
|
||||||
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
|
api={`auth/api/v1/organization/organizations_by_province?exclude=PSP&province=${item?.assigner_org?.province}`}
|
||||||
keyField="id"
|
keyField="id"
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import { TableButton } from "../../components/TableButton/TableButton";
|
|||||||
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
|
import { DistributionSpeciesModal } from "./DistributionSpeciesModal";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { TAG_DISTRIBUTION } from "../../routes/paths";
|
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() {
|
export default function TagActiveDistributions() {
|
||||||
const { openModal } = useModalStore();
|
const { openModal } = useModalStore();
|
||||||
@@ -39,12 +42,43 @@ export default function TagActiveDistributions() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { profile } = useUserProfileStore();
|
||||||
|
|
||||||
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
|
const { data: tagDashboardData, refetch: updateDashboard } = useApiRequest({
|
||||||
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false",
|
api: "/tag/web/api/v1/tag_distribution_batch/main_dashboard/?is_closed=false",
|
||||||
method: "get",
|
method: "get",
|
||||||
queryKey: ["tagDistributionActivesDashboard"],
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
refetch();
|
refetch();
|
||||||
updateDashboard();
|
updateDashboard();
|
||||||
@@ -155,8 +189,39 @@ export default function TagActiveDistributions() {
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</ShowMoreInfo>,
|
</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}>
|
<Popover key={index}>
|
||||||
<Tooltip title="جزيٓیات توزیع" position="right">
|
<Tooltip title="جزئیات توزیع" position="right">
|
||||||
<Button
|
<Button
|
||||||
variant="detail"
|
variant="detail"
|
||||||
page="tag_distribution_detail"
|
page="tag_distribution_detail"
|
||||||
@@ -321,6 +386,9 @@ export default function TagActiveDistributions() {
|
|||||||
"پلاک های باقیمانده",
|
"پلاک های باقیمانده",
|
||||||
"نوع توزیع",
|
"نوع توزیع",
|
||||||
"جزئیات توزیع",
|
"جزئیات توزیع",
|
||||||
|
...(showAssignDocColumn ? ["امضا سند خروج از انبار"] : []),
|
||||||
|
"سند خروج از انبار",
|
||||||
|
"تایید سند خروج",
|
||||||
"عملیات",
|
"عملیات",
|
||||||
]}
|
]}
|
||||||
rows={tagsTableData}
|
rows={tagsTableData}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
02.64
|
02.65
|
||||||
|
|||||||
Reference in New Issue
Block a user