diff --git a/src/menu-items/setting.js b/src/menu-items/setting.js index 8432005..9d6bbff 100644 --- a/src/menu-items/setting.js +++ b/src/menu-items/setting.js @@ -132,6 +132,15 @@ const setting = { breadcrumbs: false, ability:['VIEW','LOGIN_LOG'] }, + { + id: 'userActionLog', + title: 'Action Log', + type: 'item', + url: '/userActionLog', + icon: icons.MenuUnfoldOutlined, + breadcrumbs: false, + ability:['VIEW','USER'] + }, // { // id: 'passwordPolicy', // title: 'Password Policy', diff --git a/src/pages/lionerUserActionLog/UserActionLogSearchForm.js b/src/pages/lionerUserActionLog/UserActionLogSearchForm.js new file mode 100644 index 0000000..063d033 --- /dev/null +++ b/src/pages/lionerUserActionLog/UserActionLogSearchForm.js @@ -0,0 +1,181 @@ +// material-ui +import { + Button, + Grid, TextField,InputLabel +} from '@mui/material'; +import MainCard from "../../components/MainCard"; +import {useForm} from "react-hook-form"; +import * as React from "react"; +import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; +import {DemoItem} from "@mui/x-date-pickers/internals/demo"; +import {DatePicker} from "@mui/x-date-pickers/DatePicker"; +import dayjs from "dayjs"; +import {useState} from "react"; +import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; +import {LIONER_BUTTON_THEME} from "../../themes/colorConst"; +import {ThemeProvider} from "@emotion/react"; +import {CARD_MAX_WIDTH} from "../../themes/themeConst"; +// ==============================|| DASHBOARD - DEFAULT ||============================== // + +const UserActionLogSearchForm = ({applySearch}) => { + + const { reset, register, handleSubmit } = useForm() + const [fromDate, setFromDate] = useState(null); + const [toDate, setToDate] = useState(null); + const [fromError, setFromError] = React.useState(null); + const [toError, setToError] = React.useState(null); + const fromErrorMessage = React.useMemo(() => { + switch (fromError) { + case 'invalidDate': { + return "Invalid date"; + } + } + }, [fromError]); + + const toErrorMessage = React.useMemo(() => { + switch (toError) { + case 'invalidDate': { + return "Invalid date"; + } + } + }, [toError]); + + const onSubmit = (data) => { + const temp = { + username: data.username, + ipAddr: data.ipAddr, + fromDate: fromDate === null ? null : dayjs(fromDate).format('YYYY-MM-DD'), + toDate: toDate === null ? null : dayjs(toDate).format('YYYY-MM-DD'), + } + applySearch(temp); + }; + + function resetForm(){ + setFromDate(null); + setToDate(null); + reset(); + } + + return ( + + +
+ {/*row 1*/} + + + {/*row 2*/} + + + + Username + + + + + + + Action Description + + + + + + + Date Period + + + + + + setFromError(newError)} + slotProps={{ + field: { clearable: true }, + textField: { + helperText: fromErrorMessage, + }, + }} + value={fromDate === null ? null : dayjs(fromDate)} + onChange={(newValue) => setFromDate(newValue)} + /> + + + + + + To + + + + + + setToError(newError)} + slotProps={{ + field: { clearable: true }, + textField: { + helperText: toErrorMessage, + }, + }} + //label="To Date" + value={toDate === null ? null : dayjs(toDate)} + onChange={(newValue) => setToDate(newValue)} + /> + + + + + + + + + {/*last row*/} + + + + + + + + + + + + + +
+ ); +}; + +export default UserActionLogSearchForm; diff --git a/src/pages/lionerUserActionLog/UserActionLogTable.js b/src/pages/lionerUserActionLog/UserActionLogTable.js new file mode 100644 index 0000000..ef271d5 --- /dev/null +++ b/src/pages/lionerUserActionLog/UserActionLogTable.js @@ -0,0 +1,77 @@ +// material-ui +import * as React from 'react'; +import { + DataGrid, +} from "@mui/x-data-grid"; +import {useEffect} from "react"; +import {CustomNoRowsOverlay, dateComparator, getDateString} from "../../utils/CommonFunction"; + +export default function UserActionLogTable({recordList}) { + const [rows, setRows] = React.useState([]); + + const [paginationModel, setPaginationModel] = React.useState({ + page: 0, + pageSize:10 + }); + + useEffect(() => { + setPaginationModel({page:0,pageSize:10}); + setRows(recordList); + }, [recordList]); + + const columns = [ + { + id: 'userName', + field: 'username', + headerName: 'Username', + flex: 1, + }, + { + id: 'actionDesc', + field: 'actionDesc', + headerName: 'Action Description', + flex: 3, + }, + { + id: 'created', + field: 'created', + headerName: 'Action Time', + flex: 1, + sortComparator: dateComparator, + renderCell: (params) => ( +
+ {getDateString(params.row.created, 'dd/MM/yyyy HH:mm:ss')} +
+ ), + }, + + ]; + + return ( +
+ 'auto'} + initialState={{ + pagination: { + paginationModel: {page: 0, pageSize: 15}, + }, + }} + paginationModel={paginationModel} + onPaginationModelChange={setPaginationModel} + slots={{ + noRowsOverlay: () => ( + CustomNoRowsOverlay() + ) + }} + pageSizeOptions={[15]} + autoHeight + getRowId={(row) => row.username + row.created + row.refId + row.refType} + /> + +
+ ); +} diff --git a/src/pages/lionerUserActionLog/index.js b/src/pages/lionerUserActionLog/index.js new file mode 100644 index 0000000..07e234a --- /dev/null +++ b/src/pages/lionerUserActionLog/index.js @@ -0,0 +1,94 @@ +// material-ui +import { + Box, + Grid, Typography +} from '@mui/material'; +import MainCard from "../../components/MainCard"; +import UserActionLogSearchForm from "./UserActionLogSearchForm"; +import UserActionLogTable from "./UserActionLogTable"; +import {useEffect, useState} from "react"; +import * as React from "react"; +import axios from "axios"; +import {apiPath} from "../../auth/utils"; +import {GET_USER_ACTION_LOG_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"; + +// ==============================|| DASHBOARD - DEFAULT ||============================== // + +const UserActionLogSearchPanel = () => { + + const [record, setRecord] = useState([]); + const [searchCriteria, setSearchCriteria] = useState({}); + const [onReady, setOnReady] = useState(false); + + useEffect(() => { + getGroupList(); + }, []); + + useEffect(() => { + setOnReady(true); + }, [record]); + + useEffect(() => { + getGroupList(); + }, [searchCriteria]); + + function getGroupList() { + axios.get(`${apiPath}${GET_USER_ACTION_LOG_LIST}`, + {params: searchCriteria} + ) + .then((response) => { + if (response.status === 200) { + setRecord(response.data.records); + } + }) + .catch(error => { + console.log(error); + return false; + }); + } + + function applySearch(input) { + setSearchCriteria(input); + } + + return ( + !onReady ? + + : + + + + + + + Login Log + + + + + + {/*row 1*/} + + + + {/*row 2*/} + + + + + + + + ); +}; + +export default UserActionLogSearchPanel; diff --git a/src/pages/pdf/PdfSearchPage/PdfTable.js b/src/pages/pdf/PdfSearchPage/PdfTable.js index 36191aa..739dada 100644 --- a/src/pages/pdf/PdfSearchPage/PdfTable.js +++ b/src/pages/pdf/PdfSearchPage/PdfTable.js @@ -9,36 +9,44 @@ import { } from "@mui/x-data-grid"; import EditIcon from '@mui/icons-material/Edit'; import UploadFileIcon from '@mui/icons-material/UploadFile'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import FileDownloadIcon from '@mui/icons-material/FileDownload'; import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, Typography, - Box, // for centering and layout - CircularProgress, // Import for loading indicator + Box, + CircularProgress, } from '@mui/material'; import {useContext, useEffect} from "react"; import {useNavigate} from "react-router-dom"; // Note: Assuming these utility functions/components are defined elsewhere -import {CustomNoRowsOverlay, dateComparator, getDateString} from "../../../utils/CommonFunction"; +import {CustomNoRowsOverlay, dateComparator, getDateString} from "../../../utils/CommonFunction"; import AbilityContext from "../../../components/AbilityProvider"; import {LIONER_BUTTON_THEME} from "../../../themes/colorConst"; import {ThemeProvider} from "@emotion/react"; // ==============================|| PDF TABLE ||============================== // +// Define the structure for the row data stored in state +const initialUploadState = { + id: null, + templateName: null, // This will be the formCode + refType: null, // To differentiate between upload1 and upload2 if needed +} + export default function PdfTable({recordList}) { const [rows, setRows] = React.useState(recordList); const [rowModesModel] = React.useState({}); - // State for Dialog visibility, row ID, and Loading state + // State for Dialog visibility and Loading state const [isDialogOpen, setIsDialogOpen] = React.useState(false); - const [currentRowId, setCurrentRowId] = React.useState(null); - const [isUploading, setIsUploading] = React.useState(false); + // State to hold the ID, templateName, and refType for the current upload operation + const [currentUploadRow, setCurrentUploadRow] = React.useState(initialUploadState); + const [isUploading, setIsUploading] = React.useState(false); const navigate = useNavigate() const ability = useContext(AbilityContext); @@ -47,7 +55,7 @@ export default function PdfTable({recordList}) { page: 0, pageSize:10 }); - + // Ref for the hidden file input const fileInputRef = React.useRef(null); @@ -60,21 +68,36 @@ export default function PdfTable({recordList}) { navigate(`/pdf/maintain/${id}`); }; - // Opens the upload dialog and sets the current row ID - const handleUploadClick = (id) => () => { - setCurrentRowId(id); + /** + * Opens the upload dialog and sets the current row details for Upload 1 + */ + const handleUploadClick = (id, templateName, formCode) => () => { + setCurrentUploadRow({ + id: id, + templateName: templateName, + formCode: formCode, + refType: "upload1" + }); setIsDialogOpen(true); }; - const handleUpload2Click = (id, templateName) => () => { + /** + * Opens the upload dialog and sets the current row details for Upload 2 + */ + const handleUpload2Click = (id, templateName, formCode) => () => { // Placeholder for Upload 2 console.log(`Uploading for row ID ${id} (Upload 2)`); - setCurrentRowId(id); + setCurrentUploadRow({ + id: id, + templateName: templateName, + formCode: formCode, + refType: "upload2" // A different refType for a different upload logic/API + }); setIsDialogOpen(true); }; const handleDownloadClick = (id) => () => { - + // 1. Construct the download URL with the ID query parameter const downloadUrl = `${apiPath}/pdf/download-ff/${id}`; @@ -83,7 +106,7 @@ export default function PdfTable({recordList}) { 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 @@ -102,12 +125,12 @@ export default function PdfTable({recordList}) { 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); + link.setAttribute('download', filename); document.body.appendChild(link); link.click(); - + // 4. Clean up document.body.removeChild(link); window.URL.revokeObjectURL(url); @@ -121,17 +144,68 @@ export default function PdfTable({recordList}) { const handleCloseDialog = () => { setIsDialogOpen(false); - setCurrentRowId(null); + setCurrentUploadRow(initialUploadState); // Reset the current row if (fileInputRef.current) { fileInputRef.current.value = ""; // Clear the file input } }; + + // Function to generate the dynamic dialog title + const getUploadDialogTitle = (formCode, refType) => { + console.log("formCode:" + formCode + " refType:" + refType); + if (refType === 'upload1') { + switch (formCode) { + case "IDA": + return "Upload Page 12"; + case "FNA": + return "Upload Page 7"; + case "HSBCFIN": + return "Upload Page 11"; + case "HSBCA31": + return "Upload Page 28-29"; + case "MLB03S": + return "Upload Page 7"; + 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 16"; + 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"; + case "SLGII": + return "Upload Page 15-16"; + case "SLAPP": + return "Upload Page 18-19"; + 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 + }; // Function to handle file selection and API submission const handleFileChange = async (event) => { const file = event.target.files[0]; - - if (!file) return; + const { id, templateName, refType } = currentUploadRow; + + if (!file || !id || !refType) return; if (file.type !== "application/pdf") { alert("Please select a PDF file."); @@ -139,43 +213,58 @@ export default function PdfTable({recordList}) { return; } - const uploadUrl = apiPath + '/pdf/upload1'; + // The URL should potentially change based on refType if upload2 uses a different endpoint + const uploadUrl = `${apiPath}${POST_SIG_UPLOAD1}`; // Assuming POST_SIG_UPLOAD1 is used for both for now // 1. Create FormData const formData = new FormData(); formData.append('file', file); - formData.append('refId', currentRowId); - formData.append('refType', "upload1"); + formData.append('refId', id); + formData.append('refType', refType); // Pass the refType to the backend setIsUploading(true); try { - axios.post(`${apiPath}${POST_SIG_UPLOAD1}`, + const response = await axios.post( + uploadUrl, 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}!`); + ); + + if (response.status === 200) { + alert('Upload success'); + console.log(`PDF file ${file.name} successfully uploaded for record ID: ${id} with refType: ${refType}!`); + + // --- START: Update local state to show the green tick --- + const uploadedFileId = response.data?.fileId || 'temp-id-' + Date.now(); // Assume the response has a fileId or use a temp one + + setRows(prevRows => + prevRows.map(row => { + if (row.id === id) { + // Update the relevant file ID field based on refType + const updateField = refType === 'upload1' ? 'upload1FileId' : 'upload2FileId'; + return { + ...row, + [updateField]: uploadedFileId // Set the file ID to trigger the icon + }; + } + return row; + }) + ); + // --- END: Update local state to show the green tick --- + + } else { + throw new Error(`Upload failed with status: ${response.status}`); + } - } - setIsUploading(false); - }) - .catch((error) => { - setIsUploading(false); - console.log(error); - return false; - }); - - } catch (error) { console.error('Upload error:', error); - alert(`Error uploading file: ${error.message}`); + // Check if the error has a response and use its message if available + const errorMessage = error.response?.data?.message || error.message; + alert(`Error uploading file: ${errorMessage}`); } finally { // 3. Cleanup and close setIsUploading(false); @@ -189,7 +278,7 @@ export default function PdfTable({recordList}) { fileInputRef.current.click(); } }; - + const columns = [ { field: 'actions', @@ -211,7 +300,7 @@ export default function PdfTable({recordList}) { ] }, }, - + { id: 'templateName', field: 'templateName', @@ -237,16 +326,19 @@ export default function PdfTable({recordList}) { 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 + + // Check if a file ID exists to determine if a file is present for Upload 1 + const isUploaded1 = !!row.upload1FileId; + const isUploaded2 = !!row.upload2FileId; // <<< ADD THIS LINE <<< + const templateName = row.templateName; + const formCode = row.formCode; + + // Determine the icon and label based on upload status for Upload 1 + const upload1Icon = isUploaded1 ? // Green tick if uploaded : ; // Upload icon if not uploaded - - const upload1Label = isUploaded ? "Update Signature" : "Upload Signature"; + + const upload1Label = isUploaded1 ? "Update Signature" : "Upload Signature"; // Define the actions const actions = [ @@ -255,21 +347,29 @@ export default function PdfTable({recordList}) { icon={upload1Icon} // Use the dynamic icon label={upload1Label} // Use the dynamic label className="textPrimary" - onClick={handleUploadClick(id)} + onClick={handleUploadClick(id, templateName, formCode)} // Pass templateName here color="upload" // Use 'upload' color which will apply to the button /> ]; // Conditional rendering logic for Upload 2 - if (row.templateName === "MLB03S" || row.templateName === "SLGII" || row.templateName === "SLAPP") { + if (row.formCode === "MLB03S" || row.formCode === "SLGII" || row.formCode === "SLAPP") { + // Determine the icon and label based on upload status for Upload 2 <<< START CHANGES HERE <<< + const upload2Icon = isUploaded2 + ? // Green tick if uploaded + : ; // Upload icon if not uploaded + + const upload2Label = isUploaded2 ? "Update 2" : "Upload 2"; + // >>> END CHANGES HERE <<< + actions.push( } - label="Upload2" + icon={upload2Icon} // <<< USE DYNAMIC ICON <<< + label={upload2Label} // <<< USE DYNAMIC LABEL <<< className="textPrimary" - onClick={handleUpload2Click(id, row.templateName)} + onClick={handleUpload2Click(id, templateName, formCode)} // Pass templateName here color="upload" /> @@ -292,7 +392,7 @@ export default function PdfTable({recordList}) { icon={} label="Download" className="textPrimary" - onClick={handleDownloadClick(id, "{params.row.templateName}")} + onClick={handleDownloadClick(id)} color="download" /> @@ -344,7 +444,7 @@ export default function PdfTable({recordList}) { rows={rows} columns={columns} // Increased height to accommodate the multi-line header - columnHeaderHeight={70} + columnHeaderHeight={70} editMode="row" rowModesModel={rowModesModel} getRowHeight={() => 'auto'} @@ -358,25 +458,22 @@ export default function PdfTable({recordList}) { pageSizeOptions={[10]} autoHeight /> - + {/* The Upload Dialog Box */} - - Upload Signature Page (1-Page PDF) + {/* Dynamic Title based on currentUploadRow state */} + **{getUploadDialogTitle(currentUploadRow.formCode, currentUploadRow.refType)}** - - Please select the single-page PDF file containing the signature for record ID: **{currentRowId}**. - - {/* Button to trigger file selection */}