From 4214676dcf79ad1260a7ccb39c17f361a8d93cac Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 12 Jun 2026 21:56:22 +0800 Subject: [PATCH 1/8] CR-022 Item 1 --- src/translations/en.json | 2 +- src/translations/zh-CN.json | 2 +- src/translations/zh-HK.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/translations/en.json b/src/translations/en.json index 194e47f..01c5420 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -447,7 +447,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", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 100b61f..45316d1 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -481,7 +481,7 @@ "paymentLimitPrice2":"只适用于最小金额为 0.10 港元及最高金额为 9,999,999.99港元", "paymentLimitPPS":"付款不适用于流动装置的浏览器,请使用桌面电脑。", "paymentMethod": "付款方式", - "paymentProcessLimited":"付款过程必须在 30 分钟内完成及返回本系统。", + "paymentProcessLimited":"请于15分钟内完成付款程序。 注意:使用转数快(FPS)时,因二维码具安全时效限制,须于3分钟内完成扫码、付款及所有相关审核程序。", "publicNoticeDetailTitle": "公共启事申请资料", "applyPerson": "申请人", diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 2af3014..339603b 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -482,7 +482,7 @@ "paymentLimitPrice2":"只適用於最小金額為 0.10 港元及最高金額為 9,999,999.99港元", "paymentLimitPPS":"付款不適用於流動裝置的瀏覽器,請使用桌面電腦。", "paymentMethod": "付款方法", - "paymentProcessLimited":"付款程序必須在 30 分鐘內完成及返回本系统。", + "paymentProcessLimited":"請於15分鐘內完成付款程序。 注意:使用轉數快(FPS)時,因二維碼具安全時效限制,須於3分鐘內完成掃碼、付款及所有相關審核程序。", "publicNoticeDetailTitle": "公共啟事申請資料", "applyPerson": "申請人", From cdb9062c90e26cce76214d1547335406bde9f018 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 12 Jun 2026 22:11:57 +0800 Subject: [PATCH 2/8] CR-022 Item 2 --- src/pages/Payment/Search_Public/DataGrid.js | 19 +++++++++++++++++++ src/translations/en.json | 2 +- src/translations/zh-CN.json | 2 +- src/translations/zh-HK.json | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/pages/Payment/Search_Public/DataGrid.js b/src/pages/Payment/Search_Public/DataGrid.js index 1e16572..e912e0d 100644 --- a/src/pages/Payment/Search_Public/DataGrid.js +++ b/src/pages/Payment/Search_Public/DataGrid.js @@ -47,6 +47,12 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea navigate('/paymentPage/details/' + params.row.id); }; + const formatPayMethod = (value) => { + if (!value) return ""; + const lastCommaIndex = value.lastIndexOf(','); + return lastCommaIndex >= 0 ? value.substring(lastCommaIndex + 1) : value; + }; + const columns = [ { field: 'actions', @@ -58,6 +64,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 +81,13 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea return
{appNo}
}, }, + { + field: 'payMethod', + headerName: intl.formatMessage({id: 'paymentMethod'}), + width: isMdOrLg ? 'auto' : 160, + flex: isMdOrLg ? 1 : undefined, + valueGetter: (params) => formatPayMethod(params?.value ?? params.row?.payMethod), + }, { id: 'transDateTime', field: 'transDateTime', diff --git a/src/translations/en.json b/src/translations/en.json index 01c5420..2acb995 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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", "payReceipt": "Payment Receipt", "contactPerson": "Contact Person", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 45316d1..cf8135e 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -454,7 +454,7 @@ "payTotal": "付款总额", "payDetail": "付款详情", "payMethod": "付款方式", - "epayMethod": "电子付款方法", + "epayMethod": "付款方法", "selectPaymentMethod": "请选择付款方法", "payReceipt": "付款收据", "contactPerson": "联络人", diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 339603b..6425bef 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -455,7 +455,7 @@ "payTotal": "付款總額", "payDetail": "付款詳情", "payMethod": "付款方式", - "epayMethod": "電子付款方法", + "epayMethod": "付款方法", "selectPaymentMethod": "請選擇付款方法", "payReceipt": "付款收據", "contactPerson": "聯絡人", From c5a1153aa748a35d0d7a17e721c3cae643f42b04 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 12 Jun 2026 22:18:56 +0800 Subject: [PATCH 3/8] CR-022 Item 2 --- src/pages/Payment/Search_Public/DataGrid.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pages/Payment/Search_Public/DataGrid.js b/src/pages/Payment/Search_Public/DataGrid.js index e912e0d..c606e43 100644 --- a/src/pages/Payment/Search_Public/DataGrid.js +++ b/src/pages/Payment/Search_Public/DataGrid.js @@ -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 ||============================== // @@ -47,12 +48,6 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea navigate('/paymentPage/details/' + params.row.id); }; - const formatPayMethod = (value) => { - if (!value) return ""; - const lastCommaIndex = value.lastIndexOf(','); - return lastCommaIndex >= 0 ? value.substring(lastCommaIndex + 1) : value; - }; - const columns = [ { field: 'actions', @@ -86,7 +81,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea headerName: intl.formatMessage({id: 'paymentMethod'}), width: isMdOrLg ? 'auto' : 160, flex: isMdOrLg ? 1 : undefined, - valueGetter: (params) => formatPayMethod(params?.value ?? params.row?.payMethod), + renderCell: (params) => getPaymentMethodByCode(params?.value), }, { id: 'transDateTime', From 7b7a91339821e0e7fc7266a8b701fa6ba91925dc Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 12 Jun 2026 22:19:14 +0800 Subject: [PATCH 4/8] CR-022 Item 3 --- src/pages/Payment/Details_GLD/PaymentDetails.js | 2 +- src/pages/Payment/Search_GLD/DataGrid.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/Payment/Details_GLD/PaymentDetails.js b/src/pages/Payment/Details_GLD/PaymentDetails.js index 0e9f69e..49fc45a 100644 --- a/src/pages/Payment/Details_GLD/PaymentDetails.js +++ b/src/pages/Payment/Details_GLD/PaymentDetails.js @@ -116,7 +116,7 @@ const PaymentDetails = ({ formData,doPrint,onDownload }) => { - EGIS Reference No.: + Payment Reference No.: diff --git a/src/pages/Payment/Search_GLD/DataGrid.js b/src/pages/Payment/Search_GLD/DataGrid.js index d24fed0..f1d9891 100644 --- a/src/pages/Payment/Search_GLD/DataGrid.js +++ b/src/pages/Payment/Search_GLD/DataGrid.js @@ -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, From f447cb1dae0642d39acafab57ad39b636851c43c Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 12 Jun 2026 22:48:56 +0800 Subject: [PATCH 5/8] CR-022 Item 5 --- .../Payment/Details_GLD/PaymentDetails.js | 70 +++++++++++++++++-- src/pages/Payment/Details_GLD/index.js | 1 + src/utils/ApiPathConst.js | 1 + 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/pages/Payment/Details_GLD/PaymentDetails.js b/src/pages/Payment/Details_GLD/PaymentDetails.js index 49fc45a..f14d955 100644 --- a/src/pages/Payment/Details_GLD/PaymentDetails.js +++ b/src/pages/Payment/Details_GLD/PaymentDetails.js @@ -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 ? @@ -51,9 +80,21 @@ const PaymentDetails = ({ formData,doPrint,onDownload }) => { border={false} content={false} > - - Payment Details - + + + Payment Details + + {showMarkAsPaid && ( + + )} +
@@ -176,6 +217,25 @@ const PaymentDetails = ({ formData,doPrint,onDownload }) => { } + !markingPaid && setConfirmOpen(false)} + > + Confirm + + + {`Are you sure to mark as paid for Payment No. ${data.transNo || data.payload?.transactionid} ?`} + + + + + + + ); }; diff --git a/src/pages/Payment/Details_GLD/index.js b/src/pages/Payment/Details_GLD/index.js index b51a182..4d700ce 100644 --- a/src/pages/Payment/Details_GLD/index.js +++ b/src/pages/Payment/Details_GLD/index.js @@ -125,6 +125,7 @@ const Index = () => { formData={record} doPrint={doPrint} onDownload={onDownload} + onRefresh={loadForm} style={{ display: "flex", height: "100%", diff --git a/src/utils/ApiPathConst.js b/src/utils/ApiPathConst.js index 38daac3..d803a13 100644 --- a/src/utils/ApiPathConst.js +++ b/src/utils/ApiPathConst.js @@ -180,6 +180,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 From b0ee1b0714f3ef63e0bd8aea73792d7d2fe7ede2 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 12 Jun 2026 23:11:51 +0800 Subject: [PATCH 6/8] CR-022 Item 7 --- src/pages/Payment/Details_GLD/index.js | 48 ++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/pages/Payment/Details_GLD/index.js b/src/pages/Payment/Details_GLD/index.js index 4d700ce..2934748 100644 --- a/src/pages/Payment/Details_GLD/index.js +++ b/src/pages/Payment/Details_GLD/index.js @@ -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); + } } }); } From d1d844b63366204afba02e85347faac3586c4d35 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Wed, 17 Jun 2026 21:29:30 +0800 Subject: [PATCH 7/8] CR-024 Item 1 --- .../UserInformationCard_Individual.js | 26 ++++-- .../UserInformationCard_Individual_Pub.js | 88 +------------------ 2 files changed, 24 insertions(+), 90 deletions(-) diff --git a/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js b/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js index 5e69bdf..89b6bb5 100644 --- a/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js +++ b/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js @@ -80,7 +80,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) { @@ -132,8 +135,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) @@ -161,7 +168,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' })), @@ -185,8 +193,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 @@ -470,6 +480,11 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => {
+ {currentUserData.verifiedBy ? + + Hidden for security purpose + + : {formik.values.idDocType === "HKID" ? editMode ? @@ -548,6 +563,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { } + }
diff --git a/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual_Pub.js b/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual_Pub.js index 61e6c31..ec746c1 100644 --- a/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual_Pub.js +++ b/src/pages/User/DetailsPage_Individual/UserInformationCard_Individual_Pub.js @@ -2,8 +2,6 @@ import { Grid, Button, Typography, FormHelperText, - Stack, - IconButton } from '@mui/material'; import MainCard from "components/MainCard"; import * as React from "react"; @@ -22,7 +20,6 @@ import {notifySaveSuccess,} from 'utils/CommonFunction'; 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 ||============================== // @@ -33,15 +30,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) { @@ -251,79 +239,9 @@ const UserInformationCard_Individual_Pub = ({ formData, loadDataFun }) => { - - {formik.values.idDocType === "HKID" ? - // <> - // - // {FieldUtils.initField({ - // valueName: "identification", - // disabled: true, - // form: formik, - // placeholder: intl.formatMessage({id: 'idDocNumber'}), - // inputProps: { - // maxLength: 7, - // onKeyDown: (e) => { - // if (e.key === 'Enter') { - // e.preventDefault(); - // } - // }, - // } - // })} - - // - // - // {FieldUtils.initField({ - // valueName: "checkDigit", - // disabled: true, - // form: formik, - // })} - // - // - - - {formik.values.identification?.slice(0, 4)} - - - {/* {showId ?formik.values.identification.slice(4):"****"}{showId ? '(' + formik.values.checkDigit + ')' :null} */} - {showId ? formik.values.identification?.slice(4) : "****"}{showId ? formik.values.checkDigit?'(' +formik.values.checkDigit+ ')': "()" : ""} - - - {showId ? : } - - - : - // - // {FieldUtils.initField({ - // valueName: "identification", - // disabled: true, - // form: formik - // })} - // - - - {formik.values.identification?.slice(0, 4)} - - - {showId ?formik.values.identification?.slice(4):"****"} - - - {showId ? : } - - - } - + + {formik.values.identification?.slice(0, 4)} + From ad367709fd5d2f8883d5db485dbf0e7e66351db0 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Wed, 17 Jun 2026 22:42:51 +0800 Subject: [PATCH 8/8] CR-024 Item 3 --- src/layout/MainLayout/Header/index.js | 14 ++ src/pages/Setting/HkidKeyMigration/index.js | 101 ++++++++++++++ src/pages/Setting/UserPiiEncryption/index.js | 133 +++++++++++++++++++ src/routes/GLDUserRoutes.js | 14 ++ src/utils/ApiPathConst.js | 3 + 5 files changed, 265 insertions(+) create mode 100644 src/pages/Setting/HkidKeyMigration/index.js create mode 100644 src/pages/Setting/UserPiiEncryption/index.js diff --git a/src/layout/MainLayout/Header/index.js b/src/layout/MainLayout/Header/index.js index 335cc59..7f18891 100644 --- a/src/layout/MainLayout/Header/index.js +++ b/src/layout/MainLayout/Header/index.js @@ -516,6 +516,20 @@ function Header(props) { {isGranted("MAINTAIN_SETTING") ? ( <> +
  • + + + HKID Key Migration + + +
  • +
  • + + + User PII Encryption + + +
  • diff --git a/src/pages/Setting/HkidKeyMigration/index.js b/src/pages/Setting/HkidKeyMigration/index.js new file mode 100644 index 0000000..15d091f --- /dev/null +++ b/src/pages/Setting/HkidKeyMigration/index.js @@ -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())}
    Error
    {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())}
    + {responData?.dryRun ? "Dry-run " : ""}{statusText}
    + Total: {responData?.total ?? 0}, OK: {responData?.successCount ?? 0}, Skipped: {responData?.skipped ?? 0}, Failed: {responData?.failed ?? 0}
    + {logText} + + ); +}; + +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 ( + + +
    + + + HKID Key Migration + + +
    +
    + + + + + Re-encrypt identification and checkDigit from the legacy key to the new key. + Ensure Tomcat has both security.hkid-secret and security.hkid-secret-legacy configured before running. + + + + + {wait ? : null} + + + + + + + Result: + {resultStr} + + +
    + ); +}; + +export default HkidKeyMigration; diff --git a/src/pages/Setting/UserPiiEncryption/index.js b/src/pages/Setting/UserPiiEncryption/index.js new file mode 100644 index 0000000..e1e17b9 --- /dev/null +++ b/src/pages/Setting/UserPiiEncryption/index.js @@ -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())}
    Error
    {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())}
    + {responData?.dryRun ? "Dry-run " : ""}{statusText}
    + Total: {responData?.total ?? 0}, OK: {responData?.successCount ?? 0}, Skipped: {responData?.skipped ?? 0}, Failed: {responData?.failed ?? 0}
    + {logText} + + ); +}; + +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())}
    + {responData?.success ? "All tests passed" : "Some tests failed"}
    + Passed: {responData?.passed ?? 0}, Failed: {responData?.failed ?? 0}
    + {logText} + + ); +}; + +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 ( + + +
    + + + User PII Encryption + + +
    +
    + + + + + Encrypt enName, chName, mobileNumber, and address for existing user records, then verify data retrieval. + + + + + + {wait ? : null} + + + + + + + Result: + {resultStr} + + +
    + ); +}; + +export default UserPiiEncryption; diff --git a/src/routes/GLDUserRoutes.js b/src/routes/GLDUserRoutes.js index a4a059c..3f893ef 100644 --- a/src/routes/GLDUserRoutes.js +++ b/src/routes/GLDUserRoutes.js @@ -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: }:{}, + isGranted("MAINTAIN_SETTING")? + { + path: '/setting/hkidKeyMigration', + element: + }:{}, + + isGranted("MAINTAIN_SETTING")? + { + path: '/setting/userPiiEncryption', + element: + }:{}, + isGranted("MAINTAIN_SETTING")? { path: '/setting/auditLog', diff --git a/src/utils/ApiPathConst.js b/src/utils/ApiPathConst.js index d803a13..161dff9 100644 --- a/src/utils/ApiPathConst.js +++ b/src/utils/ApiPathConst.js @@ -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';