| @@ -1,6 +1,7 @@ | |||
| // assets | |||
| import BoyIcon from '@mui/icons-material/Boy'; | |||
| import AssignmentIcon from '@mui/icons-material/Assignment'; | |||
| import PictureAsPdfOutlinedIcon from '@mui/icons-material/PictureAsPdfOutlined'; | |||
| // icons | |||
| @@ -12,6 +13,7 @@ const icons = { | |||
| ) | |||
| }, | |||
| AssignmentIcon, | |||
| FormSigPageIcon: PictureAsPdfOutlinedIcon, | |||
| }; | |||
| // ==============================|| MENU ITEMS - DASHBOARD ||============================== // | |||
| @@ -40,6 +42,15 @@ const client = { | |||
| breadcrumbs: false, | |||
| ability:['VIEW','TEMPLATE'] | |||
| }, | |||
| { | |||
| id: 'formSigPage', | |||
| title: 'Form Sig Page', | |||
| type: 'item', | |||
| url: '/formSigPage', | |||
| icon: icons.FormSigPageIcon, | |||
| breadcrumbs: false, | |||
| ability:['VIEW','TEMPLATE'] | |||
| }, | |||
| ] | |||
| }; | |||
| @@ -13,8 +13,7 @@ import { | |||
| MenuUnfoldOutlined, | |||
| FileSearchOutlined, | |||
| MailOutlined, | |||
| ApartmentOutlined, | |||
| FilePdfOutlined | |||
| ApartmentOutlined | |||
| } from '@ant-design/icons'; | |||
| // icons | |||
| @@ -32,8 +31,7 @@ const icons = { | |||
| MenuUnfoldOutlined, | |||
| FileSearchOutlined, | |||
| MailOutlined, | |||
| ApartmentOutlined, | |||
| FilePdfOutlined | |||
| ApartmentOutlined | |||
| }; | |||
| // ==============================|| MENU ITEMS - EXTRA PAGES ||============================== // | |||
| @@ -143,15 +141,6 @@ 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', | |||
| @@ -13,7 +13,7 @@ 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 SIG_TYPES = ['upload1', 'upload2', 'upload3']; | |||
| const FormSigPageSearchForm = ({ applySearch }) => { | |||
| const { reset, register, handleSubmit } = useForm(); | |||
| @@ -37,8 +37,7 @@ 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']; | |||
| const SIG_TYPES = ['upload1', 'upload2', 'upload3']; | |||
| let newRowId = -1; | |||
| @@ -86,7 +85,6 @@ function EditToolbar({ setRows, setRowModesModel }) { | |||
| sigType: 'upload1', | |||
| pageFrom: 1, | |||
| pageTo: 1, | |||
| action: 'REPLACE', | |||
| isNew: true, | |||
| }, | |||
| ...oldRows, | |||
| @@ -178,13 +176,12 @@ export default function FormSigPageTable({ recordList }) { | |||
| 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) { | |||
| payload.pageFrom == null || payload.pageTo == null) { | |||
| reject(new Error('All fields are required')); | |||
| return; | |||
| } | |||
| @@ -280,14 +277,6 @@ export default function FormSigPageTable({ recordList }) { | |||
| }, | |||
| { 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 ( | |||
| @@ -1,13 +1,14 @@ | |||
| // material-ui | |||
| import { | |||
| Button, | |||
| Grid, | |||
| Grid, | |||
| TextField, | |||
| Typography, | |||
| Dialog, | |||
| DialogTitle, | |||
| DialogContent, | |||
| DialogActions | |||
| DialogActions, | |||
| InputLabel, | |||
| } from '@mui/material'; | |||
| import MainCard from "../../components/MainCard"; | |||
| import {useForm} from "react-hook-form"; | |||
| @@ -21,6 +22,7 @@ import {ThemeProvider} from "@emotion/react"; | |||
| import {LIONER_BUTTON_THEME} from "../../themes/colorConst"; | |||
| import {USER_GROUP_COMBO} from "../../utils/ComboConst"; | |||
| import {CARD_MAX_WIDTH} from "../../themes/themeConst"; | |||
| import {GET_CONSULTANT_COMBO_LIST} from "../../utils/ApiPathConst"; | |||
| // ==============================|| USER SEARCH FORM ||============================== // | |||
| @@ -33,9 +35,22 @@ const UserSearchForm = ({applySearch}) => { | |||
| // States for Report Dialog | |||
| const [openReportDialog, setOpenReportDialog] = useState(false); | |||
| const [reportDates, setReportDates] = useState({ fromDate: '', toDate: '' }); | |||
| const [consultantOptions, setConsultantOptions] = useState([]); | |||
| const [selectedConsultants, setSelectedConsultants] = useState([]); | |||
| const { reset, register, handleSubmit } = useForm(); | |||
| useEffect(() => { | |||
| axios | |||
| .get(`${apiPath}${GET_CONSULTANT_COMBO_LIST}`) | |||
| .then((res) => { | |||
| if (res.data?.records) { | |||
| setConsultantOptions(res.data.records); | |||
| } | |||
| }) | |||
| .catch(() => {}); | |||
| }, []); | |||
| const onSubmit = (data) => { | |||
| const temp = { | |||
| username: data.userName, | |||
| @@ -53,11 +68,26 @@ const UserSearchForm = ({applySearch}) => { | |||
| const handleCloseReport = () => { | |||
| setOpenReportDialog(false); | |||
| setReportDates({ fromDate: '', toDate: '' }); | |||
| setSelectedConsultants([]); | |||
| }; | |||
| const handleExport = () => { | |||
| const params = { | |||
| fromDate: reportDates.fromDate, | |||
| toDate: reportDates.toDate, | |||
| }; | |||
| if (selectedConsultants.length > 0) { | |||
| params.consultantIds = selectedConsultants.map((c) => c.id); | |||
| } | |||
| axios.get(`${apiPath}/client/excel-client-consultant-report`, { | |||
| params: reportDates, | |||
| params, | |||
| paramsSerializer: (p) => { | |||
| const search = new URLSearchParams(); | |||
| if (p.fromDate) search.append('fromDate', p.fromDate); | |||
| if (p.toDate) search.append('toDate', p.toDate); | |||
| (p.consultantIds || []).forEach((id) => search.append('consultantIds', String(id))); | |||
| return search.toString(); | |||
| }, | |||
| responseType: 'blob', | |||
| }) | |||
| .then((response) => { | |||
| @@ -153,7 +183,7 @@ const UserSearchForm = ({applySearch}) => { | |||
| </form> | |||
| {/* Report Criteria Dialog */} | |||
| <Dialog open={openReportDialog} onClose={handleCloseReport} fullWidth maxWidth="xs"> | |||
| <Dialog open={openReportDialog} onClose={handleCloseReport} fullWidth maxWidth="sm"> | |||
| <DialogTitle>Export Client Consultant Report</DialogTitle> | |||
| <DialogContent> | |||
| <Grid container spacing={2} sx={{ mt: 0.5 }}> | |||
| @@ -177,6 +207,27 @@ const UserSearchForm = ({applySearch}) => { | |||
| onChange={(e) => setReportDates({ ...reportDates, toDate: e.target.value })} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <InputLabel htmlFor="report-consultant-combo">Consultant (optional)</InputLabel> | |||
| <Autocomplete | |||
| multiple | |||
| disableCloseOnSelect | |||
| id="report-consultant-combo" | |||
| size="small" | |||
| options={consultantOptions} | |||
| value={selectedConsultants} | |||
| onChange={(event, newValue) => setSelectedConsultants(newValue)} | |||
| getOptionLabel={(option) => option?.name ?? ''} | |||
| isOptionEqualToValue={(a, b) => a?.id === b?.id} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| placeholder="All consultants if empty" | |||
| fullWidth | |||
| /> | |||
| )} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </DialogContent> | |||
| <DialogActions sx={{ p: 3 }}> | |||
| @@ -39,7 +39,22 @@ import {ThemeProvider} from "@emotion/react"; | |||
| const initialUploadState = { | |||
| id: null, | |||
| templateName: null, // This will be the formCode | |||
| refType: null, // To differentiate between upload1 and upload2 if needed | |||
| refType: null, // upload1 | upload2 | upload3 | |||
| } | |||
| const REF_TYPE_TO_FILE_FIELD = { | |||
| upload1: "upload1FileId", | |||
| upload2: "upload2FileId", | |||
| upload3: "upload3FileId", | |||
| }; | |||
| /** Backend adds sigTypes from form_sig_page for this row's formCode + created date. */ | |||
| function normalizeSigTypes(row) { | |||
| const st = row?.sigTypes; | |||
| if (Array.isArray(st) && st.length > 0) { | |||
| return st; | |||
| } | |||
| return ["upload1"]; | |||
| } | |||
| export default function PdfTable({recordList}) { | |||
| @@ -73,13 +88,19 @@ export default function PdfTable({recordList}) { | |||
| navigate(`/pdf/maintain/${id}`); | |||
| }; | |||
| /** Safely get YYYY-MM-DD from row.created; returns null if missing or invalid. */ | |||
| /** | |||
| * YYYY-MM-DD in local calendar (matches server JVM date for filled_form.created better than UTC ISO). | |||
| * Used only as fallback when the API cannot resolve filledFormId. | |||
| */ | |||
| 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); | |||
| const y = d.getFullYear(); | |||
| const m = String(d.getMonth() + 1).padStart(2, "0"); | |||
| const day = String(d.getDate()).padStart(2, "0"); | |||
| return `${y}-${m}-${day}`; | |||
| }; | |||
| /** | |||
| @@ -97,6 +118,7 @@ export default function PdfTable({recordList}) { | |||
| setIsDialogOpen(true); | |||
| const asOfDate = getAsOfDateString(row); | |||
| const params = new URLSearchParams({ formCode }); | |||
| params.set("filledFormId", String(id)); | |||
| if (asOfDate) params.set("asOfDate", asOfDate); | |||
| axios.get(`${apiPath}${GET_FORM_SIG_PAGE_CONFIG}?${params}`) | |||
| .then((res) => { | |||
| @@ -122,6 +144,7 @@ export default function PdfTable({recordList}) { | |||
| setIsDialogOpen(true); | |||
| const asOfDate = getAsOfDateString(row); | |||
| const params = new URLSearchParams({ formCode }); | |||
| params.set("filledFormId", String(id)); | |||
| if (asOfDate) params.set("asOfDate", asOfDate); | |||
| axios.get(`${apiPath}${GET_FORM_SIG_PAGE_CONFIG}?${params}`) | |||
| .then((res) => { | |||
| @@ -132,6 +155,28 @@ export default function PdfTable({recordList}) { | |||
| .catch(() => setCurrentUploadLabel("Upload 2")); | |||
| }; | |||
| const handleUpload3Click = (id, templateName, formCode, row) => () => { | |||
| setCurrentUploadRow({ | |||
| id: id, | |||
| templateName: templateName, | |||
| formCode: formCode, | |||
| refType: "upload3" | |||
| }); | |||
| setCurrentUploadLabel(null); | |||
| setIsDialogOpen(true); | |||
| const asOfDate = getAsOfDateString(row); | |||
| const params = new URLSearchParams({ formCode }); | |||
| params.set("filledFormId", String(id)); | |||
| if (asOfDate) params.set("asOfDate", asOfDate); | |||
| axios.get(`${apiPath}${GET_FORM_SIG_PAGE_CONFIG}?${params}`) | |||
| .then((res) => { | |||
| const configs = res.data?.configs || []; | |||
| const upload3 = configs.find((c) => c.sigType === "upload3"); | |||
| setCurrentUploadLabel(upload3?.label || "Upload 3"); | |||
| }) | |||
| .catch(() => setCurrentUploadLabel("Upload 3")); | |||
| }; | |||
| /** | |||
| * Handles the standard download (fillable file, endpoint: /download-ff/{id}) | |||
| */ | |||
| @@ -241,6 +286,7 @@ export default function PdfTable({recordList}) { | |||
| const getUploadDialogTitle = (formCode, refType) => { | |||
| if (refType === "upload1") return "Upload Signature"; | |||
| if (refType === "upload2") return "Upload 2"; | |||
| if (refType === "upload3") return "Upload 3"; | |||
| return "Upload File"; | |||
| }; | |||
| @@ -289,7 +335,7 @@ export default function PdfTable({recordList}) { | |||
| prevRows.map(row => { | |||
| if (row.id === id) { | |||
| // Update the relevant file ID field based on refType | |||
| const updateField = refType === 'upload1' ? 'upload1FileId' : 'upload2FileId'; | |||
| const updateField = REF_TYPE_TO_FILE_FIELD[refType] || "upload1FileId"; | |||
| return { | |||
| ...row, | |||
| [updateField]: uploadedFileId // Set the file ID to trigger the icon | |||
| @@ -367,46 +413,47 @@ export default function PdfTable({recordList}) { | |||
| Upload Sig. | |||
| </div> | |||
| ), | |||
| width: 100, | |||
| width: 150, | |||
| cellClassName: 'actions', | |||
| getActions: ({ id, row }) => { | |||
| // Check upload status | |||
| const isUploaded1 = !!row.upload1FileId; | |||
| const isUploaded2 = !!row.upload2FileId; | |||
| const isUploaded2 = !!row.upload2FileId; | |||
| const isUploaded3 = !!row.upload3FileId; | |||
| const templateName = row.templateName; | |||
| const formCode = row.formCode; | |||
| // FIX 5: Use conditional colors for Upload 1 | |||
| const upload1Icon = isUploaded1 | |||
| ? <CheckCircleIcon sx={{fontSize: 25, color: 'success.main'}} /> // Green tick if uploaded | |||
| : <UploadFileIcon sx={{fontSize: 25, color: 'warning.main'}} />; // Warning color if not uploaded | |||
| const sigTypes = normalizeSigTypes(row); | |||
| const showUpload1 = sigTypes.includes("upload1"); | |||
| const showUpload2 = sigTypes.includes("upload2"); | |||
| const showUpload3 = sigTypes.includes("upload3"); | |||
| const upload1Label = isUploaded1 ? "Update Signature" : "Upload Signature"; | |||
| const actions = []; | |||
| // Define the actions | |||
| const actions = [ | |||
| <ThemeProvider key="UploadSign1" theme={LIONER_BUTTON_THEME}> | |||
| <GridActionsCellItem | |||
| icon={upload1Icon} // Use the dynamic icon | |||
| label={upload1Label} // Use the dynamic label | |||
| className="textPrimary" | |||
| onClick={handleUploadClick(id, templateName, formCode, row)} | |||
| color="upload" | |||
| /> | |||
| </ThemeProvider> | |||
| ]; | |||
| if (showUpload1) { | |||
| const upload1Icon = isUploaded1 | |||
| ? <CheckCircleIcon sx={{fontSize: 25, color: 'success.main'}} /> | |||
| : <UploadFileIcon sx={{fontSize: 25, color: 'warning.main'}} />; | |||
| const upload1Label = isUploaded1 ? "Update Signature" : "Upload Signature"; | |||
| actions.push( | |||
| <ThemeProvider key="UploadSign1" theme={LIONER_BUTTON_THEME}> | |||
| <GridActionsCellItem | |||
| icon={upload1Icon} | |||
| label={upload1Label} | |||
| className="textPrimary" | |||
| onClick={handleUploadClick(id, templateName, formCode, row)} | |||
| color="upload" | |||
| /> | |||
| </ThemeProvider> | |||
| ); | |||
| } | |||
| // Conditional rendering logic for Upload 2 | |||
| if (row.formCode === "MLB03S" || row.formCode === "SLGII" || row.formCode === "SLAPP") { | |||
| // FIX 5: Use conditional colors for Upload 2 | |||
| if (showUpload2) { | |||
| const upload2Icon = isUploaded2 | |||
| ? <CheckCircleIcon sx={{fontSize: 25, color: 'success.main'}} /> // Green tick if uploaded | |||
| : <UploadFileIcon sx={{fontSize: 25, color: 'warning.main'}} />; // Warning color if not uploaded | |||
| ? <CheckCircleIcon sx={{fontSize: 25, color: 'success.main'}} /> | |||
| : <UploadFileIcon sx={{fontSize: 25, color: 'warning.main'}} />; | |||
| const upload2Label = isUploaded2 ? "Update 2" : "Upload 2"; | |||
| actions.push( | |||
| <ThemeProvider key="UploadSign2" theme={LIONER_BUTTON_THEME}> | |||
| <GridActionsCellItem | |||
| @@ -420,6 +467,24 @@ export default function PdfTable({recordList}) { | |||
| ); | |||
| } | |||
| if (showUpload3) { | |||
| const upload3Icon = isUploaded3 | |||
| ? <CheckCircleIcon sx={{fontSize: 25, color: 'success.main'}} /> | |||
| : <UploadFileIcon sx={{fontSize: 25, color: 'warning.main'}} />; | |||
| const upload3Label = isUploaded3 ? "Update 3" : "Upload 3"; | |||
| actions.push( | |||
| <ThemeProvider key="UploadSign3" theme={LIONER_BUTTON_THEME}> | |||
| <GridActionsCellItem | |||
| icon={upload3Icon} | |||
| label={upload3Label} | |||
| className="textPrimary" | |||
| onClick={handleUpload3Click(id, templateName, formCode, row)} | |||
| color="upload" | |||
| /> | |||
| </ThemeProvider> | |||
| ); | |||
| } | |||
| return actions; | |||
| }, | |||
| }, | |||
| @@ -16,6 +16,7 @@ const PdfFormUpAndDown = Loadable(lazy(() => import('pages/pdf/PdfFormUpAndDown' | |||
| const PdfViewer = Loadable(lazy(() => import('pages/pdf/PdfViewer'))); | |||
| const PdfSearchPage = Loadable(lazy(() => import('pages/pdf/PdfSearchPage'))); | |||
| const TemplateSearchPage = Loadable(lazy(() => import('pages/pdf/TemplateSearchPage'))); | |||
| const FormSigPageSearchPanel = Loadable(lazy(() => import('pages/formSigPage'))); | |||
| // ==============================|| AUTH ROUTING ||============================== // | |||
| @@ -88,6 +89,18 @@ const ClientRoutes =() => { | |||
| </Require2FA> | |||
| ), | |||
| }, | |||
| { | |||
| path: '/formSigPage', | |||
| element: ( | |||
| <Require2FA> | |||
| {handleRouteAbility( | |||
| ability.can('VIEW', 'TEMPLATE'), | |||
| <FormSigPageSearchPanel />, | |||
| <Navigate to="/" /> | |||
| )} | |||
| </Require2FA> | |||
| ), | |||
| }, | |||
| { | |||
| path: '/pdf/form-up-down/:id', | |||
| element: ( | |||
| @@ -29,8 +29,6 @@ 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 ||============================== // | |||
| const SettingRoutes = () => { | |||
| @@ -261,16 +259,6 @@ const SettingRoutes = () => { | |||
| ) | |||
| ), | |||
| }, | |||
| { | |||
| path: 'formSigPage', | |||
| element: ( | |||
| handleRouteAbility( | |||
| ability.can('MANAGE', 'SYSTEM_CONFIGURATION'), | |||
| <FormSigPageSearchPanel />, | |||
| <Navigate to="/" /> | |||
| ) | |||
| ), | |||
| }, | |||
| { | |||
| path: 'logout', | |||
| element: <LogoutPage /> | |||