Compare commits

...

13 Commits

32 changed files with 281 additions and 125 deletions
Split View
  1. +41
    -0
      public/index.html
  2. BIN
      public/public-sans-latin-400-normal.woff2
  3. BIN
      public/public-sans-latin-500-normal.woff2
  4. BIN
      public/public-sans-latin-600-normal.woff2
  5. BIN
      public/public-sans-latin-700-normal.woff2
  6. +14
    -13
      src/assets/fonts.css
  7. +2
    -3
      src/assets/style/styles.css
  8. +23
    -3
      src/auth/index.js
  9. +43
    -20
      src/auth/jwt/jwtService.js
  10. +1
    -1
      src/components/cards/AuthFooter.js
  11. +7
    -6
      src/pages/Payment/FPS/FPS.js
  12. +46
    -14
      src/pages/Payment/Search_GLD/DataGrid.js
  13. +3
    -3
      src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js
  14. +38
    -7
      src/pages/PublicNotice/Search_GLD/DataGrid.js
  15. +1
    -1
      src/pages/User/ChangePasswordPage/index.js
  16. +6
    -6
      src/pages/authentication/BusRegister.js
  17. +7
    -7
      src/pages/authentication/IAmSmartRegister.js
  18. +7
    -7
      src/pages/authentication/Register.js
  19. +6
    -1
      src/pages/authentication/auth-forms/AuthLoginCustom.js
  20. +6
    -6
      src/pages/authentication/auth-forms/BusCustomFormWizard.js
  21. +6
    -6
      src/pages/authentication/auth-forms/CustomFormWizard.js
  22. +2
    -2
      src/pages/authentication/auth-forms/IAmSmartFormWizard.js
  23. +1
    -1
      src/pages/authentication/auth-forms/PreviewUploadFileTable.js
  24. +1
    -1
      src/pages/authentication/auth-forms/UploadFileTable.js
  25. +3
    -3
      src/themes/buttonConst.js
  26. +2
    -2
      src/themes/colorConst.js
  27. +1
    -1
      src/themes/overrides/Checkbox.js
  28. +1
    -1
      src/themes/overrides/Radio.js
  29. +3
    -3
      src/themes/themeConst.js
  30. +4
    -3
      src/translations/en.json
  31. +3
    -2
      src/translations/zh-CN.json
  32. +3
    -2
      src/translations/zh-HK.json

+ 41
- 0
public/index.html View File

