From 76972a125d7728c394ed0b143f20b25180f85f1c Mon Sep 17 00:00:00 2001 From: "PC-20260115JRSN\\Administrator" Date: Mon, 16 Mar 2026 14:46:09 +0800 Subject: [PATCH] added dynamic page no. for formCode --- src/menu-items/setting.js | 15 +- .../formSigPage/FormSigPageSearchForm.js | 84 +++++ src/pages/formSigPage/FormSigPageTable.js | 325 ++++++++++++++++++ src/pages/formSigPage/index.js | 78 +++++ src/pages/pdf/PdfSearchPage/PdfTable.js | 113 +++--- src/routes/SettingRoutes.js | 11 + src/utils/ApiPathConst.js | 3 + 7 files changed, 565 insertions(+), 64 deletions(-) create mode 100644 src/pages/formSigPage/FormSigPageSearchForm.js create mode 100644 src/pages/formSigPage/FormSigPageTable.js create mode 100644 src/pages/formSigPage/index.js diff --git a/src/menu-items/setting.js b/src/menu-items/setting.js index 9d6bbff..ab652ce 100644 --- a/src/menu-items/setting.js +++ b/src/menu-items/setting.js @@ -13,7 +13,8 @@ import { MenuUnfoldOutlined, FileSearchOutlined, MailOutlined, - ApartmentOutlined + ApartmentOutlined, + FilePdfOutlined } from '@ant-design/icons'; // icons @@ -31,7 +32,8 @@ const icons = { MenuUnfoldOutlined, FileSearchOutlined, MailOutlined, - ApartmentOutlined + ApartmentOutlined, + FilePdfOutlined }; // ==============================|| MENU ITEMS - EXTRA PAGES ||============================== // @@ -141,6 +143,15 @@ const setting = { breadcrumbs: false, ability:['VIEW','USER'] }, + { + id: 'formSigPage', + title: 'Form Sig Page', + type: 'item', + url: '/formSigPage', + icon: icons.FilePdfOutlined, + breadcrumbs: false, + ability:['MANAGE','SYSTEM_CONFIGURATION'] + }, // { // id: 'passwordPolicy', // title: 'Password Policy', diff --git a/src/pages/formSigPage/FormSigPageSearchForm.js b/src/pages/formSigPage/FormSigPageSearchForm.js new file mode 100644 index 0000000..39a3f70 --- /dev/null +++ b/src/pages/formSigPage/FormSigPageSearchForm.js @@ -0,0 +1,84 @@ +import { + Button, + Grid, + InputLabel, + MenuItem, + TextField, +} from '@mui/material'; +import MainCard from "../../components/MainCard"; +import { useForm } from "react-hook-form"; +import * as React from "react"; +import { ThemeProvider } from "@emotion/react"; +import { LIONER_BUTTON_THEME } from "../../themes/colorConst"; +import { CARD_MAX_WIDTH } from "../../themes/themeConst"; + +const FORM_CODES = ['IDA', 'FNA', 'HSBCFIN', 'HSBCA31', 'MLB03S', 'MLFNA_EN', 'MLFNA_CHI', 'SLFNA_EN', 'SLFNA_CHI', 'SLAPP', 'SLGII']; +const SIG_TYPES = ['upload1', 'upload2']; + +const FormSigPageSearchForm = ({ applySearch }) => { + const { reset, register, handleSubmit } = useForm(); + + const onSubmit = (data) => { + applySearch(data); + }; + + return ( + +
+ + + + Form Code + + All + {FORM_CODES.map((c) => ( + {c} + ))} + + + + Sig Type + + All + {SIG_TYPES.map((t) => ( + {t} + ))} + + + + + + + + + + + + + + +
+ ); +}; + +export default FormSigPageSearchForm; diff --git a/src/pages/formSigPage/FormSigPageTable.js b/src/pages/formSigPage/FormSigPageTable.js new file mode 100644 index 0000000..bd72905 --- /dev/null +++ b/src/pages/formSigPage/FormSigPageTable.js @@ -0,0 +1,325 @@ +import * as React from 'react'; +import { + Button, +} from '@mui/material'; +import { + DataGrid, + GridActionsCellItem, + GridRowEditStopReasons, + GridRowModes, + GridToolbarContainer +} from "@mui/x-data-grid"; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import dayjs from 'dayjs'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/DeleteOutlined'; +import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Close'; +import AddIcon from '@mui/icons-material/Add'; +import axios from "axios"; +import { apiPath } from "../../auth/utils"; +import { + GET_FORM_SIG_PAGE_LIST, + UPDATE_FORM_SIG_PAGE_PATH, +} from "../../utils/ApiPathConst"; +import { + CustomNoRowsOverlay, + GeneralConfirmWindow, + notifyDeleteError, + notifyDeleteSuccess, + notifySaveSuccess, + removeObjectWithId +} from "../../utils/CommonFunction"; +import UploadContext from "../../components/UploadProvider"; +import { LIONER_BUTTON_THEME } from "../../themes/colorConst"; +import { ThemeProvider } from "@emotion/react"; + +const FORM_CODES = ['IDA', 'FNA', 'HSBCFIN', 'HSBCA31', 'MLB03S', 'MLFNA_EN', 'MLFNA_CHI', 'SLFNA_EN', 'SLFNA_CHI', 'SLAPP', 'SLGII']; +const SIG_TYPES = ['upload1', 'upload2']; +const ACTIONS = ['REPLACE', 'SKIP_AND_APPEND']; + +let newRowId = -1; + +function normalizeStartDate(value) { + if (value == null || value === '') return ''; + if (dayjs.isDayjs(value)) return value.format('YYYY-MM-DD'); + const d = dayjs(value); + return d.isValid() ? d.format('YYYY-MM-DD') : String(value); +} + +function toDayjsOrNull(value) { + if (value == null || value === '') return null; + if (dayjs.isDayjs(value)) return value; + if (typeof value === 'string') { + const d = dayjs(value); + return d.isValid() ? d : null; + } + if (Array.isArray(value) && value.length >= 3) { + const y = value[0]; + const m = String(value[1]).padStart(2, '0'); + const d = dayjs(`${y}-${m}-${String(value[2]).padStart(2, '0')}`); + return d.isValid() ? d : null; + } + if (typeof value === 'object' && value.year != null && value.month != null && value.day != null) { + const d = dayjs(`${value.year}-${String(value.month).padStart(2, '0')}-${String(value.day).padStart(2, '0')}`); + return d.isValid() ? d : null; + } + const d = dayjs(value); + return d.isValid() ? d : null; +} + +function formatStartDate(value) { + const d = toDayjsOrNull(value); + return d ? d.format('YYYY-MM-DD') : ''; +} + +function EditToolbar({ setRows, setRowModesModel }) { + const handleClick = () => { + const id = newRowId--; + setRows((oldRows) => [ + { + id, + formCode: 'IDA', + startDate: '2000-01-01', + sigType: 'upload1', + pageFrom: 1, + pageTo: 1, + action: 'REPLACE', + isNew: true, + }, + ...oldRows, + ]); + setRowModesModel((oldModel) => ({ + ...oldModel, + [id]: { mode: GridRowModes.Edit, fieldToFocus: 'formCode' }, + })); + }; + + return ( + + + + ); +} + +export default function FormSigPageTable({ recordList }) { + const [rows, setRows] = React.useState([]); + const [rowModesModel, setRowModesModel] = React.useState({}); + const { setIsUploading } = React.useContext(UploadContext); + const [isWindowOpen, setIsWindowOpen] = React.useState(false); + const [selectedId, setSelectedId] = React.useState(null); + const [paginationModel, setPaginationModel] = React.useState({ page: 0, pageSize: 10 }); + + React.useEffect(() => { + setPaginationModel({ page: 0, pageSize: 10 }); + setRows(recordList || []); + }, [recordList]); + + const handleClose = () => setIsWindowOpen(false); + + const handleDeleteClick = (id) => () => { + setIsWindowOpen(true); + setSelectedId(id); + }; + + const updateData = () => { + setIsUploading(true); + axios + .delete(`${apiPath}${GET_FORM_SIG_PAGE_LIST}/${selectedId}`) + .then((response) => { + if (response.status === 204) { + notifyDeleteSuccess(); + setRows((prev) => removeObjectWithId(prev, selectedId)); + setIsWindowOpen(false); + } + setIsUploading(false); + }) + .catch((err) => { + console.error(err); + notifyDeleteError(err?.response?.data?.message || 'Delete failed'); + setIsUploading(false); + }); + }; + + const handleRowEditStop = (params, event) => { + if (params.reason === GridRowEditStopReasons.rowFocusOut) { + event.defaultMuiPrevented = true; + } + }; + + const handleEditClick = (id) => () => { + setRowModesModel((prev) => ({ ...prev, [id]: { mode: GridRowModes.Edit, fieldToFocus: 'formCode' } })); + }; + + const handleSaveClick = (id) => () => { + setRowModesModel((prev) => ({ ...prev, [id]: { mode: GridRowModes.View } })); + }; + + const handleCancelClick = (id) => () => { + setRowModesModel((prev => { + const next = { ...prev, [id]: { mode: GridRowModes.View, ignoreModifications: true } }; + return next; + })); + const editedRow = rows.find((r) => r.id === id); + if (editedRow?.isNew) { + setRows((prev) => prev.filter((r) => r.id !== id)); + } + }; + + const processRowUpdate = (newRow) => { + const isCreate = newRow.isNew === true || newRow.id == null || Number(newRow.id) <= 0; + const payload = { + formCode: newRow.formCode, + startDate: normalizeStartDate(newRow.startDate), + sigType: newRow.sigType, + pageFrom: Number(newRow.pageFrom), + pageTo: Number(newRow.pageTo), + action: newRow.action, + }; + if (!isCreate) payload.id = Number(newRow.id); + + return new Promise((resolve, reject) => { + if (!payload.formCode || !payload.startDate || !payload.sigType || + payload.pageFrom == null || payload.pageTo == null || !payload.action) { + reject(new Error('All fields are required')); + return; + } + if (payload.pageFrom > payload.pageTo) { + reject(new Error('pageFrom must be <= pageTo')); + return; + } + setIsUploading(true); + axios + .post(`${apiPath}${UPDATE_FORM_SIG_PAGE_PATH}`, payload) + .then((res) => { + const savedId = res.data?.id; + const updatedRow = { ...newRow, id: savedId != null ? savedId : newRow.id, isNew: false }; + setRows((prev) => prev.map((r) => (r.id === newRow.id ? updatedRow : r))); + notifySaveSuccess(); + setIsUploading(false); + resolve(updatedRow); + }) + .catch((err) => { + setIsUploading(false); + notifyDeleteError(err?.response?.data?.message || 'Save failed'); + reject(err); + }); + }); + }; + + const columns = [ + { + field: 'actions', + type: 'actions', + headerName: 'Actions', + width: 120, + getActions: ({ id }) => { + const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; + if (isInEditMode) { + return [ + + } label="Cancel" onClick={handleCancelClick(id)} color="delete" />, + , + + } label="Save" onClick={handleSaveClick(id)} color="save" />, + , + ]; + } + return [ + + } label="Delete" onClick={handleDeleteClick(id)} color="delete" />, + , + + } label="Edit" onClick={handleEditClick(id)} color="edit" />, + , + ]; + }, + }, + { + field: 'formCode', + headerName: 'Form Code', + width: 120, + editable: true, + type: 'singleSelect', + valueOptions: FORM_CODES, + }, + { + field: 'startDate', + headerName: 'Start Date', + width: 140, + editable: true, + valueGetter: (params) => formatStartDate(params.row?.startDate), + renderCell: (params) => formatStartDate(params.row?.startDate), + renderEditCell: (params) => ( + + + params.api.setEditCellValue({ + id: params.id, + field: params.field, + value: d ? d.format('YYYY-MM-DD') : '', + }) + } + slotProps={{ textField: { size: 'small', fullWidth: true } }} + /> + + ), + }, + { + field: 'sigType', + headerName: 'Sig Type', + width: 90, + editable: true, + type: 'singleSelect', + valueOptions: SIG_TYPES, + }, + { field: 'pageFrom', headerName: 'Page From', type: 'number', width: 95, editable: true }, + { field: 'pageTo', headerName: 'Page To', type: 'number', width: 95, editable: true }, + { + field: 'action', + headerName: 'Action', + width: 140, + editable: true, + type: 'singleSelect', + valueOptions: ACTIONS, + }, + ]; + + return ( +
+ notifyDeleteError(err?.message || 'Update failed')} + getRowHeight={() => 'auto'} + paginationModel={paginationModel} + onPaginationModelChange={setPaginationModel} + pageSizeOptions={[10, 25, 50]} + slots={{ + toolbar: EditToolbar, + noRowsOverlay: () => CustomNoRowsOverlay(), + }} + slotProps={{ toolbar: { setRows, setRowModesModel } }} + autoHeight + /> + +
+ ); +} diff --git a/src/pages/formSigPage/index.js b/src/pages/formSigPage/index.js new file mode 100644 index 0000000..bebce6d --- /dev/null +++ b/src/pages/formSigPage/index.js @@ -0,0 +1,78 @@ +import { + Box, + Grid, + Typography +} from '@mui/material'; +import MainCard from "../../components/MainCard"; +import FormSigPageSearchForm from "./FormSigPageSearchForm"; +import FormSigPageTable from "./FormSigPageTable"; +import { useEffect, useState } from "react"; +import * as React from "react"; +import axios from "axios"; +import { apiPath } from "../../auth/utils"; +import { GET_FORM_SIG_PAGE_LIST } from "../../utils/ApiPathConst"; +import LoadingComponent from "../extra-pages/LoadingComponent"; +import { LIONER_FORM_THEME, CARD_MAX_WIDTH } from "../../themes/themeConst"; +import { ThemeProvider } from "@emotion/react"; + +const FormSigPageSearchPanel = () => { + const [record, setRecord] = useState([]); + const [searchCriteria, setSearchCriteria] = useState({}); + const [onReady, setOnReady] = useState(false); + + const fetchList = () => { + const params = {}; + if (searchCriteria.formCode) params.formCode = searchCriteria.formCode; + if (searchCriteria.sigType) params.sigType = searchCriteria.sigType; + axios + .get(`${apiPath}${GET_FORM_SIG_PAGE_LIST}`, { params }) + .then((response) => { + if (response.status === 200 && response.data?.records) { + setRecord(response.data.records); + } + }) + .catch((err) => { + console.error(err); + }); + }; + + useEffect(() => { + fetchList(); + }, [searchCriteria]); + + useEffect(() => { + setOnReady(true); + }, [record]); + + const applySearch = (input) => { + setSearchCriteria(input || {}); + }; + + if (!onReady) return ; + + return ( + + + + + + + Form Signature Page Config + + + + + + + + + + + + + + + ); +}; + +export default FormSigPageSearchPanel; diff --git a/src/pages/pdf/PdfSearchPage/PdfTable.js b/src/pages/pdf/PdfSearchPage/PdfTable.js index 6b93141..e61252d 100644 --- a/src/pages/pdf/PdfSearchPage/PdfTable.js +++ b/src/pages/pdf/PdfSearchPage/PdfTable.js @@ -1,7 +1,7 @@ // material-ui import * as React from 'react'; import {apiPath} from "../../../auth/utils"; -import { POST_SIG_UPLOAD1 } from "../../../utils/ApiPathConst"; +import { POST_SIG_UPLOAD1, GET_FORM_SIG_PAGE_CONFIG } from "../../../utils/ApiPathConst"; import axios from 'axios'; import { DataGrid, @@ -50,6 +50,7 @@ export default function PdfTable({recordList}) { const [isDialogOpen, setIsDialogOpen] = React.useState(false); // State to hold the ID, templateName, and refType for the current upload operation const [currentUploadRow, setCurrentUploadRow] = React.useState(initialUploadState); + const [currentUploadLabel, setCurrentUploadLabel] = React.useState(null); const [isUploading, setIsUploading] = React.useState(false); const navigate = useNavigate() @@ -72,32 +73,63 @@ export default function PdfTable({recordList}) { navigate(`/pdf/maintain/${id}`); }; + /** Safely get YYYY-MM-DD from row.created; returns null if missing or invalid. */ + const getAsOfDateString = (row) => { + const raw = row?.created; + if (raw == null || raw === "") return null; + const d = new Date(raw); + if (Number.isNaN(d.getTime())) return null; + return d.toISOString().slice(0, 10); + }; + /** - * Opens the upload dialog and sets the current row details for Upload 1 + * Opens the upload dialog and sets the current row details for Upload 1. + * Fetches form-sig-page config from API for dynamic label (page number). */ - const handleUploadClick = (id, templateName, formCode) => () => { + const handleUploadClick = (id, templateName, formCode, row) => () => { setCurrentUploadRow({ id: id, templateName: templateName, formCode: formCode, refType: "upload1" }); + setCurrentUploadLabel(null); setIsDialogOpen(true); + const asOfDate = getAsOfDateString(row); + const params = new URLSearchParams({ formCode }); + if (asOfDate) params.set("asOfDate", asOfDate); + axios.get(`${apiPath}${GET_FORM_SIG_PAGE_CONFIG}?${params}`) + .then((res) => { + const configs = res.data?.configs || []; + const upload1 = configs.find((c) => c.sigType === "upload1"); + setCurrentUploadLabel(upload1?.label || "Upload Signature"); + }) + .catch(() => setCurrentUploadLabel("Upload Signature")); }; /** - * Opens the upload dialog and sets the current row details for Upload 2 + * Opens the upload dialog and sets the current row details for Upload 2. + * Fetches form-sig-page config from API for dynamic label. */ - const handleUpload2Click = (id, templateName, formCode) => () => { - // Placeholder for Upload 2 - console.log(`Uploading for row ID ${id} (Upload 2)`); - setCurrentUploadRow({ + const handleUpload2Click = (id, templateName, formCode, row) => () => { + setCurrentUploadRow({ id: id, templateName: templateName, formCode: formCode, - refType: "upload2" // A different refType for a different upload logic/API + refType: "upload2" }); - setIsDialogOpen(true); + setCurrentUploadLabel(null); + setIsDialogOpen(true); + const asOfDate = getAsOfDateString(row); + const params = new URLSearchParams({ formCode }); + if (asOfDate) params.set("asOfDate", asOfDate); + axios.get(`${apiPath}${GET_FORM_SIG_PAGE_CONFIG}?${params}`) + .then((res) => { + const configs = res.data?.configs || []; + const upload2 = configs.find((c) => c.sigType === "upload2"); + setCurrentUploadLabel(upload2?.label || "Upload 2"); + }) + .catch(() => setCurrentUploadLabel("Upload 2")); }; /** @@ -198,60 +230,18 @@ export default function PdfTable({recordList}) { const handleCloseDialog = () => { setIsDialogOpen(false); - setCurrentUploadRow(initialUploadState); // Reset the current row + setCurrentUploadRow(initialUploadState); + setCurrentUploadLabel(null); if (fileInputRef.current) { fileInputRef.current.value = ""; // Clear the file input } }; - // Function to generate the dynamic dialog title + // Fallback dialog title when API config is not yet loaded or fails const getUploadDialogTitle = (formCode, refType) => { - console.log("formCode:" + formCode + " refType:" + refType); - if (refType === 'upload1') { - switch (formCode) { - case "IDA": - return "Upload Page 15"; - case "FNA": - return "Upload Page 10"; - case "HSBCFIN": - return "Upload Page 11"; - case "HSBCA31": - return "Upload Page 28-29"; - case "MLB03S": - return "Upload Page 9"; - case "MLFNA_EN": - return "Upload Page 4"; - case "MLFNA_CHI": - return "Upload Page 4"; - case "SLGII": - return "Upload Page 13"; - case "SLAPP": - return "Upload Page 17"; - case "SLFNA_EN": - return "Upload Page 5"; - case "SLFNA_CHI": - return "Upload Page 5"; - default: - return "Unknown Form"; - } - }else if (refType === 'upload2') { - switch (formCode) { - case "MLB03S": - return "Upload Page 12-13"; - case "SLGII": - return "Upload Page 15-16"; - case "SLAPP": - return "Upload Page 19-20"; - default: - return "Unknown Form"; - } - } - // Handle other refTypes if needed, e.g., 'upload2' - if (refType === 'upload2') { - return `Upload Template 2 for ${formCode}`; - } - - return "Upload File"; // Fallback + if (refType === "upload1") return "Upload Signature"; + if (refType === "upload2") return "Upload 2"; + return "Upload File"; }; // Function to handle file selection and API submission @@ -401,7 +391,7 @@ export default function PdfTable({recordList}) { icon={upload1Icon} // Use the dynamic icon label={upload1Label} // Use the dynamic label className="textPrimary" - onClick={handleUploadClick(id, templateName, formCode)} + onClick={handleUploadClick(id, templateName, formCode, row)} color="upload" /> @@ -423,7 +413,7 @@ export default function PdfTable({recordList}) { icon={upload2Icon} label={upload2Label} className="textPrimary" - onClick={handleUpload2Click(id, templateName, formCode)} + onClick={handleUpload2Click(id, templateName, formCode, row)} color="upload" /> @@ -551,8 +541,7 @@ export default function PdfTable({recordList}) { TransitionProps={{ onExited: handleCloseDialog }} > - {/* Dynamic Title based on currentUploadRow state */} - **{getUploadDialogTitle(currentUploadRow.formCode, currentUploadRow.refType)}** + {currentUploadLabel != null ? currentUploadLabel : getUploadDialogTitle(currentUploadRow.formCode, currentUploadRow.refType)} diff --git a/src/routes/SettingRoutes.js b/src/routes/SettingRoutes.js index 0314e19..56bf7b8 100644 --- a/src/routes/SettingRoutes.js +++ b/src/routes/SettingRoutes.js @@ -29,6 +29,7 @@ const EmailConfigPage = Loadable(lazy(() => import('pages/lionerEmailConfig'))); const GenerateReminderPage = Loadable(lazy(() => import('pages/lionerManualButtonPage'))); const ClientDepartmentPage = Loadable(lazy(() => import('pages/lionerClientDepartmentPage'))); const ProfilePage = Loadable(lazy(() => import('pages/profile/profile'))); +const FormSigPageSearchPanel = Loadable(lazy(() => import('pages/formSigPage'))); // ==============================|| AUTH ROUTING ||============================== // @@ -260,6 +261,16 @@ const SettingRoutes = () => { ) ), }, + { + path: 'formSigPage', + element: ( + handleRouteAbility( + ability.can('MANAGE', 'SYSTEM_CONFIGURATION'), + , + + ) + ), + }, { path: 'logout', element: diff --git a/src/utils/ApiPathConst.js b/src/utils/ApiPathConst.js index 889aaba..c2a49e3 100644 --- a/src/utils/ApiPathConst.js +++ b/src/utils/ApiPathConst.js @@ -72,6 +72,9 @@ export const GET_PDF_TEMPLATE_PATH = "/pdf/template" export const GET_MERGE_PDF = "/pdf/merge-pdf" export const GET_REMOVE_PDF_PASSWORD = "/pdf/remove-pdf-password" export const POST_SIG_UPLOAD1 = "/pdf/upload1" +export const GET_FORM_SIG_PAGE_CONFIG = "/pdf/form-sig-page-config" +export const GET_FORM_SIG_PAGE_LIST = "/pdf/form-sig-page" +export const UPDATE_FORM_SIG_PAGE_PATH = "/pdf/form-sig-page/save" export const GET_CLIENT_PATH = "/client" export const POST_CLIENT_PATH = "/client/save" export const GET_EVENT_PATH = "/event"