From 1d0526cddadb79e65ec739b3c3752f7de6907d95 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Tue, 30 Sep 2025 17:21:35 +0800 Subject: [PATCH] no message --- src/pages/pdf/PdfFormUpAndDown/index.js | 16 + src/pages/pdf/PdfSearchPage/PdfTable.js | 404 +++++++++++++----- .../pdf/PdfSearchPage/SelectTemplateWindow.js | 99 +++-- src/themes/colorConst.js | 4 + src/utils/ApiPathConst.js | 2 + 5 files changed, 385 insertions(+), 140 deletions(-) diff --git a/src/pages/pdf/PdfFormUpAndDown/index.js b/src/pages/pdf/PdfFormUpAndDown/index.js index ed17099..e3cd339 100644 --- a/src/pages/pdf/PdfFormUpAndDown/index.js +++ b/src/pages/pdf/PdfFormUpAndDown/index.js @@ -3,6 +3,7 @@ import { Button, Grid } from '@mui/material'; import axios from 'axios'; import { apiPath, appURL } from "../../../auth/utils"; import { + POST_MERGE_PDF, GET_PDF_TEMPLATE_PATH, GET_PDF_PATH, // Still potentially used for fetching record data if needed, but not PDF content for editor POST_UPLOAD_PDF_PATH @@ -254,6 +255,12 @@ function PDF() { () => setModalOpen(false) // Cancel action ); }; + + const doMerge = async () => { + const response = await axios.get(`${apiPath}${POST_MERGE_PDF}`, { + responseType: 'blob', // Crucial: get the response as a binary blob + }); + }; const handleBack = async () => { // No PDF editor to clear now @@ -310,6 +317,15 @@ function PDF() { Cancel + + + diff --git a/src/pages/pdf/PdfSearchPage/PdfTable.js b/src/pages/pdf/PdfSearchPage/PdfTable.js index f13733a..36191aa 100644 --- a/src/pages/pdf/PdfSearchPage/PdfTable.js +++ b/src/pages/pdf/PdfSearchPage/PdfTable.js @@ -1,14 +1,30 @@ // material-ui import * as React from 'react'; +import {apiPath} from "../../../auth/utils"; +import { POST_SIG_UPLOAD1 } from "../../../utils/ApiPathConst"; +import axios from 'axios'; import { DataGrid, GridActionsCellItem, } from "@mui/x-data-grid"; import EditIcon from '@mui/icons-material/Edit'; -import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Typography, + Box, // for centering and layout + CircularProgress, // Import for loading indicator +} from '@mui/material'; import {useContext, useEffect} from "react"; import {useNavigate} from "react-router-dom"; -import {CustomNoRowsOverlay, dateComparator, getDateString} from "../../../utils/CommonFunction"; +// Note: Assuming these utility functions/components are defined elsewhere +import {CustomNoRowsOverlay, dateComparator, getDateString} from "../../../utils/CommonFunction"; import AbilityContext from "../../../components/AbilityProvider"; import {LIONER_BUTTON_THEME} from "../../../themes/colorConst"; import {ThemeProvider} from "@emotion/react"; @@ -19,6 +35,11 @@ export default function PdfTable({recordList}) { const [rows, setRows] = React.useState(recordList); const [rowModesModel] = React.useState({}); + // State for Dialog visibility, row ID, and Loading state + const [isDialogOpen, setIsDialogOpen] = React.useState(false); + const [currentRowId, setCurrentRowId] = React.useState(null); + const [isUploading, setIsUploading] = React.useState(false); + const navigate = useNavigate() const ability = useContext(AbilityContext); @@ -26,6 +47,9 @@ export default function PdfTable({recordList}) { page: 0, pageSize:10 }); + + // Ref for the hidden file input + const fileInputRef = React.useRef(null); useEffect(() => { setPaginationModel({page:0,pageSize:10}); @@ -36,16 +60,141 @@ export default function PdfTable({recordList}) { navigate(`/pdf/maintain/${id}`); }; - const handleFormUpDownClick = (id) => () => { - navigate(`/pdf/form-up-down/${id}`); + // Opens the upload dialog and sets the current row ID + const handleUploadClick = (id) => () => { + setCurrentRowId(id); + setIsDialogOpen(true); + }; + + const handleUpload2Click = (id, templateName) => () => { + // Placeholder for Upload 2 + console.log(`Uploading for row ID ${id} (Upload 2)`); + setCurrentRowId(id); + setIsDialogOpen(true); + }; + + const handleDownloadClick = (id) => () => { + + // 1. Construct the download URL with the ID query parameter + const downloadUrl = `${apiPath}/pdf/download-ff/${id}`; + + // Use axios to fetch the PDF as a Blob + axios.get(downloadUrl, { + responseType: 'blob', // IMPORTANT: Tells axios to handle the response as binary data + }) + .then((response) => { + + // 2. Extract Filename from Content-Disposition Header + const contentDisposition = response.headers['content-disposition']; + let filename = `document-${id}.pdf`; // Fallback filename + + if (contentDisposition) { + // Regex to find filename="name.pdf" or filename*=UTF-8''name.pdf + // The server should be setting the filename header correctly. + const filenameMatch = contentDisposition.match(/filename\*?=['"]?([^'"]+)/); + if (filenameMatch && filenameMatch[1]) { + // Decode URI component and remove extra quotes + filename = decodeURIComponent(filenameMatch[1].replace(/\\"/g, '')); + } + } + + // 3. Create a temporary anchor tag () to trigger the download + const blob = new Blob([response.data], { type: 'application/pdf' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + + link.href = url; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + + // 4. Clean up + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }) + .catch((error) => { + console.error(`Download failed for ID ${id}:`, error); + // Handle error response (e.g., if the backend returns a 404 or a JSON error) + alert('Failed to download the PDF file. Check server logs.'); + }); + }; + + const handleCloseDialog = () => { + setIsDialogOpen(false); + setCurrentRowId(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; // Clear the file input + } + }; + + // Function to handle file selection and API submission + const handleFileChange = async (event) => { + const file = event.target.files[0]; + + if (!file) return; + + if (file.type !== "application/pdf") { + alert("Please select a PDF file."); + event.target.value = ""; + return; + } + + const uploadUrl = apiPath + '/pdf/upload1'; + + // 1. Create FormData + const formData = new FormData(); + formData.append('file', file); + formData.append('refId', currentRowId); + formData.append('refType', "upload1"); + setIsUploading(true); + + try { + axios.post(`${apiPath}${POST_SIG_UPLOAD1}`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + } + } + ) + .then((response) => { + if (response.status === 200) { + // Optional: Read response if the server returns data + alert('Upload success'); + console.log(`PDF file ${file.name} successfully uploaded for record ID: ${currentRowId}!`); + + } + setIsUploading(false); + }) + .catch((error) => { + setIsUploading(false); + console.log(error); + return false; + }); + + + } catch (error) { + console.error('Upload error:', error); + alert(`Error uploading file: ${error.message}`); + } finally { + // 3. Cleanup and close + setIsUploading(false); + handleCloseDialog(); + } }; + const handleChooseFile = () => { + // Trigger the hidden file input click + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + const columns = [ { field: 'actions', type: 'actions', - headerName: 'Actions', - // flex: 0.5, + headerName: 'Edit', width: 100, cellClassName: 'actions', getActions: ({id}) => { @@ -57,28 +206,12 @@ export default function PdfTable({recordList}) { className="textPrimary" onClick={handleEditClick(id)} color="edit" - // disabled={'true'} - // disabled={!ability.can('VIEW','DASHBOARD')} /> ] }, }, - // { - // id: 'title', - // field: 'title', - // headerName: 'Title', - // // sortComparator: dateComparator, - // flex: 0.75, - // renderCell: (params) => ( - //
- // {params.value} - //
- // //
- // // {getDateString(params.row.pdfFrom,false)} - // //
- // ), - // }, + { id: 'templateName', field: 'templateName', @@ -86,11 +219,85 @@ export default function PdfTable({recordList}) { flex: 2, renderCell: (params) => { return ( -
+
{params.value} {params.row.vNum} -
+
); - } + } + }, + { + field: 'upload1', + type: 'actions', + // Multi-line header + headerName: ( +
+ Upload Sig. +
+ ), + width: 100, + cellClassName: 'actions', + getActions: ({ id, row }) => { + + // Check if a file ID exists to determine if a file is present + const isUploaded = !!row.upload1FileId; + + // Determine the icon and label based on upload status + const upload1Icon = isUploaded + ? // Green tick if uploaded + : ; // Upload icon if not uploaded + + const upload1Label = isUploaded ? "Update Signature" : "Upload Signature"; + + // Define the actions + const actions = [ + + + + ]; + + // Conditional rendering logic for Upload 2 + if (row.templateName === "MLB03S" || row.templateName === "SLGII" || row.templateName === "SLAPP") { + actions.push( + + } + label="Upload2" + className="textPrimary" + onClick={handleUpload2Click(id, row.templateName)} + color="upload" + /> + + ); + } + + return actions; + }, + }, + { + field: 'actions2', + type: 'actions', + headerName: 'Download', + width: 100, + cellClassName: 'actions', + getActions: ({id}) => { + return [ + + } + label="Download" + className="textPrimary" + onClick={handleDownloadClick(id, "{params.row.templateName}")} + color="download" + /> + + ] + }, }, { id: 'createDate', @@ -129,89 +336,6 @@ export default function PdfTable({recordList}) { ), }, - // { - // id: 'lastname', - // field: 'lastname', - // headerName: 'Last Name', - // flex: 1.5, - // sortComparator: dateComparator, - // renderCell: (params) => ( - //
- // {params.value} - //
- // //
- // // {getDateString(params.row.startDate,false)} - // //
- // ), - // }, - // { - // id: 'firstname', - // field: 'firstname', - // headerName: 'First Name', - // // sortComparator: dateComparator, - // flex: 2, - // renderCell: (params) => ( - //
- // {params.value} - //
- // //
- // // {getDateString(params.row.applicationDeadline,false)} - // //
- // ), - // }, - // { - // id: 'email', - // field: 'email', - // headerName: 'Email', - // flex: 1.5, - // renderCell: (params) => { - // return ( - //
- // {params.value} - //
- // ); - // } - // }, - // { - // id: 'phone1', - // field: 'phone1', - // headerName: 'Phone No.', - // flex: 1, - // renderCell: (params) => { - // return ( - //
- // {params.value} - //
- // ); - // } - // }, - // { - // id: 'phone2', - // field: 'phone2', - // headerName: '2nd Phone No.', - // flex: 1, - // renderCell: (params) => { - // return ( - //
- // {params.value} - //
- // ); - // } - // }, - // { - // id: 'remarks', - // field: 'remarks', - // headerName: 'Remarks', - // flex: 2, - // renderCell: (params) => { - // return ( - //
- // {params.value} - //
- // ); - // } - // }, - ]; return ( @@ -219,9 +343,9 @@ export default function PdfTable({recordList}) { 'auto'} paginationModel={paginationModel} @@ -234,6 +358,62 @@ export default function PdfTable({recordList}) { pageSizeOptions={[10]} autoHeight /> + + {/* The Upload Dialog Box */} + + + Upload Signature Page (1-Page PDF) + + + + Please select the single-page PDF file containing the signature for record ID: **{currentRowId}**. + + + + {/* Button to trigger file selection */} + + + {/* Hidden File Input */} + + + {/* Display the selected file name if a file is chosen */} + {fileInputRef.current?.files[0] && ( + + Selected: **{fileInputRef.current.files[0].name}** + + )} + + + + + + + ); -} +} \ No newline at end of file diff --git a/src/pages/pdf/PdfSearchPage/SelectTemplateWindow.js b/src/pages/pdf/PdfSearchPage/SelectTemplateWindow.js index f8e66fe..7735c9e 100644 --- a/src/pages/pdf/PdfSearchPage/SelectTemplateWindow.js +++ b/src/pages/pdf/PdfSearchPage/SelectTemplateWindow.js @@ -21,6 +21,8 @@ export function SelectTemplateWindow({ isWindowOpen, onNormalClose, onConfirmClo const [templateId, setTemplateId] = React.useState(0) const [templateList, setTemplateList] = React.useState([]) const fileInputRef = useRef(null); + const [currentView, setCurrentView] = useState('prefixSelection'); + const [selectedPrefix, setSelectedPrefix] = useState(null); const navigate = useNavigate() @@ -30,17 +32,27 @@ export function SelectTemplateWindow({ isWindowOpen, onNormalClose, onConfirmClo useEffect(() => { axios.get(`${apiPath}${GET_TEMPLATE_PATH}`) - .then((response) => { - if (response.status === 200) { - const record = response.data.records; - console.log("record", record); - setTemplateList(record); - } - }) - .catch(error => { - console.log(error); - return false; - }); + .then((response) => { + if (response.status === 200) { + const record = response.data.records; + // Group templates by their prefix + const grouped = record.reduce((acc, template) => { + const prefix = template.name.split(' ')[0]; + if (!acc[prefix]) { + acc[prefix] = []; + } + //let's test ML first, so hide all SL forms + //if(prefix != "SL") + acc[prefix].push(template); + return acc; + }, {}); + setTemplateList(grouped); // Store the grouped data + } + }) + .catch(error => { + console.log(error); + return false; + }); }, []); const createNewForm = (templateId) => { @@ -80,23 +92,54 @@ export function SelectTemplateWindow({ isWindowOpen, onNormalClose, onConfirmClo - {templateList.length === 0 ? ( - - No available template, please add! - - ) : ( templateList.map(record => ( -
- -
- ) - ))} + {Object.keys(templateList).length === 0 ? ( + + No available template, please add! + + ) : ( + <> + {currentView === 'prefixSelection' ? ( + // Step 1: Show the prefix selection buttons + Object.keys(templateList).map(prefix => ( +
+ +
+ )) + ) : ( + // Step 2: Show the templates for the selected prefix + <> + + + Select a {selectedPrefix} Template + + {templateList[selectedPrefix].map(record => ( +
+ +
+ ))} + + )} + + )}
diff --git a/src/themes/colorConst.js b/src/themes/colorConst.js index 11941dc..78513ac 100644 --- a/src/themes/colorConst.js +++ b/src/themes/colorConst.js @@ -86,6 +86,10 @@ export const LIONER_BUTTON_THEME = createTheme({ main: '#F3AF2B', contrastText: '#FFFFFF', }, + upload:{ + main: '#6A8B9E', + contrastText: '#FFFFFF', + }, exportExcel:{ main: '#6A8B9E', contrastText: '#FFFFFF', diff --git a/src/utils/ApiPathConst.js b/src/utils/ApiPathConst.js index 0b5841f..b653b0a 100644 --- a/src/utils/ApiPathConst.js +++ b/src/utils/ApiPathConst.js @@ -68,6 +68,8 @@ export const GET_PDF_PATH = "/pdf" export const POST_PDF_PATH = "/pdf/save" export const POST_UPLOAD_PDF_PATH = "/pdf2/upload" export const GET_PDF_TEMPLATE_PATH = "/pdf/template" +export const POST_MERGE_PDF = "/pdf/merge-pdf" +export const POST_SIG_UPLOAD1 = "/pdf/upload1" export const GET_CLIENT_PATH = "/client" export const POST_CLIENT_PATH = "/client/save" export const GET_EVENT_PATH = "/event"