From e8690f0baf6f5dc2c3afa0fcc10960de1b18aca8 Mon Sep 17 00:00:00 2001 From: Alex Cheung Date: Fri, 13 Feb 2026 01:01:49 +0800 Subject: [PATCH 01/63] update 1.1.1 and test audio in window --- src/components/cards/AuthFooter.js | 74 ++++++++----------- .../auth-forms/CustomFormWizard.js | 17 ++++- src/pages/extra-pages/UserMenuPub/index.js | 48 ++++++++++-- src/translations/en.json | 7 +- src/translations/zh-CN.json | 5 ++ src/translations/zh-HK.json | 5 ++ src/utils/ApiPathConst.js | 3 + 7 files changed, 105 insertions(+), 54 deletions(-) diff --git a/src/components/cards/AuthFooter.js b/src/components/cards/AuthFooter.js index daaa61f..cc6207c 100644 --- a/src/components/cards/AuthFooter.js +++ b/src/components/cards/AuthFooter.js @@ -1,7 +1,7 @@ // material-ui -import { useMediaQuery, Container, Link, Typography, Stack } from '@mui/material'; +import { useMediaQuery, Container, Stack } from '@mui/material'; import bhkLogo from 'assets/images/BHK_logo_rgb_zh-hk.png'; -import {FormattedMessage} from "react-intl"; +import {useIntl} from "react-intl"; import { isGLDLoggedIn, } from "utils/Utils"; @@ -9,57 +9,43 @@ import { const AuthFooter = () => { const matchDownSM = useMediaQuery((theme) => theme.breakpoints.down('sm')); + const intl = useIntl(); + + const bhkAlt = intl.formatMessage({ id: "bhkLogoAlt" }); + const wcagAlt = intl.formatMessage({ id: "wcagAaAlt" }); return ( - + + - - 2024 © - - - - - - - - - - {!isGLDLoggedIn()? - - Level AA conformance,
-                        W3C WAI Web Content Accessibility Guidelines 2.0 - :null - } - - - logo - + ) : null} + + + {bhkAlt} + + ); }; diff --git a/src/pages/authentication/auth-forms/CustomFormWizard.js b/src/pages/authentication/auth-forms/CustomFormWizard.js index 55e4dd6..14c9127 100644 --- a/src/pages/authentication/auth-forms/CustomFormWizard.js +++ b/src/pages/authentication/auth-forms/CustomFormWizard.js @@ -25,7 +25,7 @@ import * as yup from 'yup'; import { strengthColorChi, strengthIndicator } from 'utils/password-strength'; // import {apiPath} from "auth/utils"; import axios from "axios"; -import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER, POST_IDNO } from "utils/ApiPathConst"; +import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER, POST_IDNO, POST_CAPTCHA_AUDIO } from "utils/ApiPathConst"; // import * as HttpUtils from 'utils/HttpUtils'; import * as ComboData from "utils/ComboData"; @@ -137,6 +137,16 @@ const CustomFormWizard = (props) => { onCaptchaChange(); } }, []); + + const playCaptchaAudio = async () => { + const resp = await axios.post(`${POST_CAPTCHA_AUDIO}`, + { base64Url, lang: intl.locale }, + { responseType: "arraybuffer" } + ); + const blob = new Blob([resp.data], { type: "audio/wav" }); + const url = URL.createObjectURL(blob); + new Audio(url).play(); + }; useEffect(() => { if (selectedIdDocType.type === "HKID"){ @@ -1881,6 +1891,11 @@ const CustomFormWizard = (props) => { {formik.errors.captchaField} )} + + + diff --git a/src/pages/extra-pages/UserMenuPub/index.js b/src/pages/extra-pages/UserMenuPub/index.js index 414c2bb..4c09a93 100644 --- a/src/pages/extra-pages/UserMenuPub/index.js +++ b/src/pages/extra-pages/UserMenuPub/index.js @@ -40,17 +40,49 @@ const UserMenuPub = () => { padding: "8px" }; - const getRow = ({ title, orgEn, orgZh, orgCn, indEn, indZh, indCn }) => { + const getRow = ({ titleId, orgEn, orgZh, orgCn, indEn, indZh, indCn }) => { + const guideTitle = intl.formatMessage({ id: titleId }); + const orgUserType = intl.formatMessage({ id: "forOrgUser" }); + const indUserType = intl.formatMessage({ id: "forIndUser" }); - return <> + const orgHref = locale === "zh-HK" ? orgZh : locale === "en" ? orgEn : orgCn; + const indHref = locale === "zh-HK" ? indZh : locale === "en" ? indEn : indCn; + + return ( - {title} - - + {guideTitle} + + + + + + + + + + - - ; - } + ); + }; + const pnspsurl = "https://"+window.location.hostname; diff --git a/src/translations/en.json b/src/translations/en.json index 0f41b37..2ef12dd 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -8,7 +8,12 @@ "datetimeStrFormat": "DD MMMM YYYY HH:mm:ss", "paymentMethodDatetimeStrFormat": "DD MMMM YYYY h:mm a", "datetimeFormate": "DD MMMM YYYY h:mm a", - + + "bhkLogoAlt": "Brand Hong Kong logo", + "wcagAaAlt": "Level AA conformance, W3C WAI Web Content Accessibility Guidelines 2.0", + "downloadPdfAria": "Download PDF: {guide} ({userType})", + "captchaPlayAudio": "Play audio CAPTCHA", + "PNSPS": "PNSPS", "PNSPS_fullname": "Public Notice Submission and Payment System", "HKSARGOV": "The Government of the Hong Kong Special Administrative Region", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 260d437..591b0bc 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -9,6 +9,11 @@ "paymentMethodDatetimeStrFormat": "YYYY年MM月DD日 ah时mm分", "datetimeFormate": "YYYY年MM月DD日 ah时mm分", + "bhkLogoAlt": "香港品牌标志", + "wcagAaAlt": "符合 WCAG 2.0 AA 级别(W3C WAI 网页内容无障碍指引)", + "downloadPdfAria": "下载 PDF:{guide}({userType})", + "captchaPlayAudio": "播放语音验证码", + "PNSPS": "宪报公共启事提交及缴费系统", "PNSPS_fullname": "公共启事提交及缴费系统", "HKSARGOV": "香港特别行政区政府", diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 48fdc35..9157472 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -8,6 +8,11 @@ "datetimeStrFormat": "YYYY年MM月DD日 HH:mm:ss", "paymentMethodDatetimeStrFormat": "YYYY年MM月DD日 ah時mm分", "datetimeFormate": "YYYY年MM月DD日 ah時mm分", + + "bhkLogoAlt": "香港品牌標誌", + "wcagAaAlt": "符合 WCAG 2.0 AA 級別(W3C WAI 網頁內容無障礙指引)", + "downloadPdfAria": "下載 PDF:{guide}({userType})", + "captchaPlayAudio": "播放語音驗證碼", "PNSPS": "憲報公共啟事提交及繳費系統", "PNSPS_fullname": "公共啟事提交及繳費系統", diff --git a/src/utils/ApiPathConst.js b/src/utils/ApiPathConst.js index 5008030..ffc148d 100644 --- a/src/utils/ApiPathConst.js +++ b/src/utils/ApiPathConst.js @@ -94,6 +94,9 @@ export const POST_CAPTCHA = apiPath+'/captcha'; export const GET_CONTENT = apiPath+'/getContent'; export const POST_VERIFY_CAPTCHA = apiPath+'/verifyCaptcha'; export const GET_COMBO = apiPath+'/combo-data'; +export const POST_CAPTCHA_AUDIO = apiPath+'/captcha/audio'; + + //register export const POST_PUBLIC_USER_REGISTER = apiPath+'/user/register'; From f3f75f97c6a7e9cdf1b704dfaaca4c788b45f64a Mon Sep 17 00:00:00 2001 From: Alex Cheung Date: Fri, 13 Feb 2026 02:31:07 +0800 Subject: [PATCH 02/63] add audio for captcha --- .../auth-forms/BusCustomFormWizard.js | 17 ++++++++++++++++- .../auth-forms/IAmSmartFormWizard.js | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/pages/authentication/auth-forms/BusCustomFormWizard.js b/src/pages/authentication/auth-forms/BusCustomFormWizard.js index 866d7cb..81401ff 100644 --- a/src/pages/authentication/auth-forms/BusCustomFormWizard.js +++ b/src/pages/authentication/auth-forms/BusCustomFormWizard.js @@ -30,7 +30,7 @@ import * as yup from 'yup'; import { strengthColorChi, strengthIndicator } from 'utils/password-strength'; // import {apiPath} from "auth/utils"; import axios from "axios"; -import { POST_PUBLIC_USER_REGISTER, POST_CAPTCHA, POST_USERNAME, POST_USER_EMAIL } from "utils/ApiPathConst"; +import { POST_PUBLIC_USER_REGISTER, POST_CAPTCHA, POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA_AUDIO } from "utils/ApiPathConst"; // import * as HttpUtils from 'utils/HttpUtils'; import * as ComboData from "utils/ComboData"; @@ -118,6 +118,16 @@ const BusCustomFormWizard = (props) => { changePassword(''); }, []); + const playCaptchaAudio = async () => { + const resp = await axios.post(`${POST_CAPTCHA_AUDIO}`, + { base64Url, lang: intl.locale }, + { responseType: "arraybuffer" } + ); + const blob = new Blob([resp.data], { type: "audio/wav" }); + const url = URL.createObjectURL(blob); + new Audio(url).play(); + }; + const handleCheckUsername = async () => { if (values?.username) { const response = await axios.post(`${POST_USERNAME}`, { @@ -1538,6 +1548,11 @@ const BusCustomFormWizard = (props) => { {formik.errors.captchaField} )} + + + diff --git a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js index e51747a..04132d1 100644 --- a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js +++ b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js @@ -21,7 +21,7 @@ import { useFormik, FormikProvider } from 'formik'; import * as yup from 'yup'; import axios from "axios"; -import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL } from "utils/ApiPathConst"; +import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL, POST_CAPTCHA_AUDIO } from "utils/ApiPathConst"; import * as ComboData from "utils/ComboData"; @@ -79,6 +79,16 @@ const CustomFormWizard = (props) => { const [base64Url, setBase64Url] = useState("") const [checkCode, setCheckCode] = useState("") + const playCaptchaAudio = async () => { + const resp = await axios.post(`${POST_CAPTCHA_AUDIO}`, + { base64Url, lang: intl.locale }, + { responseType: "arraybuffer" } + ); + const blob = new Blob([resp.data], { type: "audio/wav" }); + const url = URL.createObjectURL(blob); + new Audio(url).play(); + }; + useEffect(() => { location.state?.responseData ?? window.location.assign("/login"); if (captchaImg == "") @@ -1067,6 +1077,11 @@ const CustomFormWizard = (props) => { {formik.errors.captchaField} )} + + + From bfecc333d6eb781193be3ee7d67053d1da389526 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Sun, 22 Feb 2026 01:04:39 +0800 Subject: [PATCH 03/63] WCAG 2.1, AA 1.4.11 Non-Text Contrast --- src/themes/themeConst.js | 127 ++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/src/themes/themeConst.js b/src/themes/themeConst.js index 40247d4..931ac3a 100644 --- a/src/themes/themeConst.js +++ b/src/themes/themeConst.js @@ -39,6 +39,23 @@ export const PNSPS_THEME = createTheme({ }, }, }, + MuiButton: { + styleOverrides: { + outlined: { + borderColor: "#0050B3", + color: "#0050B3", + + "&:hover": { + borderColor: "#0050B3", + backgroundColor: "transparent", + }, + + "& .MuiSvgIcon-root": { + color: "inherit", + }, + }, + }, + }, MuiInputAdornment: { styleOverrides: { root: { @@ -181,47 +198,89 @@ export const PNSPS_THEME = createTheme({ MuiOutlinedInput: { styleOverrides: { input: { - padding: '10.5px 14px 10.5px 12px', - '&.MuiOutlinedInput-input.Mui-disabled': { - WebkitTextFillColor: 'rgba(0, 0, 0, 1)', - }, - color: 'rgba(0, 0, 0, 0.85)' + padding: '10.5px 14px 10.5px 12px', + '&.MuiOutlinedInput-input.Mui-disabled': { + WebkitTextFillColor: 'rgba(0, 0, 0, 1)', + }, + color: 'rgba(0, 0, 0, 0.85)', }, + root: { - '&:hover .MuiOutlinedInput-notchedOutline': { - borderColor: theme.palette.primary.light - }, - '&.Mui-focused': { - boxShadow: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.2)}`, - '& .MuiOutlinedInput-notchedOutline': { - border: `1px solid ${theme.palette.primary.light}` - } - }, - '&.Mui-error': { - '&:hover .MuiOutlinedInput-notchedOutline': { - borderColor: theme.palette.error.light - }, - '&.Mui-focused': { - boxShadow: `0 0 0 2px ${alpha(theme.palette.error.main, 0.2)}`, - '& .MuiOutlinedInput-notchedOutline': { - border: `1px solid ${theme.palette.error.light}` - } - } - }, - height: ROW_HEIGHT + /* ============================= + Issue 1: Input boundary contrast + ============================= */ + '& .MuiOutlinedInput-notchedOutline': { + borderColor: '#767676', // >= 3:1 on white + borderWidth: 1, + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: '#767676', + }, + + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.primary.main, + borderWidth: 3, // thick focus indicator + }, + + /* Prevent “shrinking” of inner area when border gets thicker */ + '&.Mui-focused .MuiOutlinedInput-input': { + padding: '8.5px 12px 8.5px 10px', // 10.5-2, 14-2, 10.5-2, 12-2 }, + '&.Mui-focused.MuiInputBase-sizeSmall .MuiOutlinedInput-input': { + padding: '5.5px 6px 5.5px 10px', // 7.5-2, 8-2, 7.5-2, 12-2 + }, + + /* Error state */ + '&.Mui-error .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.error.main, + borderWidth: 1, + }, + '&.Mui-error:hover .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.error.main, + }, + '&.Mui-error.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.error.main, + borderWidth: 3, + }, + + height: ROW_HEIGHT, + }, + inputSizeSmall: { - padding: '7.5px 8px 7.5px 12px' + padding: '7.5px 8px 7.5px 12px', }, + inputMultiline: { - root:{ - minHeight: ROW_HEIGHT, - maxHeight: '50vh' - }, - padding: '7.5px 8px 7.5px 12px' - } + root: { + minHeight: ROW_HEIGHT, + maxHeight: '50vh', + }, + padding: '7.5px 8px 7.5px 12px', + }, + }, + }, + MuiCheckbox: { + styleOverrides: { + root: { + /* Default (unchecked) state — darker */ + color: '#595959', // >= 3:1 on white - } + /* Hover (keep contrast) */ + '&:hover': { + backgroundColor: 'transparent', + }, + + /* Checked state */ + '&.Mui-checked': { + color: theme.palette.primary.main, + }, + + /* Focus-visible */ + '&.Mui-focusVisible': { + boxShadow: `0 0 0 3px ${alpha(theme.palette.primary.main, 0.35)}`, + }, + }, + }, }, MuiTextField: { styleOverrides: { From 27b5128b233b35df2e724ac16de3258b4dd5f16f Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Sun, 22 Feb 2026 01:42:33 +0800 Subject: [PATCH 04/63] WCAG 2.0, AA 2.4.7 Focus Visible --- src/assets/style/navbarStyles.css | 15 ++++- src/assets/style/styles.css | 58 ++++++++++++++++++- src/components/AdminLogo/index.js | 25 +++++--- src/components/MobileLogo/index.js | 25 +++++--- .../Header/HeaderContent/LocaleSelector.js | 28 ++++++--- .../Header/HeaderContent/MobileSection.js | 33 ++++++----- .../Header/HeaderContent/Notification.js | 20 +++++-- 7 files changed, 159 insertions(+), 45 deletions(-) diff --git a/src/assets/style/navbarStyles.css b/src/assets/style/navbarStyles.css index a85e592..9d5d352 100644 --- a/src/assets/style/navbarStyles.css +++ b/src/assets/style/navbarStyles.css @@ -43,7 +43,7 @@ color: #0C489E; } #navbar div li a:hover::after, -#navbar div li a:focus::after{ +#navbar div li a:focus-visible::after{ content: ""; width: 80%; height: 3px; @@ -73,6 +73,19 @@ opacity: 1; display: block } + +/* Navbar: don't show focus ring on mouse click */ +#navbar a:focus { + outline: none; +} + +/* Navbar: show focus ring for keyboard navigation */ +#navbar a:focus-visible { + outline: 3px solid #0C489E; + outline-offset: 2px; + border-radius: 10px; /* tweak to match your design */ +} + /* #navbar div li:focus-within > ul, #navbar div li ul:hover, #navbar div li ul:focus { diff --git a/src/assets/style/styles.css b/src/assets/style/styles.css index 196ca43..f438c04 100644 --- a/src/assets/style/styles.css +++ b/src/assets/style/styles.css @@ -74,4 +74,60 @@ a:active { text-decoration: none; } -/* iframe#webpack-dev-server-client-overlay{display:none!important} */ \ No newline at end of file +/* iframe#webpack-dev-server-client-overlay{display:none!important} */ + +/* ===== WCAG 2.4.7 Focus Visible (Global) ===== */ +:where( + a, + button, + input, + select, + textarea, + summary, + [role="button"], + [role="link"], + [tabindex]:not([tabindex="-1"]) +):focus-visible { + outline: 3px solid #0C489E; + outline-offset: 2px; + border-radius: 4px; +} + +/* Fallback for browsers that don't support :focus-visible */ +:where( + a, + button, + input, + select, + textarea, + summary, + [role="button"], + [role="link"], + [tabindex]:not([tabindex="-1"]) +):focus { + outline: 3px solid #0C489E; + outline-offset: 2px; + border-radius: 4px; +} + +/* ===== MUI DataGrid focus visible (WCAG 2.4.7) ===== */ + +/* Column headers */ +.MuiDataGrid-columnHeader:focus, +.MuiDataGrid-columnHeader:focus-within { + outline: 3px solid #0C489E; + outline-offset: -2px; +} + +/* Cells */ +.MuiDataGrid-cell:focus, +.MuiDataGrid-cell:focus-within { + outline: 3px solid #0C489E; + outline-offset: -2px; +} + +/* If outline is clipped, add halo */ +.MuiDataGrid-columnHeader:focus-within, +.MuiDataGrid-cell:focus-within { + box-shadow: 0 0 0 3px rgba(12, 72, 158, 0.25); +} \ No newline at end of file diff --git a/src/components/AdminLogo/index.js b/src/components/AdminLogo/index.js index 8a75781..f847e3b 100644 --- a/src/components/AdminLogo/index.js +++ b/src/components/AdminLogo/index.js @@ -23,14 +23,23 @@ const LogoSection = ({ sx, to }) => { return ( dispatch(activeItem({ openItem: [defaultId] }))} - to={!to ? config.defaultPath : to} - sx={sx} - > - - + disableRipple + component={Link} + onClick={() => dispatch(activeItem({ openItem: [defaultId] }))} + to={!to ? config.defaultPath : to} + sx={{ + ...sx, + + /* ✅ WCAG 2.4.7 focus indicator */ + '&:focus-visible': { + outline: '3px solid #0C489E', + outlineOffset: '2px', + borderRadius: '6px' + } + }} + > + + PNSPS diff --git a/src/components/MobileLogo/index.js b/src/components/MobileLogo/index.js index eb2a16d..d5386ff 100644 --- a/src/components/MobileLogo/index.js +++ b/src/components/MobileLogo/index.js @@ -17,14 +17,23 @@ const LogoSection = ({ sx, to }) => { const dispatch = useDispatch(); return ( dispatch(activeItem({ openItem: [defaultId] }))} - to={!to ? config.defaultPath : to} - sx={sx} - > - - + disableRipple + component={Link} + onClick={() => dispatch(activeItem({ openItem: [defaultId] }))} + to={!to ? config.defaultPath : to} + sx={{ + ...sx, + + /* WCAG 2.4.7 – visible keyboard focus */ + '&:focus-visible': { + outline: '3px solid #0C489E', + outlineOffset: '2px', + borderRadius: '6px' + } + }} + > + + ); }; diff --git a/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js b/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js index ad676aa..cd0016f 100644 --- a/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js +++ b/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js @@ -47,16 +47,26 @@ const LocaleSelector = () => { return ( - + { <> - - + component="span" + disableRipple + sx={{ + bgcolor: open ? 'grey.300' : 'grey.100', + + /* WCAG 2.4.7 – visible keyboard focus */ + '&:focus-visible': { + outline: '3px solid #0C489E', + outlineOffset: '2px', + borderRadius: '6px' + } + }} + ref={anchorRef} + aria-controls={open ? 'menu-list-grow' : undefined} + aria-haspopup="true" + onClick={handleToggle} + color="inherit" + > + + { - - - - + + + + Date: Sun, 22 Feb 2026 04:07:52 +0800 Subject: [PATCH 05/63] add back footer info --- src/components/cards/AuthFooter.js | 39 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/cards/AuthFooter.js b/src/components/cards/AuthFooter.js index cc6207c..59e5f8c 100644 --- a/src/components/cards/AuthFooter.js +++ b/src/components/cards/AuthFooter.js @@ -1,6 +1,7 @@ // material-ui -import { useMediaQuery, Container, Stack } from '@mui/material'; +import { useMediaQuery, Container, Link, Typography, Stack } from '@mui/material'; import bhkLogo from 'assets/images/BHK_logo_rgb_zh-hk.png'; +import {FormattedMessage} from "react-intl"; import {useIntl} from "react-intl"; import { isGLDLoggedIn, @@ -19,11 +20,41 @@ const AuthFooter = () => { - {!isGLDLoggedIn() ? ( + + 2024 © + + + + + + + + + + {!isGLDLoggedIn()? ( Date: Mon, 23 Feb 2026 01:42:05 +0800 Subject: [PATCH 06/63] WCAG 2.0 AA 1.4.3 Contrast (Minimum) --- src/assets/style/styles.css | 91 ++++++----- src/layout/MainLayout/Header/index.js | 2 +- src/pages/authentication/BusRegister.js | 39 +++-- src/pages/authentication/IAmSmartRegister.js | 51 +++--- src/pages/authentication/Register.js | 39 +++-- src/pages/authentication/RegisterCustom.js | 39 ++++- .../auth-forms/BusCustomFormWizard.js | 115 +++++-------- .../auth-forms/CustomFormWizard.js | 136 ++++++---------- .../auth-forms/IAmSmartFormWizard.js | 154 +++++------------- src/pages/dashboard/Public/index.js | 63 +++++-- src/pages/pnspsUserGroupDetailPage/index.js | 2 +- src/themes/buttonConst.js | 99 ++++++++--- src/themes/themeConst.js | 73 ++++++--- src/utils/statusUtils/DnStatus.js | 6 +- src/utils/statusUtils/PaymentStatus.js | 10 +- src/utils/statusUtils/ProofStatus.js | 10 +- .../statusUtils/PublicNoteStatusUtils.js | 108 ++++++------ 17 files changed, 541 insertions(+), 496 deletions(-) diff --git a/src/assets/style/styles.css b/src/assets/style/styles.css index f438c04..3dc79aa 100644 --- a/src/assets/style/styles.css +++ b/src/assets/style/styles.css @@ -52,31 +52,18 @@ img img:hover{-webkit-filter:none;} - -a:link { - color: #1890ff; - background-color: transparent; - text-decoration: none; -} -a:visited { - color: #1890ff; - background-color: transparent; - text-decoration: none; -} -a:hover { - color: #1890ff; - background-color: transparent; - text-decoration: none; -} +a:link, +a:visited, +a:hover, a:active { - color: #1890ff; + color: #005FCC; /* WCAG AA compliant */ background-color: transparent; - text-decoration: none; + text-decoration: underline; } /* iframe#webpack-dev-server-client-overlay{display:none!important} */ -/* ===== WCAG 2.4.7 Focus Visible (Global) ===== */ +/* ===== WCAG 2.4.7 Focus Visible (Keyboard only) ===== */ :where( a, button, @@ -93,7 +80,7 @@ a:active { border-radius: 4px; } -/* Fallback for browsers that don't support :focus-visible */ +/* Suppress mouse focus */ :where( a, button, @@ -104,30 +91,60 @@ a:active { [role="button"], [role="link"], [tabindex]:not([tabindex="-1"]) -):focus { - outline: 3px solid #0C489E; - outline-offset: 2px; - border-radius: 4px; +):focus:not(:focus-visible) { + outline: none; } -/* ===== MUI DataGrid focus visible (WCAG 2.4.7) ===== */ - -/* Column headers */ -.MuiDataGrid-columnHeader:focus, -.MuiDataGrid-columnHeader:focus-within { +/* ===== 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(12, 72, 158, 0.25); +} + +/* Contained buttons only */ +.MuiButton-contained { + border: 2px solid #0C489E; + box-shadow: none; +} + +.MuiButton-contained:hover { + border: 2px solid #0C489E; +} + +/* iAM Smart button — keep green border */ +.MuiButton-outlinedIAmSmart { + color: #2E7D32; /* green text */ + border: 1px solid #2E7D32; /* keep your green */ +} + +.MuiButton-outlinedIAmSmart:hover { + color: #2E7D32; /* green text */ + border: 1px solid #2E7D32; + background-color: rgba(46, 125, 50, 0.08); /* optional */ } -/* Cells */ -.MuiDataGrid-cell:focus, -.MuiDataGrid-cell:focus-within { +/* ===== Outlined button focus ===== */ +.MuiButton-outlined:focus-visible { outline: 3px solid #0C489E; - outline-offset: -2px; + outline-offset: 2px; } -/* If outline is clipped, add halo */ -.MuiDataGrid-columnHeader:focus-within, -.MuiDataGrid-cell:focus-within { - box-shadow: 0 0 0 3px rgba(12, 72, 158, 0.25); +/* Step icon number color (text inside the circle) */ +.MuiStepIcon-text { + fill: #1A1A1A; /* dark text, high contrast */ +} +/* Placeholder text contrast */ +.MuiInputBase-input::placeholder { + color: #6B6B6B; /* passes 4.5:1 on white */ + opacity: 1; /* IMPORTANT */ +} + +/* WCAG-safe error text color */ +.Mui-error, +.MuiFormHelperText-root.Mui-error, +.MuiTypography-errorMessage1 { + color: #B00020; /* dark red, AA compliant */ + opacity: 1; /* remove faded appearance */ } \ No newline at end of file diff --git a/src/layout/MainLayout/Header/index.js b/src/layout/MainLayout/Header/index.js index 7182086..c1dd25e 100644 --- a/src/layout/MainLayout/Header/index.js +++ b/src/layout/MainLayout/Header/index.js @@ -746,7 +746,7 @@ function Header(props) { {/*公共啟事提交*/} {/*及繳費系統*/} - + diff --git a/src/pages/authentication/BusRegister.js b/src/pages/authentication/BusRegister.js index 1648237..179ef5b 100644 --- a/src/pages/authentication/BusRegister.js +++ b/src/pages/authentication/BusRegister.js @@ -42,29 +42,28 @@ const BusRegister = () => { ]; const stepStyle = { - width: {lg:"40%", md:"70%", xs:"100%"}, + width: { lg: "40%", md: "70%", xs: "100%" }, boxShadow: 1, backgroundColor: "#FFFFFF", padding: 2, + + /* Inactive (default) */ + "& .MuiStepIcon-root": { color: "#757575", fontSize: "2rem" }, // darker grey + "& .MuiStepLabel-label": { color: "#424242" }, // label darker + "& .MuiStepConnector-line": { borderColor: "#9E9E9E" }, // connector darker + + /* Active */ "& .Mui-active": { - "&.MuiStepIcon-root": { - color: "warning.main", - fontSize: "2rem", - }, - "& .MuiStepConnector-line": { - borderColor: "warning.main" - } + "&.MuiStepIcon-root": { color: "warning.main", fontSize: "2rem" }, + "& .MuiStepConnector-line": { borderColor: "warning.main" } }, + + /* Completed */ "& .Mui-completed": { - "&.MuiStepIcon-root": { - color: "secondary.main", - fontSize: "2rem", - }, - "& .MuiStepConnector-line": { - borderColor: "secondary.main" - } + "&.MuiStepIcon-root": { color: "#0C489E", fontSize: "2rem" }, // use your strong blue + "& .MuiStepConnector-line": { borderColor: "#0C489E" } } - } + }; const totalSteps = () => { return steps.length; @@ -153,7 +152,13 @@ const BusRegister = () => { ) : (} // onClick={handleStep(index)} > diff --git a/src/pages/authentication/IAmSmartRegister.js b/src/pages/authentication/IAmSmartRegister.js index 322eecc..9a63104 100644 --- a/src/pages/authentication/IAmSmartRegister.js +++ b/src/pages/authentication/IAmSmartRegister.js @@ -26,30 +26,29 @@ import {FormattedMessage, useIntl} from "react-intl"; const CustomFormWizard = Loadable(lazy(() => import('./auth-forms/IAmSmartFormWizard'))); const AuthWrapper = Loadable(lazy(() => import('./AuthWrapperCustom'))); // ================================|| REGISTER ||================================ // -const stepStyle = { - width: {lg:"40%", md:"70%", xs:"100%"}, - boxShadow: 1, - backgroundColor: "#FFFFFF", - padding: 2, - "& .Mui-active": { - "&.MuiStepIcon-root": { - color: "warning.main", - fontSize: "2rem", - }, - "& .MuiStepConnector-line": { - borderColor: "warning.main" - } - }, - "& .Mui-completed": { - "&.MuiStepIcon-root": { - color: "secondary.main", - fontSize: "2rem", + const stepStyle = { + width: { lg: "40%", md: "70%", xs: "100%" }, + boxShadow: 1, + backgroundColor: "#FFFFFF", + padding: 2, + + /* Inactive (default) */ + "& .MuiStepIcon-root": { color: "#757575", fontSize: "2rem" }, // darker grey + "& .MuiStepLabel-label": { color: "#424242" }, // label darker + "& .MuiStepConnector-line": { borderColor: "#9E9E9E" }, // connector darker + + /* Active */ + "& .Mui-active": { + "&.MuiStepIcon-root": { color: "warning.main", fontSize: "2rem" }, + "& .MuiStepConnector-line": { borderColor: "warning.main" } }, - "& .MuiStepConnector-line": { - borderColor: "secondary.main" + + /* Completed */ + "& .Mui-completed": { + "&.MuiStepIcon-root": { color: "#0C489E", fontSize: "2rem" }, // use your strong blue + "& .MuiStepConnector-line": { borderColor: "#0C489E" } } - } -} + }; const Register = () => { const [activeStep, setActiveStep] = useState(0); @@ -151,7 +150,13 @@ const Register = () => { {label} ) : (} // onClick={handleStep(index)} > diff --git a/src/pages/authentication/Register.js b/src/pages/authentication/Register.js index e36e96b..9d542d1 100644 --- a/src/pages/authentication/Register.js +++ b/src/pages/authentication/Register.js @@ -42,29 +42,28 @@ const Register = () => { ]; const stepStyle = { - width: {lg:"40%", md:"70%", xs:"100%"}, + width: { lg: "40%", md: "70%", xs: "100%" }, boxShadow: 1, backgroundColor: "#FFFFFF", padding: 2, + + /* Inactive (default) */ + "& .MuiStepIcon-root": { color: "#757575", fontSize: "2rem" }, // darker grey + "& .MuiStepLabel-label": { color: "#424242" }, // label darker + "& .MuiStepConnector-line": { borderColor: "#9E9E9E" }, // connector darker + + /* Active */ "& .Mui-active": { - "&.MuiStepIcon-root": { - color: "warning.main", - fontSize: "2rem", - }, - "& .MuiStepConnector-line": { - borderColor: "warning.main" - } + "&.MuiStepIcon-root": { color: "warning.main", fontSize: "2rem" }, + "& .MuiStepConnector-line": { borderColor: "warning.main" } }, + + /* Completed */ "& .Mui-completed": { - "&.MuiStepIcon-root": { - color: "secondary.main", - fontSize: "2rem", - }, - "& .MuiStepConnector-line": { - borderColor: "secondary.main" - } + "&.MuiStepIcon-root": { color: "#0C489E", fontSize: "2rem" }, // use your strong blue + "& .MuiStepConnector-line": { borderColor: "#0C489E" } } - } + }; const totalSteps = () => { return steps.length; @@ -155,7 +154,13 @@ const Register = () => { ) : (} // onClick={handleStep(index)} > diff --git a/src/pages/authentication/RegisterCustom.js b/src/pages/authentication/RegisterCustom.js index 50cccd0..38eb1bb 100644 --- a/src/pages/authentication/RegisterCustom.js +++ b/src/pages/authentication/RegisterCustom.js @@ -134,9 +134,22 @@ const RegisterCustom = () => { - +
@@ -146,9 +159,23 @@ const RegisterCustom = () => { - +
diff --git a/src/pages/authentication/auth-forms/BusCustomFormWizard.js b/src/pages/authentication/auth-forms/BusCustomFormWizard.js index 81401ff..51c45cd 100644 --- a/src/pages/authentication/auth-forms/BusCustomFormWizard.js +++ b/src/pages/authentication/auth-forms/BusCustomFormWizard.js @@ -30,7 +30,7 @@ import * as yup from 'yup'; import { strengthColorChi, strengthIndicator } from 'utils/password-strength'; // import {apiPath} from "auth/utils"; import axios from "axios"; -import { POST_PUBLIC_USER_REGISTER, POST_CAPTCHA, POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA_AUDIO } from "utils/ApiPathConst"; +import { POST_PUBLIC_USER_REGISTER, POST_CAPTCHA, POST_USERNAME, POST_USER_EMAIL } from "utils/ApiPathConst"; // import * as HttpUtils from 'utils/HttpUtils'; import * as ComboData from "utils/ComboData"; @@ -118,16 +118,6 @@ const BusCustomFormWizard = (props) => { changePassword(''); }, []); - const playCaptchaAudio = async () => { - const resp = await axios.post(`${POST_CAPTCHA_AUDIO}`, - { base64Url, lang: intl.locale }, - { responseType: "arraybuffer" } - ); - const blob = new Blob([resp.data], { type: "audio/wav" }); - const url = URL.createObjectURL(blob); - new Audio(url).play(); - }; - const handleCheckUsername = async () => { if (values?.username) { const response = await axios.post(`${POST_USERNAME}`, { @@ -193,21 +183,17 @@ const BusCustomFormWizard = (props) => { } }, [checkDistrictBlur]) - const handleCheckDistrict = () => { + const handleCheckDistrict = async () => { setDistrictErrStr(""); - if (selectedAddress5?.type === "hongKong") { - if (!selectedAddress4 || Object.keys(selectedAddress4).length === 0) { - setCheckDistrict(true); - setDistrictErrStr(getRequiredErrStr("district")); - return false; + if (selectedAddress4 == null || selectedAddress4 == "" || selectedAddress4 == {}){ + setCheckDistrict(true) + setDistrictErrStr(getRequiredErrStr("district")) + }else { + setCheckDistrict(false) } } - - setCheckDistrict(false); - return true; - }; - + } function getRequiredErrStr(fieldname){ return displayErrorMsg(intl.formatMessage({ id: 'require'},{fieldname:fieldname?intl.formatMessage({ id: fieldname}):""})); @@ -231,12 +217,8 @@ const BusCustomFormWizard = (props) => { } const checkDataField = (data) => { - const districtValid = - selectedAddress5?.type !== "hongKong" || - (selectedAddress4 && Object.keys(selectedAddress4).length > 0); - - - const valid = + // console.log(data.brExpiryDate) + if ( handleCaptcha(data.captchaField) && data.username !== "" && data.password !== "" && @@ -260,15 +242,19 @@ const BusCustomFormWizard = (props) => { handlePhone(data.phone) && handleUserName(data.username) && handleBrNo(data.brNo) && - districtValid && - !checkUsername && - !checkEmail; - - setisValid(valid); + handleCheckDistrict()&& + !checkUsername&& + !checkEmail&& + !checkDistrict + ) { + setisValid(true) + return isValid + } else { + setisValid(false) + return isValid + } }; - - const handleCheckBoxChange = (event) => { // console.log(event.target) if (event.target.name == 'termsAndConAccept') { @@ -372,18 +358,7 @@ const BusCustomFormWizard = (props) => { const { handleSubmit } = useForm({}) const _onSubmit = () => { - if (!handleCheckDistrict()) { - setLoding(false); - return; - } - - if (!isValid) { - setLoding(false); - return; - } - setLoding(true); - values.address4 = selectedAddress4==null?"":selectedAddress4.type values.address5 = selectedAddress5.type // console.log(values) @@ -633,15 +608,6 @@ const BusCustomFormWizard = (props) => { checkDataField(values) }, [values]) - useEffect(() => { - if ( - selectedAddress5?.type === "hongKong" && - values?.captchaField?.length === 5 - ) { - handleCheckDistrict(); - } - }, [values?.captchaField, selectedAddress5, selectedAddress4]); - return (
@@ -658,7 +624,7 @@ const BusCustomFormWizard = (props) => {
- + @@ -676,7 +642,7 @@ const BusCustomFormWizard = (props) => { - * + * {/* - @@ -1569,7 +1530,7 @@ const BusCustomFormWizard = (props) => {
- {/* 註有*的項目必須輸入資料 */} + {/* 註有*的項目必須輸入資料 */} @@ -1798,4 +1759,4 @@ const BusCustomFormWizard = (props) => { ); } -export default BusCustomFormWizard; +export default BusCustomFormWizard; \ No newline at end of file diff --git a/src/pages/authentication/auth-forms/CustomFormWizard.js b/src/pages/authentication/auth-forms/CustomFormWizard.js index 14c9127..4c36244 100644 --- a/src/pages/authentication/auth-forms/CustomFormWizard.js +++ b/src/pages/authentication/auth-forms/CustomFormWizard.js @@ -25,7 +25,7 @@ import * as yup from 'yup'; import { strengthColorChi, strengthIndicator } from 'utils/password-strength'; // import {apiPath} from "auth/utils"; import axios from "axios"; -import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER, POST_IDNO, POST_CAPTCHA_AUDIO } from "utils/ApiPathConst"; +import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER, POST_IDNO } from "utils/ApiPathConst"; // import * as HttpUtils from 'utils/HttpUtils'; import * as ComboData from "utils/ComboData"; @@ -137,16 +137,6 @@ const CustomFormWizard = (props) => { onCaptchaChange(); } }, []); - - const playCaptchaAudio = async () => { - const resp = await axios.post(`${POST_CAPTCHA_AUDIO}`, - { base64Url, lang: intl.locale }, - { responseType: "arraybuffer" } - ); - const blob = new Blob([resp.data], { type: "audio/wav" }); - const url = URL.createObjectURL(blob); - new Audio(url).play(); - }; useEffect(() => { if (selectedIdDocType.type === "HKID"){ @@ -207,21 +197,17 @@ const CustomFormWizard = (props) => { } } - const handleCheckDistrict = () => { + const handleCheckDistrict = async () => { setDistrictErrStr(""); - if (selectedAddress5?.type === "hongKong") { - if (!selectedAddress4 || Object.keys(selectedAddress4).length === 0) { - setCheckDistrict(true); - setDistrictErrStr(getRequiredErrStr("district")); - return false; + if (selectedAddress4 == null || selectedAddress4 == "" || selectedAddress4 == {}){ + setCheckDistrict(true) + setDistrictErrStr(getRequiredErrStr("district")) + }else { + setCheckDistrict(false) } } - - setCheckDistrict(false); - return true; - }; - + } useEffect(() => { if (username) { @@ -327,43 +313,46 @@ const CustomFormWizard = (props) => { } const checkDataField = (data) => { - const districtValid = - selectedAddress5?.type !== "hongKong" || - (selectedAddress4 && Object.keys(selectedAddress4).length > 0); - - - const valid = + // console.log(data) + if ( handleCaptcha(data.captchaField) && data.username !== "" && data.password !== "" && data.confirmPassword !== "" && - data.password === data.confirmPassword && + data.password == data.confirmPassword && selectedIdDocType.type !== "" && data.idNo !== "" && + // (data.enName !== "" || selectedIdDocType.type === "CNID") && + // data.chName !== "" && handleName(data.enName, data.chName) && data.address1 !== "" && data.email !== "" && data.emailConfirm !== "" && - data.email === data.emailConfirm && + data.email == data.emailConfirm && data.phone !== "" && data.phoneCountryCode !== "" && - termsAndConAccept === true && + termsAndConAccept == true && fileList.length !== 0 && + // data.captchaField && handlePassword(data.password) && handleEmail(data.email) && handleIdNo(data.idNo, selectedIdDocType.type, data.checkDigit) && handlePhone(data.phone) && handleUsername(data.username) && - districtValid && + handleCheckDistrict()&& !checkUsername && !checkEmail && - !checkIdDocNumber; - - setisValid(valid); + !checkIdDocNumber&& + !checkDistrict + ) { + setisValid(true) + return isValid + } else { + setisValid(false) + return isValid + } }; - - const handleCheckBoxChange = (event) => { if (event.target.name == 'termsAndConAccept') { setTermsAndConAccept(event.target.checked) @@ -460,28 +449,14 @@ const CustomFormWizard = (props) => { // }, [selectedAddress4, selectedAddress5]) useEffect(() => { - if (props.step == 2) { - handleCheckDistrict(); - _onSubmit(); - } - if (captchaImg == "") onCaptchaChange(); - checkDataField(values); - }, [props.step]); - + props.step == 2 ? _onSubmit() : null; + if (captchaImg == "") + onCaptchaChange(); + checkDataField(values) + }, [props.step]) const { handleSubmit } = useForm({}) const _onSubmit = () => { - // hard stop - if (!handleCheckDistrict()) { - setLoding(false); - return; - } - - if (!isValid) { - setLoding(false); - return; - } - setLoding(true); values.idDocType = selectedIdDocType.type values.address4 = selectedAddress4 == null ? "" : selectedAddress4.type @@ -863,15 +838,6 @@ const CustomFormWizard = (props) => { const { values } = formik - useEffect(() => { - if ( - selectedAddress5?.type === "hongKong" && - values?.captchaField?.length === 5 - ) { - handleCheckDistrict(); - } - }, [values?.captchaField, selectedAddress5, selectedAddress4]); - useEffect(() => { checkDataField(values) }, [values]) @@ -894,7 +860,7 @@ const CustomFormWizard = (props) => {
- + @@ -909,7 +875,7 @@ const CustomFormWizard = (props) => { - * + * {/* { - * + *
@@ -1023,7 +989,7 @@ const CustomFormWizard = (props) => { - * + * { - * + * {/* {formik.touched.enName && formik.errors.enName && ( @@ -1306,7 +1272,7 @@ const CustomFormWizard = (props) => { - {selectedIdDocType.type === "CNID" ? "" : } + {selectedIdDocType.type === "CNID" ? "" : } { - + { - * + * { getOptionLabel={(option) => option.type ? intl.formatMessage({ id: option.type }) : ""} onChange={(event, newValue) => { setSelectedAddress4(newValue); - - setCheckDistrict(false); - setDistrictErrStr(""); }} sx={{ '& .MuiInputBase-root': { alignItems: 'center' }, @@ -1536,7 +1499,7 @@ const CustomFormWizard = (props) => { - * + * { - * + * { - * + * @@ -1761,7 +1724,7 @@ const CustomFormWizard = (props) => { - * + * @@ -1801,7 +1764,7 @@ const CustomFormWizard = (props) => { - * + * @@ -1856,7 +1819,7 @@ const CustomFormWizard = (props) => { - * + * @@ -1891,11 +1854,6 @@ const CustomFormWizard = (props) => { {formik.errors.captchaField} )} - - - @@ -1912,7 +1870,7 @@ const CustomFormWizard = (props) => { - {/* 註有*的項目必須輸入資料 */} + {/* 註有*的項目必須輸入資料 */} @@ -2147,4 +2105,4 @@ const CustomFormWizard = (props) => { ); } -export default CustomFormWizard; +export default CustomFormWizard; \ No newline at end of file diff --git a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js index 04132d1..7c4d734 100644 --- a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js +++ b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js @@ -21,7 +21,7 @@ import { useFormik, FormikProvider } from 'formik'; import * as yup from 'yup'; import axios from "axios"; -import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL, POST_CAPTCHA_AUDIO } from "utils/ApiPathConst"; +import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL } from "utils/ApiPathConst"; import * as ComboData from "utils/ComboData"; @@ -79,16 +79,6 @@ const CustomFormWizard = (props) => { const [base64Url, setBase64Url] = useState("") const [checkCode, setCheckCode] = useState("") - const playCaptchaAudio = async () => { - const resp = await axios.post(`${POST_CAPTCHA_AUDIO}`, - { base64Url, lang: intl.locale }, - { responseType: "arraybuffer" } - ); - const blob = new Blob([resp.data], { type: "audio/wav" }); - const url = URL.createObjectURL(blob); - new Audio(url).play(); - }; - useEffect(() => { location.state?.responseData ?? window.location.assign("/login"); if (captchaImg == "") @@ -96,15 +86,6 @@ const CustomFormWizard = (props) => { responseToData(); }, []); - useEffect(() => { - if ( - selectedAddress5?.type === "hongKong" && - values?.captchaField?.length === 5 - ) { - handleCheckDistrict(); - } - }, [values?.captchaField, selectedAddress5, selectedAddress4]); - const handleClickShowId = () => { setshowId(!showId); }; @@ -136,22 +117,17 @@ const CustomFormWizard = (props) => { } }, [checkDistrictBlur]) - const handleCheckDistrict = () => { + const handleCheckDistrict = async () => { setDistrictErrStr(""); - if (selectedAddress5?.type === "hongKong") { - if (!selectedAddress4 || Object.keys(selectedAddress4).length === 0) { - setCheckDistrict(true); - setDistrictErrStr(getRequiredErrStr("district")); - return false; + if (selectedAddress4 == null || selectedAddress4 == "" || selectedAddress4 == {}){ + setCheckDistrict(true) + setDistrictErrStr(getRequiredErrStr("district")) + }else { + setCheckDistrict(false) } } - - setCheckDistrict(false); - return true; - }; - - + } function getRequiredErrStr(fieldname) { return displayErrorMsg(intl.formatMessage({ id: 'require' }, { fieldname: fieldname ? intl.formatMessage({ id: fieldname }) : "" })); @@ -232,16 +208,13 @@ const CustomFormWizard = (props) => { const handleCheckEmail = async () => { if (values?.email) { - if (handleEmail(values.email)) { - const response = await axios.post(`${POST_USER_EMAIL}`, { - e1: values.email, - }); - setCheckEmail((Number(response.data[0]) === 1)); - return Number(response.data[0]) === 1; - } + const response = await axios.post(`${POST_USER_EMAIL}`, { + e1: values.email, + }) + setCheckEmail((Number(response.data[0]) === 1)) + return Number(response.data[0]) === 1 } - }; - + } useEffect(() => { if (email) { @@ -271,10 +244,6 @@ const CustomFormWizard = (props) => { formik.setFieldValue("address1", iAmSmartData.address1 ?? ""); props.setIdNo(iAmSmartData.idNo ?? ""); setLodingData(false) - - if (iAmSmartData.email) { - handleCheckEmail(); - } } }, [iAmSmartData]) @@ -293,30 +262,29 @@ const CustomFormWizard = (props) => { const checkDataField = (data) => { - const districtValid = - selectedAddress5?.type !== "hongKong" || - (selectedAddress4 && Object.keys(selectedAddress4).length > 0); - - - const valid = - data.address1 !== "" && + if (data.address1 !== "" && data.email !== "" && data.emailConfirm !== "" && - data.email === data.emailConfirm && + data.email == data.emailConfirm && data.phone !== "" && data.phoneCountryCode !== "" && - termsAndConAccept === true && + termsAndConAccept == true && data.captchaField && handleEmail(data.email) && handlePhone(data.phone) && handleCaptcha(data.captchaField) && - districtValid && - !checkEmail; - - setisValid(valid); + handleCheckDistrict()&& + !checkEmail&& + !checkDistrict + ) { + setisValid(true) + return isValid + } else { + setisValid(false) + return isValid + } }; - const handleCheckBoxChange = (event) => { if (event.target.name == 'termsAndConAccept') { setTermsAndConAccept(event.target.checked) @@ -339,30 +307,14 @@ const CustomFormWizard = (props) => { termsAndConAccept, termsAndConNotAccept]) useEffect(() => { - if (props.step == 2) { - handleCheckDistrict(); - _onSubmit(); - } - if (captchaImg == "") onCaptchaChange(); - checkDataField(values); - }, [props.step]); + props.step == 2 ? _onSubmit() : null; + if (captchaImg == "") + onCaptchaChange(); + checkDataField(values) + }, [props.step]) const { handleSubmit } = useForm({}) - const _onSubmit = async () => { - // hard stop: district - if (!handleCheckDistrict()) { - setLoding(false); - return; - } - - await handleCheckEmail(); - - // hard stop: overall validation (now includes fresh email result) - if (!isValid) { - setLoding(false); - return; - } - + const _onSubmit = () => { setLoding(true); const userAddress = { @@ -496,15 +448,6 @@ const CustomFormWizard = (props) => { checkDataField(values) }, [values]) - useEffect(() => { - if ( - selectedAddress5?.type === "hongKong" && - values?.captchaField?.length === 5 - ) { - handleCheckDistrict(); - } - }, [values?.captchaField, selectedAddress5, selectedAddress4]); - return ( isLoadingData ? : @@ -523,7 +466,7 @@ const CustomFormWizard = (props) => { - + iAM Smart: @@ -589,7 +532,7 @@ const CustomFormWizard = (props) => { - * + * @@ -671,16 +614,13 @@ const CustomFormWizard = (props) => { getOptionLabel={(option) => option.type ? intl.formatMessage({ id: option.type }) : ""} onChange={(event, newValue) => { setSelectedAddress4(newValue); - - setCheckDistrict(false); - setDistrictErrStr(""); }} sx={{ '& .MuiInputBase-root': { alignItems: 'center' }, '& .MuiAutocomplete-endAdornment': { top: '50%', transform: 'translateY(-50%)' }, '& .MuiOutlinedInput-root': { height: 40 } }} - renderInput={(params) => } /> { - * + * @@ -760,10 +700,7 @@ const CustomFormWizard = (props) => { type="email" value={formik.values.email.trim()} name="email" - onChange={(e) => { - setCheckEmail(false); - formik.handleChange(e); - }} + onChange={formik.handleChange} placeholder={intl.formatMessage({ id: 'userContactEmail' })} onBlur={formik.handleBlur} inputProps={{ @@ -794,7 +731,7 @@ const CustomFormWizard = (props) => { - * + * @@ -841,7 +778,7 @@ const CustomFormWizard = (props) => { - * + * @@ -985,7 +922,7 @@ const CustomFormWizard = (props) => { - * + * @@ -1042,7 +979,7 @@ const CustomFormWizard = (props) => { - * + * @@ -1077,11 +1014,6 @@ const CustomFormWizard = (props) => { {formik.errors.captchaField} )} - - - @@ -1282,4 +1214,4 @@ const CustomFormWizard = (props) => { ); } -export default CustomFormWizard; +export default CustomFormWizard; \ No newline at end of file diff --git a/src/pages/dashboard/Public/index.js b/src/pages/dashboard/Public/index.js index 75f88ac..9af059a 100644 --- a/src/pages/dashboard/Public/index.js +++ b/src/pages/dashboard/Public/index.js @@ -93,7 +93,25 @@ const DashboardDefault = () => { let list = [] response.map((item) => { list.push( - @@ -206,9 +241,17 @@ const DashboardDefault = () => { diff --git a/src/pages/pnspsUserGroupDetailPage/index.js b/src/pages/pnspsUserGroupDetailPage/index.js index 8c938fd..fd60e30 100644 --- a/src/pages/pnspsUserGroupDetailPage/index.js +++ b/src/pages/pnspsUserGroupDetailPage/index.js @@ -203,7 +203,7 @@ const UserMaintainPage = () => { alignItems: 'end' }} onClick={() => { location.reload() }} - color="gray" + color="secondary" > Reset & Back diff --git a/src/themes/buttonConst.js b/src/themes/buttonConst.js index 025c764..23635c5 100644 --- a/src/themes/buttonConst.js +++ b/src/themes/buttonConst.js @@ -70,31 +70,84 @@ export const PNSPS_BUTTON_THEME = createTheme({ }, MuiButton: { styleOverrides: { - startIcon:{ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - '& > *:nth-of-type(1)': { - fontSize: '28px', - }, + /* ----------------------------- + Icon alignment + ------------------------------ */ + startIcon: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + '& > *:nth-of-type(1)': { fontSize: '28px' }, }, + + /* ----------------------------- + Base button styles + ------------------------------ */ root: { - fontSize: '1.2rem', - fontWeight: '600', - height: '45px', - minWidth: '35' + - 'vw', // Default width for xs screen sizes - '@media (min-width: 600px)': { // sm breakpoint - minWidth: '20vw', - }, - '@media (min-width: 960px)': { // md breakpoint - minWidth: '15vw', - }, - '@media (min-width: 1280px)': { // lg breakpoint - minWidth: '9vw', - }, - textTransform: "none", - alignItems: 'normal', + fontSize: '1.2rem', + fontWeight: '600', + height: '45px', + minWidth: '35vw', + '@media (min-width: 600px)': { minWidth: '20vw' }, + '@media (min-width: 960px)': { minWidth: '15vw' }, + '@media (min-width: 1280px)': { minWidth: '9vw' }, + textTransform: 'none', + alignItems: 'normal', + }, + + /* ----------------------------- + Remove default contained elevation / blue focus + ------------------------------ */ + contained: { + boxShadow: 'none', + '&:hover': { boxShadow: 'none' }, + '&:active': { boxShadow: 'none' }, + '&.Mui-focusVisible': { boxShadow: 'none' }, + }, + + /* ----------------------------- + RESET / CANCEL (contained + cancel) + WCAG 2.1 AA compliant + ------------------------------ */ + containedCancel: { + backgroundColor: '#616161', // dark grey (passes with white text) + color: '#FFFFFF', + + border: '0 !important', + outline: 'none', + boxShadow: 'none !important', + + '&:hover': { + backgroundColor: '#545454', + boxShadow: 'none !important', + }, + + '&:active': { + backgroundColor: '#4A4A4A', + boxShadow: 'none !important', + }, + + /* ✅ Keyboard TAB focus (visible & accessible) */ + '&.Mui-focusVisible': { + boxShadow: '0 0 0 3px rgba(255,255,255,0.6) !important', + }, + }, + + /* ----------------------------- + Optional: outlined cancel variant + ------------------------------ */ + outlinedCancel: { + borderColor: '#616161', + color: '#424242', + + '&:hover': { + borderColor: '#616161', + backgroundColor: 'transparent', + }, + + '&.Mui-focusVisible': { + boxShadow: '0 0 0 3px rgba(66,66,66,0.4)', + }, }, }, }, diff --git a/src/themes/themeConst.js b/src/themes/themeConst.js index 931ac3a..82583a4 100644 --- a/src/themes/themeConst.js +++ b/src/themes/themeConst.js @@ -17,6 +17,10 @@ const themeTypography = Typography( export const PNSPS_THEME = createTheme({ palette: theme.palette, typography: themeTypography, + cancel: { + main: '#424242', // dark gray, WCAG safe + contrastText: '#FFFFFF' + }, components: { MuiImage: { root: { @@ -41,19 +45,17 @@ export const PNSPS_THEME = createTheme({ }, MuiButton: { styleOverrides: { - outlined: { - borderColor: "#0050B3", - color: "#0050B3", - - "&:hover": { - borderColor: "#0050B3", - backgroundColor: "transparent", - }, - - "& .MuiSvgIcon-root": { - color: "inherit", - }, - }, + root: ({ ownerState }) => ({ + ...(ownerState.color === "cancel" && { + borderColor: "#9E9E9E", + }), + }), + contained: ({ ownerState }) => ({ + ...(ownerState.color === "cancel" && { + border: "2px solid #9E9E9E", + boxShadow: "none", + }), + }), }, }, MuiInputAdornment: { @@ -416,18 +418,45 @@ export const PNSPS_THEME = createTheme({ MuiTab: { styleOverrides: { root: { - fontSize: TABLE_FONT_SIZE, - minHeight: 46, - color: theme.palette.text.primary - } - } - }, + fontSize: TABLE_FONT_SIZE, // ✅ unchanged + minHeight: 46, + textTransform: 'none', + + // Unselected tab text on white – AA compliant + color: '#424242', + + // Selected tab text – AA compliant blue + '&.Mui-selected': { + color: '#1565C0', + }, + + // Hover (optional but consistent) + '&:hover': { + color: '#1565C0', + }, + + // ✅ Keyboard TAB focus (visible & accessible) + '&.Mui-focusVisible': { + outline: '3px solid rgba(21, 101, 192, 0.55)', + outlineOffset: '3px', + borderRadius: '6px', + }, + }, + }, + }, + MuiTabs: { styleOverrides: { vertical: { - overflow: 'visible' - } - } + overflow: 'visible', + }, + + // ✅ Selected tab indicator – AA compliant blue + indicator: { + backgroundColor: '#1565C0', + height: '3px', + }, + }, }, MuiFormLabel: { styleOverrides: { diff --git a/src/utils/statusUtils/DnStatus.js b/src/utils/statusUtils/DnStatus.js index 9c7e96e..527ab36 100644 --- a/src/utils/statusUtils/DnStatus.js +++ b/src/utils/statusUtils/DnStatus.js @@ -1,8 +1,8 @@ import {getStatusTag} from "utils/statusUtils/Base"; -const pending = {color:"#f5a83d", eng:"Pending", cht:"待辦", cn: "待办"} -const toBePaid = {color:"#f5a83d", eng:"To be Paid", cht:"待付款", cn: "待付款"} -const paid = {color:"#22a13f", eng:"Paid", cht:"已付款", cn: "已付款"} +const pending = { color: "#8F7200", eng: "Pending", cht: "待辦", cn: "待办" } // was #f5a83d +const toBePaid = { color: "#8F7200", eng: "To be Paid", cht: "待付款", cn: "待付款" } // was #f5a83d +const paid = { color: "#1D8735", eng: "Paid", cht: "已付款", cn: "已付款" } // was #22a13f export function getStatus_Cht(params) { let status = getStatus(params); diff --git a/src/utils/statusUtils/PaymentStatus.js b/src/utils/statusUtils/PaymentStatus.js index 2e7d5fd..f24f643 100644 --- a/src/utils/statusUtils/PaymentStatus.js +++ b/src/utils/statusUtils/PaymentStatus.js @@ -1,10 +1,10 @@ import {getStatusTag} from "utils/statusUtils/Base"; -const APPR = {color:"#22a13f", eng:"Success", cht:"成功"} -const REJT = {color:"#d9372b", eng:"Reject", cht:"拒絕"} -const CANC = {color:"#8a8784", eng:"Cancelled", cht:"取消"} -const INPR = {color:"#f5a83d", eng:"In Progress", cht:"進行中"} -const DUPA = {color:"#8a8784", eng:"", cht:""} +const APPR = { color: "#1D8735", eng: "Success", cht: "成功" } // was #22a13f +const REJT = { color: "#d9372b", eng: "Reject", cht: "拒絕" } // unchanged +const CANC = { color: "#797674", eng: "Cancelled", cht: "取消" } // was #8a8784 +const INPR = { color: "#8F7200", eng: "In Progress", cht: "進行中" } // was #f5a83d +const DUPA = { color: "#797674", eng: "", cht: "" } // was #8a8784 export function getStatus_Cht(params) { let status = getStatus(params); diff --git a/src/utils/statusUtils/ProofStatus.js b/src/utils/statusUtils/ProofStatus.js index e0fd6db..f374e18 100644 --- a/src/utils/statusUtils/ProofStatus.js +++ b/src/utils/statusUtils/ProofStatus.js @@ -1,11 +1,11 @@ import {getStatusTag} from "utils/statusUtils/Base"; import * as DateUtils from "utils/DateUtils"; -const confirm = {color:"#22a13f", eng:"Pass for printing", cht:"可以付印", cn:"可以付印"} -const unable = {color:"#d9372b", eng:"Re-proofing", cht:"未能付印", cn:"未能付印"} -const timeOut = {color:"#8a8784", eng:"Proofing timed out. Please apply again", cht:"校對回覆逾時,請重新申請", cn:"校对回复逾时,请重新申请"} -const pendingReply = {color:"#f5a83d", eng:"Proofing reply pending", cht:"校對待覆", cn:"校对待复"} -const cancel = {color:"#000", textColor:"#fff", eng:"Cancelled", cht:"已取消", cn:"已取消"} +const confirm = { color: "#1D8735", eng: "Pass for printing", cht: "可以付印", cn: "可以付印" } // was #22a13f +const unable = { color: "#d9372b", eng: "Re-proofing", cht: "未能付印", cn: "未能付印" } // unchanged +const timeOut = { color: "#797674", eng: "Proofing timed out. Please apply again", cht: "校對回覆逾時,請重新申請", cn: "校对回复逾时,请重新申请" } // was #8a8784 +const pendingReply = { color: "#9D6C27", eng: "Proofing reply pending", cht: "校對待覆", cn: "校对待复" } // was #f5a83d +const cancel = { color: "#000", textColor: "#fff", eng: "Cancelled", cht: "已取消", cn: "已取消" } // unchanged export function getStatus_Cht(params) { let status = getStatus(params); diff --git a/src/utils/statusUtils/PublicNoteStatusUtils.js b/src/utils/statusUtils/PublicNoteStatusUtils.js index 03978eb..fab1794 100644 --- a/src/utils/statusUtils/PublicNoteStatusUtils.js +++ b/src/utils/statusUtils/PublicNoteStatusUtils.js @@ -7,33 +7,43 @@ export function getStatus(params) { } export function getStatusByText(status, creditor) { - switch (status) { - case "submitted": - return getStatusTag({ color: "#f5a83d", text: "處理中" }) - case "reviewed": - return getStatusTag({ color: "#f5a83d", text: "處理中" }) - case "confirmed": - if (creditor) - return getStatusTag({ color: "#22a13f", text: "待發佈" }) - else - return getStatusTag({ color: "#22a13f", text: "待付款" }) - case "published": - return getStatusTag({ color: "#22a13f", text: "待付款" }) - case "paid": - return getStatusTag({ color: "#22a13f", text: "待發佈" }) - case "completed": - return getStatusTag({ color: "#4D4D4D", text: "已完成" }) - case "notAccepted": - return getStatusTag({ color: "#d9372b", text: "不接受" }) - case "resubmit": - return getStatusTag({ color: "#757373", text: "需重新提交" }) - case "cancelled": - return getStatusTag({ color: "#909497", text: "已取消" }) - case "withdrawn": - return getStatusTag({ color: "#6E14CC", text: "已撤銷" }) - default: - return getStatusTag({ text: status }) - } + switch (status) { + case "submitted": + return getStatusTag({ color: "#8F7200", text: "處理中" }) // was #f5a83d + + case "reviewed": + return getStatusTag({ color: "#8F7200", text: "處理中" }) // was #f5a83d + + case "confirmed": + if (creditor) + return getStatusTag({ color: "#1D8735", text: "待發佈" }) // was #22a13f + else + return getStatusTag({ color: "#1D8735", text: "待付款" }) // was #22a13f + + case "published": + return getStatusTag({ color: "#1D8735", text: "待付款" }) // was #22a13f + + case "paid": + return getStatusTag({ color: "#1D8735", text: "待發佈" }) // was #22a13f + + case "completed": + return getStatusTag({ color: "#4D4D4D", text: "已完成" }) // unchanged (already AA) + + case "notAccepted": + return getStatusTag({ color: "#d9372b", text: "不接受" }) // unchanged (already AA) + + case "resubmit": + return getStatusTag({ color: "#757373", text: "需重新提交" }) // unchanged (already AA) + + case "cancelled": + return getStatusTag({ color: "#737679", text: "已取消" }) // was #909497 + + case "withdrawn": + return getStatusTag({ color: "#6E14CC", text: "已撤銷" }) // unchanged (already AA) + + default: + return getStatusTag({ text: status }) + } } export function getStatusEng(params) { @@ -42,28 +52,28 @@ export function getStatusEng(params) { export function getStatusByTextEng(status, creditor) { switch (status) { case "submitted": - return getStatusTag({ color: "#F1C40F", text: "Submitted" }) + return getStatusTag({ color: "#8F7200", text: "Submitted" }) // was #F1C40F case "reviewed": - return getStatusTag({ color: "#0C489E", text: "Reviewed" }) + return getStatusTag({ color: "#0C489E", text: "Reviewed" }) // unchanged case "confirmed": if (creditor) - return getStatusTag({ color: "#3498DB", text: "Pending Publish" }) + return getStatusTag({ color: "#217CBA", text: "Pending Publish" }) // was #3498DB else - return getStatusTag({ color: "#F39C12", text: "Pending Payment" }) + return getStatusTag({ color: "#B45309", text: "Pending Payment" }) // was #F39C12 case "published": - return getStatusTag({ color: "#F39C12", text: "Pending Payment" }) + return getStatusTag({ color: "#B45309", text: "Pending Payment" }) // was #F39C12 case "paid": - return getStatusTag({ color: "#3498DB", text: "Pending Publish" }) + return getStatusTag({ color: "#217CBA", text: "Pending Publish" }) // was #3498DB case "completed": - return getStatusTag({ color: "#4D4D4D", text: "Completed" }) + return getStatusTag({ color: "#4D4D4D", text: "Completed" }) // unchanged case "notAccepted": - return getStatusTag({ color: "#d9372b", text: "Not Accepted" }) + return getStatusTag({ color: "#d9372b", text: "Not Accepted" }) // unchanged case "resubmit": - return getStatusTag({ color: "#757373", text: "Re-Submit Required" }) + return getStatusTag({ color: "#757373", text: "Re-Submit Required" }) // unchanged case "cancelled": - return getStatusTag({ color: "#8a8784", text: "Cancelled" }) + return getStatusTag({ color: "#797674", text: "Cancelled" }) // was #8a8784 case "withdrawn": - return getStatusTag({ color: "#6E14CC", text: "Withdrawn" }) + return getStatusTag({ color: "#6E14CC", text: "Withdrawn" }) // unchanged default: return getStatusTag({ text: status }) } @@ -76,28 +86,28 @@ export function getStatusIntl(params, intl) { export function getStatusByTextIntl(status, creditor, intl) { switch (status) { case "submitted": - return getStatusTag({ color: "#F1C40F", text: intl.formatMessage({id: 'processing'}) }) + return getStatusTag({ color: "#8F7200", text: intl.formatMessage({id: 'processing'}) }) // was #F1C40F case "reviewed": - return getStatusTag({ color: "#F1C40F", text: intl.formatMessage({id: 'processing'}) }) + return getStatusTag({ color: "#8F7200", text: intl.formatMessage({id: 'processing'}) }) // was #F1C40F case "confirmed": if (creditor) - return getStatusTag({ color: "#3498DB", text: intl.formatMessage({id: 'pendingPublish'}) }) + return getStatusTag({ color: "#217CBA", text: intl.formatMessage({id: 'pendingPublish'}) }) // was #3498DB else - return getStatusTag({ color: "#F39C12", text: intl.formatMessage({id: 'pendingPayment'}) }) + return getStatusTag({ color: "#B45309", text: intl.formatMessage({id: 'pendingPayment'}) }) // was #F39C12 case "published": - return getStatusTag({ color: "#F39C12", text: intl.formatMessage({id: 'pendingPayment'}) }) + return getStatusTag({ color: "#B45309", text: intl.formatMessage({id: 'pendingPayment'}) }) // was #F39C12 case "paid": - return getStatusTag({ color: "#3498DB", text: intl.formatMessage({id: 'pendingPublish'}) }) + return getStatusTag({ color: "#217CBA", text: intl.formatMessage({id: 'pendingPublish'}) }) // was #3498DB case "completed": - return getStatusTag({ color: "#4D4D4D", text: intl.formatMessage({id: 'completed'}) }) + return getStatusTag({ color: "#4D4D4D", text: intl.formatMessage({id: 'completed'}) }) // unchanged case "notAccepted": - return getStatusTag({ color: "#d9372b", text: intl.formatMessage({id: 'notAccepted'}) }) + return getStatusTag({ color: "#d9372b", text: intl.formatMessage({id: 'notAccepted'}) }) // unchanged case "resubmit": - return getStatusTag({ color: "#757373", text: intl.formatMessage({id: 'resubmit'}) }) + return getStatusTag({ color: "#757373", text: intl.formatMessage({id: 'resubmit'}) }) // unchanged case "cancelled": - return getStatusTag({ color: "#8a8784", text: intl.formatMessage({id: 'cancelled'}) }) + return getStatusTag({ color: "#797674", text: intl.formatMessage({id: 'cancelled'}) }) // was #8a8784 case "withdrawn": - return getStatusTag({ color: "#6E14CC", text: intl.formatMessage({id: 'withdrawn'}) }) + return getStatusTag({ color: "#6E14CC", text: intl.formatMessage({id: 'withdrawn'}) }) // unchanged default: return getStatusTag({ text: status }) } From a412582bbc2185f64828975f40126577859145ba Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Mon, 23 Feb 2026 02:07:04 +0800 Subject: [PATCH 07/63] remove border --- src/pages/dashboard/Public/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/dashboard/Public/index.js b/src/pages/dashboard/Public/index.js index 9af059a..29a7fa6 100644 --- a/src/pages/dashboard/Public/index.js +++ b/src/pages/dashboard/Public/index.js @@ -191,7 +191,7 @@ const DashboardDefault = () => { p: 4, justifyContent: "flex-start", backgroundColor: "#e1edfc", - border: "3px solid #0C489E", // DARKER border (3:1+) + border: "0px solid #0C489E", // DARKER border (3:1+) borderRadius: "10px", color: "#0C489E", // icon + default text color "&:hover": { From 1a74f172d9da702cd65732a3f5a4618fa51e6482 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Mon, 23 Feb 2026 03:39:33 +0800 Subject: [PATCH 08/63] WCAG 2.0, A 4.1.2 Name, Role, Value --- .../Header/HeaderContent/LocaleSelector.js | 6 +- .../Header/HeaderContent/Notification.js | 2 +- .../Header/HeaderContent/Profile/index.js | 2 +- src/layout/MainLayout/Header/index.js | 13 +++- src/pages/authentication/BusRegister.js | 63 ++++++++-------- src/pages/authentication/IAmSmartRegister.js | 41 +++++----- src/pages/authentication/Register.js | 75 ++++++++++--------- .../auth-forms/PreviewUploadFileTable.js | 12 ++- .../auth-forms/UploadFileTable.js | 12 ++- src/translations/en.json | 3 + src/translations/zh-CN.json | 3 + src/translations/zh-HK.json | 3 + 12 files changed, 127 insertions(+), 108 deletions(-) diff --git a/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js b/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js index cd0016f..66d0b68 100644 --- a/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js +++ b/src/layout/MainLayout/Header/HeaderContent/LocaleSelector.js @@ -16,7 +16,7 @@ import { import Transitions from 'components/@extended/Transitions'; import LanguageIcon from '@mui/icons-material/Language'; -import {FormattedMessage} from "react-intl"; +import {FormattedMessage, useIntl} from "react-intl"; import * as React from "react"; import LocaleContext from "components/I18nProvider"; @@ -27,6 +27,8 @@ const LocaleSelector = () => { const matchesXs = useMediaQuery(theme.breakpoints.down('md')); const { setLocale } = useContext(LocaleContext); + const intl = useIntl(); + const anchorRef = useRef(null); const [open, setOpen] = useState(false); const handleToggle = () => { @@ -60,7 +62,7 @@ const LocaleSelector = () => { borderRadius: '6px' } }} - aria-label="open profile" + aria-label={intl.formatMessage({id: 'openLanguage'})} ref={anchorRef} aria-controls={open ? 'profile-grow' : undefined} aria-haspopup="true" diff --git a/src/layout/MainLayout/Header/HeaderContent/Notification.js b/src/layout/MainLayout/Header/HeaderContent/Notification.js index 359fab7..a300a0b 100644 --- a/src/layout/MainLayout/Header/HeaderContent/Notification.js +++ b/src/layout/MainLayout/Header/HeaderContent/Notification.js @@ -82,7 +82,7 @@ const Notification = () => { borderRadius: '6px' } }} - aria-label="open profile" + aria-label={intl.formatMessage({id: 'openLanguage'})} ref={anchorRef} aria-controls={open ? 'profile-grow' : undefined} aria-haspopup="true" diff --git a/src/layout/MainLayout/Header/HeaderContent/Profile/index.js b/src/layout/MainLayout/Header/HeaderContent/Profile/index.js index aa5e13c..5fe8ad9 100644 --- a/src/layout/MainLayout/Header/HeaderContent/Profile/index.js +++ b/src/layout/MainLayout/Header/HeaderContent/Profile/index.js @@ -101,7 +101,7 @@ const Profile = () => { borderRadius: 1, '&:hover': { bgcolor: 'secondary.lighter' } }} - aria-label="open profile" + aria-label={intl.formatMessage({id: 'openLanguage'})} ref={anchorRef} aria-controls={open ? 'profile-grow' : undefined} aria-haspopup="true" diff --git a/src/layout/MainLayout/Header/index.js b/src/layout/MainLayout/Header/index.js index c1dd25e..f1f8fcd 100644 --- a/src/layout/MainLayout/Header/index.js +++ b/src/layout/MainLayout/Header/index.js @@ -66,7 +66,7 @@ import { isGranted, isGrantedAny } from "auth/utils"; // import { AppBar } from '../../../../node_modules/@mui/material/index'; import { Link } from "react-router-dom"; import LocaleSelector from "./HeaderContent/LocaleSelector"; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; const drawerWidth = 300; @@ -99,6 +99,8 @@ function Header(props) { const dispatch = useDispatch() const navigate = useNavigate() + const intl = useIntl(); + const handleDrawerToggle = () => { setMobileOpen((prevState) => !prevState); }; @@ -714,7 +716,8 @@ function Header(props) {
{ {steps.map((label, index) => ( - { - index < 2 ? - ( + - - {label} - - ) : - (} + {label} + + + ) : ( + } // onClick={handleStep(index)} + > + - - {label} - - ) - } - + {label} + + + )} ))} diff --git a/src/pages/authentication/IAmSmartRegister.js b/src/pages/authentication/IAmSmartRegister.js index 9a63104..ee6e844 100644 --- a/src/pages/authentication/IAmSmartRegister.js +++ b/src/pages/authentication/IAmSmartRegister.js @@ -142,28 +142,29 @@ const Register = () => { {steps.map((label, index) => ( - { - index < 2 ? - ( - {label} - ) : - (} + > + {label} + + ) : ( + } // onClick={handleStep(index)} - > - {label} - ) - } - + > + {label} + + )} ))} diff --git a/src/pages/authentication/Register.js b/src/pages/authentication/Register.js index 9d542d1..e525e2d 100644 --- a/src/pages/authentication/Register.js +++ b/src/pages/authentication/Register.js @@ -139,42 +139,45 @@ const Register = () => { {steps.map((label, index) => ( - - { - index < 2 ? - ( - - {label} - - ) : - (} - // onClick={handleStep(index)} - > - - {label} - - ) - } - - + + {index < 2 ? ( + + + {label} + + + ) : ( + } + // onClick={handleStep(index)} + > + + {label} + + + )} + ))} {allStepsCompleted() ? ( diff --git a/src/pages/authentication/auth-forms/PreviewUploadFileTable.js b/src/pages/authentication/auth-forms/PreviewUploadFileTable.js index b7ba4a0..0b2d048 100644 --- a/src/pages/authentication/auth-forms/PreviewUploadFileTable.js +++ b/src/pages/authentication/auth-forms/PreviewUploadFileTable.js @@ -14,7 +14,7 @@ import { Stack, Typography } from '@mui/material'; -import {FormattedMessage} from "react-intl"; +import {FormattedMessage, useIntl} from "react-intl"; // ==============================|| EVENT TABLE ||============================== // export default function PreviewUploadFileTable({ recordList, }) { @@ -23,6 +23,8 @@ export default function PreviewUploadFileTable({ recordList, }) { const [rowModesModel] = React.useState({}); // const theme = useTheme(); + const intl = useIntl(); + // const navigate = useNavigate() useEffect(() => { @@ -75,17 +77,13 @@ export default function PreviewUploadFileTable({ recordList, }) { { id: 'name', field: 'name', - headerName: - - , + headerName: intl.formatMessage({ id: 'fileName' }), flex: 1, }, { id: 'size', field: 'size', - headerName: - - , + headerName: intl.formatMessage({ id: 'fileSize' }), valueGetter: (params) => { // console.log(params) return Math.ceil(params.value / 1024) + " KB"; diff --git a/src/pages/authentication/auth-forms/UploadFileTable.js b/src/pages/authentication/auth-forms/UploadFileTable.js index 21cc7af..3ead606 100644 --- a/src/pages/authentication/auth-forms/UploadFileTable.js +++ b/src/pages/authentication/auth-forms/UploadFileTable.js @@ -14,7 +14,7 @@ import { Stack, Typography } from '@mui/material'; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; // ==============================|| EVENT TABLE ||============================== // export default function UploadFileTable({ recordList, setUpdateRows, }) { @@ -23,6 +23,8 @@ export default function UploadFileTable({ recordList, setUpdateRows, }) { const [rowModesModel, setRowModesModel] = React.useState({}); // const theme = useTheme(); + const intl = useIntl(); + // const navigate = useNavigate() useEffect(() => { @@ -77,17 +79,13 @@ export default function UploadFileTable({ recordList, setUpdateRows, }) { { id: 'name', field: 'name', - headerName: - - , + headerName: intl.formatMessage({ id: 'fileName' }), flex: 4, }, { id: 'size', field: 'size', - headerName: - - , + headerName: intl.formatMessage({ id: 'fileSize' }), valueGetter: (params) => { // console.log(params) return Math.ceil(params.value / 1024) + " KB"; diff --git a/src/translations/en.json b/src/translations/en.json index 2ef12dd..745e9c5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -9,6 +9,9 @@ "paymentMethodDatetimeStrFormat": "DD MMMM YYYY h:mm a", "datetimeFormate": "DD MMMM YYYY h:mm a", + "openMenu": "Navigation Menu", + "openLanguage": "Language Menu", + "bhkLogoAlt": "Brand Hong Kong logo", "wcagAaAlt": "Level AA conformance, W3C WAI Web Content Accessibility Guidelines 2.0", "downloadPdfAria": "Download PDF: {guide} ({userType})", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 591b0bc..76513b0 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -9,6 +9,9 @@ "paymentMethodDatetimeStrFormat": "YYYY年MM月DD日 ah时mm分", "datetimeFormate": "YYYY年MM月DD日 ah时mm分", + "openMenu": "系统选单", + "openLanguage": "语言选单", + "bhkLogoAlt": "香港品牌标志", "wcagAaAlt": "符合 WCAG 2.0 AA 级别(W3C WAI 网页内容无障碍指引)", "downloadPdfAria": "下载 PDF:{guide}({userType})", diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 9157472..341a52e 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -9,6 +9,9 @@ "paymentMethodDatetimeStrFormat": "YYYY年MM月DD日 ah時mm分", "datetimeFormate": "YYYY年MM月DD日 ah時mm分", + "openMenu": "系統選單", + "openLanguage": "語言選單", + "bhkLogoAlt": "香港品牌標誌", "wcagAaAlt": "符合 WCAG 2.0 AA 級別(W3C WAI 網頁內容無障礙指引)", "downloadPdfAria": "下載 PDF:{guide}({userType})", From 617904b649ffd8ee1bf3740bc0b1031c8d0cb14f Mon Sep 17 00:00:00 2001 From: Alex Cheung Date: Wed, 25 Feb 2026 03:42:25 +0800 Subject: [PATCH 09/63] WCAG 1.3.1 and add back 1.1.1 --- src/components/FiDataGrid.js | 29 +- .../Announcement/Search_Public/SearchForm.js | 4 +- src/pages/DemandNote/Create/SearchForm.js | 4 + src/pages/DemandNote/Export/SearchForm.js | 4 + src/pages/DemandNote/Search/SearchForm.js | 12 + .../DemandNote/Search_Public/SearchForm.js | 12 +- src/pages/Message/Search/SearchForm.js | 4 +- .../DetailPage/OrganizationCard.js | 2 +- .../OrganizationCard_loadFromUser.js | 2 +- src/pages/Payment/Search_Public/SearchForm.js | 8 +- src/pages/Proof/Search_Public/SearchForm.js | 12 +- .../ListPanel/PendingPaymentTab.js | 4 + .../ListPanel/SearchPublicNoticeForm.js | 8 +- src/pages/User/ChangePasswordPage/index.js | 8 +- .../User/DetailPage/UserInformationCard.js | 4 +- .../User/DetailsPage_Individual/index.js | 7 +- .../User/DetailsPage_Organization/index.js | 2 +- .../ForgotPassword/AuthCallback/index.js | 8 +- .../ForgotUsername/AuthCallback/index.js | 8 +- .../authentication/auth-forms/AuthLogin.js | 4 +- .../auth-forms/AuthLoginCustom.js | 4 +- .../authentication/auth-forms/AuthRegister.js | 4 +- .../auth-forms/BusCustomFormWizard.js | 58 +++- .../auth-forms/CustomFormWizard.js | 62 ++++- .../auth-forms/IAmSmartFormWizard.js | 63 ++++- .../PrivacyPolicyPage/PrivacyPolicy_cn.js | 90 +++---- .../PrivacyPolicyPage/PrivacyPolicy_en.js | 247 +++++++++--------- .../PrivacyPolicyPage/PrivacyPolicy_zh.js | 209 +++++++-------- src/translations/en.json | 18 +- src/translations/zh-CN.json | 18 +- src/translations/zh-HK.json | 18 +- src/utils/Combo.js | 6 + src/utils/DateUtils.js | 50 ++++ src/utils/SelectBase.js | 8 +- 34 files changed, 651 insertions(+), 350 deletions(-) diff --git a/src/components/FiDataGrid.js b/src/components/FiDataGrid.js index 67c389c..6b28f0f 100644 --- a/src/components/FiDataGrid.js +++ b/src/components/FiDataGrid.js @@ -239,16 +239,25 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, ? { Pagination: () => ( - `${(_rows?.length ? page * pageSize + 1 : 0)}-${page * pageSize + (_rows?.length ?? 0)} ${intl.formatMessage({ id: "of" })} ${rowCount}` - } - labelRowsPerPage={intl.formatMessage({ id: "rowsPerPage" }) + ":"} - onPageChange={handleChangePage} - onRowsPerPageChange={handleChangePageSize} + count={rowCount || 0} + page={page} + rowsPerPage={pageSize} + rowsPerPageOptions={_pageSizeOptions} + labelDisplayedRows={() => + `${(_rows?.length ? page * pageSize + 1 : 0)}-${page * pageSize + (_rows?.length ?? 0)} ${intl.formatMessage({ id: "of" })} ${rowCount}` + } + labelRowsPerPage={intl.formatMessage({ id: "rowsPerPage" }) + ":"} + getItemAriaLabel={(type) => { + if (type === 'previous') { + return intl.formatMessage({ id: 'paginationPrev' }); + } + if (type === 'next') { + return intl.formatMessage({ id: 'paginationNext' }); + } + return ''; + }} + onPageChange={handleChangePage} + onRowsPerPageChange={handleChangePageSize} /> ), } diff --git a/src/pages/Announcement/Search_Public/SearchForm.js b/src/pages/Announcement/Search_Public/SearchForm.js index 7d8f618..156b538 100644 --- a/src/pages/Announcement/Search_Public/SearchForm.js +++ b/src/pages/Announcement/Search_Public/SearchForm.js @@ -116,7 +116,7 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => - + - + )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> diff --git a/src/pages/DemandNote/Export/SearchForm.js b/src/pages/DemandNote/Export/SearchForm.js index 7596bb7..c33d201 100644 --- a/src/pages/DemandNote/Export/SearchForm.js +++ b/src/pages/DemandNote/Export/SearchForm.js @@ -187,6 +187,10 @@ const SearchPublicNoticeForm = ({ applySearch, issueComboData }) => { }} /> )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> diff --git a/src/pages/DemandNote/Search/SearchForm.js b/src/pages/DemandNote/Search/SearchForm.js index debc2dd..3b2d891 100644 --- a/src/pages/DemandNote/Search/SearchForm.js +++ b/src/pages/DemandNote/Search/SearchForm.js @@ -224,6 +224,10 @@ const SearchDemandNoteForm = ({ applySearch, orgComboData, searchCriteria, issue }} /> )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> @@ -282,6 +286,10 @@ const SearchDemandNoteForm = ({ applySearch, orgComboData, searchCriteria, issue {params.children} )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> : <> @@ -445,6 +453,10 @@ const SearchDemandNoteForm = ({ applySearch, orgComboData, searchCriteria, issue }} /> )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> diff --git a/src/pages/DemandNote/Search_Public/SearchForm.js b/src/pages/DemandNote/Search_Public/SearchForm.js index bbed57c..d4402be 100644 --- a/src/pages/DemandNote/Search_Public/SearchForm.js +++ b/src/pages/DemandNote/Search_Public/SearchForm.js @@ -180,6 +180,10 @@ const SearchDemandNoteForm = ({ applySearch, searchCriteria, issueComboData, onG }} /> )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> @@ -210,7 +214,7 @@ const SearchDemandNoteForm = ({ applySearch, searchCriteria, issueComboData, onG - + - + diff --git a/src/pages/Message/Search/SearchForm.js b/src/pages/Message/Search/SearchForm.js index 78bdc4d..e76cd7b 100644 --- a/src/pages/Message/Search/SearchForm.js +++ b/src/pages/Message/Search/SearchForm.js @@ -119,7 +119,7 @@ const SearchForm = ({ applySearch, searchCriteria, onGridReady }) => { - + { - + { value={fromDate != null ? DateUtils.dateStr(fromDate) : DateUtils.dateStr(currentFromDate)} disabled={true} /> : - + { {FieldUtils.notNullFieldLabel("Expiry Date:")} - + - + - + InputLabelProps={{ shrink: true }} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> diff --git a/src/pages/Proof/Search_Public/SearchForm.js b/src/pages/Proof/Search_Public/SearchForm.js index d2c39ad..0a40d5c 100644 --- a/src/pages/Proof/Search_Public/SearchForm.js +++ b/src/pages/Proof/Search_Public/SearchForm.js @@ -223,6 +223,10 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o }} /> )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> @@ -252,7 +256,7 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o - + - + diff --git a/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js b/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js index 01b1242..ed97bf6 100644 --- a/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js +++ b/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js @@ -401,6 +401,10 @@ export default function SubmittedTab({ setCount, url }) { '& .MuiOutlinedInput-root': { height: 40 } }} renderInput={(params) => } + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> : null diff --git a/src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js b/src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js index 6721000..446f865 100644 --- a/src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js +++ b/src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js @@ -138,7 +138,7 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => - + - + }} /> )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} // InputLabelProps={{ // shrink: true // }} diff --git a/src/pages/User/ChangePasswordPage/index.js b/src/pages/User/ChangePasswordPage/index.js index 24f4484..4e8cea4 100644 --- a/src/pages/User/ChangePasswordPage/index.js +++ b/src/pages/User/ChangePasswordPage/index.js @@ -241,7 +241,9 @@ const Index = () => { endAdornment:( { endAdornment:( { + const intl = useIntl(); + const params = useParams(); const navigate = useNavigate(); const [formData, setFormData] = React.useState({}) @@ -148,7 +150,8 @@ const UserMaintainPage_Individual = () => { - diff --git a/src/pages/authentication/ForgotPassword/AuthCallback/index.js b/src/pages/authentication/ForgotPassword/AuthCallback/index.js index 1b378e0..43a7d26 100644 --- a/src/pages/authentication/ForgotPassword/AuthCallback/index.js +++ b/src/pages/authentication/ForgotPassword/AuthCallback/index.js @@ -281,7 +281,9 @@ const Index = () => { endAdornment:( { endAdornment:( { endAdornment:( { endAdornment:( { endAdornment={ { endAdornment={ { endAdornment={ { @@ -118,6 +120,39 @@ const BusCustomFormWizard = (props) => { changePassword(''); }, []); + const playCaptchaAudio = async () => { + try { + const resp = await axios.post( + `${POST_CAPTCHA_AUDIO}`, + { base64Url, lang: intl.locale }, + { responseType: "arraybuffer" } + ); + + const blob = new Blob([resp.data], { type: "audio/wav" }); + const url = URL.createObjectURL(blob); + + const audio = new Audio(url); + + await audio.play(); + + } catch (error) { + + let message = intl.formatMessage({ id: "captchaAudioError" }); + + if (error.response) { + if (error.response.status === 404) { + message = intl.formatMessage({ id: "captchaExpired" }); + } else if (error.response.status === 500) { + message = intl.formatMessage({ id: "captchaAudioServerError" }); + } + } else if (error.request) { + message = intl.formatMessage({ id: "connectionError" }); + } + + notifyActionError(message); + } + }; + const handleCheckUsername = async () => { if (values?.username) { const response = await axios.post(`${POST_USERNAME}`, { @@ -712,7 +747,9 @@ const BusCustomFormWizard = (props) => { endAdornment={ { endAdornment={ { }} renderInput={(params) => } + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> { '& .MuiOutlinedInput-root': { height: 40 } }} renderInput={(params) => } + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> {formik.touched.address1 && formik.errors.address1 && ( @@ -1514,6 +1561,11 @@ const BusCustomFormWizard = (props) => { {formik.errors.captchaField} )} + + + diff --git a/src/pages/authentication/auth-forms/CustomFormWizard.js b/src/pages/authentication/auth-forms/CustomFormWizard.js index 4c36244..f84767c 100644 --- a/src/pages/authentication/auth-forms/CustomFormWizard.js +++ b/src/pages/authentication/auth-forms/CustomFormWizard.js @@ -25,7 +25,7 @@ import * as yup from 'yup'; import { strengthColorChi, strengthIndicator } from 'utils/password-strength'; // import {apiPath} from "auth/utils"; import axios from "axios"; -import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER, POST_IDNO } from "utils/ApiPathConst"; +import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER, POST_IDNO, POST_CAPTCHA_AUDIO } from "utils/ApiPathConst"; // import * as HttpUtils from 'utils/HttpUtils'; import * as ComboData from "utils/ComboData"; @@ -49,7 +49,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { Link } from 'react-router-dom'; import { PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; import * as HttpUtils from "../../../utils/HttpUtils"; - +import { notifyActionError } from 'utils/CommonFunction'; // ============================|| FIREBASE - REGISTER ||============================ // const CustomFormWizard = (props) => { @@ -137,6 +137,39 @@ const CustomFormWizard = (props) => { onCaptchaChange(); } }, []); + + const playCaptchaAudio = async () => { + try { + const resp = await axios.post( + `${POST_CAPTCHA_AUDIO}`, + { base64Url, lang: intl.locale }, + { responseType: "arraybuffer" } + ); + + const blob = new Blob([resp.data], { type: "audio/wav" }); + const url = URL.createObjectURL(blob); + + const audio = new Audio(url); + + await audio.play(); + + } catch (error) { + + let message = intl.formatMessage({ id: "captchaAudioError" }); + + if (error.response) { + if (error.response.status === 404) { + message = intl.formatMessage({ id: "captchaExpired" }); + } else if (error.response.status === 500) { + message = intl.formatMessage({ id: "captchaAudioServerError" }); + } + } else if (error.request) { + message = intl.formatMessage({ id: "connectionError" }); + } + + notifyActionError(message); + } + }; useEffect(() => { if (selectedIdDocType.type === "HKID"){ @@ -945,7 +978,9 @@ const CustomFormWizard = (props) => { endAdornment={ { endAdornment={ { error={formik.touched.idDocType && (selectedIdDocType === null || selectedIdDocType?.type == null)} placeholder={intl.formatMessage({ id: 'idDocType' })} />} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> {formik.touched.idDocType && ( selectedIdDocType === null || selectedIdDocType?.type == null ? @@ -1412,6 +1453,10 @@ const CustomFormWizard = (props) => { }} renderInput={(params) => } + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> { '& .MuiOutlinedInput-root': { height: 40 } }} renderInput={(params) => } + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> {formik.touched.address1 && formik.errors.address1 && ( @@ -1854,6 +1903,11 @@ const CustomFormWizard = (props) => { {formik.errors.captchaField} )} + + + diff --git a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js index 7c4d734..105c644 100644 --- a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js +++ b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js @@ -21,7 +21,7 @@ import { useFormik, FormikProvider } from 'formik'; import * as yup from 'yup'; import axios from "axios"; -import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL } from "utils/ApiPathConst"; +import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL, POST_CAPTCHA_AUDIO} from "utils/ApiPathConst"; import * as ComboData from "utils/ComboData"; @@ -40,6 +40,7 @@ import LoopIcon from '@mui/icons-material/Loop'; import { useTheme } from '@mui/material/styles'; import { useLocation } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; +import { notifyActionError } from 'utils/CommonFunction'; // ============================|| FIREBASE - REGISTER ||============================ // @@ -86,6 +87,39 @@ const CustomFormWizard = (props) => { responseToData(); }, []); + const playCaptchaAudio = async () => { + try { + const resp = await axios.post( + `${POST_CAPTCHA_AUDIO}`, + { base64Url, lang: intl.locale }, + { responseType: "arraybuffer" } + ); + + const blob = new Blob([resp.data], { type: "audio/wav" }); + const url = URL.createObjectURL(blob); + + const audio = new Audio(url); + + await audio.play(); + + } catch (error) { + + let message = intl.formatMessage({ id: "captchaAudioError" }); + + if (error.response) { + if (error.response.status === 404) { + message = intl.formatMessage({ id: "captchaExpired" }); + } else if (error.response.status === 500) { + message = intl.formatMessage({ id: "captchaAudioServerError" }); + } + } else if (error.request) { + message = intl.formatMessage({ id: "connectionError" }); + } + + notifyActionError(message); + } + }; + const handleClickShowId = () => { setshowId(!showId); }; @@ -620,8 +654,12 @@ const CustomFormWizard = (props) => { '& .MuiAutocomplete-endAdornment': { top: '50%', transform: 'translateY(-50%)' }, '& .MuiOutlinedInput-root': { height: 40 } }} - renderInput={(params) => } + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> { setCheckCountry(true) } }} - sx={{ - '& .MuiInputBase-root': { alignItems: 'center' }, - '& .MuiAutocomplete-endAdornment': { top: '50%', transform: 'translateY(-50%)' }, - '& .MuiOutlinedInput-root': { height: 40 } - }} - renderInput={(params) => } + sx={{ + '& .MuiInputBase-root': { alignItems: 'center' }, + '& .MuiAutocomplete-endAdornment': { top: '50%', transform: 'translateY(-50%)' }, + '& .MuiOutlinedInput-root': { height: 40 } + }} + renderInput={(params) => } + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> {formik.touched.address1 && formik.errors.address1 && ( @@ -1014,6 +1056,11 @@ const CustomFormWizard = (props) => { {formik.errors.captchaField} )} + + + diff --git a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js index 0f554b3..a994845 100644 --- a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js +++ b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js @@ -1,8 +1,8 @@ -const AboutUs = () => { +const Page = () => { - const content =` - + .privacy-policy p { margin: 0 0 1rem; } + .privacy-policy ol { margin: 0.5rem 0 1.6rem; padding-left: 1.4rem; } + .privacy-policy ol li { margin: 0.6rem 0; } + .privacy-policy a { text-decoration: underline; } + .privacy-policy address { font-style: normal; line-height: 1.6; margin-bottom: 1rem; } + .privacy-policy .note { font-size: 0.95rem; font-style: italic; } + -
+
-

私隐政策

+

私隐政策

保障个人资料私隐是政府物流服务署每位成员所关注的事宜。我们尊重个人资料私隐,并承诺全面执行及遵从保障资料原则及《个人资料(私隐)条例》的所有相关条文。政府物流服务署建立及实施系统监控,以确保遵从以下六项保障资料原则: @@ -75,22 +47,22 @@ const AboutUs = () => {

  • 所收集的个人资料只可用作收集资料时资料会被使用于的目的或直接有关的目的,除非有关个人已就更改使用明示同意或法律准许有关使用;
  • 采取所有切实可行的步骤,以确保个人资料受保障而不受未获准许或意外的查阅、处理、删除或使用所影响;
  • 采取所有切实可行的步骤,以确保任何人能获政府物流服务署告知所持有的个人资料的种类及资料将会为什么目的而使用的;以及
  • -
  • 准许资料当事人查阅/改正其个人资料及以法律准许或规定的方式处理任何查阅/改正个人资料的要求。
  • +
  • 准许资料当事人查阅/改正其个人资料及以法律准许或规定的方式处理任何查阅/改正个人资料的要求。
  • -

    收集个人资料声明

    +

    收集个人资料声明

    -

    收集资料的目的

    +

    收集资料的目的

    申请人在本网站内所提供的个人资料会供申请刊登公共启事及为承担法律责任用途。在本网站提供的个人资料是自愿的。如果申请人未有提供足够资料,我们可能无法处理相关公共启事的刊登事宜。

    -

    披露个人资料

    +

    披露个人资料

    政府物流服务署可能会向其他政策局/部门披露你在本网站提供的个人资料。

    -

    查阅个人资料

    +

    查阅个人资料

    如要查阅或改正本署所持有的个人资料,请向以下人士提出:

    @@ -103,14 +75,18 @@ const AboutUs = () => {

    - (注:请以个人资料私隐专员指定的表格提出查阅资料申请。) + (注:请以个人资料私隐专员指定的 + + 表格 + + 提出查阅资料申请。)

    -
    - ` - ; +
    + `; - return (
    ); + return (
    ); } - export default AboutUs; \ No newline at end of file +export default Page; \ No newline at end of file diff --git a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js index 9e5b4f7..b6854b6 100644 --- a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js +++ b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js @@ -1,148 +1,137 @@ - const Page = () => { - - const content = ` - - -
    -

    Privacy Policy

    - -

    - The protection of personal data privacy is the concern of every member of Government Logistics - Department. We respect personal data privacy and are committed to fully implementing and complying - with the data protection principles and all relevant provisions of the Personal Data (Privacy) - Ordinance. We develop and implement programme controls that give effect to the six data protection - principles below: -

    - -
      -
    1. + const content = ` + + +
      +

      Privacy Policy

      + +

      + The protection of personal data privacy is the concern of every member of Government Logistics + Department. We respect personal data privacy and are committed to fully implementing and complying + with the data protection principles and all relevant provisions of the Personal Data (Privacy) + Ordinance. We develop and implement programme controls that give effect to the six data protection + principles below: +

      + +
        +
      1. collect adequate, but not excessive, personal data by lawful and fair means only for lawful purposes related to our functions or activities; -
      2. -
      3. +
      4. +
      5. take all reasonably practicable steps to ensure that the personal data collected or retained are accurate, having regard to the purposes for which they are to be used. Erase personal data no longer than necessary for the purposes for which they are to be used; -
      6. -
      7. +
      8. +
      9. use the personal data collected only for purposes or directly related purposes for which the data were to be used at the time of collection, unless the individual concerned has given express consent for a change of use or such use is permitted by law; -
      10. -
      11. +
      12. +
      13. take all reasonably practicable steps to ensure that personal data are protected against unauthorized or accidental access, processing, erasure or other use; -
      14. -
      15. +
      16. +
      17. take all reasonably practicable steps to ensure that a person can be informed of the kinds of personal data that GLD holds and the purposes for which the data are to be used; and -
      18. -
      19. +
      20. +
      21. permit persons to access and correct personal data of which they are the data subject and process any access/correction requests in a manner permitted or required by law. -
      22. -
      - -

      Personal Information Collection Statement

      - -

      Purpose of Collection

      -

      - The personal data provided by the applicant in this website would be used for application of public - notice publication and for assumption of liability. The provision of the personal data is voluntary. - If the applicant does not provide sufficient information, we may not be able to process publication - of the relevant public notice. -

      - -

      Disclosure of Personal Data

      -

      - The personal data provided in this website may be disclosed to other bureaux / departments. -

      - -

      Access to Personal Data

      -

      - Requests for access to or correction of personal data held by us should be addressed to - -

      - -
      - Data Protection Officer
      - Government Logistics Department
      - 10/F, North Point Government Offices
      - 333 Java Road North Point
      - Hong Kong -
      - -

      - (Note: Data access requests should be made on a - +

    2. +
    + +

    Personal Information Collection Statement

    + +

    Purpose of Collection

    +

    + The personal data provided by the applicant in this website would be used for application of public + notice publication and for assumption of liability. The provision of the personal data is voluntary. + If the applicant does not provide sufficient information, we may not be able to process publication + of the relevant public notice. +

    + +

    Disclosure of Personal Data

    +

    + The personal data provided in this website may be disclosed to other bureaux / departments. +

    + +

    Access to Personal Data

    +

    + Requests for access to or correction of personal data held by us should be addressed to - +

    + +
    + Data Protection Officer
    + Government Logistics Department
    + 10/F, North Point Government Offices
    + 333 Java Road North Point
    + Hong Kong +
    + +

    + (Note: Data access requests should be made on a + form - - specified by the Privacy Commissioner for Personal Data.) -

    -
    - ` - ; - - return (
    ); - -} + + specified by the Privacy Commissioner for Personal Data.) +

    +
    + `; + + return
    ; +}; export default Page; \ No newline at end of file diff --git a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js index 202a132..e77b6b0 100644 --- a/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js +++ b/src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js @@ -1,116 +1,109 @@ - -const AboutUs = () => { - - const content =` +const Page = () => { + const content = `
    -

    私隱政策

    - -

    - 保障個人資料私隱是政府物流服務署每位成員所關注的事宜。我們尊重個人資料私隱,並承諾全面執行及遵從保障資料原則及《個人資料(私隱)條例》的所有相關條文。政府物流服務署建立及實施系統監控,以確保遵從以下六項保障資料原則 - -

    - -
      -
    1. 以合法和公平的方法收集足夠但不超乎適度的個人資料,只供用於與政府物流服務署的職能或活動有關的合法目的;
    2. -
    3. 採取所有切實可行的步驟,以確保在顧及有關的個人資料會被使用於的目的下,所收集或保留的個人資料是準確的。刪除就使用目的而言不再屬有需要的個人資料;
    4. -
    5. 所收集的個人資料只可用作收集資料時資料會被使用於的目的或直接有關的目的,除非有關個人已就更改使用明示同意或法律准許有關使用;
    6. -
    7. 採取所有切實可行的步驟,以確保個人資料受保障而不受未獲准許或意外的查閱、處理、刪除或使用所影響;
    8. -
    9. 採取所有切實可行的步驟,以確保任何人能獲政府物流服務署告知所持有的個人資料的種類及資料將會為甚麼目的而使用的;以及
    10. -
    11. 准許資料當事人查閱/改正其個人資料及以法律准許或規定的方式處理任何查閱/改正個人資料的要求。
    12. -
    - -

    收集個人資料聲明

    - -

    收集資料的目的

    -

    - 申請人在本網站內所提供的個人資料會供申請刊登公共啟事及為承擔法律責任用途。在本網站提供的個人資料是自願的。如果申請人未有提供足夠資料,我們可能無法處理相關公共啟事的刊登事宜。 -

    - -

    披露個人資料

    -

    - 政府物流服務署可能會向其他政策局/部門披露你在本網站提供的個人資料。 -

    - -

    查閱個人資料

    -

    - 如要查閱或改正本署所持有的個人資料,請向以下人士提出: -

    - -
    - 香港北角渣華道333號
    - 北角政府合署10樓
    - 政府物流服務署
    - 保障資料主任 -
    - -

    - (註:請以個人資料私隱專員指定的表格提出查閱資料要求。) -

    +

    私隱政策

    + +

    + 保障個人資料私隱是政府物流服務署每位成員所關注的事宜。我們尊重個人資料私隱,並承諾全面執行及遵從保障資料原則及《個人資料(私隱)條例》的所有相關條文。政府物流服務署建立及實施系統監控,以確保遵從以下六項保障資料原則 - +

    + +
      +
    1. 以合法和公平的方法收集足夠但不超乎適度的個人資料,只供用於與政府物流服務署的職能或活動有關的合法目的;
    2. +
    3. 採取所有切實可行的步驟,以確保在顧及有關的個人資料會被使用於的目的下,所收集或保留的個人資料是準確的。刪除就使用目的而言不再屬有需要的個人資料;
    4. +
    5. 所收集的個人資料只可用作收集資料時資料會被使用於的目的或直接有關的目的,除非有關個人已就更改使用明示同意或法律准許有關使用;
    6. +
    7. 採取所有切實可行的步驟,以確保個人資料受保障而不受未獲准許或意外的查閱、處理、刪除或使用所影響;
    8. +
    9. 採取所有切實可行的步驟,以確保任何人能獲政府物流服務署告知所持有的個人資料的種類及資料將會為甚麼目的而使用的;以及
    10. +
    11. 准許資料當事人查閱/改正其個人資料及以法律准許或規定的方式處理任何查閱/改正個人資料的要求。
    12. +
    + +

    收集個人資料聲明

    + +

    收集資料的目的

    +

    + 申請人在本網站內所提供的個人資料會供申請刊登公共啟事及為承擔法律責任用途。在本網站提供的個人資料是自願的。如果申請人未有提供足夠資料,我們可能無法處理相關公共啟事的刊登事宜。 +

    + +

    披露個人資料

    +

    + 政府物流服務署可能會向其他政策局/部門披露你在本網站提供的個人資料。 +

    + +

    查閱個人資料

    +

    + 如要查閱或改正本署所持有的個人資料,請向以下人士提出: +

    + +
    + 香港北角渣華道333號
    + 北角政府合署10樓
    + 政府物流服務署
    + 保障資料主任 +
    + +

    + (註:請以個人資料私隱專員指定的 + + 表格 + + 提出查閱資料要求。) +

    - ` - ; - - return (
    ); +`; -} + return
    ; +}; - export default AboutUs; \ No newline at end of file +export default Page; \ No newline at end of file diff --git a/src/translations/en.json b/src/translations/en.json index 745e9c5..be58bcd 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -610,5 +610,21 @@ "Dashboard": "Dashboard", "event": "Event", "lgce_alt": "2025 Legislative Council General Election", - "lgce_title": "2025 Legislative Council General Election" + "lgce_title": "2025 Legislative Council General Election", + + "ariaShowPassword": "Show password", + "ariaHidePassword": "Hide password", + + "captchaAudioError": "Unable to play audio CAPTCHA.", + "captchaAudioServerError": "Audio CAPTCHA is temporarily unavailable. Please refresh the CAPTCHA.", + "captchaExpired": "CAPTCHA expired. Please refresh and try again.", + "connectionError": "Connection error. Please try again.", + + "muiClear": "Clear", + "muiClose": "Close", + "muiOpen": "Open", + "muiNoOptions": "No options", + + "paginationPrev": "Go to previous page", + "paginationNext": "Go to next page" } \ No newline at end of file diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 76513b0..7f19d12 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -606,5 +606,21 @@ "Dashboard": "仪表板", "event": "活动", "lgce_alt": "2025年立法会换届选举", - "lgce_title": "2025年立法会换届选举" + "lgce_title": "2025年立法会换届选举", + + "ariaShowPassword": "显示密码", + "ariaHidePassword": "隐藏密码", + + "captchaAudioError": "无法播放语音验证码。", + "captchaAudioServerError": "语音验证码暂时不可用,请刷新验证码。", + "captchaExpired": "验证码已过期,请刷新后再试。", + "connectionError": "连接错误,请稍后再试。", + + "muiClear": "清除", + "muiClose": "关闭", + "muiOpen": "打开", + "muiNoOptions": "没有选项", + + "paginationPrev": "上一页", + "paginationNext": "下一页" } \ No newline at end of file diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 341a52e..195eb60 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -607,5 +607,21 @@ "Dashboard": "儀表板", "event": "活動", "lgce_alt": "2025年立法會換屆選舉", - "lgce_title": "2025年立法會換屆選舉" + "lgce_title": "2025年立法會換屆選舉", + + "ariaShowPassword": "顯示密碼", + "ariaHidePassword": "隱藏密碼", + + "captchaAudioError": "無法播放語音驗證碼。", + "captchaAudioServerError": "語音驗證碼暫時無法使用,請重新整理驗證碼。", + "captchaExpired": "驗證碼已過期,請重新整理並再試。", + "connectionError": "連線錯誤,請稍後再試。", + + "muiClear": "清除", + "muiClose": "關閉", + "muiOpen": "開啟", + "muiNoOptions": "沒有選項", + + "paginationPrev": "上一頁", + "paginationNext": "下一頁" } \ No newline at end of file diff --git a/src/utils/Combo.js b/src/utils/Combo.js index 484d01a..6a04200 100644 --- a/src/utils/Combo.js +++ b/src/utils/Combo.js @@ -1,5 +1,6 @@ import { Autocomplete, TextField } from '@mui/material'; import { useEffect, useState } from 'react'; +import {useIntl } from "react-intl"; export default function Combo({ valueName, @@ -14,6 +15,7 @@ export default function Combo({ ...props }) { const formValue = form.values[valueName] ?? null; + const intl = useIntl(); const [inputValue, setInputValue] = useState(""); @@ -84,6 +86,10 @@ export default function Combo({ }} /> )} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> ); } diff --git a/src/utils/DateUtils.js b/src/utils/DateUtils.js index 5877593..c8af554 100644 --- a/src/utils/DateUtils.js +++ b/src/utils/DateUtils.js @@ -1,4 +1,6 @@ import dayjs from 'dayjs'; +import "dayjs/locale/zh-hk"; +import "dayjs/locale/zh-cn"; var days_ZH = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']; var days_CN = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']; @@ -156,3 +158,51 @@ export const getWeekdayStr_CN = (date) =>{ if(date) return days_CN[date.getDay()] return ""; }; + +const formatPickerAriaDate = (dateStr, locale) => { + if (!dateStr) return ""; + + // MUI may pass a full Date string; parse via native Date first + const dNative = new Date(dateStr); + const d = isNaN(dNative.getTime()) ? null : dNative; + + if (!d) return dateStr; // fallback if parsing fails + + // Format only the date part (ignore time/GMT) + const dj = dayjs(d); + + if (locale === "zh-HK" || locale === "zh") { + return dj.locale("zh-hk").format("YYYY年MM月DD日"); + } + if (locale === "zh-CN") { + return dj.locale("zh-cn").format("YYYY年MM月DD日"); + } + return dj.locale("en").format("DD MMM YYYY"); +}; + +export const getPickerLocaleText = (locale) => { + if (locale === "zh-HK" || locale === "zh") { + return { + openDatePickerDialogue: (dateStr) => { + const s = formatPickerAriaDate(dateStr, locale); + return s ? `選擇日期,已選日期為 ${s}` : "選擇日期"; + } + }; + } + + if (locale === "zh-CN") { + return { + openDatePickerDialogue: (dateStr) => { + const s = formatPickerAriaDate(dateStr, locale); + return s ? `选择日期,已选日期为 ${s}` : "选择日期"; + } + }; + } + + return { + openDatePickerDialogue: (dateStr) => { + const s = formatPickerAriaDate(dateStr, locale); + return s ? `Choose date, selected date is ${s}` : "Choose date"; + } + }; +}; diff --git a/src/utils/SelectBase.js b/src/utils/SelectBase.js index 806ff0d..ef47b2c 100644 --- a/src/utils/SelectBase.js +++ b/src/utils/SelectBase.js @@ -3,10 +3,12 @@ import { } from '@mui/material'; import * as React from 'react'; import Select, { SelectChangeEvent } from '@mui/material/Select'; +import {useIntl } from "react-intl"; export default function Combo ({valueName, disabled, form, dataList, filterOptions, getOptionLabel, isOptionEqualToValue, onInputChange, onChange, ...props}){ const [value, setValue] = React.useState(form.values[valueName]); const [inputValue, setInputValue] = React.useState(""); + const intl = useIntl(); return ( } + }}/>} + clearText={intl.formatMessage({ id: "muiClear" })} + closeText={intl.formatMessage({ id: "muiClose" })} + openText={intl.formatMessage({ id: "muiOpen" })} + noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} /> ); } \ No newline at end of file From e7d1c3402e0d77471c91061e4171d0884f60cb6f Mon Sep 17 00:00:00 2001 From: Alex Cheung Date: Thu, 26 Feb 2026 04:23:40 +0800 Subject: [PATCH 10/63] update WCAG to 4.1.2 --- src/assets/style/navbarStyles.css | 425 +++-- src/auth/utils.js | 3 + src/components/AdminLogo/index.js | 15 +- src/components/FiDataGrid.js | 44 +- src/components/Logo/index.js | 4 + src/components/MobileLogo/index.js | 4 + src/components/usePageTitle.js | 23 + src/layout/MainLayout/Header/index.js | 1563 +++++++++-------- src/pages/Announcement/Search_Public/index.js | 3 +- src/pages/DemandNote/Search_Public/index.js | 3 +- src/pages/Message/Details/index.js | 4 + src/pages/Message/Search/index.js | 3 +- src/pages/Payment/Details_Public/index.js | 3 + src/pages/Payment/Search_Public/index.js | 2 + src/pages/Proof/Reply_Public/index.js | 3 + src/pages/Proof/Search_Public/index.js | 4 +- .../ApplyForm/PublicNoticeApplyForm.js | 9 +- src/pages/PublicNotice/ApplyForm/index.js | 4 +- .../PublicNotice/Details_Public/index.js | 4 +- src/pages/PublicNotice/ListPanel/index.js | 5 +- src/pages/User/ChangePasswordPage/index.js | 3 + .../User/DetailsPage_Individual/index.js | 2 + .../User/DetailsPage_Organization/index.js | 6 +- .../ForgotPassword/AuthCallback/index.js | 1 + .../ForgotPassword/ForgotPasswordApplyForm.js | 2 + .../ForgotUsername/AuthCallback/index.js | 1 + .../ForgotUsername/ForgotUsernameApplyForm.js | 2 + .../auth-forms/AuthLoginCustom.js | 4 + .../auth-forms/BusCustomFormWizard.js | 58 +- .../auth-forms/CustomFormWizard.js | 57 +- .../auth-forms/IAmSmartFormWizard.js | 6 + src/pages/dashboard/Public/index.js | 2 + src/pages/extra-pages/UserMenuPub1/index.js | 441 +++-- src/translations/en.json | 4 +- src/translations/zh-CN.json | 4 +- src/translations/zh-HK.json | 4 +- src/utils/CommonFunction.js | 2 +- 37 files changed, 1627 insertions(+), 1100 deletions(-) create mode 100644 src/components/usePageTitle.js diff --git a/src/assets/style/navbarStyles.css b/src/assets/style/navbarStyles.css index 9d5d352..d282fa9 100644 --- a/src/assets/style/navbarStyles.css +++ b/src/assets/style/navbarStyles.css @@ -1,175 +1,358 @@ +/* assets/style/navbarStyles.css */ + +/* ===== FIX typo ===== */ #nav{ - display: flex; - align-items: center; - justify-content: space-between; - background-color: white; - /* padding: 20px 80px; */ - box-shadow: 0 5px 15px rgba(0,0 0,0,0.06); - position:fixed; - top: 0px; - width: 100%; - z-index: 9999; - border-bottom: 3px solid #0C489E; + display: flex; + align-items: center; + justify-content: space-between; + background-color: white; + box-shadow: 0 5px 15px rgba(0,0,0,0.06); /* <-- fixed */ + position: fixed; + top: 0px; + width: 100%; + z-index: 9999; + border-bottom: 3px solid #0C489E; + min-height: 77px; +} + +#navbar > div > li { + height: 77px; /* one single source of truth */ + display: flex; + align-items: stretch; + padding: 0; /* IMPORTANT: stop li padding making different widths */ + margin: 0; +} + +#navbar div li{ + padding: 0; /* override your earlier padding: 0 1vw */ +} + +/* Make dropdown button identical to anchor */ +#navbar > div > li > button.navTrigger { + background: transparent; + border: 0; + padding: 0; /* remove default button padding */ + margin: 0; + font: inherit; /* inherit font from li */ + line-height: 1; /* normalize */ + height: auto; + display: inline-flex; + align-items: center; + cursor: pointer; +} + +/* Make both anchor and button same vertical box */ +#navbar > div > li > a, +#navbar > div > li > button.navTrigger { + height: 100% !important; /* override the old 72px */ + padding: 0 24px !important; /* bigger hit area */ + display: flex; + align-items: center; + box-sizing: border-box; } + +/* Make ONLY the dropdown button not bold */ +#navbar > div > li > button.navTrigger, +#navbar > div > li > button.navTrigger .MuiTypography-root { + font-weight: 400 !important; +} + +/* (optional) keep links bold */ +#navbar > div > li > a, +#navbar > div > li > a .MuiTypography-root { + font-weight: 600; +} + #navbar div{ - display: flex; - align-items: center; - justify-content: center; - + display: flex; + align-items: center; + justify-content: center; } + #navbar div li{ - list-style: none; - padding: 0 1vw; - position: relative; + list-style: none; + padding: 0 1vw; + position: relative; } + +/* keep your styling */ #navbar div li a{ - text-decoration: none; - font-size: 1.2rem; - font-weight: 600; - /* font-family: 微軟正黑體; */ - color: black; - transition: 0.3s ease-in-out; + text-decoration: none; + font-size: 1.2rem; + font-weight: 600; + color: black; + transition: 0.3s ease-in-out; } -#navbar div li a span{ - font-size: 1vw !important; +/* NEW: style dropdown trigger buttons to look like links */ +#navbar div li button.navTrigger{ + background: transparent; + border: 0; + padding: 0; + cursor: pointer; + + text-decoration: none; + font-size: 1.2rem; + font-weight: 600; + color: black; + transition: 0.3s ease-in-out; + + display: inline-flex; + align-items: center; + gap: 0.25rem; } +#navbar div li a span, #navbar div li a svg{ - font-size: 1vw !important; + font-size: 1vw !important; } -#navbar div li a:hover{ - color: #0C489E; +#navbar div li a:hover, +#navbar div li button.navTrigger:hover{ + color: #0C489E; } + #navbar div li a:hover::after, -#navbar div li a:focus-visible::after{ - content: ""; - width: 80%; - height: 3px; - background:#0C489E; - position: absolute; - bottom: -5px; - left: 20px; -} -#navbar div li ul { +#navbar div li a:focus-visible::after, +#navbar div li button.navTrigger:hover::after, +#navbar div li button.navTrigger:focus-visible::after{ + content: ""; + width: 80%; + height: 3px; + background:#0C489E; + position: absolute; + bottom: -5px; + left: 20px; +} + +/* submenu base */ +#navbar div li ul{ background: white; visibility: hidden; opacity: 0; min-width: 18rem; position: absolute; - /* transition: all 0.5s ease; */ left: 0; display: none; padding-left: 0px; padding-bottom: 7px; - /* border: 1px solid #0C489E; */ background-clip: padding-box; border: 1px solid rgba(0,0,0,.15); border-radius: 0.25rem; } -#navbar div li:hover > ul{ - visibility: visible; - opacity: 1; - display: block - } -/* Navbar: don't show focus ring on mouse click */ -#navbar a:focus { +/* hover open */ +#navbar div li:hover > ul{ + visibility: visible; + opacity: 1; + display: block; +} + +/* keyboard open */ +#navbar div li:focus-within > ul{ + visibility: visible; + opacity: 1; + display: block; +} + +/* Navbar focus ring */ +#navbar a:focus, +#navbar button.navTrigger:focus{ outline: none; } -/* Navbar: show focus ring for keyboard navigation */ -#navbar a:focus-visible { +#navbar a:focus-visible, +#navbar button.navTrigger:focus-visible{ outline: 3px solid #0C489E; outline-offset: 2px; - border-radius: 10px; /* tweak to match your design */ + border-radius: 10px; } -/* #navbar div li:focus-within > ul, -#navbar div li ul:hover, -#navbar div li ul:focus { - visibility: visible; - opacity: 1; - display: block -} */ +#navbar a.dashboard, +#navbar a.application, +#navbar a.proof, +#navbar a.myDocumet, +#navbar a.documentRecord, +#navbar a.manageOrgUser, +#navbar a.manageUser, +#navbar a.systemSetting, +#navbar a.report, +#navbar a.payment, +#navbar a.user, +#navbar a.logout { + font-size: 1.1rem !important; + font-weight: 600 !important; +} + +#navbar a.dashboard .MuiTypography-root, +#navbar a.application .MuiTypography-root, +#navbar a.proof .MuiTypography-root, +#navbar a.myDocumet .MuiTypography-root, +#navbar a.documentRecord .MuiTypography-root, +#navbar a.manageOrgUser .MuiTypography-root, +#navbar a.manageUser .MuiTypography-root, +#navbar a.systemSetting .MuiTypography-root, +#navbar a.report .MuiTypography-root, +#navbar a.payment .MuiTypography-root, +#navbar a.user .MuiTypography-root, +#navbar a.logout .MuiTypography-root { + font-size: 1.1rem !important; + font-weight: 600 !important; +} + +/* Make them bigger + bold */ +#navbar a.login, +#navbar a.register, +#navbar a.login .MuiTypography-root, +#navbar a.register .MuiTypography-root { + font-size: 1.1rem !important; + font-weight: 600 !important; +} + +/* ===== your existing sidebar (unchanged) ===== */ #systemTitle{ - text-decoration: none; - font-size: 1.3rem; - font-weight: 600; - color: #0C489E; - transition: 0.3s ease-in-out; - /* font-family: 微軟正黑體; */ - text-align: center; + text-decoration: none; + font-size: 1.1rem; + font-weight: 600; + color: #0C489E; + transition: 0.3s ease-in-out; + text-align: center; } #mobileTitle{ - text-decoration: none; - font-size: 1.2rem; - font-weight: 600; - color: #0C489E; - transition: 0.3s ease-in-out; - /* font-family: 微軟正黑體; */ - text-align: center; + text-decoration: none; + font-size: 1.1rem; + font-weight: 600; + color: #0C489E; + transition: 0.3s ease-in-out; + text-align: center; } #sidebar{ - font-size: 1.3rem; - font-weight: 600; - /* font-family: 微軟正黑體; */ + font-size: 1.1rem; + font-weight: 600; } #sidebartop{ - align-items: center; - justify-content: start; - padding: 0; - display: flex; - width: 100%; + align-items: center; + justify-content: start; + padding: 0; + display: flex; + width: 100%; } #logoutContent{ - align-items: center; - justify-content: center; - padding: 0; - display: flex; - width: 100%; + align-items: center; + justify-content: center; + padding: 0; + display: flex; + width: 100%; } #sidebarbottom{ - align-items: center; - justify-content: center; - padding: 0; - display: flex; + align-items: center; + justify-content: center; + padding: 0; + display: flex; } #sidebar li{ - list-style: none; - padding: 0 20px; - position: relative; - text-align: left; + list-style: none; + padding: 0 20px; + position: relative; + text-align: left; } #sidebar li a{ - text-decoration: none; - font-size: 1.3rem; - font-weight: 600; - /* font-family: 微軟正黑體; */ - color: black; - transition: 0.3s ease-in-out; + text-decoration: none; + font-size: 1.1rem; + font-weight: 600; + color: black; + transition: 0.3s ease-in-out; } #sidebar li a:hover{ - color: #0C489E; -} -#sidebar div li ul { - background: white; - visibility: hidden; - opacity: 0; - min-width: 16rem; - position: relative; - /* transition: all 0.5s ease; */ - left: 0; - display: none; - padding-left: 0px; - /* border: 1px solid #0C489E; */ - } - #sidebar div li:hover > ul, - #sidebar div li:focus-within > ul, - #sidebar div li ul:hover, - #sidebar div li ul:focus { - visibility: visible; - opacity: 1; - display: block - } \ No newline at end of file + color: #0C489E; +} +#sidebar div li ul{ + background: white; + visibility: hidden; + opacity: 0; + min-width: 16rem; + position: relative; + left: 0; + display: none; + padding-left: 0px; +} +#sidebar div li:hover > ul, +#sidebar div li:focus-within > ul, +#sidebar div li ul:hover, +#sidebar div li ul:focus{ + visibility: visible; + opacity: 1; + display: block; +} + +/* ===== Mobile Drawer / Sidebar menu styling ===== */ +#sidebar ul { + width: 100%; + padding: 0; + margin: 0; +} + +#sidebar li{ + width: 100%; + padding: 0; /* remove the old 0 20px */ + margin: 0; +} + +/* Make BOTH links and dropdown buttons look the same */ +#sidebar li > a, +#sidebar li > button.navTrigger{ + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; /* text left, arrow right */ + gap: 12px; + + background: transparent; + border: 0; /* kill grey border */ + box-shadow: none; + outline: none; + + padding: 14px 20px; + text-align: left; + + font-size: 1.3rem; + font-weight: 600; + color: black; + cursor: pointer; +} + +/* Hover / focus */ +#sidebar li > a:hover, +#sidebar li > button.navTrigger:hover{ + color: #0C489E; +} + +#sidebar li > a:focus-visible, +#sidebar li > button.navTrigger:focus-visible{ + outline: 3px solid #0C489E; + outline-offset: 2px; + border-radius: 10px; +} + +/* Submenu (indent + full width) */ +#sidebar li ul{ + width: 100%; + padding: 6px 0 6px 0; + margin: 0; + display: none; + visibility: hidden; + opacity: 0; +} + +/* Open on focus-within (tap/click focuses button) */ +#sidebar li:focus-within > ul{ + display: block; + visibility: visible; + opacity: 1; +} + +#sidebar li ul li > a{ + padding: 12px 20px 12px 40px; /* indent submenu */ + font-size: 1.15rem; + font-weight: 600; +} \ No newline at end of file diff --git a/src/auth/utils.js b/src/auth/utils.js index 23b94a4..b6d0357 100644 --- a/src/auth/utils.js +++ b/src/auth/utils.js @@ -171,3 +171,6 @@ export const getPaymentMethodByCode = (code) => { return "other"; }; +export function setPageTitle(title) { + document.title = `${title} - PNSPS | Government Logistics Department`; +} \ No newline at end of file diff --git a/src/components/AdminLogo/index.js b/src/components/AdminLogo/index.js index f847e3b..4fa7a84 100644 --- a/src/components/AdminLogo/index.js +++ b/src/components/AdminLogo/index.js @@ -15,21 +15,23 @@ import { checkSysEnv } from "utils/Utils"; +import {useIntl} from "react-intl"; // ==============================|| MAIN LOGO ||============================== // const LogoSection = ({ sx, to }) => { const { defaultId } = useSelector((state) => state.menu); const dispatch = useDispatch(); + const intl = useIntl(); + return ( - - dispatch(activeItem({ openItem: [defaultId] }))} to={!to ? config.defaultPath : to} + aria-label={intl.formatMessage({ id: "PNSPS", defaultMessage: "PNSPS" })} sx={{ ...sx, - /* ✅ WCAG 2.4.7 focus indicator */ '&:focus-visible': { outline: '3px solid #0C489E', @@ -38,10 +40,11 @@ const LogoSection = ({ sx, to }) => { } }} > - + + + PNSPS + - PNSPS - ); }; diff --git a/src/components/FiDataGrid.js b/src/components/FiDataGrid.js index 6b28f0f..89c3e25 100644 --- a/src/components/FiDataGrid.js +++ b/src/components/FiDataGrid.js @@ -1,5 +1,5 @@ // material-ui -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Box } from "@mui/material"; import { DataGrid, GridOverlay, @@ -202,9 +202,49 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, }); } + const gridRootRef = useRef(null); + + useEffect(() => { + const root = gridRootRef.current; + if (!root) return; + + const sortText = intl.formatMessage({ id: "sort", defaultMessage: "Sort" }); + + const apply = () => { + // 1) Make ALL column headers tabbable (optional; DataGrid already manages focus well) + root + .querySelectorAll('.MuiDataGrid-columnHeaders [role="columnheader"]') + .forEach((el) => { + if (el.getAttribute("tabindex") !== "0") el.setAttribute("tabindex", "0"); + }); + + // 2) Localize sort icon button label (handles "sort"/"Sort"/any old value) + const sortButtons = root.querySelectorAll( + '.MuiDataGrid-columnHeaders button.MuiIconButton-root' + ); + + sortButtons.forEach((btn) => { + const al = (btn.getAttribute("aria-label") || "").trim().toLowerCase(); + const ti = (btn.getAttribute("title") || "").trim().toLowerCase(); + + // Only rewrite the ones that are the sort icon buttons + if (al === "sort" || ti === "sort") { + btn.setAttribute("aria-label", sortText); + btn.setAttribute("title", sortText); + } + }); + }; + + apply(); + + const obs = new MutationObserver(apply); + obs.observe(root, { childList: true, subtree: true }); + + return () => obs.disconnect(); + }, [intl]); return ( - + { const { defaultId } = useSelector((state) => state.menu); const dispatch = useDispatch(); + const intl = useIntl(); + return ( dispatch(activeItem({ openItem: [defaultId] }))} to={!to ? config.defaultPath : to} + aria-label={intl.formatMessage({ id: "PNSPS", defaultMessage: "PNSPS" })} sx={sx} > diff --git a/src/components/MobileLogo/index.js b/src/components/MobileLogo/index.js index d5386ff..da2ff62 100644 --- a/src/components/MobileLogo/index.js +++ b/src/components/MobileLogo/index.js @@ -9,10 +9,13 @@ import { useDispatch, useSelector } from 'react-redux'; import Logo from './MobileLogo'; import config from 'config'; import { activeItem } from 'store/reducers/menu'; +import {useIntl} from "react-intl"; // ==============================|| MAIN LOGO ||============================== // const LogoSection = ({ sx, to }) => { + const intl = useIntl(); + const { defaultId } = useSelector((state) => state.menu); const dispatch = useDispatch(); return ( @@ -21,6 +24,7 @@ const LogoSection = ({ sx, to }) => { component={Link} onClick={() => dispatch(activeItem({ openItem: [defaultId] }))} to={!to ? config.defaultPath : to} + aria-label={intl.formatMessage({ id: "PNSPS" })} sx={{ ...sx, diff --git a/src/components/usePageTitle.js b/src/components/usePageTitle.js new file mode 100644 index 0000000..8656c27 --- /dev/null +++ b/src/components/usePageTitle.js @@ -0,0 +1,23 @@ +import { useEffect } from "react"; +import { useIntl } from "react-intl"; + +export default function usePageTitle(messageIdOrText) { + const intl = useIntl(); + + useEffect(() => { + let pageTitle; + let systemName; + let gldName; + + // If string looks like an intl id, try translate + try { + pageTitle = intl.formatMessage({ id: messageIdOrText }); + systemName = intl.formatMessage({ id: "PNSPS_fullname" }); + gldName = intl.formatMessage({ id: "HKGLD" }); + } catch { + pageTitle = messageIdOrText; + } + + document.title = `${pageTitle} - ${systemName} | ${gldName}`; + }, [messageIdOrText, intl]); +} \ No newline at end of file diff --git a/src/layout/MainLayout/Header/index.js b/src/layout/MainLayout/Header/index.js index f1f8fcd..5840641 100644 --- a/src/layout/MainLayout/Header/index.js +++ b/src/layout/MainLayout/Header/index.js @@ -1,49 +1,30 @@ -import PropTypes from 'prop-types'; -import React -, { useState, useContext } - from 'react'; +import PropTypes from "prop-types"; +import React, { useState, useContext, useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { SysContext } from "components/SysSettingProvider" - -import { checkIsOnlyOnlinePayment } from '../../../utils/Utils'; +import { useNavigate, Link } from "react-router-dom"; +import { SysContext } from "components/SysSettingProvider"; +import { checkIsOnlyOnlinePayment } from "../../../utils/Utils"; // material-ui -// import { useTheme } from '@mui/material/styles'; import { AppBar, - // Container, Typography, Box, Stack, - // IconButton, - // Menu, - // MenuItem, - // Button, - // Tooltip, - // Avatar, - // Stack, Toolbar, Divider, - // List, - // ListItem, - // ListItemButton, - // ListItemText, IconButton, - Drawer, Grid, - // useMediaQuery -} from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; + Drawer, + Grid, +} from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; // project import -// import AppBarStyled from './AppBarStyled'; -// import HeaderContent from './HeaderContent'; -import Logo from 'components/Logo'; -import AdminLogo from 'components/AdminLogo'; -import MobileLogo from 'components/MobileLogo'; -//import Profile from './HeaderContent/Profile'; +import Logo from "components/Logo"; +import AdminLogo from "components/AdminLogo"; +import MobileLogo from "components/MobileLogo"; import "assets/style/navbarStyles.css"; import { isUserLoggedIn, @@ -55,574 +36,703 @@ import { haveOrgPaymentRecord, haveOrgDnRecord, isORGLoggedIn, - checkSysEnv - // getUserId + checkSysEnv, } from "utils/Utils"; -import { handleLogoutFunction } from 'auth/index'; +import { handleLogoutFunction } from "auth/index"; import { isGranted, isGrantedAny } from "auth/utils"; -// assets -// import { MenuFoldOutlined,MenuOutlined } from '@ant-design/icons'; -// import { AppBar } from '../../../../node_modules/@mui/material/index'; -import { Link } from "react-router-dom"; import LocaleSelector from "./HeaderContent/LocaleSelector"; import { FormattedMessage, useIntl } from "react-intl"; const drawerWidth = 300; - const isAfterAboutSwitchDate = checkIsOnlyOnlinePayment; - const getAboutExternalUrlByLocale = () => { const locale = (localStorage.getItem("locale") || "").toLowerCase(); - - if (locale === "zh-hk") { - // TChinese - return "https://www.gld.gov.hk/zh-hk/our-services/printing/advertising-gov-gazette/"; - } - if (locale === "zh-cn") { - // Simplified Chinese (你未貼,我用 zh-cn 版) - return "https://www.gld.gov.hk/zh-cn/our-services/printing/advertising-gov-gazette/"; - } - // English (en-us) / fallback - return "https://www.gld.gov.hk/en/our-services/printing/advertising-gov-gazette/"; + if (locale === "zh-hk") return "https://www.gld.gov.hk/zh-hk/our-services/printing/advertising-gov-gazette/"; + if (locale === "zh-cn") return "https://www.gld.gov.hk/zh-cn/our-services/printing/advertising-gov-gazette/"; + return "https://www.gld.gov.hk/en/our-services/printing/advertising-gov-gazette/"; }; - -// const navItems = ['Home', 'About', 'Contact']; -// ==============================|| MAIN LAYOUT - HEADER ||============================== // - +/** + * Accessible dropdown trigger: + * - button is always focusable + * - opens on focus (keyboard tab) via CSS :focus-within + optional state + * - supports Enter/Space/ArrowDown to jump to first submenu item + * - supports Escape to close + return focus + */ function Header(props) { const { sysSetting } = useContext(SysContext); const { window } = props; - const [mobileOpen, setMobileOpen] = useState(false); - const dispatch = useDispatch() - const navigate = useNavigate() + const [mobileOpen, setMobileOpen] = useState(false); + const dispatch = useDispatch(); + const navigate = useNavigate(); const intl = useIntl(); - const handleDrawerToggle = () => { - setMobileOpen((prevState) => !prevState); - }; + // which dropdown is open (optional but useful for aria-expanded + closing) + const [openMenu, setOpenMenu] = useState({ + payment: false, + client: false, + report: false, + settings: false, + paymentHistory: false, + userSetting: false, + }); + + const rootNavRef = useRef(null); + + const handleDrawerToggle = () => setMobileOpen((prev) => !prev); const handleLogout = async () => { await dispatch(handleLogoutFunction()); - //await handleLogoutFunction(); + await navigate("/login"); + }; - await navigate('/login'); + const openKey = (key) => setOpenMenu((p) => ({ ...p, [key]: true })); + const closeKey = (key) => setOpenMenu((p) => ({ ...p, [key]: false })); + + const closeAll = () => + setOpenMenu({ + payment: false, + client: false, + report: false, + settings: false, + paymentHistory: false, + userSetting: false, + }); + + // close dropdowns on outside click / focus + useEffect(() => { + const onDocMouseDown = (e) => { + if (!rootNavRef.current) return; + if (!rootNavRef.current.contains(e.target)) closeAll(); + }; + document.addEventListener("mousedown", onDocMouseDown, true); + return () => document.removeEventListener("mousedown", onDocMouseDown, true); + }, []); + + const onMenuKeyDown = (key) => (e) => { + if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { + e.preventDefault(); + openKey(key); + requestAnimationFrame(() => { + const first = document.querySelector( + `[data-submenu="${key}"] a, [data-submenu="${key}"] button` + ); + first?.focus?.(); + }); + } else if (e.key === "Escape") { + e.preventDefault(); + closeKey(key); + e.currentTarget?.focus?.(); + } else if (e.key === "ArrowUp") { + // optional: close on ArrowUp when on trigger + closeKey(key); + } }; - const loginContent = ( - isGLDLoggedIn() ? -
    - {isPasswordExpiry() ? -
    + const onTriggerBlur = (key) => (e) => { + // close only when focus leaves the whole LI (trigger + submenu) + const li = e.currentTarget.closest("li"); + if (li && li.contains(e.relatedTarget)) return; + closeKey(key); + }; + + // ============================= + // Desktop top nav content (login) + // ============================= + const loginContent = isGLDLoggedIn() ? ( +
    + {isPasswordExpiry() ? ( +
    +
  • + + + + + +
  • +
    + ) : ( +
    +
  • + + + Dashboard + + +
  • + + {isGrantedAny(["VIEW_APPLICATION", "MAINTAIN_APPLICATION"]) ? (
  • - - - + + + Application
  • -
    - : -
    + ) : null} + + {isGrantedAny(["VIEW_PROOF", "MAINTAIN_PROOF"]) ? (
  • - - - Dashboard + + + Proof
  • - { - isGrantedAny(["VIEW_APPLICATION", "MAINTAIN_APPLICATION"]) ? -
  • - Application -
  • - : <> - } - { - isGrantedAny(["VIEW_PROOF", "MAINTAIN_PROOF"]) ? -
  • - Proof -
  • - : <> - } + ) : null} + + {/* ===== Payment dropdown (admin) ===== */} + {isGrantedAny([ + "MAINTAIN_PROOF", + "MAINTAIN_PAYMENT", + "MAINTAIN_RECON", + "VIEW_DEMANDNOTE", + "MAINTAIN_DEMANDNOTE", + ]) ? ( +
  • + - { - isGrantedAny(["MAINTAIN_PROOF", "MAINTAIN_PAYMENT", "MAINTAIN_RECON", "VIEW_DEMANDNOTE", "MAINTAIN_DEMANDNOTE"]) ? -
  • - Payment -
      - { - isGranted("MAINTAIN_PROOF") ? -
    • - Export for GDN -
    • - : - <> - } - - { - isGranted("MAINTAIN_PAYMENT") ? -
    • - Mark Payment -
    • - : - <> - } - - { - isGranted("MAINTAIN_PAYMENT") ? -
    • - Online Payment Record -
    • - : - <> - } - - { - isGranted("MAINTAIN_RECON") ? - <> -
    • - GFMIS Generate XML -
    • - - - : - <> - } - - { - isGranted("MAINTAIN_DEMANDNOTE") ? -
    • - Create Demand Note -
    • - : - <> - } - { - isGrantedAny(["VIEW_DEMANDNOTE", "MAINTAIN_DEMANDNOTE"]) ? -
    • - Demand Note -
    • - : - <> - } - { - isGranted("MAINTAIN_RECON") ? - <> -
    • - Recon Report -
    • - - - : - <> - } -
    -
  • - : - <> - } +
      + {isGranted("MAINTAIN_PROOF") ? ( +
    • + + + Export for GDN + + +
    • + ) : null} - { - isGrantedAny(["VIEW_USER", "MAINTAIN_USER", "VIEW_ORG", "MAINTAIN_ORG", "VIEW_GROUP", "MAINTAIN_GROUP", "VIEW_GLD_USER", "VIEW_IND_USER", "VIEW_ORG_USER", "MAINTAIN_GLD_USER", "MAINTAIN_IND_USER", "MAINTAIN_ORG_USER"]) ? -
    • - Client -
        - { - isGrantedAny(["VIEW_USER","MAINTAIN_USER"]) ? - <> -
      • - Users (GLD) -
      • -
      • - Users (Individual) -
      • -
      • - Users (Organisation) -
      • - - : - <> - { - isGrantedAny(["VIEW_GLD_USER" ,"MAINTAIN_GLD_USER"]) ? -
      • - Users (GLD) -
      • : <> - } - { - isGrantedAny(["VIEW_IND_USER", "MAINTAIN_IND_USER"]) ? -
      • - Users (Individual) -
      • : <> - } - { - isGrantedAny(["VIEW_ORG_USER", "MAINTAIN_ORG_USER"]) ? -
      • - Users (Organisation) -
      • : <> - } - - } - { - isGrantedAny(["VIEW_ORG", "MAINTAIN_ORG"]) ? -
      • - Organisation -
      • - : - <> - } - { - isGrantedAny(["VIEW_GROUP", "MAINTAIN_GROUP"]) ? -
      • - User Group -
      • - : - <> - } - -
      -
    • - : - <> - } -
    • - Report -
        -
      • - - - Summary of Gazette Notice - - -
      • -
      • - - - Gazette Notice Full List - - -
      • -
      -
    • -
    • - Settings -
        -
      • - My Profile -
      • -
      • - - - - - -
      • - { - isGranted("VIEW_GAZETTE_ISSUE", "MAINTAIN_GAZETTE_ISSUE") ? - <> -
      • - Holiday Settings -
      • -
      • - Gazette Issues -
      • - - : - <> - } + {isGranted("MAINTAIN_PAYMENT") ? ( +
      • + + + Mark Payment + + +
      • + ) : null} - { - isGranted("MAINTAIN_ANNOUNCEMENT") ? -
      • - Announcement -
      • - : - <> - } + {isGranted("MAINTAIN_PAYMENT") ? ( +
      • + + + Online Payment Record + + +
      • + ) : null} - {isGranted("MAINTAIN_EMAIL") ? + {isGranted("MAINTAIN_RECON") ? (
      • - Email Template + + + GFMIS Generate XML + +
      • - : - <> - } + ) : null} - { - isGranted("MAINTAIN_DR") ? -
      • - DR Import -
      • - : - <> - } + {isGranted("MAINTAIN_DEMANDNOTE") ? ( +
      • + + + Create Demand Note + + +
      • + ) : null} - { - isGranted("MAINTAIN_SETTING") ? -
      • - System Settings -
      • - : - <> - } - { - isGranted("MAINTAIN_SETTING") ? -
      • - Audit Log -
      • - : - <> - } -
      -
    • - -
    • - Logout -
    • -
      -
    - } -
    - : -
    - {isPasswordExpiry() ? -
    -
  • - - - - - -
  • -
    - : -
    -
  • - - - -
  • -
  • - - - -
  • -
  • - - - -
  • -
  • - {isCreditorLoggedIn() ? - haveOrgPaymentRecord() ? - <> - + {isGrantedAny(["VIEW_DEMANDNOTE", "MAINTAIN_DEMANDNOTE"]) ? ( +
  • + - + Demand Note - -
      -
    • - - - -
    • -
    • - - - -
    • -
    - - : - - - - : - isORGLoggedIn() ? - haveOrgPaymentRecord() ? - <> - - - - - - -
      -
    • - - - -
    • - {haveOrgDnRecord()? -
    • - - - -
    • :null - } -
    - - : - - - - : - <> - +
  • + ) : null} + + {isGranted("MAINTAIN_RECON") ? ( +
  • + - + Recon Report - -
      -
    • - - - -
    • - {haveOrgDnRecord()? -
    • - - - -
    • :null - } -
    - - } +
  • + ) : null} + + ) : null} + + {/* ===== Client dropdown (admin) ===== */} + {isGrantedAny([ + "VIEW_USER", + "MAINTAIN_USER", + "VIEW_ORG", + "MAINTAIN_ORG", + "VIEW_GROUP", + "MAINTAIN_GROUP", + "VIEW_GLD_USER", + "VIEW_IND_USER", + "VIEW_ORG_USER", + "MAINTAIN_GLD_USER", + "MAINTAIN_IND_USER", + "MAINTAIN_ORG_USER", + ]) ? (
  • + - {isPrimaryLoggedIn() ? - <> - - console.log(event)}> - - - - -
      -
    • - - - - - -
    • +
        + {isGrantedAny(["VIEW_USER", "MAINTAIN_USER"]) ? ( + <>
      • - - - {/* */} - + + + Users (GLD)
      • - - - {/* */} - + + + Users (Individual)
      • - - - + + + Users (Organisation)
      • -
      - - : - isINDLoggedIn() ? + + ) : ( <> - - console.log(event)}> - - - - -
        -
      • - - - {/* */} - - - -
      • + {isGrantedAny(["VIEW_GLD_USER", "MAINTAIN_GLD_USER"]) ? (
      • - - - + + + Users (GLD)
      • -
      - - : - <> - - console.log(event)}> - - - - -
        + ) : null} + {isGrantedAny(["VIEW_IND_USER", "MAINTAIN_IND_USER"]) ? (
      • - - - + + + Users (Individual)
      • + ) : null} + {isGrantedAny(["VIEW_ORG_USER", "MAINTAIN_ORG_USER"]) ? (
      • - - - + + + Users (Organisation)
      • -
      + ) : null} - } + )} + + {isGrantedAny(["VIEW_ORG", "MAINTAIN_ORG"]) ? ( +
    • + + + Organisation + + +
    • + ) : null} + + {isGrantedAny(["VIEW_GROUP", "MAINTAIN_GROUP"]) ? ( +
    • + + + User Group + + +
    • + ) : null} +
  • -
    - } - + ) : null} + + {/* ===== Report dropdown (admin) ===== */}
  • - - - + + +
      +
    • + + + Summary of Gazette Notice + + +
    • +
    • + + + Gazette Notice Full List + + +
    • +
  • -
    -
    + + {/* ===== Settings dropdown (admin) ===== */} +
  • + + +
      +
    • + + + My Profile + + +
    • + +
    • + + + + + +
    • + + {isGranted("VIEW_GAZETTE_ISSUE", "MAINTAIN_GAZETTE_ISSUE") ? ( + <> +
    • + + + Holiday Settings + + +
    • +
    • + + + Gazette Issues + + +
    • + + ) : null} + + {isGranted("MAINTAIN_ANNOUNCEMENT") ? ( +
    • + + + Announcement + + +
    • + ) : null} + + {isGranted("MAINTAIN_EMAIL") ? ( +
    • + + + Email Template + + +
    • + ) : null} + + {isGranted("MAINTAIN_DR") ? ( +
    • + + + DR Import + + +
    • + ) : null} + + {isGranted("MAINTAIN_SETTING") ? ( + <> +
    • + + + System Settings + + +
    • +
    • + + + Audit Log + + +
    • + + ) : null} +
    +
  • + + +
  • + + + Logout + + +
  • +
    +
    + )} +
    + ) : ( + // ===== Non-GLD login content (your original logic, only dropdown triggers changed to + +
      +
    • + + + + + +
    • + + {(isCreditorLoggedIn() && haveOrgPaymentRecord()) || + (isORGLoggedIn() && haveOrgPaymentRecord() && haveOrgDnRecord()) || + (!isCreditorLoggedIn() && !isORGLoggedIn() && haveOrgDnRecord()) ? ( +
    • + + + + + +
    • + ) : null} +
    + + ) : ( + + + + + + )} + + + {/* ===== User Setting dropdown (non-admin) ===== */} +
  • + + +
      + {isPrimaryLoggedIn() ? ( + <> +
    • + + + + + +
    • +
    • + + + + + +
    • +
    • + + + + + +
    • + + ) : isINDLoggedIn() ? ( +
    • + + + + + +
    • + ) : ( +
    • + + + + + +
    • + )} + +
    • + + + + + +
    • +
    +
  • +
    + )} + + +
  • + + + + + +
  • +
    +
    ); + // ============================= + // Logged-out top nav content + // ============================= const logoutContent = ( ); - const drawer = ( - isUserLoggedIn() ? - - {/* + const drawer = isUserLoggedIn() ? ( + + + + PNSPS - */} - - - PNSPS - - -
      - {loginContent} -
    - -
      -
    • - - - -
    • -
    -
    - : - - - - PNSPS - - -
      - {logoutContent} -
    - -
    + + + +
      {loginContent}
    + +
      +
    • + + + +
    • +
    + + ) : ( + + + + + PNSPS + + + +
      {logoutContent}
    + +
    ); const container = window !== undefined ? () => window().document.body : undefined; - return ( - isUserLoggedIn() ? - // User Login success - - - - {isGLDLoggedIn() - ? - - - - - - - - - - - PNSPS - - - RESTRICTED - - - - : - + + + {isGLDLoggedIn() ? ( + + + + + - - - - - {/*公共啟事提交*/} - {/*及繳費系統*/} - - - - + + + + + + + + PNSPS + - - - - - - - - - - - - - - - - + + RESTRICTED - - - } - - - - - - - - { - isGLDLoggedIn() ? - - RESTRICTED - - : - - - - } - {/**/} - - - - - - - - - {drawer} - - - : - - - - - - + + + + ) : ( + + + - + + - + - + - + - - - - - + )} + + + + - {/**/} + + + {isGLDLoggedIn() ? ( + + RESTRICTED + + ) : ( + + + + )} + + + + + + + + + + {drawer} + + + + ) : ( + + + + + + + + + + + + - - - - - {drawer} - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {drawer} + + ); } + Header.propTypes = { - /** - * Injected by the documentation to work in an iframe. - * You won't need it on your project. - */ window: PropTypes.func, }; -export default Header; + +export default Header; \ No newline at end of file diff --git a/src/pages/Announcement/Search_Public/index.js b/src/pages/Announcement/Search_Public/index.js index 46ca7c1..b282c39 100644 --- a/src/pages/Announcement/Search_Public/index.js +++ b/src/pages/Announcement/Search_Public/index.js @@ -15,6 +15,7 @@ const EventTable = Loadable(React.lazy(() => import('./DataGrid'))); import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import { FormattedMessage } from "react-intl"; import { getSearchCriteria } from "auth/utils"; +import usePageTitle from "components/usePageTitle"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, @@ -29,7 +30,7 @@ const BackgroundHead = { // ==============================|| DASHBOARD - DEFAULT ||============================== // const UserSearchPage_Individual = () => { - + usePageTitle("announcement"); const [searchCriteria, setSearchCriteria] = React.useState({}); const [onReady, setOnReady] = React.useState(false); const [onGridReady, setGridOnReady] = React.useState(false); diff --git a/src/pages/DemandNote/Search_Public/index.js b/src/pages/DemandNote/Search_Public/index.js index 7abee35..8f174a1 100644 --- a/src/pages/DemandNote/Search_Public/index.js +++ b/src/pages/DemandNote/Search_Public/index.js @@ -18,6 +18,7 @@ const SearchForm = Loadable(React.lazy(() => import('./SearchForm'))); const EventTable = Loadable(React.lazy(() => import('./DataGrid'))); import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage} from "react-intl"; +import usePageTitle from "components/usePageTitle"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, @@ -32,7 +33,7 @@ const BackgroundHead = { // ==============================|| DASHBOARD - DEFAULT ||============================== // const SearchPage_DemandNote_Pub = () => { - + usePageTitle("paymentInfoRecord"); const [orgCombo, setOrgCombo] = React.useState([]); const [issueCombo, setIssueCombo] = React.useState([]); const [searchCriteria, setSearchCriteria] = React.useState({}); diff --git a/src/pages/Message/Details/index.js b/src/pages/Message/Details/index.js index e7eadeb..117c9a2 100644 --- a/src/pages/Message/Details/index.js +++ b/src/pages/Message/Details/index.js @@ -17,6 +17,8 @@ const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/Loa import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import { FormattedMessage } from "react-intl"; +import usePageTitle from 'components/usePageTitle'; + const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -30,6 +32,8 @@ const BackgroundHead = { // ==============================|| DASHBOARD - DEFAULT ||============================== // const Index = () => { + usePageTitle("msgDetails"); + const params = useParams(); const navigate = useNavigate() diff --git a/src/pages/Message/Search/index.js b/src/pages/Message/Search/index.js index 5745347..c994f28 100644 --- a/src/pages/Message/Search/index.js +++ b/src/pages/Message/Search/index.js @@ -17,7 +17,7 @@ const EventTable = Loadable(React.lazy(() => import('./DataGrid'))); import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage} from "react-intl"; import { getSearchCriteria } from "auth/utils"; - +import usePageTitle from "components/usePageTitle"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -31,6 +31,7 @@ const BackgroundHead = { // ==============================|| DASHBOARD - DEFAULT ||============================== // const Index = () => { + usePageTitle("systemMessage"); const [searchCriteria, setSearchCriteria] = React.useState({}); const [onReady, setOnReady] = React.useState(false); diff --git a/src/pages/Payment/Details_Public/index.js b/src/pages/Payment/Details_Public/index.js index c611f7b..cc62f7e 100644 --- a/src/pages/Payment/Details_Public/index.js +++ b/src/pages/Payment/Details_Public/index.js @@ -20,6 +20,7 @@ const DataGrid = Loadable(React.lazy(() => import('./DataGrid'))); import ForwardIcon from '@mui/icons-material/Forward'; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage,useIntl} from "react-intl"; +import usePageTitle from "components/usePageTitle"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -33,6 +34,8 @@ const BackgroundHead = { // ==============================|| DASHBOARD - DEFAULT ||============================== // const Index = () => { + usePageTitle("payDetail"); + const params = useParams(); const navigate = useNavigate() const intl = useIntl(); diff --git a/src/pages/Payment/Search_Public/index.js b/src/pages/Payment/Search_Public/index.js index d426395..278e445 100644 --- a/src/pages/Payment/Search_Public/index.js +++ b/src/pages/Payment/Search_Public/index.js @@ -15,6 +15,7 @@ const EventTable = Loadable(React.lazy(() => import('./DataGrid'))); import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage} from "react-intl"; import { getSearchCriteria } from "auth/utils"; +import usePageTitle from "components/usePageTitle"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, @@ -29,6 +30,7 @@ const BackgroundHead = { // ==============================|| DASHBOARD - DEFAULT ||============================== // const Index = () => { + usePageTitle("onlinePaymentHistory"); const [searchCriteria, setSearchCriteria] = React.useState({ dateTo: DateUtils.dateValue(new Date()), dateFrom: DateUtils.dateValue(new Date().setDate(new Date().getDate()-14)), diff --git a/src/pages/Proof/Reply_Public/index.js b/src/pages/Proof/Reply_Public/index.js index ee00046..80a80d2 100644 --- a/src/pages/Proof/Reply_Public/index.js +++ b/src/pages/Proof/Reply_Public/index.js @@ -21,6 +21,7 @@ const ProofForm = Loadable(React.lazy(() => import('./ProofForm'))); import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import MainCard from "../../../components/MainCard"; import {FormattedMessage} from "react-intl"; +import usePageTitle from "components/usePageTitle"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, width: '100%', @@ -35,6 +36,8 @@ import {useIntl} from "react-intl"; // ==============================|| DASHBOARD - DEFAULT ||============================== // const Index = () => { + usePageTitle("proofRecord"); + const params = useParams(); const navigate = useNavigate() const intl = useIntl(); diff --git a/src/pages/Proof/Search_Public/index.js b/src/pages/Proof/Search_Public/index.js index d7348cb..3f5f85e 100644 --- a/src/pages/Proof/Search_Public/index.js +++ b/src/pages/Proof/Search_Public/index.js @@ -17,6 +17,7 @@ const SearchForm = Loadable(React.lazy(() => import('./SearchForm'))); const EventTable = Loadable(React.lazy(() => import('./DataGrid'))); import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import {FormattedMessage} from "react-intl"; +import usePageTitle from "components/usePageTitle"; const BackgroundHead = { backgroundImage: `url(${titleBackgroundImg})`, @@ -31,7 +32,8 @@ const BackgroundHead = { // ==============================|| DASHBOARD - DEFAULT ||============================== // const UserSearchPage_Individual = () => { - + usePageTitle("proofRecord"); + const [issueCombo,setIssueCombo] = React.useState([]); const [searchCriteria, setSearchCriteria] = React.useState({ dateTo: DateUtils.dateValue(new Date()), diff --git a/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js b/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js index 5574dae..41745f3 100644 --- a/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js +++ b/src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js @@ -545,6 +545,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => label: intl.formatMessage({ id: 'careOf' }) + ":", valueName: "careOf", form: formik, + inputProps: { "aria-label": intl.formatMessage({ id: 'careOf' }) } // disabled: true })} @@ -572,7 +573,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => label: intl.formatMessage({ id: 'extraMark' }) + ":", valueName: "remarks", form: formik, - inputProps: { maxLength: 255 } + inputProps: { maxLength: 255, "aria-label": intl.formatMessage({ id: 'extraMark' }) } })} } @@ -601,6 +602,9 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => name="tickAccept" color="primary" size="small" + inputProps={{ + "aria-label": intl.formatMessage({ id: "applyTickStr" }) + }} />
    @@ -629,9 +633,6 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => - - - ) : null} diff --git a/src/pages/PublicNotice/ApplyForm/index.js b/src/pages/PublicNotice/ApplyForm/index.js index ddb1692..fa59aed 100644 --- a/src/pages/PublicNotice/ApplyForm/index.js +++ b/src/pages/PublicNotice/ApplyForm/index.js @@ -16,7 +16,7 @@ import Loadable from 'components/Loadable'; import { lazy } from 'react'; const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); const PublicNoticeApplyForm = Loadable(lazy(() => import('./PublicNoticeApplyForm'))); - +import usePageTitle from "components/usePageTitle"; import { // isORGLoggedIn, isDummyLoggedIn, @@ -27,6 +27,8 @@ import { // ==============================|| DASHBOARD - DEFAULT ||============================== // const ApplyForm = () => { + usePageTitle("applyPublicNotice"); + const [userData, setUserData] = React.useState(null); const [gazetteIssueList, setGazetteIssueList] = React.useState([]); diff --git a/src/pages/PublicNotice/Details_Public/index.js b/src/pages/PublicNotice/Details_Public/index.js index e73704f..831b940 100644 --- a/src/pages/PublicNotice/Details_Public/index.js +++ b/src/pages/PublicNotice/Details_Public/index.js @@ -32,10 +32,12 @@ import { useNavigate } from "react-router-dom"; import ForwardIcon from '@mui/icons-material/Forward'; import { notifyActionSuccess } from "utils/CommonFunction"; import { FormattedMessage, useIntl } from "react-intl"; - +import usePageTitle from "components/usePageTitle"; // ==============================|| Body - DEFAULT ||============================== // const DashboardDefault = () => { + usePageTitle("myPublicNotice"); + const params = useParams(); const [applicationDetailData, setApplicationDetailData] = useState({}); const [appNo, setAapNo] = useState(""); diff --git a/src/pages/PublicNotice/ListPanel/index.js b/src/pages/PublicNotice/ListPanel/index.js index 0c38de9..192ff9c 100644 --- a/src/pages/PublicNotice/ListPanel/index.js +++ b/src/pages/PublicNotice/ListPanel/index.js @@ -31,11 +31,12 @@ import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import { PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; import { ThemeProvider } from "@emotion/react"; import { FormattedMessage, useIntl } from "react-intl"; - - +import usePageTitle from 'components/usePageTitle'; // ==============================|| DASHBOARD - DEFAULT ||============================== // const PublicNotice = () => { + usePageTitle("myPublicNotice"); + const [submittedCount, setSubmittedCount] = useState(0); const [pendingPaymentCount, setPendingPaymentCount] = useState(0); const [pendingPublishCount, setPendingPublishCount] = useState(0); diff --git a/src/pages/User/ChangePasswordPage/index.js b/src/pages/User/ChangePasswordPage/index.js index 4e8cea4..978895a 100644 --- a/src/pages/User/ChangePasswordPage/index.js +++ b/src/pages/User/ChangePasswordPage/index.js @@ -34,9 +34,12 @@ import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import { useDispatch } from "react-redux"; import { handleLogoutFunction} from 'auth/index'; import { isPasswordExpiry } from "utils/Utils"; +import usePageTitle from "components/usePageTitle"; // ==============================|| DASHBOARD - DEFAULT ||============================== // const Index = () => { + usePageTitle("userChangePassword"); + const dispatch = useDispatch() const navigate = useNavigate() const [showPassword, setShowPassword] = React.useState(false); diff --git a/src/pages/User/DetailsPage_Individual/index.js b/src/pages/User/DetailsPage_Individual/index.js index c0fe119..bdcb18f 100644 --- a/src/pages/User/DetailsPage_Individual/index.js +++ b/src/pages/User/DetailsPage_Individual/index.js @@ -36,10 +36,12 @@ import { isORGLoggedIn } from "utils/Utils"; import {FormattedMessage, useIntl} from "react-intl"; +import usePageTitle from "components/usePageTitle"; // ==============================|| DASHBOARD - DEFAULT ||============================== // const UserMaintainPage_Individual = () => { + usePageTitle("userProfile"); const intl = useIntl(); const params = useParams(); diff --git a/src/pages/User/DetailsPage_Organization/index.js b/src/pages/User/DetailsPage_Organization/index.js index c7cf53b..9551c44 100644 --- a/src/pages/User/DetailsPage_Organization/index.js +++ b/src/pages/User/DetailsPage_Organization/index.js @@ -40,11 +40,13 @@ import { isINDLoggedIn } from "utils/Utils"; import {FormattedMessage, useIntl} from "react-intl"; - +import usePageTitle from "components/usePageTitle"; // ==============================|| DASHBOARD - DEFAULT ||============================== // const UserMaintainPage_Organization = () => { + usePageTitle("userProfile"); + const params = useParams(); const navigate = useNavigate(); const [userData, setUserData] = useState({}) @@ -121,7 +123,7 @@ const UserMaintainPage_Organization = () => { // window.location.reload(false); // } - const loadData = () => { + const loadData = () => { const reqId = ++reqIdRef.current; setLoding(true); if (isGLDLoggedIn()){ diff --git a/src/pages/authentication/ForgotPassword/AuthCallback/index.js b/src/pages/authentication/ForgotPassword/AuthCallback/index.js index 43a7d26..0d6406c 100644 --- a/src/pages/authentication/ForgotPassword/AuthCallback/index.js +++ b/src/pages/authentication/ForgotPassword/AuthCallback/index.js @@ -343,6 +343,7 @@ const Index = () => { aria-label={intl.formatMessage({ id: showConfirmPassword ? "ariaHidePassword" : "ariaShowPassword" })} + aria-describedby="helper-text-confirmPassword-signup" onClick={handleClickShowConfirmPassword} onMouseDown={handleMouseDownPassword} edge="end" diff --git a/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js b/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js index a350ff4..c75b704 100644 --- a/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js +++ b/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js @@ -150,6 +150,8 @@ const ForgotPasswordApplyForm = () => { onBlur={formik.handleBlur} autoFocus inputProps={{ + "aria-label": intl.formatMessage({id: 'userLoginName'}), + "aria-describedby" : 'standard-weight-helper-text-username-login', maxLength: 50, onKeyDown: (e) => { if (e.key === 'Enter') { diff --git a/src/pages/authentication/ForgotUsername/AuthCallback/index.js b/src/pages/authentication/ForgotUsername/AuthCallback/index.js index b7bf798..e33b162 100644 --- a/src/pages/authentication/ForgotUsername/AuthCallback/index.js +++ b/src/pages/authentication/ForgotUsername/AuthCallback/index.js @@ -406,6 +406,7 @@ const Index = () => { aria-label={intl.formatMessage({ id: showConfirmPassword ? "ariaHidePassword" : "ariaShowPassword" })} + aria-describedby="helper-text-confirmPassword-signup" onClick={handleClickShowConfirmPassword} onMouseDown={handleMouseDownPassword} edge="end" diff --git a/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js b/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js index 524e700..e67e0d4 100644 --- a/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js +++ b/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js @@ -149,6 +149,8 @@ const ForgotUsernameApplyForm = () => { error={Boolean(formik.touched.emailAddress && formik.errors.emailAddress)} onBlur={formik.handleBlur} inputProps={{ + "aria-describedby" : 'standard-weight-helper-text-emailAddress-login', + "aria-label": intl.formatMessage({id: 'userContactEmail'}), maxLength: 50, onKeyDown: (e) => { if (e.key === 'Enter') { diff --git a/src/pages/authentication/auth-forms/AuthLoginCustom.js b/src/pages/authentication/auth-forms/AuthLoginCustom.js index 38970c2..cd5aae3 100644 --- a/src/pages/authentication/auth-forms/AuthLoginCustom.js +++ b/src/pages/authentication/auth-forms/AuthLoginCustom.js @@ -291,6 +291,7 @@ const AuthLoginCustom = () => { error={Boolean(formik.touched.username && formik.errors.username)} onBlur={formik.handleBlur} inputProps={{ + "aria-describedby": 'standard-weight-helper-text-username-login', maxLength: 50, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -336,6 +337,9 @@ const AuthLoginCustom = () => { } placeholder="" + inputProps={{ + "aria-describedby": 'standard-weight-helper-text-password-login', + }} /> {formik.touched.password && formik.errors.password && ( diff --git a/src/pages/authentication/auth-forms/BusCustomFormWizard.js b/src/pages/authentication/auth-forms/BusCustomFormWizard.js index 8f26137..28a3303 100644 --- a/src/pages/authentication/auth-forms/BusCustomFormWizard.js +++ b/src/pages/authentication/auth-forms/BusCustomFormWizard.js @@ -687,7 +687,7 @@ const BusCustomFormWizard = (props) => { { error={Boolean((formik.touched.username && formik.errors.username) || checkUsername)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userLoginName" }), + "aria-describedby": 'helper-text-username-login', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -762,6 +764,8 @@ const BusCustomFormWizard = (props) => { placeholder={intl.formatMessage({id: 'userPassword'})} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userPassword" }), + "aria-describedby": 'helper-text-password-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -797,7 +801,7 @@ const BusCustomFormWizard = (props) => { { // changePassword(e.target.value); }} inputProps={{ + "aria-label": intl.formatMessage({ id: "confirmPassword" }), + "aria-describedby": 'helper-text-confirmPassword-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -879,7 +885,7 @@ const BusCustomFormWizard = (props) => { { error={Boolean(formik.touched.enCompanyName && formik.errors.enCompanyName && selectedAddress5 !== "內地")} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "businessEngName" }), + "aria-describedby": 'helper-text-enCompanyName-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -921,6 +929,8 @@ const BusCustomFormWizard = (props) => { placeholder={intl.formatMessage({id: 'sameAsBusinessRegistrationCert'})} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "businessChName" }), + "aria-describedby": 'helper-text-chCompanyName-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -954,6 +964,8 @@ const BusCustomFormWizard = (props) => { onBlur={formik.handleBlur} placeholder={intl.formatMessage({id: 'sameAsBusinessRegistrationCert'})} inputProps={{ + "aria-label": intl.formatMessage({ id: "businessRegCertNumber" }), + "aria-describedby": 'helper-text-brNo-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -987,6 +999,8 @@ const BusCustomFormWizard = (props) => { onBlur={formik.handleBlur} placeholder={intl.formatMessage({id: 'sameAsBusinessRegistrationCert'})} inputProps={{ + "aria-label": intl.formatMessage({ id: "businessRegCertExpiryDate" }), + "aria-describedby": 'helper-text-brExpiryDate-signup', max: "2099-12-31", min: new Date().toISOString().split("T")[0], onKeyDown: (e) => { @@ -1021,6 +1035,7 @@ const BusCustomFormWizard = (props) => { placeholder={intl.formatMessage({id: 'addressLine1'})} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "addressLine1" }), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1038,6 +1053,7 @@ const BusCustomFormWizard = (props) => { onChange={formik.handleChange} placeholder={intl.formatMessage({id: 'addressLine2'})} inputProps={{ + "aria-label": intl.formatMessage({ id: "addressLine2" }), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1055,6 +1071,7 @@ const BusCustomFormWizard = (props) => { onChange={formik.handleChange} placeholder={intl.formatMessage({id: 'addressLine3'})} inputProps={{ + "aria-label": intl.formatMessage({ id: "addressLine3" }), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1177,7 +1194,7 @@ const BusCustomFormWizard = (props) => { { error={Boolean(formik.touched.enName && formik.errors.enName)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userContactName" }), + "aria-describedby": 'helper-text-enName-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1214,7 +1233,7 @@ const BusCustomFormWizard = (props) => { { placeholder={intl.formatMessage({id: 'userContactEmail'})} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userContactEmail" }), + "aria-describedby": 'helper-text-email-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1252,7 +1273,7 @@ const BusCustomFormWizard = (props) => { { onCopy={handleCCPChange} onPaste={handleCCPChange} inputProps={{ + "aria-label": intl.formatMessage({ id: "confirmEmail" }), + "aria-describedby": 'helper-text-emailConfirm-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1294,7 +1317,7 @@ const BusCustomFormWizard = (props) => { { error={Boolean(formik.touched.phone && formik.errors.phone)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "phoneCountryCode" }), maxLength: 3, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1322,7 +1346,7 @@ const BusCustomFormWizard = (props) => { sx={{ width: '33%', mr:1 }} /> { placeholder={intl.formatMessage({id: 'userContactNumber'})} error={Boolean(formik.touched.phone && formik.errors.phone)} inputProps={{ + "aria-label": intl.formatMessage({ id: "userContactNumber" }), + "aria-describedby": 'helper-text-phone-signup', maxLength: 11, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1369,7 +1395,7 @@ const BusCustomFormWizard = (props) => { { placeholder={intl.formatMessage({id: 'dialingCode'})} endAdornment={-} inputProps={{ + "aria-label": intl.formatMessage({ id: "faxCountryCode" }), maxLength: 3, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1395,7 +1422,7 @@ const BusCustomFormWizard = (props) => { sx={{ width: '33%', mr:1 }} /> { }} placeholder={intl.formatMessage({id: 'userFaxNumber'})} inputProps={{ + "aria-label": intl.formatMessage({ id: "userFaxNumber" }), maxLength: 8, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1493,6 +1521,9 @@ const BusCustomFormWizard = (props) => { name="termsAndConAccept" color="primary" size="small" + inputProps={{ + "aria-label": intl.formatMessage({ id: "iConfirm" }) + }} /> @@ -1509,6 +1540,9 @@ const BusCustomFormWizard = (props) => { name="termsAndConNotAccept" color="primary" size="small" + inputProps={{ + "aria-label": intl.formatMessage({ id: "rejectTerms" }) + }} /> @@ -1553,6 +1587,10 @@ const BusCustomFormWizard = (props) => { formik.setFieldValue("captchaField", value); }} sx={{ width: '75%' }} + inputProps={{ + "aria-label": intl.formatMessage({ id: "verify" }), + "aria-describedby": 'helper-text-captcha-signup' + }} /> diff --git a/src/pages/authentication/auth-forms/CustomFormWizard.js b/src/pages/authentication/auth-forms/CustomFormWizard.js index f84767c..f423115 100644 --- a/src/pages/authentication/auth-forms/CustomFormWizard.js +++ b/src/pages/authentication/auth-forms/CustomFormWizard.js @@ -1,7 +1,5 @@ import { useEffect, useState } from 'react'; - -// material-ui import { Box, Button, Checkbox @@ -918,7 +916,7 @@ const CustomFormWizard = (props) => { { error={Boolean((formik.touched.username && formik.errors.username) || checkUsername)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userLoginName" }), + "aria-describedby": 'helper-text-username-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -993,6 +993,8 @@ const CustomFormWizard = (props) => { placeholder={intl.formatMessage({ id: 'userPassword' })} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userPassword" }), + "aria-describedby": 'helper-text-password-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1028,7 +1030,7 @@ const CustomFormWizard = (props) => { { // changePassword(e.target.value); }} inputProps={{ + "aria-label": intl.formatMessage({ id: "confirmPassword" }), + "aria-describedby": 'helper-text-confirmPassword-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1119,6 +1123,7 @@ const CustomFormWizard = (props) => { { error={Boolean(formik.touched.idNo && formik.errors.idNo || checkIdDocNumber)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "idDocNumber" }), + "aria-describedby": 'helper-text-idNo-signup', maxLength: selectedIdDocType.type === 'HKID' ? 8 : 18, onKeyDown: (e) => { // console.log(e) @@ -1280,6 +1287,8 @@ const CustomFormWizard = (props) => { error={Boolean(formik.touched.idNo && formik.errors.idNo || checkIdDocNumber)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "idDocNumber" }), + "aria-describedby": 'helper-text-idNo-signup', maxLength: 18, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1317,7 +1326,7 @@ const CustomFormWizard = (props) => { { error={Boolean(formik.touched.enName && formik.errors.enName && selectedIdDocType.type !== "CNID")} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userEnglishName" }), + "aria-describedby": 'helper-text-enName-signup', onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1360,6 +1371,8 @@ const CustomFormWizard = (props) => { placeholder={intl.formatMessage({ id: 'sameAsYourIdDoc' })} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userChineseName" }), + "aria-describedby": 'helper-text-chName-signup', maxLength: 6, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1393,6 +1406,7 @@ const CustomFormWizard = (props) => { placeholder={intl.formatMessage({ id: 'addressLine1' })} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "addressLine1" }), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1410,6 +1424,7 @@ const CustomFormWizard = (props) => { onBlur={formik.handleBlur} placeholder={intl.formatMessage({ id: 'addressLine2' })} inputProps={{ + "aria-label": intl.formatMessage({ id: "addressLine2" }), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1427,6 +1442,7 @@ const CustomFormWizard = (props) => { onBlur={formik.handleBlur} placeholder={intl.formatMessage({ id: 'addressLine3' })} inputProps={{ + "aria-label": intl.formatMessage({ id: "addressLine3" }), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1554,7 +1570,7 @@ const CustomFormWizard = (props) => { { placeholder={intl.formatMessage({ id: 'userContactEmail' })} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userContactEmail" }), + "aria-describedby": "helper-text-email-signup", onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1592,7 +1610,7 @@ const CustomFormWizard = (props) => { { onCopy={handleCCPChange} onPaste={handleCCPChange} inputProps={{ + "aria-label": intl.formatMessage({ id: "confirmEmail" }), + "aria-describedby": "helper-text-emailConfirm-signup", onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -1634,7 +1654,7 @@ const CustomFormWizard = (props) => { { onBlur={formik.handleBlur} endAdornment={-} inputProps={{ + "aria-label": intl.formatMessage({ id: "dialingCode" }), maxLength: 3, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1662,7 +1683,7 @@ const CustomFormWizard = (props) => { sx={{ width: '33%', mr: 1 }} /> { error={Boolean(formik.touched.phone && formik.errors.phone)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userContactNumber" }), + "aria-describedby": 'helper-text-phone-signup', maxLength: 11, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1710,7 +1733,7 @@ const CustomFormWizard = (props) => { { onBlur={formik.handleBlur} endAdornment={-} inputProps={{ + "aria-label": intl.formatMessage({ id: "faxCountryCode" }), maxLength: 3, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1736,7 +1760,7 @@ const CustomFormWizard = (props) => { sx={{ width: '33%', mr: 1 }} /> { }} placeholder={intl.formatMessage({ id: 'userFaxNumber' })} inputProps={{ + "aria-label": intl.formatMessage({ id: "userFaxNumber" }), maxLength: 8, onKeyDown: (e) => { if (e.key === 'Enter') { @@ -1835,6 +1860,9 @@ const CustomFormWizard = (props) => { name="termsAndConAccept" color="primary" size="small" + inputProps={{ + "aria-label": intl.formatMessage({ id: "iConfirm" }) + }} /> @@ -1851,6 +1879,9 @@ const CustomFormWizard = (props) => { name="termsAndConNotAccept" color="primary" size="small" + inputProps={{ + "aria-label": intl.formatMessage({ id: "rejectTerms" }) + }} /> @@ -1895,6 +1926,10 @@ const CustomFormWizard = (props) => { formik.setFieldValue("captchaField", value); }} sx={{ width: '75%' }} + inputProps={{ + "aria-label": intl.formatMessage({ id: "verify" }), + "aria-describedby": 'helper-text-captcha-signup' + }} /> diff --git a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js index 105c644..d0a9a14 100644 --- a/src/pages/authentication/auth-forms/IAmSmartFormWizard.js +++ b/src/pages/authentication/auth-forms/IAmSmartFormWizard.js @@ -746,6 +746,8 @@ const CustomFormWizard = (props) => { placeholder={intl.formatMessage({ id: 'userContactEmail' })} onBlur={formik.handleBlur} inputProps={{ + "aria-describedby": "helper-text-email-signup", + "aria-label": intl.formatMessage({ id: "userContactEmail" }), onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -792,6 +794,8 @@ const CustomFormWizard = (props) => { onCopy={handleCCPChange} onPaste={handleCCPChange} inputProps={{ + "aria-label": intl.formatMessage({ id: "confirmEmail" }), + "aria-describedby": "helper-text-emailConfirm-signup", onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); @@ -870,6 +874,8 @@ const CustomFormWizard = (props) => { error={Boolean(formik.touched.phone && formik.errors.phone)} onBlur={formik.handleBlur} inputProps={{ + "aria-label": intl.formatMessage({ id: "userContactNumber" }), + "aria-describedby": 'helper-text-phone-signup', maxLength: 11, onKeyDown: (e) => { if (e.key === 'Enter') { diff --git a/src/pages/dashboard/Public/index.js b/src/pages/dashboard/Public/index.js index 29a7fa6..4e97036 100644 --- a/src/pages/dashboard/Public/index.js +++ b/src/pages/dashboard/Public/index.js @@ -27,10 +27,12 @@ const Notice = Loadable(React.lazy(() => import('./Notice'))); const LoadingComponent = Loadable(React.lazy(() => import('../../extra-pages/LoadingComponent'))); import { useNavigate } from "react-router-dom"; +import usePageTitle from "components/usePageTitle"; // ==============================|| DASHBOARD - DEFAULT ||============================== // const DashboardDefault = () => { + usePageTitle("Dashboard"); const navigate = useNavigate() const intl = useIntl(); const userData = JSON.parse(localStorage.getItem("userData")); diff --git a/src/pages/extra-pages/UserMenuPub1/index.js b/src/pages/extra-pages/UserMenuPub1/index.js index 40094a7..481caae 100644 --- a/src/pages/extra-pages/UserMenuPub1/index.js +++ b/src/pages/extra-pages/UserMenuPub1/index.js @@ -1,207 +1,276 @@ -import { Grid, Typography, Stack, } from '@mui/material'; -import { useState, useEffect, lazy } from "react"; +// File: src/pages/userGuide/UserMenuPub1.jsx +// Notes: +// - No new libraries. +// - Fixes: target="_blank", rel="noopener noreferrer" +// - Adds aria-label for icon-only download links (WCAG 2.4.4 / 1.1.1 friendly) +// - Fixes invalid HTML: no

    wrapping +// - Adds /, scope, and React keys -import Loadable from 'components/Loadable'; +import { Grid, Typography, Stack } from "@mui/material"; +import { useState, useEffect, lazy } from "react"; +import Loadable from "components/Loadable"; import { useIntl, FormattedMessage } from "react-intl"; -import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' +import DownloadIcon from "@mui/icons-material/Download"; +import titleBackgroundImg from "assets/images/dashboard/gazette-bar.png"; + +const LoadingComponent = Loadable(lazy(() => import("pages/extra-pages/LoadingComponent"))); + const BackgroundHead = { - backgroundImage: `url(${titleBackgroundImg})`, - width: 'auto', - height: 'auto', - backgroundSize: 'contain', - backgroundRepeat: 'no-repeat', - backgroundColor: '#0C489E', - backgroundPosition: 'right' -} + backgroundImage: `url(${titleBackgroundImg})`, + width: "auto", + height: "auto", + backgroundSize: "contain", + backgroundRepeat: "no-repeat", + backgroundColor: "#0C489E", + backgroundPosition: "right", +}; -const LoadingComponent = Loadable(lazy(() => import('pages/extra-pages/LoadingComponent'))); +const tableStyle = { + fontFamily: "arial, sans-serif", + borderCollapse: "collapse", + width: "100%", +}; -import DownloadIcon from '@mui/icons-material/Download'; +const cellStyle = { + border: "1px solid #dddddd", + textAlign: "left", + padding: "8px", +}; const UserMenuPub1 = () => { - const intl = useIntl(); - const { locale } = intl; - const [onReady, setOnReady] = useState(false); + const intl = useIntl(); + const { locale } = intl; + + const [onReady, setOnReady] = useState(false); - useEffect(() => { - setOnReady(true); - }, [locale]); + useEffect(() => { + setOnReady(true); + }, [locale]); - const tableStyle = { - fontFamily: "arial, sans-serif", - borderCollapse: "collapse", - width: "100%", - } + const pnspsurl = `https://${window.location.hostname}`; - const cellStyle = { - border: "1px solid #dddddd", - textAlign: "left", - padding: "8px" - }; + const pickByLocale = (en, zhHK, zhCN) => (locale === "zh-HK" ? zhHK : locale === "en" ? en : zhCN); - const getRow = ({ title, orgEn, orgZh, orgCn, indEn, indZh, indCn }) => { + // If you already created downloadPdfAria in i18n, keep it. + // Example en: "Download {guide} PDF ({userType})" + const buildAria = (guideId, userTypeId) => { + const guideTitle = intl.formatMessage({ id: guideId }); + const userType = intl.formatMessage({ id: userTypeId }); - return <> - - - - - - - ; - } + return intl.formatMessage( + { id: "downloadPdfAria", defaultMessage: "Download {guide} PDF ({userType})" }, + { guide: guideTitle, userType } + ); + }; - const pnspsurl = "https://"+window.location.hostname; + const rows = [ + { + id: "userGuide1", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/01 - Create account - c 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/01c - Create account - c 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/01sc - Create account - c 1.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/01 - Create account - p 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/01c - Create account - p 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/01sc - Create account - p 1.pdf`, + }, + }, + { + id: "userGuide2", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/02 - Login - c 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/02c - Login - c 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/02sc - Login - c 1.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/02 - Login - p 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/02c - Login - p 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/02sc - Login - p 1.pdf`, + }, + }, + { + id: "userGuide3", // UPDATED TO v2 + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/03 - Application for publishing a Public Notice in the Gazette - c 2.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/03c - Application for publishing a Public Notice in the Gazette - c 2.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/03sc - Application for publishing a Public Notice in the Gazette - c 2.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/03 - Application for publishing a Public Notice in the Gazette - p 2.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/03c - Application for publishing a Public Notice in the Gazette - p 2.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/03sc - Application for publishing a Public Notice in the Gazette - p 2.pdf`, + }, + }, + { + id: "userGuide4", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/04 - Proofreading reply (with correction) - c 2.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/04c - Proofreading reply (with correction) - c 2.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/04sc - Proofreading reply (with correction) - c 2.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/04 - Proofreading reply (with correction) - p 2.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/04c - Proofreading reply (with correction) - p 2.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/04sc - Proofreading reply (with correction) - p 2.pdf`, + }, + }, + { + id: "userGuide5", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/05 - Proofreading reply (pass for printing) - c 2.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/05c - Proofreading reply (pass for printing) - c 2.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/05sc - Proofreading reply (pass for printing) - c 2.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/05 - Proofreading reply (pass for printing) - p 2.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/05c - Proofreading reply (pass for printing) - p 2.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/05sc - Proofreading reply (pass for printing) - p 2.pdf`, + }, + }, + { + id: "userGuide6", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/06 - Cancellation of application for publishing a Public Notice in the Gazette - c 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/06c - Cancellation of application for publishing a Public Notice in the Gazette - c 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/06sc - Cancellation of application for publishing a Public Notice in the Gazette - c 1.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/06 - Cancellation of application for publishing a Public Notice in the Gazette - p 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/06c - Cancellation of application for publishing a Public Notice in the Gazette - p 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/06sc - Cancellation of application for publishing a Public Notice in the Gazette - p 1.pdf`, + }, + }, + { + id: "userGuide7", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/07 - Forgot password - c 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/07c - Forgot password - c 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/07sc - Forgot password - c 1.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/07 - Forgot password - p 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/07c - Forgot password - p 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/07sc - Forgot password - p 1.pdf`, + }, + }, + { + id: "userGuide8", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/08 - Change password - c 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/08c - Change password - c 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/08sc - Change password - c 1.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/08 - Change password - p 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/08c - Change password - p 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/08sc - Change password - p 1.pdf`, + }, + }, + { + id: "userGuide9", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/09 - Language of email notification - c 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/09c - Language of email notification - c 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/09sc - Language of email notification - c 1.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/09 - Language of email notification - p 1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/09c - Language of email notification - p 1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/09sc - Language of email notification - p 1.pdf`, + }, + }, + { + id: "userGuidePub10", + org: { + en: `${pnspsurl}/user-guide-pub-1/eng/10-Payment-c1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/10c-Payment-c1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/10sc-Payment-c1.pdf`, + }, + ind: { + en: `${pnspsurl}/user-guide-pub-1/eng/10-Payment-p1.pdf`, + zhHK: `${pnspsurl}/user-guide-pub-1/cht/10c-Payment-p1.pdf`, + zhCN: `${pnspsurl}/user-guide-pub-1/chs/10sc-Payment-p1.pdf`, + }, + }, + ]; + const renderDownloadLink = (href, ariaLabel) => ( + + + ); + if (!onReady) { return ( - !onReady ? - - - - - - : - ( - - -
    - - - - - -
    -
    - - -
    -

    - -

    {title}
    - - - - - - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/01 - Create account - c 1.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/01c - Create account - c 1.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/01sc - Create account - c 1.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/01 - Create account - p 1.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/01c - Create account - p 1.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/01sc - Create account - p 1.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/02 - Login - c 1.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/02c - Login - c 1.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/02sc - Login - c 1.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/02 - Login - p 1.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/02c - Login - p 1.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/02sc - Login - p 1.pdf" - })} - - {/* UPDATED TO v2 */} - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/03 - Application for publishing a Public Notice in the Gazette - c 2.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/03c - Application for publishing a Public Notice in the Gazette - c 2.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/03sc - Application for publishing a Public Notice in the Gazette - c 2.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/03 - Application for publishing a Public Notice in the Gazette - p 2.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/03c - Application for publishing a Public Notice in the Gazette - p 2.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/03sc - Application for publishing a Public Notice in the Gazette - p 2.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/04 - Proofreading reply (with correction) - c 2.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/04c - Proofreading reply (with correction) - c 2.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/04sc - Proofreading reply (with correction) - c 2.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/04 - Proofreading reply (with correction) - p 2.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/04c - Proofreading reply (with correction) - p 2.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/04sc - Proofreading reply (with correction) - p 2.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/05 - Proofreading reply (pass for printing) - c 2.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/05c - Proofreading reply (pass for printing) - c 2.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/05sc - Proofreading reply (pass for printing) - c 2.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/05 - Proofreading reply (pass for printing) - p 2.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/05c - Proofreading reply (pass for printing) - p 2.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/05sc - Proofreading reply (pass for printing) - p 2.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/06 - Cancellation of application for publishing a Public Notice in the Gazette - c 1.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/06c - Cancellation of application for publishing a Public Notice in the Gazette - c 1.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/06sc - Cancellation of application for publishing a Public Notice in the Gazette - c 1.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/06 - Cancellation of application for publishing a Public Notice in the Gazette - p 1.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/06c - Cancellation of application for publishing a Public Notice in the Gazette - p 1.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/06sc - Cancellation of application for publishing a Public Notice in the Gazette - p 1.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/07 - Forgot password - c 1.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/07c - Forgot password - c 1.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/07sc - Forgot password - c 1.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/07 - Forgot password - p 1.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/07c - Forgot password - p 1.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/07sc - Forgot password - p 1.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/08 - Change password - c 1.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/08c - Change password - c 1.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/08sc - Change password - c 1.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/08 - Change password - p 1.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/08c - Change password - p 1.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/08sc - Change password - p 1.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/09 - Language of email notification - c 1.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/09c - Language of email notification - c 1.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/09sc - Language of email notification - c 1.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/09 - Language of email notification - p 1.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/09c - Language of email notification - p 1.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/09sc - Language of email notification - p 1.pdf" - })} - - {getRow({ - title: , - orgEn: pnspsurl + "/user-guide-pub-1/eng/10-Payment-c1.pdf", - orgZh: pnspsurl + "/user-guide-pub-1/cht/10c-Payment-c1.pdf", - orgCn: pnspsurl + "/user-guide-pub-1/chs/10sc-Payment-c1.pdf", - indEn: pnspsurl + "/user-guide-pub-1/eng/10-Payment-p1.pdf", - indZh: pnspsurl + "/user-guide-pub-1/cht/10c-Payment-p1.pdf", - indCn: pnspsurl + "/user-guide-pub-1/chs/10sc-Payment-p1.pdf" - })} - - -
    - -

    -
    - - - - ) + + + + + ); + } + + return ( + + +
    + + + + + +
    +
    + + + +
    + + + + + + + + + + + {rows.map((r) => { + const orgHref = pickByLocale(r.org.en, r.org.zhHK, r.org.zhCN); + const indHref = pickByLocale(r.ind.en, r.ind.zhHK, r.ind.zhCN); + + const guideTitle = intl.formatMessage({ id: r.id }); + + return ( + + + + + + ); + })} + +
    + + + +
    {guideTitle} + {renderDownloadLink(orgHref, buildAria(r.id, "forOrgUser"))} + + {renderDownloadLink(indHref, buildAria(r.id, "forIndUser"))} +
    -} +

    + +

    +
    +
    +
    +
    + ); +}; export default UserMenuPub1; \ No newline at end of file diff --git a/src/translations/en.json b/src/translations/en.json index be58bcd..29d714c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -626,5 +626,7 @@ "muiNoOptions": "No options", "paginationPrev": "Go to previous page", - "paginationNext": "Go to next page" + "paginationNext": "Go to next page", + + "sort": "Sort" } \ No newline at end of file diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 7f19d12..6f5b738 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -622,5 +622,7 @@ "muiNoOptions": "没有选项", "paginationPrev": "上一页", - "paginationNext": "下一页" + "paginationNext": "下一页", + + "sort": "排序" } \ No newline at end of file diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 195eb60..6b42cff 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -623,5 +623,7 @@ "muiNoOptions": "沒有選項", "paginationPrev": "上一頁", - "paginationNext": "下一頁" + "paginationNext": "下一頁", + + "sort": "排序" } \ No newline at end of file diff --git a/src/utils/CommonFunction.js b/src/utils/CommonFunction.js index 4955a1f..2137e61 100644 --- a/src/utils/CommonFunction.js +++ b/src/utils/CommonFunction.js @@ -208,7 +208,7 @@ export const notifyActionSuccess = (actionMsg) => { export const notifyActionError = (actionMsg) => { toast.error(`${actionMsg}`, { position: "bottom-right", - autoClose: 5000, + autoClose: 60000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, From bc87f5df36f81b4ff4cc8d1477d1bf2fae3c4024 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 27 Feb 2026 01:33:22 +0800 Subject: [PATCH 11/63] Async Queue for Fallback Excel with Email --- src/pages/JVM/index.js | 98 +++++++++++++++++++++++++++++++------ src/routes/GLDUserRoutes.js | 2 +- src/utils/ApiPathConst.js | 1 + 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/pages/JVM/index.js b/src/pages/JVM/index.js index a719cad..c79c0b9 100644 --- a/src/pages/JVM/index.js +++ b/src/pages/JVM/index.js @@ -11,7 +11,7 @@ import { Button } from '@mui/material'; import * as React from "react"; -import { GET_JVM_INFO } from "utils/ApiPathConst"; +import { GET_JVM_INFO, GET_NOTIFICATION_QUEUE_STATUS } from "utils/ApiPathConst"; import axios from "axios"; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' @@ -20,7 +20,11 @@ const JVMDefault = () => { const [jvmInfo, setJvmInfo] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); - + + const [queueStatus, setQueueStatus] = React.useState(null); + const [queueLoading, setQueueLoading] = React.useState(false); + const [queueError, setQueueError] = React.useState(null); + const fetchJvmInfo = () => { setLoading(true); setError(null); @@ -37,6 +41,24 @@ const JVMDefault = () => { }); }; + const fetchNotificationQueueStatus = () => { + setQueueLoading(true); + setQueueError(null); + setQueueStatus(null); + axios.get(`${GET_NOTIFICATION_QUEUE_STATUS}`) + .then((response) => { + if (response.status === 200) { + setQueueStatus(response.data); + } + }) + .catch(err => { + setQueueError(err); + }) + .finally(() => { + setQueueLoading(false); + }); + }; + React.useEffect(() => { localStorage.setItem('searchCriteria', ""); setLoading(false); @@ -66,25 +88,40 @@ const JVMDefault = () => {
    - JVM Information + System Background Status
    - + + + + @@ -114,6 +151,35 @@ const JVMDefault = () => { )} + + + Notification Queue Status + {queueLoading ? ( + + + + ) : queueError ? ( + Error: {queueError.message} + ) : queueStatus ? ( + + {JSON.stringify(queueStatus, null, 2)} + + ) : ( + Click "Notification Queue Status" to load data. + )} + + ); }; diff --git a/src/routes/GLDUserRoutes.js b/src/routes/GLDUserRoutes.js index 1fd1668..e096bb3 100644 --- a/src/routes/GLDUserRoutes.js +++ b/src/routes/GLDUserRoutes.js @@ -197,7 +197,7 @@ const GLDUserRoutes = { }:{}, isGranted("MAINTAIN_SETTING")? { - path: '/jvm', + path: '/sys-status', element: }:{}, diff --git a/src/utils/ApiPathConst.js b/src/utils/ApiPathConst.js index ffc148d..38daac3 100644 --- a/src/utils/ApiPathConst.js +++ b/src/utils/ApiPathConst.js @@ -253,3 +253,4 @@ export const GET_HOLIDAY_TEMPLATE = apiPath+'/holiday/export'; //GET export const GET_JVM_INFO = apiPath+'/jvm-info'; //GET +export const GET_NOTIFICATION_QUEUE_STATUS = apiPath+'/notification-queue-status'; //GET \ No newline at end of file From a865e4e3f3a3a1a18b2eff6a7006d2a962d5d6e7 Mon Sep 17 00:00:00 2001 From: Jason Chuang Date: Fri, 27 Feb 2026 04:07:14 +0800 Subject: [PATCH 12/63] WCAG 2.0 A 1.3.1 Info and Relationships --- src/pages/Announcement/Search_Public/index.js | 2 +- src/pages/DemandNote/Search_Public/index.js | 2 +- src/pages/Message/Details/index.js | 6 ++-- src/pages/Message/Search/index.js | 2 +- src/pages/Organization/DetailPage/index.js | 4 +-- src/pages/Payment/Details_Public/index.js | 2 +- src/pages/Payment/Search_Public/index.js | 2 +- src/pages/Proof/Reply_Public/index.js | 2 +- src/pages/Proof/Search_Public/index.js | 2 +- .../ApplyForm/PublicNoticeApplyForm.js | 2 +- .../PublicNotice/Details_Public/index.js | 4 +-- src/pages/PublicNotice/ListPanel/index.js | 2 +- src/pages/User/ChangePasswordPage/index.js | 2 +- .../User/DetailsPage_Individual/index.js | 4 +-- .../User/DetailsPage_Organization/index.js | 4 +-- src/pages/User/ManagePage_OrgPublic/index.js | 2 +- .../ForgotPassword/AuthCallback/index.js | 2 +- .../ForgotPassword/ForgotPasswordApplyForm.js | 2 +- .../ForgotUsername/AuthCallback/index.js | 2 +- .../ForgotUsername/ForgotUsernameApplyForm.js | 2 +- src/pages/authentication/RegisterCustom.js | 6 ++-- src/pages/dashboard/Public/index.js | 12 ++++---- src/pages/extra-pages/AboutUs/AboutUs_cn.js | 12 ++++---- src/pages/extra-pages/AboutUs/AboutUs_en.js | 12 ++++---- src/pages/extra-pages/AboutUs/AboutUs_zh.js | 12 ++++---- src/pages/extra-pages/AboutUs/index.js | 2 +- .../extra-pages/ImportantNoticePage/index.js | 2 +- .../PrivacyPolicyPage/PrivacyPolicy_cn.js | 22 +++++++-------- .../PrivacyPolicyPage/PrivacyPolicy_en.js | 28 ++++++++----------- .../PrivacyPolicyPage/PrivacyPolicy_zh.js | 27 ++++++++---------- .../extra-pages/PrivacyPolicyPage/index.js | 2 +- src/pages/extra-pages/UserMenuPub1/index.js | 2 +- 32 files changed, 89 insertions(+), 102 deletions(-) diff --git a/src/pages/Announcement/Search_Public/index.js b/src/pages/Announcement/Search_Public/index.js index b282c39..78ee469 100644 --- a/src/pages/Announcement/Search_Public/index.js +++ b/src/pages/Announcement/Search_Public/index.js @@ -75,7 +75,7 @@ const UserSearchPage_Individual = () => {
    - +
    diff --git a/src/pages/DemandNote/Search_Public/index.js b/src/pages/DemandNote/Search_Public/index.js index 8f174a1..f1d49e0 100644 --- a/src/pages/DemandNote/Search_Public/index.js +++ b/src/pages/DemandNote/Search_Public/index.js @@ -102,7 +102,7 @@ const SearchPage_DemandNote_Pub = () => {
    - + diff --git a/src/pages/Message/Details/index.js b/src/pages/Message/Details/index.js index 117c9a2..5f05984 100644 --- a/src/pages/Message/Details/index.js +++ b/src/pages/Message/Details/index.js @@ -76,7 +76,7 @@ const Index = () => {
    - + @@ -87,7 +87,7 @@ const Index = () => {
    - + {record?.subject} {DateUtils.datetimeStr(record?.sentDate)} @@ -95,7 +95,7 @@ const Index = () => {
    - + - {title} + {title}
    diff --git a/src/pages/PublicNotice/ListPanel/index.js b/src/pages/PublicNotice/ListPanel/index.js index 192ff9c..aa8e520 100644 --- a/src/pages/PublicNotice/ListPanel/index.js +++ b/src/pages/PublicNotice/ListPanel/index.js @@ -114,7 +114,7 @@ const PublicNotice = () => {
    - + diff --git a/src/pages/User/ChangePasswordPage/index.js b/src/pages/User/ChangePasswordPage/index.js index 978895a..b5c3ebd 100644 --- a/src/pages/User/ChangePasswordPage/index.js +++ b/src/pages/User/ChangePasswordPage/index.js @@ -174,7 +174,7 @@ const Index = () => {
    - + diff --git a/src/pages/User/DetailsPage_Individual/index.js b/src/pages/User/DetailsPage_Individual/index.js index bdcb18f..7704dbf 100644 --- a/src/pages/User/DetailsPage_Individual/index.js +++ b/src/pages/User/DetailsPage_Individual/index.js @@ -140,11 +140,11 @@ const UserMaintainPage_Individual = () => {
    {isGLDLoggedIn()? - + Maintain Individual User : - + } diff --git a/src/pages/User/DetailsPage_Organization/index.js b/src/pages/User/DetailsPage_Organization/index.js index 9551c44..c5e4727 100644 --- a/src/pages/User/DetailsPage_Organization/index.js +++ b/src/pages/User/DetailsPage_Organization/index.js @@ -235,11 +235,11 @@ const UserMaintainPage_Organization = () => {
    {isGLDLoggedIn()? - + Maintain Organisation User : - + } diff --git a/src/pages/User/ManagePage_OrgPublic/index.js b/src/pages/User/ManagePage_OrgPublic/index.js index 56294bc..5d1ee6c 100644 --- a/src/pages/User/ManagePage_OrgPublic/index.js +++ b/src/pages/User/ManagePage_OrgPublic/index.js @@ -226,7 +226,7 @@ const ManageOrgUserPage = () => {
    - + diff --git a/src/pages/authentication/ForgotPassword/AuthCallback/index.js b/src/pages/authentication/ForgotPassword/AuthCallback/index.js index 0d6406c..a87b478 100644 --- a/src/pages/authentication/ForgotPassword/AuthCallback/index.js +++ b/src/pages/authentication/ForgotPassword/AuthCallback/index.js @@ -193,7 +193,7 @@ const Index = () => {
    - + diff --git a/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js b/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js index c75b704..c08ef4b 100644 --- a/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js +++ b/src/pages/authentication/ForgotPassword/ForgotPasswordApplyForm.js @@ -83,7 +83,7 @@ const ForgotPasswordApplyForm = () => {
    - + diff --git a/src/pages/authentication/ForgotUsername/AuthCallback/index.js b/src/pages/authentication/ForgotUsername/AuthCallback/index.js index e33b162..82c84f6 100644 --- a/src/pages/authentication/ForgotUsername/AuthCallback/index.js +++ b/src/pages/authentication/ForgotUsername/AuthCallback/index.js @@ -204,7 +204,7 @@ const Index = () => {
    - + diff --git a/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js b/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js index e67e0d4..4261a26 100644 --- a/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js +++ b/src/pages/authentication/ForgotUsername/ForgotUsernameApplyForm.js @@ -85,7 +85,7 @@ const ForgotUsernameApplyForm = () => {
    - + diff --git a/src/pages/authentication/RegisterCustom.js b/src/pages/authentication/RegisterCustom.js index 38eb1bb..465bcf3 100644 --- a/src/pages/authentication/RegisterCustom.js +++ b/src/pages/authentication/RegisterCustom.js @@ -99,7 +99,7 @@ const RegisterCustom = () => { - + {" " + intl.formatMessage({ id: 'registerTitle2' }) + " "} @@ -114,7 +114,7 @@ const RegisterCustom = () => { - + { }, }} > - + diff --git a/src/pages/dashboard/Public/index.js b/src/pages/dashboard/Public/index.js index 4e97036..ae35e8d 100644 --- a/src/pages/dashboard/Public/index.js +++ b/src/pages/dashboard/Public/index.js @@ -155,7 +155,7 @@ const DashboardDefault = () => {
    {/* 我的公共啟事 */} - + {isORGLoggedIn() ? userData.fullenName: (locale === 'en' ?userData.fullenName: userData.fullchName)}, {getWelcomeMsg()} @@ -175,7 +175,7 @@ const DashboardDefault = () => {
    {/* 我的公共啟事 */} - + {isORGLoggedIn() ? userData.fullenName: (locale === 'en' ?userData.fullenName: userData.fullchName)}, {getWelcomeMsg()} @@ -204,7 +204,7 @@ const DashboardDefault = () => { - + @@ -214,7 +214,7 @@ const DashboardDefault = () => { - +