| @@ -516,6 +516,20 @@ function Header(props) { | |||
| {isGranted("MAINTAIN_SETTING") ? ( | |||
| <> | |||
| <li> | |||
| <Link className="systemSetting" to="/setting/hkidKeyMigration"> | |||
| <Typography style={{ opacity: 0.9 }} variant={"pnspsHeaderTitle"} sx={{ ml: 2, mt: 1, mb: 1 }}> | |||
| HKID Key Migration | |||
| </Typography> | |||
| </Link> | |||
| </li> | |||
| <li> | |||
| <Link className="systemSetting" to="/setting/userPiiEncryption"> | |||
| <Typography style={{ opacity: 0.9 }} variant={"pnspsHeaderTitle"} sx={{ ml: 2, mt: 1, mb: 1 }}> | |||
| User PII Encryption | |||
| </Typography> | |||
| </Link> | |||
| </li> | |||
| <li> | |||
| <Link className="systemSetting" to="/setting/sys"> | |||
| <Typography style={{ opacity: 0.9 }} variant={"pnspsHeaderTitle"} sx={{ ml: 2, mt: 1, mb: 1 }}> | |||
| @@ -3,24 +3,33 @@ import { | |||
| Grid, | |||
| Typography, | |||
| FormLabel, | |||
| Button | |||
| Button, | |||
| Dialog, | |||
| DialogTitle, | |||
| DialogContent, | |||
| DialogActions, | |||
| Box | |||
| } from '@mui/material'; | |||
| import * as React from "react"; | |||
| import * as FormatUtils from "utils/FormatUtils"; | |||
| import * as PaymentStatus from "utils/statusUtils/PaymentStatus"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import { PAYMENT_MARK_AS_PAID } from "utils/ApiPathConst"; | |||
| import Loadable from 'components/Loadable'; | |||
| const MainCard = Loadable(React.lazy(() => import('components/MainCard'))); | |||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | |||
| import DownloadIcon from '@mui/icons-material/Download'; | |||
| import {useIntl} from "react-intl"; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const PaymentDetails = ({ formData,doPrint,onDownload }) => { | |||
| const PaymentDetails = ({ formData, doPrint, onDownload, onRefresh }) => { | |||
| const intl = useIntl(); | |||
| const [data, setData] = React.useState({}); | |||
| const [onReady, setOnReady] = React.useState(false); | |||
| const [confirmOpen, setConfirmOpen] = React.useState(false); | |||
| const [markingPaid, setMarkingPaid] = React.useState(false); | |||
| // const { locale } = intl; | |||
| React.useEffect(() => { | |||
| @@ -40,9 +49,29 @@ const PaymentDetails = ({ formData,doPrint,onDownload }) => { | |||
| if("01" == paymentmethod) return "PPS"; | |||
| if("02" == paymentmethod || "03" == paymentmethod) return "Credit Card"; | |||
| if("04" == paymentmethod) return "FPS"; | |||
| if (data.payMethod === "04,BCFP,FPS") return "FPS"; | |||
| return paymentmethod; | |||
| } | |||
| const showMarkAsPaid = data.status === "REJT" && getPaymentMethod() === "FPS"; | |||
| const handleMarkAsPaid = () => { | |||
| setMarkingPaid(true); | |||
| HttpUtils.post({ | |||
| url: PAYMENT_MARK_AS_PAID + "/" + data.id, | |||
| onSuccess: () => { | |||
| setConfirmOpen(false); | |||
| setMarkingPaid(false); | |||
| if (onRefresh) { | |||
| onRefresh(); | |||
| } | |||
| }, | |||
| onError: () => { | |||
| setMarkingPaid(false); | |||
| } | |||
| }); | |||
| }; | |||
| return ( | |||
| !onReady ? | |||
| <LoadingComponent /> | |||
| @@ -51,9 +80,21 @@ const PaymentDetails = ({ formData,doPrint,onDownload }) => { | |||
| border={false} | |||
| content={false} | |||
| > | |||
| <Typography variant="h5" sx={{ textAlign: "left", mb: 2, borderBottom: "1px solid black" }}> | |||
| Payment Details | |||
| </Typography> | |||
| <Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 2, borderBottom: "1px solid black" }}> | |||
| <Typography variant="h5" sx={{ textAlign: "left" }}> | |||
| Payment Details | |||
| </Typography> | |||
| {showMarkAsPaid && ( | |||
| <Button | |||
| className="printHidden" | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => setConfirmOpen(true)} | |||
| > | |||
| Mark as Paid | |||
| </Button> | |||
| )} | |||
| </Box> | |||
| <form> | |||
| <Grid container> | |||
| <Grid item xs={12} md={12} > | |||
| @@ -116,7 +157,7 @@ const PaymentDetails = ({ formData,doPrint,onDownload }) => { | |||
| <Grid container > | |||
| <Grid item xs={6} md={6} sx={{ml:-5, textAlign: "right" }}> | |||
| <FormLabel sx={{ color: "#000000" }}> | |||
| EGIS Reference No.: | |||
| Payment Reference No.: | |||
| </FormLabel> | |||
| </Grid> | |||
| <Grid item xs={6} md={5} sx={{ml:5, textAlign: "left" }}> | |||
| @@ -176,6 +217,25 @@ const PaymentDetails = ({ formData,doPrint,onDownload }) => { | |||
| } | |||
| </Grid> | |||
| </form> | |||
| <Dialog | |||
| open={confirmOpen} | |||
| onClose={() => !markingPaid && setConfirmOpen(false)} | |||
| > | |||
| <DialogTitle>Confirm</DialogTitle> | |||
| <DialogContent> | |||
| <Typography variant="h5" sx={{ padding: '16px' }}> | |||
| {`Are you sure to mark as paid for Payment No. ${data.transNo || data.payload?.transactionid} ?`} | |||
| </Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={() => setConfirmOpen(false)} disabled={markingPaid}> | |||
| <Typography variant="h5">Cancel</Typography> | |||
| </Button> | |||
| <Button onClick={handleMarkAsPaid} disabled={markingPaid}> | |||
| <Typography variant="h5">Confirm</Typography> | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| </MainCard> | |||
| ); | |||
| }; | |||
| @@ -35,6 +35,9 @@ const Index = () => { | |||
| const params = useParams(); | |||
| const navigate = useNavigate() | |||
| const [responeData, setResponeData] = React.useState({}); | |||
| const [transactionData, setTransactionData] = React.useState({}); | |||
| const [record, setRecord] = React.useState(); | |||
| const [itemList, setItemList] = React.useState([]); | |||
| const [onReady, setOnReady] = React.useState(false); | |||
| @@ -47,8 +50,16 @@ const Index = () => { | |||
| }, []); | |||
| React.useEffect(() => { | |||
| setOnReady(true); | |||
| }, [record]); | |||
| if (Object.keys(responeData).length > 0) { | |||
| setTransactionData(responeData); | |||
| } | |||
| }, [responeData]); | |||
| React.useEffect(() => { | |||
| if (Object.keys(transactionData).length > 0) { | |||
| setOnReady(true); | |||
| } | |||
| }, [transactionData]); | |||
| // const handleResize = () => { | |||
| // setDetailsOrder(window.innerWidth > 1023 ? 2 : -1); | |||
| @@ -70,17 +81,40 @@ const Index = () => { | |||
| const loadForm = () => { | |||
| if (params.id > 0) { | |||
| HttpUtils.get({ | |||
| url: UrlUtils.PAYMENT_LOAD + "/" + params.id, | |||
| onSuccess: (responseData) => { | |||
| if (!responseData.data?.id) { | |||
| navigate("/paymentPage/search"); | |||
| } | |||
| responseData.data["transDateStr"] = responseData.data.transDateTime; | |||
| responseData.data["transTimeStr"] = DateUtils.dateFormat(responseData.data.transDateTime, "HH:mm:ss"); | |||
| setItemList(responseData.paymentItemList) | |||
| setRecord(responseData.data); | |||
| if (responseData.data.status == null || responseData.data.status == "INPR") { | |||
| HttpUtils.post({ | |||
| url: UrlUtils.PAYMENT_RETRY_STATUS_API, | |||
| params: { | |||
| "paymentId": params.id | |||
| }, | |||
| onSuccess: function (responseData2) { | |||
| responseData2.data["transDateStr"] = responseData2.data.transDateTime; | |||
| responseData2.data["transTimeStr"] = DateUtils.dateFormat(responseData2.data.transDateTime, "HH:mm:ss"); | |||
| setResponeData(responseData2.transactionData); | |||
| setItemList(responseData2.paymentItemList); | |||
| setRecord(responseData2.data); | |||
| }, | |||
| onError: function () { | |||
| responseData.data["transDateStr"] = responseData.data.transDateTime; | |||
| responseData.data["transTimeStr"] = DateUtils.dateFormat(responseData.data.transDateTime, "HH:mm:ss"); | |||
| setResponeData(responseData); | |||
| setItemList(responseData.paymentItemList); | |||
| setRecord(responseData.data); | |||
| } | |||
| }); | |||
| } else { | |||
| responseData.data["transDateStr"] = responseData.data.transDateTime; | |||
| responseData.data["transTimeStr"] = DateUtils.dateFormat(responseData.data.transDateTime, "HH:mm:ss"); | |||
| setResponeData(responseData); | |||
| setItemList(responseData.paymentItemList); | |||
| setRecord(responseData.data); | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| @@ -125,6 +159,7 @@ const Index = () => { | |||
| formData={record} | |||
| doPrint={doPrint} | |||
| onDownload={onDownload} | |||
| onRefresh={loadForm} | |||
| style={{ | |||
| display: "flex", | |||
| height: "100%", | |||
| @@ -87,7 +87,7 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||
| }, | |||
| { | |||
| field: 'actions', | |||
| headerName: 'Transaction No.', | |||
| headerName: 'Payment No.', | |||
| flex: 1, | |||
| minWidth: 200, | |||
| cellClassName: 'actions', | |||
| @@ -95,6 +95,12 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||
| return clickableLink('/paymentPage/details/' + params.row.id, params.row.transNo); | |||
| }, | |||
| }, | |||
| { | |||
| field: 'egisRefNo', | |||
| headerName: 'Payment Reference No.', | |||
| flex: 1, | |||
| minWidth: 200, | |||
| }, | |||
| { | |||
| field: 'payMethod', | |||
| headerName: 'Payment Method', | |||
| @@ -107,7 +113,7 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||
| { | |||
| id: 'transDateTime', | |||
| field: 'transDateTime', | |||
| headerName: 'Transaction Date', | |||
| headerName: 'Payment Date', | |||
| flex: 1, | |||
| minWidth: 150, | |||
| @@ -11,6 +11,7 @@ import { FiDataGrid } from "components/FiDataGrid"; | |||
| import {useTheme} from "@emotion/react"; | |||
| import {useIntl} from "react-intl"; | |||
| import { clickableLink } from 'utils/CommonFunction'; | |||
| import { getPaymentMethodByCode } from "auth/utils"; | |||
| import {PAYMENT_LIST} from "utils/ApiPathConst"; | |||
| // ==============================|| EVENT TABLE ||============================== // | |||
| @@ -58,6 +59,12 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| return clickableLink('/paymentPage/details/' + params.row.id, params.row.transNo); | |||
| }, | |||
| }, | |||
| { | |||
| field: 'egisRefNo', | |||
| headerName: intl.formatMessage({id: 'paymentRefCode'}), | |||
| width: isMdOrLg ? 'auto' : 160, | |||
| flex: isMdOrLg ? 1 : undefined, | |||
| }, | |||
| { | |||
| id: 'appNos', | |||
| field: 'appNos', | |||
| @@ -69,6 +76,13 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| return <div style={{ margin: 4 }}>{appNo}</div> | |||
| }, | |||
| }, | |||
| { | |||
| field: 'payMethod', | |||
| headerName: intl.formatMessage({id: 'paymentMethod'}), | |||
| width: isMdOrLg ? 'auto' : 160, | |||
| flex: isMdOrLg ? 1 : undefined, | |||
| renderCell: (params) => getPaymentMethodByCode(params?.value), | |||
| }, | |||
| { | |||
| id: 'transDateTime', | |||
| field: 'transDateTime', | |||
| @@ -0,0 +1,101 @@ | |||
| import * as React from "react"; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import * as UrlUtils from "utils/ApiPathConst"; | |||
| import { | |||
| Grid, Typography, Button, | |||
| Stack, Box, CircularProgress, | |||
| } from '@mui/material'; | |||
| import { notifyActionError } from 'utils/CommonFunction'; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| const formatLog = (responData) => { | |||
| if (responData?.msg && !responData?.log) { | |||
| return <>{DateUtils.datetimeStr(new Date())}<br/><span style={{ color: "red" }}>Error</span><br/>{responData.msg}</>; | |||
| } | |||
| const statusColor = responData?.success ? "green" : "red"; | |||
| const statusText = responData?.success ? "Success" : "Completed with errors"; | |||
| const logText = (responData?.log || []).join("\n"); | |||
| return ( | |||
| <> | |||
| {DateUtils.datetimeStr(new Date())}<br/> | |||
| <span style={{ color: statusColor }}>{responData?.dryRun ? "Dry-run " : ""}{statusText}</span><br/> | |||
| Total: {responData?.total ?? 0}, OK: {responData?.successCount ?? 0}, Skipped: {responData?.skipped ?? 0}, Failed: {responData?.failed ?? 0}<br/> | |||
| <span style={{ whiteSpace: "pre-line" }}>{logText}</span> | |||
| </> | |||
| ); | |||
| }; | |||
| const HkidKeyMigration = () => { | |||
| const [resultStr, setResultStr] = React.useState(""); | |||
| const [wait, setWait] = React.useState(false); | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: 'auto', | |||
| height: 'auto', | |||
| backgroundSize: 'contain', | |||
| backgroundRepeat: 'no-repeat', | |||
| backgroundColor: '#0C489E', | |||
| backgroundPosition: 'right' | |||
| }; | |||
| const runMigration = (dryRun) => { | |||
| setWait(true); | |||
| HttpUtils.post({ | |||
| url: `${UrlUtils.HKID_REKEY_MIGRATION}?dryRun=${dryRun}&batchSize=100`, | |||
| params: {}, | |||
| onSuccess: function (responData) { | |||
| setWait(false); | |||
| setResultStr(formatLog(responData)); | |||
| }, | |||
| onError: function () { | |||
| setWait(false); | |||
| notifyActionError("HKID re-key migration failed"); | |||
| } | |||
| }); | |||
| }; | |||
| return ( | |||
| <Grid container sx={{ minHeight: '87vh', backgroundColor: "backgroundColor.default" }} direction="column"> | |||
| <Grid item xs={12}> | |||
| <div style={BackgroundHead}> | |||
| <Stack direction="row" height='70px' justifyContent="space-between" alignItems="center"> | |||
| <Typography ml={15} color='#FFF' variant="h4" sx={{ textShadow: "0px 0px 25px #0C489E" }}> | |||
| HKID Key Migration | |||
| </Typography> | |||
| </Stack> | |||
| </div> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1 }} width="98%"> | |||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||
| <Typography variant="body1" sx={{ mb: 2 }}> | |||
| Re-encrypt identification and checkDigit from the legacy key to the new key. | |||
| Ensure Tomcat has both <code>security.hkid-secret</code> and <code>security.hkid-secret-legacy</code> configured before running. | |||
| </Typography> | |||
| <Stack direction="row" spacing={2}> | |||
| <Button variant="outlined" size="large" disabled={wait} onClick={() => runMigration(true)}> | |||
| Dry Run | |||
| </Button> | |||
| <Button variant="contained" size="large" disabled={wait} onClick={() => runMigration(false)}> | |||
| Run Migration | |||
| </Button> | |||
| {wait ? <CircularProgress size={32} /> : null} | |||
| </Stack> | |||
| </Box> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1, mb: 2 }} width="98%"> | |||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||
| <Typography variant="h4">Result:</Typography> | |||
| <Box sx={{ pl: 2, pt: 2 }}>{resultStr}</Box> | |||
| </Box> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default HkidKeyMigration; | |||
| @@ -0,0 +1,133 @@ | |||
| import * as React from "react"; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import * as UrlUtils from "utils/ApiPathConst"; | |||
| import { | |||
| Grid, Typography, Button, | |||
| Stack, Box, CircularProgress, | |||
| } from '@mui/material'; | |||
| import { notifyActionError } from 'utils/CommonFunction'; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| const formatEncryptLog = (responData) => { | |||
| if (responData?.msg && !responData?.log) { | |||
| return <>{DateUtils.datetimeStr(new Date())}<br/><span style={{ color: "red" }}>Error</span><br/>{responData.msg}</>; | |||
| } | |||
| const statusColor = responData?.success ? "green" : "red"; | |||
| const statusText = responData?.success ? "Success" : "Completed with errors"; | |||
| const logText = (responData?.log || []).join("\n"); | |||
| return ( | |||
| <> | |||
| {DateUtils.datetimeStr(new Date())}<br/> | |||
| <span style={{ color: statusColor }}>{responData?.dryRun ? "Dry-run " : ""}{statusText}</span><br/> | |||
| Total: {responData?.total ?? 0}, OK: {responData?.successCount ?? 0}, Skipped: {responData?.skipped ?? 0}, Failed: {responData?.failed ?? 0}<br/> | |||
| <span style={{ whiteSpace: "pre-line" }}>{logText}</span> | |||
| </> | |||
| ); | |||
| }; | |||
| const formatVerifyLog = (responData) => { | |||
| const tests = responData?.tests || []; | |||
| const logText = tests.map(t => `${t.passed ? "PASS" : "FAIL"} - ${t.name}: ${t.message}`).join("\n"); | |||
| const statusColor = responData?.success ? "green" : "red"; | |||
| return ( | |||
| <> | |||
| {DateUtils.datetimeStr(new Date())}<br/> | |||
| <span style={{ color: statusColor }}>{responData?.success ? "All tests passed" : "Some tests failed"}</span><br/> | |||
| Passed: {responData?.passed ?? 0}, Failed: {responData?.failed ?? 0}<br/> | |||
| <span style={{ whiteSpace: "pre-line" }}>{logText}</span> | |||
| </> | |||
| ); | |||
| }; | |||
| const UserPiiEncryption = () => { | |||
| const [resultStr, setResultStr] = React.useState(""); | |||
| const [wait, setWait] = React.useState(false); | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: 'auto', | |||
| height: 'auto', | |||
| backgroundSize: 'contain', | |||
| backgroundRepeat: 'no-repeat', | |||
| backgroundColor: '#0C489E', | |||
| backgroundPosition: 'right' | |||
| }; | |||
| const runEncrypt = (dryRun) => { | |||
| setWait(true); | |||
| HttpUtils.post({ | |||
| url: `${UrlUtils.USER_PII_ENCRYPT_MIGRATION}?dryRun=${dryRun}&batchSize=100`, | |||
| params: {}, | |||
| onSuccess: function (responData) { | |||
| setWait(false); | |||
| setResultStr(formatEncryptLog(responData)); | |||
| }, | |||
| onError: function () { | |||
| setWait(false); | |||
| notifyActionError("User PII encryption failed"); | |||
| } | |||
| }); | |||
| }; | |||
| const runVerify = () => { | |||
| setWait(true); | |||
| HttpUtils.post({ | |||
| url: UrlUtils.USER_PII_VERIFY_MIGRATION, | |||
| params: {}, | |||
| onSuccess: function (responData) { | |||
| setWait(false); | |||
| setResultStr(formatVerifyLog(responData)); | |||
| }, | |||
| onError: function () { | |||
| setWait(false); | |||
| notifyActionError("User PII verification failed"); | |||
| } | |||
| }); | |||
| }; | |||
| return ( | |||
| <Grid container sx={{ minHeight: '87vh', backgroundColor: "backgroundColor.default" }} direction="column"> | |||
| <Grid item xs={12}> | |||
| <div style={BackgroundHead}> | |||
| <Stack direction="row" height='70px' justifyContent="space-between" alignItems="center"> | |||
| <Typography ml={15} color='#FFF' variant="h4" sx={{ textShadow: "0px 0px 25px #0C489E" }}> | |||
| User PII Encryption | |||
| </Typography> | |||
| </Stack> | |||
| </div> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1 }} width="98%"> | |||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||
| <Typography variant="body1" sx={{ mb: 2 }}> | |||
| Encrypt enName, chName, mobileNumber, and address for existing user records, then verify data retrieval. | |||
| </Typography> | |||
| <Stack direction="row" spacing={2} flexWrap="wrap"> | |||
| <Button variant="outlined" size="large" disabled={wait} onClick={() => runEncrypt(true)}> | |||
| Dry Run Encrypt | |||
| </Button> | |||
| <Button variant="contained" size="large" disabled={wait} onClick={() => runEncrypt(false)}> | |||
| Run Encrypt | |||
| </Button> | |||
| <Button variant="contained" color="secondary" size="large" disabled={wait} onClick={runVerify}> | |||
| Verify Retrieval | |||
| </Button> | |||
| {wait ? <CircularProgress size={32} /> : null} | |||
| </Stack> | |||
| </Box> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ backgroundColor: '#ffffff', ml: 2, mt: 1, mb: 2 }} width="98%"> | |||
| <Box sx={{ borderRadius: '10px', backgroundColor: '#fff', p: 4 }}> | |||
| <Typography variant="h4">Result:</Typography> | |||
| <Box sx={{ pl: 2, pt: 2 }}>{resultStr}</Box> | |||
| </Box> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default UserPiiEncryption; | |||
| @@ -81,7 +81,10 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| addressLine3: yup.string().max(40, getMaxErrStr(40)).nullable(), | |||
| emailAddress: yup.string().email(intl.formatMessage({ id: 'validEmailFormat' })).max(255).required(intl.formatMessage({ id: 'requireEmail' })), | |||
| idDocType: yup.string().max(255, getMaxErrStr(255)).required(intl.formatMessage({ id: 'requireIdDocType' })), | |||
| identification: yup.string().required(getRequiredErrStr('number')) | |||
| identification: yup.string().when('verifiedBy', { | |||
| is: (verifiedBy) => verifiedBy != null, | |||
| then: (schema) => schema.notRequired(), | |||
| otherwise: (schema) => schema.required(getRequiredErrStr('number')) | |||
| .matches(/^[aA-zZ0-9\s]+$/, { message: displayErrorMsg(`${selectedIdDocInputType}${intl.formatMessage({ id: 'noSpecialCharacter' })}`) }) | |||
| .matches(/^\S*$/, { message: displayErrorMsg(`${selectedIdDocInputType}${intl.formatMessage({ id: 'noSpace' })}`) }) | |||
| .test('checkIDCardFormat', displayErrorMsg(`${intl.formatMessage({ id: 'requiredValid' })}${selectedIdDocInputType}${intl.formatMessage({ id: 'number' })}`), function (value) { | |||
| @@ -133,8 +136,12 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| break; | |||
| } | |||
| } | |||
| }) | |||
| }), | |||
| checkDigit: yup.string().max(1, getMaxErrStr(1)).nullable() | |||
| checkDigit: yup.string().when('verifiedBy', { | |||
| is: (verifiedBy) => verifiedBy != null, | |||
| then: (schema) => schema.notRequired(), | |||
| otherwise: (schema) => schema.max(1, getMaxErrStr(1)).nullable() | |||
| .matches(/^[A-Z0-9\s]+$/, { message: displayErrorMsg(`${selectedIdDocInputType}${intl.formatMessage({ id: 'noSpecialCharacter' })}`) }) | |||
| .test('checkIDCardFormat', displayErrorMsg(`${intl.formatMessage({ id: 'requiredNumberInQuote' })}`), function (value) { | |||
| // console.log(selectedIdDocInputType) | |||
| @@ -162,7 +169,8 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| return false | |||
| } | |||
| } | |||
| }), | |||
| }) | |||
| }), | |||
| tel_countryCode: yup.string().min(3, intl.formatMessage({ id: 'require3Number' })).required(intl.formatMessage({ id: 'requireDialingCode' })), | |||
| fax_countryCode: yup.string().min(3, intl.formatMessage({ id: 'require3Number' })), | |||
| phoneNumber: yup.string().min(8, intl.formatMessage({ id: 'require8Number' })).required(intl.formatMessage({ id: 'requireContactNumber' })), | |||
| @@ -186,8 +194,10 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| countryCode: values.tel_countryCode, | |||
| phoneNumber: values.phoneNumber | |||
| }, | |||
| identification: values.identification, | |||
| checkDigit: values.checkDigit, | |||
| ...(values.verifiedBy == null ? { | |||
| identification: values.identification, | |||
| checkDigit: values.checkDigit, | |||
| } : {}), | |||
| faxNo: { | |||
| countryCode: values.fax_countryCode, | |||
| faxNumber: values.faxNumber | |||
| @@ -471,6 +481,11 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| </Grid> | |||
| <Grid item xs={12} sm={12} md={9} lg={6}> | |||
| {currentUserData.verifiedBy ? | |||
| <Typography variant="h5" mt={1}> | |||
| Hidden for security purpose | |||
| </Typography> | |||
| : | |||
| <Grid container> | |||
| {formik.values.idDocType === "HKID" ? | |||
| editMode ? | |||
| @@ -549,6 +564,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| </Stack> | |||
| } | |||
| </Grid> | |||
| } | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| @@ -2,8 +2,6 @@ | |||
| import { | |||
| Grid, Button, Typography, | |||
| FormHelperText, | |||
| Stack, | |||
| IconButton | |||
| } from '@mui/material'; | |||
| import MainCard from "components/MainCard"; | |||
| import * as React from "react"; | |||
| @@ -23,7 +21,6 @@ import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst'; | |||
| import {FormattedMessage, useIntl} from "react-intl"; | |||
| import {PNSPS_BUTTON_THEME} from "../../../themes/buttonConst"; | |||
| import {ThemeProvider} from "@emotion/react"; | |||
| import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| @@ -34,15 +31,6 @@ const UserInformationCard_Individual_Pub = ({ formData, loadDataFun }) => { | |||
| const [editMode, setEditMode] = useState(false); | |||
| const [onReady, setOnReady] = useState(false); | |||
| const [errorMsg, setErrorMsg] = useState(""); | |||
| const [showId, setshowId] = useState(false); | |||
| const handleClickShowId = () => { | |||
| setshowId(!showId); | |||
| }; | |||
| const handleMouseDownId = (event) => { | |||
| event.preventDefault(); | |||
| }; | |||
| useEffect(() => { | |||
| if (Object.keys(formData).length > 0) { | |||
| @@ -252,79 +240,9 @@ const UserInformationCard_Individual_Pub = ({ formData, loadDataFun }) => { | |||
| </Grid> | |||
| <Grid item xs={12} sm={12} md={9} lg={6}> | |||
| <Grid container> | |||
| {formik.values.idDocType === "HKID" ? | |||
| // <> | |||
| // <Grid item xs={6} sm={6} md={6} lg={7.5} sx={{mr:1}}> | |||
| // {FieldUtils.initField({ | |||
| // valueName: "identification", | |||
| // disabled: true, | |||
| // form: formik, | |||
| // placeholder: intl.formatMessage({id: 'idDocNumber'}), | |||
| // inputProps: { | |||
| // maxLength: 7, | |||
| // onKeyDown: (e) => { | |||
| // if (e.key === 'Enter') { | |||
| // e.preventDefault(); | |||
| // } | |||
| // }, | |||
| // } | |||
| // })} | |||
| // </Grid> | |||
| // <Grid item xs={2} sm={2} md={2} lg={2} style={{minWidth:40}}> | |||
| // {FieldUtils.initField({ | |||
| // valueName: "checkDigit", | |||
| // disabled: true, | |||
| // form: formik, | |||
| // })} | |||
| // </Grid> | |||
| // </> | |||
| <Stack direction="row"> | |||
| <Typography variant="h5" mt={1}> | |||
| {formik.values.identification?.slice(0, 4)} | |||
| </Typography> | |||
| <Typography variant="h5"mt={1}> | |||
| {/* {showId ?formik.values.identification.slice(4):"****"}{showId ? '(' + formik.values.checkDigit + ')' :null} */} | |||
| {showId ? formik.values.identification?.slice(4) : "****"}{showId ? formik.values.checkDigit?'(' +formik.values.checkDigit+ ')': "()" : ""} | |||
| </Typography> | |||
| <IconButton | |||
| aria-label={intl.formatMessage({ id: 'ariaToggleIdVisibility' })} | |||
| onClick={handleClickShowId} | |||
| onMouseDown={handleMouseDownId} | |||
| edge="end" | |||
| size="large" | |||
| > | |||
| {showId ? <EyeOutlined /> : <EyeInvisibleOutlined />} | |||
| </IconButton> | |||
| </Stack> | |||
| : | |||
| // <Grid item xs={10} sm={4} md={4} lg={10}> | |||
| // {FieldUtils.initField({ | |||
| // valueName: "identification", | |||
| // disabled: true, | |||
| // form: formik | |||
| // })} | |||
| // </Grid> | |||
| <Stack direction="row"> | |||
| <Typography variant="h5" mt={1}> | |||
| {formik.values.identification?.slice(0, 4)} | |||
| </Typography> | |||
| <Typography variant="h5"mt={1}> | |||
| {showId ?formik.values.identification?.slice(4):"****"} | |||
| </Typography> | |||
| <IconButton | |||
| aria-label={intl.formatMessage({ id: 'ariaToggleIdVisibility' })} | |||
| onClick={handleClickShowId} | |||
| onMouseDown={handleMouseDownId} | |||
| edge="end" | |||
| size="large" | |||
| > | |||
| {showId ? <EyeOutlined /> : <EyeInvisibleOutlined />} | |||
| </IconButton> | |||
| </Stack> | |||
| } | |||
| </Grid> | |||
| <Typography variant="h5" mt={1}> | |||
| {formik.values.identification?.slice(0, 4)} | |||
| </Typography> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| @@ -30,6 +30,8 @@ const EmailTemplateDetailPage = Loadable(lazy(() => import('pages/EmailTemplate/ | |||
| const HolidayPage = Loadable(lazy(() => import('pages/Holiday'))); | |||
| const GazetteIssuePage = Loadable(lazy(() => import('pages/GazetteIssue/index'))); | |||
| const DrImport = Loadable(lazy(() => import('pages/Setting/DrImport'))); | |||
| const HkidKeyMigration = Loadable(lazy(() => import('pages/Setting/HkidKeyMigration'))); | |||
| const UserPiiEncryption = Loadable(lazy(() => import('pages/Setting/UserPiiEncryption'))); | |||
| const AuditLogPage = Loadable(lazy(() => import('pages/AuditLog/index'))); | |||
| const ReconReportPage = Loadable(lazy(() => import('pages/Recon'))); | |||
| const ChangePasswordPage = Loadable(lazy(() => import('pages/User/ChangePasswordPage'))); | |||
| @@ -190,6 +192,18 @@ const GLDUserRoutes = { | |||
| element: <DrImport /> | |||
| }:{}, | |||
| isGranted("MAINTAIN_SETTING")? | |||
| { | |||
| path: '/setting/hkidKeyMigration', | |||
| element: <HkidKeyMigration /> | |||
| }:{}, | |||
| isGranted("MAINTAIN_SETTING")? | |||
| { | |||
| path: '/setting/userPiiEncryption', | |||
| element: <UserPiiEncryption /> | |||
| }:{}, | |||
| isGranted("MAINTAIN_SETTING")? | |||
| { | |||
| path: '/setting/auditLog', | |||
| @@ -421,7 +421,7 @@ | |||
| "payTotal": "Total Payment Amount", | |||
| "payDetail": "Payment Details", | |||
| "payMethod": "Payment methods", | |||
| "epayMethod": " e-Payment Method", | |||
| "epayMethod": " Payment Method", | |||
| "selectPaymentMethod": "Please select a payment method", | |||
| "selectPaymentMethodBtn": "Select payment method", | |||
| "payReceipt": "Payment Receipt", | |||
| @@ -448,7 +448,7 @@ | |||
| "paymentLimitPrice2":" is only applicable when minimum amount is HK$0.10 and maximum amount is HK$9,999,999.99", | |||
| "paymentLimitPPS":" Payment could not be made via mobile device browsers, please use desktop computers to make payment.", | |||
| "paymentMethod": "Payment Method", | |||
| "paymentProcessLimited":"Payment process must be completed within 30 minutes and return to this system.", | |||
| "paymentProcessLimited":"Please complete the payment process within 15 minutes. Note: For FPS payments, scanning, payment and all necessary approvals must be finished within 3 minutes due to security-related QR code expiry.", | |||
| "publicNoticeDetailTitle": "Public Notice Application Information", | |||
| "applyPerson": "Applicant", | |||
| @@ -454,7 +454,7 @@ | |||
| "payTotal": "付款总额", | |||
| "payDetail": "付款详情", | |||
| "payMethod": "付款方式", | |||
| "epayMethod": "电子付款方法", | |||
| "epayMethod": "付款方法", | |||
| "selectPaymentMethod": "请选择付款方法", | |||
| "selectPaymentMethodBtn": "选择付款方法", | |||
| "payReceipt": "付款收据", | |||
| @@ -482,7 +482,7 @@ | |||
| "paymentLimitPrice2":"只适用于最小金额为 0.10 港元及最高金额为 9,999,999.99港元", | |||
| "paymentLimitPPS":"付款不适用于流动装置的浏览器,请使用桌面电脑。", | |||
| "paymentMethod": "付款方式", | |||
| "paymentProcessLimited":"付款过程必须在 30 分钟内完成及返回本系统。", | |||
| "paymentProcessLimited":"请于15分钟内完成付款程序。 注意:使用转数快(FPS)时,因二维码具安全时效限制,须于3分钟内完成扫码、付款及所有相关审核程序。", | |||
| "publicNoticeDetailTitle": "公共启事申请资料", | |||
| "applyPerson": "申请人", | |||
| @@ -455,7 +455,7 @@ | |||
| "payTotal": "付款總額", | |||
| "payDetail": "付款詳情", | |||
| "payMethod": "付款方式", | |||
| "epayMethod": "電子付款方法", | |||
| "epayMethod": "付款方法", | |||
| "selectPaymentMethod": "請選擇付款方法", | |||
| "selectPaymentMethodBtn": "選擇付款方法", | |||
| "payReceipt": "付款收據", | |||
| @@ -483,7 +483,7 @@ | |||
| "paymentLimitPrice2":"只適用於最小金額為 0.10 港元及最高金額為 9,999,999.99港元", | |||
| "paymentLimitPPS":"付款不適用於流動裝置的瀏覽器,請使用桌面電腦。", | |||
| "paymentMethod": "付款方法", | |||
| "paymentProcessLimited":"付款程序必須在 30 分鐘內完成及返回本系统。", | |||
| "paymentProcessLimited":"請於15分鐘內完成付款程序。 注意:使用轉數快(FPS)時,因二維碼具安全時效限制,須於3分鐘內完成掃碼、付款及所有相關審核程序。", | |||
| "publicNoticeDetailTitle": "公共啟事申請資料", | |||
| "applyPerson": "申請人", | |||
| @@ -84,6 +84,9 @@ export const GET_FILE_DELETE = apiPath+'/file/delete'; | |||
| export const DR_EXPORT = apiPath+'/settings/dr/export'; | |||
| export const DR_IMPORT = apiPath+'/settings/dr/import'; | |||
| export const OFFLINE_IMPORT = apiPath+'/settings/dr/importOffline'; | |||
| export const HKID_REKEY_MIGRATION = apiPath+'/settings/migration/hkid-rekey'; | |||
| export const USER_PII_ENCRYPT_MIGRATION = apiPath+'/settings/migration/user-pii-encrypt'; | |||
| export const USER_PII_VERIFY_MIGRATION = apiPath+'/settings/migration/user-pii-verify'; | |||
| export const AUDIT_LOG_EXPORT = apiPath+'/settings/auditLog-export'; | |||
| @@ -180,6 +183,7 @@ export const PAYMENT_LOAD = apiPath+'/payment/load';//GET | |||
| export const PAYMENT_APP_LIST = apiPath+'/payment/applist';//POST | |||
| export const PAYMENT_CHECK = apiPath+'/payment/check-payment';//GET | |||
| export const PAYMENT_BIB = apiPath+'/payment/set-bib';//POST | |||
| export const PAYMENT_MARK_AS_PAID = apiPath+'/payment/mark-as-paid';//POST | |||
| export const PAYMENT_GFMIS_LIST = apiPath+'/payment/listGFMIS';//GET | |||
| export const PAYMENT_LIMIT_SETTING_LIST = apiPath+'/settings/payment';//GET | |||