From 071c3e159b6f7f2bdb08a3bb5b3b7feafa442755 Mon Sep 17 00:00:00 2001 From: wixarm Date: Sun, 8 Feb 2026 16:52:00 +0330 Subject: [PATCH] feat: document operation --- .../DocumentOperation/DocumentOperation.tsx | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/components/DocumentOperation/DocumentOperation.tsx diff --git a/src/components/DocumentOperation/DocumentOperation.tsx b/src/components/DocumentOperation/DocumentOperation.tsx new file mode 100644 index 0000000..2e2d513 --- /dev/null +++ b/src/components/DocumentOperation/DocumentOperation.tsx @@ -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(null); + const [uploadedFileName, setUploadedFileName] = useState(""); + 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) => { + 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) => { + 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 ( + <> +
+ + + + + +
+ + {contextMenu && page && access && ( + setContextMenu(null)} + /> + )} + + ); +};