@@ -2,6 +2,47 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Preload critical Public Sans webfont files and declare @font-face so the browser
uses the custom font immediately on page render, eliminating the flash of the
default/fallback font (FOIT/FOUT). -->
<link rel="preload" as="font" href="%PUBLIC_URL%/public-sans-latin-400-normal.woff2" type="font/woff2" crossorigin="anonymous" />
<link rel="preload" as="font" href="%PUBLIC_URL%/public-sans-latin-500-normal.woff2" type="font/woff2" crossorigin="anonymous" />
<link rel="preload" as="font" href="%PUBLIC_URL%/public-sans-latin-600-normal.woff2" type="font/woff2" crossorigin="anonymous" />
<link rel="preload" as="font" href="%PUBLIC_URL%/public-sans-latin-700-normal.woff2" type="font/woff2" crossorigin="anonymous" />
<style>
@font-face {
font-family: 'Public Sans';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(%PUBLIC_URL%/public-sans-latin-400-normal.woff2) format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Public Sans';
font-style: normal;
font-display: swap;
font-weight: 500;
src: url(%PUBLIC_URL%/public-sans-latin-500-normal.woff2) format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Public Sans';
font-style: normal;
font-display: swap;
font-weight: 600;
src: url(%PUBLIC_URL%/public-sans-latin-600-normal.woff2) format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Public Sans';
font-style: normal;
font-display: swap;
font-weight: 700;
src: url(%PUBLIC_URL%/public-sans-latin-700-normal.woff2) format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
</style>
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png">
<link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#5bbad5">


BIN
public/public-sans-latin-400-normal.woff2 View File


BIN
public/public-sans-latin-500-normal.woff2 View File


BIN
public/public-sans-latin-600-normal.woff2 View File


BIN
public/public-sans-latin-700-normal.woff2 View File


+ 14
- 13
src/assets/fonts.css View File

@@ -1,19 +1,20 @@
/* Self-hosted fonts (bundled via @fontsource; no Google Fonts CDN — CSP-friendly). */
/* All @fontsource v5 packages already use font-display: swap by default. */

/* Public Sans — MUI theme (`themeConst.js`), charts, config */
@import "@fontsource/public-sans/400.css";
@import "@fontsource/public-sans/500.css";
@import "@fontsource/public-sans/600.css";
@import "@fontsource/public-sans/700.css";
@import url("@fontsource/public-sans/400.css");
@import url("@fontsource/public-sans/500.css");
@import url("@fontsource/public-sans/600.css");
@import url("@fontsource/public-sans/700.css");

/* Noto Sans HK — `styles.css` / navbar (Latin + Hong Kong glyphs) */
@import "@fontsource/noto-sans-hk/latin-400.css";
@import "@fontsource/noto-sans-hk/chinese-hongkong-400.css";
@import "@fontsource/noto-sans-hk/latin-600.css";
@import "@fontsource/noto-sans-hk/chinese-hongkong-600.css";
/* Noto Sans HK — navbar (Latin + Hong Kong glyphs) */
@import url("@fontsource/noto-sans-hk/latin-400.css");
@import url("@fontsource/noto-sans-hk/chinese-hongkong-400.css");
@import url("@fontsource/noto-sans-hk/latin-600.css");
@import url("@fontsource/noto-sans-hk/chinese-hongkong-600.css");

/* Noto Sans SC — fallback for simplified Chinese */
@import "@fontsource/noto-sans-sc/latin-400.css";
@import "@fontsource/noto-sans-sc/chinese-simplified-400.css";
@import "@fontsource/noto-sans-sc/latin-600.css";
@import "@fontsource/noto-sans-sc/chinese-simplified-600.css";
@import url("@fontsource/noto-sans-sc/latin-400.css");
@import url("@fontsource/noto-sans-sc/chinese-simplified-400.css");
@import url("@fontsource/noto-sans-sc/latin-600.css");
@import url("@fontsource/noto-sans-sc/chinese-simplified-600.css");

+ 2
- 3
src/assets/style/styles.css View File

@@ -95,9 +95,8 @@ a:active {
/* ===== MUI DataGrid keyboard focus (WCAG 2.4.7 / 2.4.11) ===== */
.MuiDataGrid-columnHeader:focus-visible,
.MuiDataGrid-cell:focus-visible {
outline: 3px solid #0c489e;
outline-offset: -2px;
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.25);
outline: none;
box-shadow: inset 0 0 0 3px #0c489e;
}

/* Contained buttons only */


+ 23
- 3
src/auth/index.js View File

@@ -20,6 +20,16 @@ let axiosInterceptorsSetup = false;
// In-memory guard so only one 401/logout flow shows the alert (avoids race when multiple 401s or interceptors run)
let expiredAlertShownInMemory = false;

/** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */
const isPublicAuthRequest = (reqUrl) =>
reqUrl.includes('/login') || reqUrl.includes('/ldap-login');

/** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */
export const clearExpiredSessionAlert = () => {
localStorage.removeItem('expiredAlertShown');
expiredAlertShownInMemory = false;
};

// ** Handle User Login
export const handleLogin = data => {
return dispatch => {
@@ -152,6 +162,12 @@ export const SetupAxiosInterceptors = () => {
// const { config, response: { status } } = error
const status = error.response?.status
const reqUrl = error.config?.url || ''

// Let login forms handle their own errors (wrong password, locked account, etc.)
if (isPublicAuthRequest(reqUrl)) {
return Promise.reject(error)
}

// iAM Smart registration: HKID already linked — caller must show /iamsmart/pleaseLogin (not refresh/logout)
if (
status === 401 &&
@@ -160,11 +176,15 @@ export const SetupAxiosInterceptors = () => {
) {
return Promise.reject(error)
}
if (status === 401 && error.config?.url !== apiPath + REFRESH_TOKEN) {
if (
status === 401 &&
!isPublicAuthRequest(reqUrl) &&
error.config?.url !== apiPath + REFRESH_TOKEN
) {
// Make a request to refresh the access token
const refreshToken = localStorage.getItem('refreshToken');
if (isRefreshToken) {
return;
return Promise.reject(error);
}
isRefreshToken = true;
return axios
@@ -217,7 +237,7 @@ export const SetupAxiosInterceptors = () => {
// }
// }
if (localStorage.getItem("expiredAlertShown")) {
if (localStorage.getItem("expiredAlertShown") && !isPublicAuthRequest(reqUrl)) {
await dispatch(handleLogoutFunction());
await navigate('/login');
await window.location.reload();


+ 43
- 20
src/auth/jwt/jwtService.js View File

@@ -40,29 +40,46 @@ export default class JwtService {
// ** const { config, response: { status } } = error
const {config, response} = error
const originalRequest = config
const reqUrl = config?.url || ''

// ** if (status === 401) {
if (response && response.status === 401) {
const isPublicAuthRequest =
reqUrl.includes('/login') ||
reqUrl.includes('/ldap-login') ||
reqUrl.includes('/refresh-token')
if (isPublicAuthRequest) {
return Promise.reject(error)
}

if (!this.isAlreadyFetchingAccessToken) {
this.isAlreadyFetchingAccessToken = true
this.refreshToken().then(r => {
this.isAlreadyFetchingAccessToken = false

// ** Update accessToken in localStorage
this.setToken(r.data.accessToken)
this.setRefreshToken(r.data.refreshToken)

this.onAccessTokenFetched(r.data.accessToken)
})
this.refreshToken()
.then(r => {
this.isAlreadyFetchingAccessToken = false

// ** Update accessToken in localStorage
this.setToken(r.data.accessToken)
this.setRefreshToken(r.data.refreshToken)

this.onAccessTokenFetched(r.data.accessToken)
})
.catch(refreshError => {
this.isAlreadyFetchingAccessToken = false
this.onAccessTokenFetchFailed(refreshError)
})
}
const retryOriginalRequest = new Promise(resolve => {
this.addSubscriber(accessToken => {
// ** Make sure to assign accessToken according to your response.
// ** Check: https://pixinvent.ticksy.com/ticket/2413870
// ** Change Authorization header
originalRequest.headers.Authorization = `${this.jwtConfig.tokenType} ${accessToken}`
resolve(this.axios(originalRequest))
})
const retryOriginalRequest = new Promise((resolve, reject) => {
this.addSubscriber(
accessToken => {
// ** Make sure to assign accessToken according to your response.
// ** Check: https://pixinvent.ticksy.com/ticket/2413870
// ** Change Authorization header
originalRequest.headers.Authorization = `${this.jwtConfig.tokenType} ${accessToken}`
resolve(axios(originalRequest))
},
reject
)
})
return retryOriginalRequest
}
@@ -72,11 +89,17 @@ export default class JwtService {
}

onAccessTokenFetched(accessToken) {
this.subscribers = this.subscribers.filter(callback => callback(accessToken))
this.subscribers.forEach(({ onSuccess }) => onSuccess(accessToken))
this.subscribers = []
}

onAccessTokenFetchFailed(refreshError) {
this.subscribers.forEach(({ onFailure }) => onFailure(refreshError))
this.subscribers = []
}

addSubscriber(callback) {
this.subscribers.push(callback)
addSubscriber(onSuccess, onFailure) {
this.subscribers.push({ onSuccess, onFailure })
}

getToken() {


+ 1
- 1
src/components/cards/AuthFooter.js View File

@@ -30,7 +30,7 @@ const AuthFooter = () => {
component="span"
sx={{ color: '#4A4A4A' }}
>
2024 &copy; <FormattedMessage id="HKGLD" />
2024-{new Date().getFullYear()} &copy; <FormattedMessage id="HKGLD" />
</Typography>
<Typography
variant="subtitle2"


+ 7
- 6
src/pages/Payment/FPS/FPS.js View File

@@ -398,8 +398,8 @@ const Index = () => {
<Grid container justifyContent="flex-start" alignItems="center" >
<center>
<Grid item xs={12} md={12} >
<Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}>
<br /><br />
<Typography component="span" variant="body1" sx={{ width: '80%', margin: 'auto', mt: 4, textAlign: "left", fontSize: '20px' }}>
<FormattedMessage id="payAlert"/>
<br /><br />
<img src={FpsIcon} width="80" height="80" alt="FPS"></img>
@@ -408,8 +408,9 @@ const Index = () => {
<br />
{"HK$ " + currencyFormat(paymentData.amount)}
</Typography>
<br /><br />
{isError ?
<Typography variant="h3" color="error" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}>
<Typography component="span" variant="body1" color="error" sx={{ width: '80%', margin: 'auto', mt: 4, textAlign: "left", fontSize: '20px' }}>
<FormattedMessage id="fpsPaymentErrorMsg"/>
</Typography>
:
@@ -426,7 +427,7 @@ const Index = () => {
mobliePaymentPrd();
}}
sx={{ m: 4 }}
>請選擇支付程式付款</Button>
><FormattedMessage id="fpsSelectPaymentApp"/></Button>
:
<>
<Button
@@ -438,7 +439,7 @@ const Index = () => {
mobliePayment();
}}
sx={{ m: 4 }}
>請選擇支付程式付款-Testing</Button>
><FormattedMessage id="fpsSelectPaymentApp"/>-Testing</Button>
<Button
component="span"
variant="contained"
@@ -448,7 +449,7 @@ const Index = () => {
mobliePaymentFps();
}}
sx={{ m: 4 }}
>請選擇支付程式付款-fps prefix</Button>
><FormattedMessage id="fpsSelectPaymentApp"/>-fps prefix</Button>
</>
}
</Typography>


+ 46
- 14
src/pages/Payment/Search_GLD/DataGrid.js View File

@@ -13,9 +13,10 @@ import { getPaymentMethodByCode} from "auth/utils";
import {
Checkbox,
Dialog, DialogTitle, DialogContent, DialogActions,
Button,Typography
Button, Typography
// MenuItem
} from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
// ==============================|| EVENT TABLE ||============================== //

export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, applySearch}) {
@@ -26,6 +27,7 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
const [bib, setBib] = React.useState();
const [appNo, setAppNo] = React.useState();
const [refreshTrigger, setRefreshTrigger] = React.useState(0);
const [bibLoading, setBibLoading] = React.useState(false);

const forceRefresh = () => {
setRefreshTrigger(prev => prev + 1);
@@ -55,23 +57,43 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
navigate('/paymentPage/details/' + params.row.id);
};

const finishBibRequest = () => {
setBibLoading(false);
};

const doBIB = () => {
setIsPopUp(false);
// console.log(refreshTrigger)
if (bibLoading) {
return;
}
setBibLoading(true);
HttpUtils.post({
url: PAYMENT_BIB + "/" + bibId,
onSuccess: function () {
forceRefresh()
}
setIsPopUp(false);
forceRefresh();
finishBibRequest();
},
onFail: finishBibRequest,
onError: finishBibRequest,
});
}
};
const popUPBib = (id, bibFlag, appNo) => {
setBibId(id)
setBib(bibFlag)
setAppNo(appNo)
if (bibLoading) {
return;
}
setBibId(id);
setBib(bibFlag);
setAppNo(appNo);
setIsPopUp(true);
}
};

const closeBibDialog = () => {
if (bibLoading) {
return;
}
setIsPopUp(false);
};

const columns = [
{
@@ -134,7 +156,13 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
headerName: 'BIB',
width: 150,
renderCell: (params) => {
return <Checkbox checked={params.row.bib} onChange={() => {popUPBib(params.row.id, params.row.bib, params.row.appNos)}}/>;
return (
<Checkbox
checked={params.row.bib}
disabled={bibLoading}
onChange={() => popUPBib(params.row.id, params.row.bib, params.row.appNos)}
/>
);
}
},
{
@@ -178,7 +206,7 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
<div>
<Dialog
open={isPopUp}
onClose={() => setIsPopUp(false)}
onClose={closeBibDialog}
PaperProps={{
sx: {
minWidth: '40vw',
@@ -192,8 +220,12 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
<Typography variant="h5" style={{ padding: '16px' }}>{bib?"Cancel Bank-in-bank?":"Set "+appNo+" as Bank-in-bank?"}</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsPopUp(false)}><Typography variant="h5">Cancel</Typography></Button>
<Button onClick={() => doBIB()}><Typography variant="h5">Confirm</Typography></Button>
<Button onClick={closeBibDialog} disabled={bibLoading}>
<Typography variant="h5">Cancel</Typography>
</Button>
<LoadingButton onClick={doBIB} loading={bibLoading} disabled={bibLoading}>
<Typography variant="h5">Confirm</Typography>
</LoadingButton>
</DialogActions>
</Dialog>
</div>


+ 3
- 3
src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js View File

@@ -557,7 +557,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
</Grid>
</Grid>
<Grid item xs={12} md={12} lg={12} sx={{ mb: 3 }}>
<Typography display="inline" variant="subtitle1" sx={{ color: 'primary.primary' }}>
<Typography component="span" display="inline" variant="subtitle1" sx={{ color: 'primary.primary' }}>
<FormattedMessage id="uploadApplicationFileRemark" />
</Typography>
</Grid>
@@ -573,7 +573,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
})}
</Grid>
<Grid item xs={12} md={12} lg={12} sx={{ mb: 3 }}>
<Typography display="inline" variant="subtitle1" sx={{ color: 'primary.primary' }}>
<Typography component="span" sx={{ color: 'primary.primary' }}>
<FormattedMessage id="noteOnClientRemark" />
</Typography>
</Grid>
@@ -605,7 +605,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
<Typography variant="pnspsFormParagraphBold">
<span style={{ textAlign: 'justify', }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr0" }) }} />
</Typography>
<Typography display="inline" variant="subtitle1" sx={{ color: 'primary.primary' }} >
<Typography display="inline" variant="subtitle1" component="span" sx={{ color: 'primary.primary' }} >
<ol style={{ textAlign: 'justify', }}>
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr1" }) }} />
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr2" }) }} />


+ 38
- 7
src/pages/PublicNotice/Search_GLD/DataGrid.js View File

@@ -8,6 +8,7 @@ import {
Stack,
Dialog, DialogTitle, DialogContent, DialogActions,
} from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import { GET_PUBLIC_NOTICE_LIST, SET_PUBLIC_NOTICE_STATUS_PUBLISH_BULK } from "utils/ApiPathConst";
import * as HttpUtils from "utils/HttpUtils";
import * as DateUtils from "utils/DateUtils";
@@ -29,6 +30,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
const [selectedRowItems, setSelectedRowItems] = React.useState([]);
const [isConfirmPopUp, setIsConfirmPopUp] = React.useState(false);
const [isWarningPopUp, setIsWarningPopUp] = React.useState(false);
const [publishLoading, setPublishLoading] = React.useState(false);

const [reload, setReload] = React.useState(new Date());

@@ -39,6 +41,12 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
set_searchCriteria(searchCriteria);
}, [searchCriteria]);

React.useEffect(() => {
if (!isConfirmPopUp) {
setPublishLoading(false);
}
}, [isConfirmPopUp]);


function genIssueNo(params) {
const issueNo = params.row.issueYear + " Vol. " + FormatUtils.zeroPad(params.row.issueVolume, 3) + ", No. " + FormatUtils.zeroPad(params.row.issueNo, 2) + ", " + DateUtils.dateFormat(params.row.issueDate, "D MMM YYYY (ddd)")
@@ -158,26 +166,36 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
}

const doPublish = () => {
setIsConfirmPopUp(false);
if (publishLoading) return;

let appIdList = [];
const datas = rows?.filter((row) =>
selectedRowItems.includes(row.id)
);
// console.log(datas)
for (var i = 0; i < datas?.length; i++) {
appIdList.push(datas[i].id);
}
if (appIdList.length < 1) {
setIsConfirmPopUp(false);
setIsWarningPopUp(true);
} else {
setPublishLoading(true);
HttpUtils.post({
url: SET_PUBLIC_NOTICE_STATUS_PUBLISH_BULK,
params: {
ids: appIdList
},
onSuccess: () => {
setPublishLoading(false);
setIsConfirmPopUp(false);
setReload(new Date());
notifyActionSuccess("Action Success!")
},
onFail: () => {
setPublishLoading(false);
},
onError: () => {
setPublishLoading(false);
}
});
}
@@ -190,6 +208,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
<Grid container direction="row" justifyContent="flex-start" alignItems="center" sx={{ p: 1 }} >
<Button
variant="contained"
disabled={publishLoading}
onClick={() => setIsConfirmPopUp(true)}
>
Published
@@ -228,7 +247,10 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
<div>
<Dialog
open={isConfirmPopUp}
onClose={() => setIsConfirmPopUp(false)}
onClose={() => {
if (publishLoading) return;
setIsConfirmPopUp(false);
}}
PaperProps={{
sx: {
minWidth: '40vw',
@@ -248,15 +270,24 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsConfirmPopUp(false)} aria-label={intl.formatMessage({ id: 'close' })}>
<Button
onClick={() => setIsConfirmPopUp(false)}
disabled={publishLoading}
aria-label={intl.formatMessage({ id: 'close' })}
>
<Typography variant="h5">
<FormattedMessage id="close" />
</Typography></Button>

<Button onClick={() => doPublish()} aria-label={intl.formatMessage({ id: 'confirm' })}>
<Typography variant="h5">
<LoadingButton
onClick={() => doPublish()}
loading={publishLoading}
aria-label={intl.formatMessage({ id: 'confirm' })}
>
<Typography variant="h5" component="span">
<FormattedMessage id="confirm" />
</Typography></Button>
</Typography>
</LoadingButton>
</DialogActions>
</Dialog>
</div>


+ 1
- 1
src/pages/User/ChangePasswordPage/index.js View File

@@ -394,7 +394,7 @@ const Index = () => {
</Grid>
<Grid container spacing={2} alignItems="center">
<Grid item sx={{ mt: 1 }}>
<Typography variant="subtitle1">
<Typography variant="subtitle1" component="p">
•<FormattedMessage id="pwRemark1" /><br />
•<FormattedMessage id="pwRemark2" /><br />
•<FormattedMessage id="pwRemark3" /><br />


+ 6
- 6
src/pages/authentication/BusRegister.js View File

@@ -154,7 +154,7 @@ const BusRegister = () => {
</Stepper>
{allStepsCompleted() ? (
<React.Fragment>
<Typography variant="h4" sx={{ mt: 2, mb: 1 }}>
<Typography component="span" variant="h4" sx={{ mt: 2, mb: 1, display: 'block' }}>
All steps completed - you&apos;re finished
</Typography>
<Stack direction="row" sx={{ pb: 2 }}>
@@ -185,7 +185,7 @@ const BusRegister = () => {
sx={{ mr: 1 }}
variant="h5"
>
<Typography variant="h5">
<Typography component="span" variant="h5">
<FormattedMessage id="back"/>
</Typography>
</Button>
@@ -197,7 +197,7 @@ const BusRegister = () => {
sx={{ mr: 1 }}
variant="h5"
>
<Typography variant="h5">
<Typography component="span" variant="h5">
<FormattedMessage id="back"/>
</Typography>
</Button>
@@ -216,7 +216,7 @@ const BusRegister = () => {
{isNextBusy ? (
<CircularProgress size={22} color="inherit" aria-hidden />
) : (
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="submit"/>
</Typography>
)}
@@ -225,7 +225,7 @@ const BusRegister = () => {
(
<Button variant="contained" color="inherit"
disabled={true} sx={{ mr: 1, ...PRIMARY_CONTAINED_BUTTON_SX }}>
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="submit"/>
</Typography>
</Button>
@@ -233,7 +233,7 @@ const BusRegister = () => {
(
// <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}>
<Button disabled={!updateValid || isNextBusy} variant="contained" onClick={handleNext} sx={{ mr: 1, ...PRIMARY_CONTAINED_BUTTON_SX }}>
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="continue"/>
</Typography>
</Button>


+ 7
- 7
src/pages/authentication/IAmSmartRegister.js View File

@@ -161,12 +161,12 @@ const Register = () => {
</Stepper>
{allStepsCompleted() ? (
<React.Fragment>
<Typography variant="h4" sx={{ mt: 2, mb: 1 }}>
<Typography component="span" variant="h4" sx={{ mt: 2, mb: 1, display: 'block' }}>
All steps completed - you&apos;re finished
</Typography>
<Stack direction="row" sx={{ pt: 2 }}>
<Stack sx={{ flex: '1 1 auto' }} />
<Button onClick={handleReset}><Typography variant="h5"> <FormattedMessage id="reset"/></Typography></Button>
<Button onClick={handleReset}><Typography component="span" variant="h5"> <FormattedMessage id="reset"/></Typography></Button>
</Stack>
</React.Fragment>
) : (
@@ -192,7 +192,7 @@ const Register = () => {
onClick={handleBack}
sx={{ mr: 1 }}
>
<Typography variant="h5">
<Typography component="span" variant="h5">
<FormattedMessage id="back"/>
</Typography>
</Button>
@@ -203,7 +203,7 @@ const Register = () => {
onClick={handleBack}
sx={{ mr: 1 }}
>
<Typography variant="h5">
<Typography component="span" variant="h5">
<FormattedMessage id="back"/>
</Typography>
</Button>
@@ -222,7 +222,7 @@ const Register = () => {
{isNextBusy ? (
<CircularProgress size={22} color="inherit" aria-hidden />
) : (
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="submit"/>
</Typography>
)}
@@ -231,7 +231,7 @@ const Register = () => {
(
<Button variant="contained" color="inherit"
disabled={true} sx={{ mr: 1, ...PRIMARY_CONTAINED_BUTTON_SX }}>
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="submit"/>
</Typography>
</Button>
@@ -239,7 +239,7 @@ const Register = () => {
(
// <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}>
<Button disabled={!updateValid || isNextBusy} variant="contained" onClick={handleNext} sx={{ mr: 1, ...PRIMARY_CONTAINED_BUTTON_SX }}>
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="continue"/>
</Typography>
</Button>


+ 7
- 7
src/pages/authentication/Register.js View File

@@ -160,12 +160,12 @@ const Register = () => {
</Stepper>
{allStepsCompleted() ? (
<React.Fragment>
<Typography variant="h4" sx={{ mt: 2, mb: 1 }}>
<Typography component="span" variant="h4" sx={{ mt: 2, mb: 1, display: 'block' }}>
All steps completed - you&apos;re finished
</Typography>
<Stack direction="row" sx={{ pt: 2 }}>
<Stack sx={{ flex: '1 1 auto' }} />
<Button onClick={handleReset}><Typography variant="h5">Reset</Typography></Button>
<Button onClick={handleReset}><Typography component="span" variant="h5">Reset</Typography></Button>
</Stack>
</React.Fragment>
) : (
@@ -191,7 +191,7 @@ const Register = () => {
sx={{ mr: 1 }}
variant="h5"
>
<Typography variant="h5">
<Typography component="span" variant="h5">
<FormattedMessage id="back"/>
</Typography>
</Button>
@@ -203,7 +203,7 @@ const Register = () => {
sx={{ mr: 1 }}
variant="h5"
>
<Typography variant="h5">
<Typography component="span" variant="h5">
<FormattedMessage id="back"/>
</Typography>
</Button>
@@ -222,7 +222,7 @@ const Register = () => {
{isNextBusy ? (
<CircularProgress size={22} color="inherit" aria-hidden />
) : (
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="submit"/>
</Typography>
)}
@@ -231,7 +231,7 @@ const Register = () => {
(
<Button variant="contained" color="inherit"
disabled={true} sx={{ mr: 1, ...PRIMARY_CONTAINED_BUTTON_SX }}>
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="submit"/>
</Typography>
</Button>
@@ -239,7 +239,7 @@ const Register = () => {
(
// <Button disabled={updateValid} variant="outlined" onClick={handleNext} sx={{ mr: 1 }}>
<Button disabled={!updateValid || isNextBusy} variant="contained" onClick={handleNext} sx={{ mr: 1, ...PRIMARY_CONTAINED_BUTTON_SX }}>
<Typography variant="h5" sx={{ color: 'inherit' }}>
<Typography component="span" variant="h5" sx={{ color: 'inherit' }}>
<FormattedMessage id="continue"/>
</Typography>
</Button>


+ 6
- 1
src/pages/authentication/auth-forms/AuthLoginCustom.js View File

@@ -40,7 +40,7 @@ import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
// import axios from "axios";

import { useDispatch } from "react-redux";
import { handleLogin } from "auth/index";
import { handleLogin, clearExpiredSessionAlert } from "auth/index";
import useJwt from "auth/jwt/useJwt";
import { FormattedMessage, useIntl } from "react-intl";
import { SysContext } from "components/SysSettingProvider"
@@ -74,6 +74,10 @@ const AuthLoginCustom = () => {
/** Blocks a second login before React re-renders (double-click / rapid taps). */
const loginInFlightRef = useRef(false);

useEffect(() => {
clearExpiredSessionAlert();
}, []);

const handleMouseDownPassword = (event) => {
event.preventDefault();
};
@@ -84,6 +88,7 @@ const AuthLoginCustom = () => {
return;
}
loginInFlightRef.current = true;
clearExpiredSessionAlert();
setOnLogin(true)
useJwt
.login({ username: values.username, password: values.password })


+ 6
- 6
src/pages/authentication/auth-forms/BusCustomFormWizard.js View File

@@ -739,7 +739,7 @@ const BusCustomFormWizard = (props) => {
</Typography>
</Button>
<div style={{ borderBottom: "3px solid #1A4399", width: "100%", margin_right: "15px" }}>
<Typography display="inline" variant="h3"component="h1" sx={{ color: '#1A4399' }}>
<Typography display="inline" variant="h3" component="h1" sx={{ color: '#1A4399' }}>
<FormattedMessage id="becomeNewBusinessUser"/>
</Typography>
</div>
@@ -872,7 +872,7 @@ const BusCustomFormWizard = (props) => {
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
</Grid>
<Grid item>
<Typography variant="subtitle1">
<Typography component="span" variant="subtitle1" sx={{ display: 'block' }}>
<FormattedMessage id={level ? level?.label : "pwWeak" }/>
</Typography>
</Grid>
@@ -939,7 +939,7 @@ const BusCustomFormWizard = (props) => {
</Stack>
<Grid container spacing={2} alignItems="center">
<Grid item sx={{mt:1}}>
<Typography variant="subtitle1">
<Typography component="span" variant="subtitle1" sx={{ display: 'block' }}>
•<FormattedMessage id="pwRemark1"/><br />
•<FormattedMessage id="pwRemark2"/><br />
•<FormattedMessage id="pwRemark3"/><br />
@@ -963,7 +963,7 @@ const BusCustomFormWizard = (props) => {
</Grid>

<Grid item xs={12} md={12}>
<Typography variant="subtitle1">
<Typography component="span" variant="subtitle1" sx={{ display: 'block' }}>
•<FormattedMessage id="pleaseEnterOrgOrCompName"/>
</Typography>
</Grid>
@@ -1617,7 +1617,7 @@ const BusCustomFormWizard = (props) => {
<Grid item xs={12} md={12}>
<Grid container>
<Grid item xs={12} md={12}>
<Typography variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<Typography component="span" variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e", display: "block" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
</Typography>
</Grid>
@@ -1744,7 +1744,7 @@ const BusCustomFormWizard = (props) => {
<Grid item xs={12}>
<Stack direction="column" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<div style={{ borderBottom: "3px solid #1A4399", width: "100%", margin_right: "15px" }}>
<Typography display="inline" variant="h3"component="h1" sx={{ color: '#1A4399' }}>
<Typography display="inline" variant="h3" component="h1" sx={{ color: '#1A4399' }}>
<FormattedMessage id="becomeNewBusinessUser"/>
</Typography>
</div>


+ 6
- 6
src/pages/authentication/auth-forms/CustomFormWizard.js View File

@@ -1090,7 +1090,7 @@ const CustomFormWizard = (props) => {
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
</Grid>
<Grid item>
<Typography variant="subtitle1">
<Typography component="span" variant="subtitle1" sx={{ display: 'block' }}>
<FormattedMessage id={level ? level?.label : "pwWeak"} />
</Typography>
</Grid>
@@ -1157,7 +1157,7 @@ const CustomFormWizard = (props) => {
</Stack>
<Grid container spacing={2} alignItems="center">
<Grid item sx={{ mt: 1 }}>
<Typography variant="subtitle1">
<Typography component="span" variant="subtitle1" sx={{ display: 'block' }}>
•<FormattedMessage id="pwRemark1" /><br />
•<FormattedMessage id="pwRemark2" /><br />
•<FormattedMessage id="pwRemark3" /><br />
@@ -1392,7 +1392,7 @@ const CustomFormWizard = (props) => {
</Grid>
</Grid>
<Grid item xs={12} md={12}>
<Typography variant="subtitle1">
<Typography component="span" variant="subtitle1" sx={{ display: 'block' }}>
<FormattedMessage id="registerNameLabel" />
</Typography>
</Grid>
@@ -1881,10 +1881,10 @@ const CustomFormWizard = (props) => {
<Typography display="inline" variant="h4" component="h2" sx={{ color: 'primary.primary' }}>
<FormattedMessage id="userIdDoc" />
<span style={{ color: '#B00020' }}>*</span></Typography>
<Typography display="inline" variant="subtitle1" sx={{ color: 'primary.primary' }}>
<Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}>
<FormattedMessage id="pleaseUploadIdDoc" />
</Typography>
<Typography display="inline" variant="subtitle1" sx={{ color: 'primary.primary' }}>
<Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}>
<FormattedMessage id="pleaseUploadIdDocSubTitle" />
</Typography>
<Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
@@ -1945,7 +1945,7 @@ const CustomFormWizard = (props) => {
<Grid item xs={12} md={12}>
<Grid container>
<Grid item xs={12} md={12}>
<Typography height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<Typography height="100%" component="span" sx={{ textAlign: "left", display: "block", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
</Typography>
</Grid>


+ 2
- 2
src/pages/authentication/auth-forms/IAmSmartFormWizard.js View File

@@ -1053,7 +1053,7 @@ const CustomFormWizard = (props) => {
<Grid item xs={12} md={12}>
<Grid container>
<Grid item xs={12} md={12}>
<Typography sx={{ textAlign: "left", borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<Typography component="span" sx={{ textAlign: "left", display: "block", borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "termsAndCon" }) }} />
</Typography>
@@ -1192,7 +1192,7 @@ const CustomFormWizard = (props) => {
</Grid>
<Grid item xs={12} md={12} >
<Stack direction="row">
<Typography variant="h5" component="h2" color={theme.palette.grey[600]} sx={{ mr: 1 }}>
<Typography variant="h5" component="span" color={theme.palette.grey[600]} sx={{ mr: 1 }}>
<FormattedMessage id="userIdDoc" />
</Typography>
<Typography variant="h5" component="span" name="preview-idDocType-1">


+ 1
- 1
src/pages/authentication/auth-forms/PreviewUploadFileTable.js View File

@@ -34,7 +34,7 @@ export default function PreviewUploadFileTable({ recordList, }) {
function NoRowsOverlay() {
return (
<Stack height="100%" alignItems="center" justifyContent="center">
<Typography variant="h6">
<Typography component="span" variant="h6">
<FormattedMessage id="noFile"/>
</Typography>
{/* <pre>(rows=&#123;[]&#125;)</pre> */}


+ 1
- 1
src/pages/authentication/auth-forms/UploadFileTable.js View File

@@ -48,7 +48,7 @@ export default function UploadFileTable({ recordList, setUpdateRows, }) {
justifyContent: 'center',
}}
>
<Typography variant="h6">
<Typography component="span" variant="h6">
<FormattedMessage id="noFile"/>
</Typography>
</Box>


+ 3
- 3
src/themes/buttonConst.js View File

@@ -20,7 +20,7 @@ export const PNSPS_BUTTON_THEME = createTheme({

},
save:{
main: '#448DF2',
main: '#0c489e',
contrastText: '#FFFFFF',
},
export:{
@@ -44,7 +44,7 @@ export const PNSPS_BUTTON_THEME = createTheme({
contrastText: '#FFFFFF',
},
success:{
main: '#448DF2',
main: '#0c489e',
contrastText: '#FFFFFF',
},
green:{
@@ -194,7 +194,7 @@ export const PNSPS_LONG_BUTTON_THEME = createTheme({

},
save:{
main: '#448DF2',
main: '#0c489e',
contrastText: '#FFFFFF',
},
export:{


+ 2
- 2
src/themes/colorConst.js View File

@@ -28,7 +28,7 @@ export const piechartColor2 = [
];

export const cardBorderColor = [
'#efb142', '#4bb641', '#448df2', '#e03c04'
'#efb142', '#4bb641', '#0c489e', '#e03c04'
];

export const chartLineColor = [
@@ -39,7 +39,7 @@ export const GENERAL_RED_COLOR = '#e03c04';

export const TABLE_HEADER_TEXT_COLOR = "#1565C0";

export const GENERAL_INFO_COLOR = '#448df2';
export const GENERAL_INFO_COLOR = '#0c489e';

export const GENERAL_SETTING_COLOR = '#666666';



+ 1
- 1
src/themes/overrides/Checkbox.js View File

@@ -24,7 +24,7 @@ export default function Checkbox(theme) {
color: CONTAINED_PRIMARY_BLUE
},
'&.Mui-focusVisible': {
boxShadow: '0 0 0 3px rgba(25, 118, 210, 0.35)'
boxShadow: `0 0 0 3px ${CONTAINED_PRIMARY_BLUE}`
},
svg: {
width: checkboxSize,


+ 1
- 1
src/themes/overrides/Radio.js View File

@@ -21,7 +21,7 @@ export default function Radio() {
color: CONTAINED_PRIMARY_BLUE
},
'&.Mui-focusVisible': {
boxShadow: '0 0 0 3px rgba(25, 118, 210, 0.35)'
boxShadow: `0 0 0 3px ${CONTAINED_PRIMARY_BLUE}`
}
}
}


+ 3
- 3
src/themes/themeConst.js View File

@@ -310,7 +310,7 @@ export const PNSPS_THEME = createTheme({

/* Focus-visible */
'&.Mui-focusVisible': {
boxShadow: '0 0 0 3px rgba(25, 118, 210, 0.35)',
boxShadow: `0 0 0 3px ${CONTAINED_PRIMARY_BLUE}`,
},
},
},
@@ -336,7 +336,7 @@ export const PNSPS_THEME = createTheme({
},

'&.Mui-focusVisible': {
boxShadow: '0 0 0 3px rgba(25, 118, 210, 0.35)',
boxShadow: `0 0 0 3px ${CONTAINED_PRIMARY_BLUE}`,
},
},
},
@@ -505,7 +505,7 @@ export const PNSPS_THEME = createTheme({

// ✅ Keyboard TAB focus (visible & accessible)
'&.Mui-focusVisible': {
outline: '3px solid rgba(21, 101, 192, 0.55)',
outline: `3px solid ${CONTAINED_PRIMARY_BLUE}`,
outlineOffset: '3px',
borderRadius: '6px',
},


+ 4
- 3
src/translations/en.json View File

@@ -423,7 +423,7 @@
"payMethod": "Payment methods",
"epayMethod": " e-Payment Method",
"selectPaymentMethod": "Please select a payment method",
"selectPaymentMethodBtn": "Select payment method",
"selectPaymentMethodBtn": "Pay Method",
"payReceipt": "Payment Receipt",
"contactPerson": "Contact Person",
"requireContactPerson": "Please enter contact person",
@@ -511,8 +511,9 @@
"fpsQrcodeTitle3":"Please complete the payment process within the specified time",
"fpsQrcodeTitle4":"Remaining time:",
"fpsQrcodeTitle5":"s",
"fpsQrcodeExpired":"QR code has expired.",
"fpsPaymentErrorMsg":"An error occurred while loading the payment QR code. Please do not refresh/reload this page manually during the payment. If the payment was not made successfully, please click 'Cancel payment' button and make the payment again. Sorry for the inconvenience caused.",
"fpsQrcodeExpired":"QR code has expired.",
"fpsPaymentErrorMsg":"An error occurred while loading the payment QR code. Please do not refresh/reload this page manually during the payment. If the payment was not made successfully, please click 'Cancel payment' button and make the payment again. Sorry for the inconvenience caused.",
"fpsSelectPaymentApp":"Please Select Bank App",

"payDnRemark": "Payment proof (e.g. ATM receipt, internet banking record) to be sent to [email protected] by {date} 12:30 p.m.",
"payNPGORemark" :"Payment to be completed at NPGO Collection Office by {date} 12:30 p.m.",


+ 3
- 2
src/translations/zh-CN.json View File

@@ -89,8 +89,9 @@
"fpsQrcodeTitle3":"请在规定时间内完成付款流程",
"fpsQrcodeTitle4":"剩余时间:",
"fpsQrcodeTitle5":"秒",
"fpsQrcodeExpired":"二维码已过期",
"fpsPaymentErrorMsg":"载入支付二维码时发生错误。请勿在付款过程中更新此页面。如果付款未完成,请点击「取消支付」按钮并重新支付。由此造成的不便,敬请谅解。",
"fpsQrcodeExpired":"二维码已过期",
"fpsPaymentErrorMsg":"载入支付二维码时发生错误。请勿在付款过程中更新此页面。如果付款未完成,请点击「取消支付」按钮并重新支付。由此造成的不便,敬请谅解。",
"fpsSelectPaymentApp":"请选择付款支付程序",
"payDnRemark": "在{date}下午12时30分前将付款证明(例如银行入数纸或网上银行付款记录)电邮至 [email protected]",
"payNPGORemark" :"在{date}下午12时30分前到北角政府合署收款处完成付款",


+ 3
- 2
src/translations/zh-HK.json View File

@@ -89,8 +89,9 @@
"fpsQrcodeTitle3":"請在規定時間內完成付款流程",
"fpsQrcodeTitle4":"剩餘時間:",
"fpsQrcodeTitle5":"秒",
"fpsQrcodeExpired":"二維碼已過期",
"fpsPaymentErrorMsg":"載入支付二維碼時發生錯誤。請勿在付款過程中更新此頁面。如果付款未完成,請點擊「取消支付」按鈕並重新支付。由此造成的不便,敬請諒解。",
"fpsQrcodeExpired":"二維碼已過期",
"fpsPaymentErrorMsg":"載入支付二維碼時發生錯誤。請勿在付款過程中更新此頁面。如果付款未完成,請點擊「取消支付」按鈕並重新支付。由此造成的不便,敬請諒解。",
"fpsSelectPaymentApp":"請選擇付款支付程式",

"payDnRemark": "在{date}下午12時30分前將付款證明(例如銀行入數紙或網上銀行付款記錄)電郵至 [email protected]",
"payNPGORemark" :"在{date}下午12時30分前到北角政府合署收款處完成付款",


Loading…
Cancel
Save