diff --git a/src/components/BrStatusDialog.js b/src/components/BrStatusDialog.js new file mode 100644 index 00000000..c7f648e8 --- /dev/null +++ b/src/components/BrStatusDialog.js @@ -0,0 +1,130 @@ +import { + Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography +} from '@mui/material'; +import { FormattedMessage, useIntl } from "react-intl"; +import { useNavigate } from "react-router-dom"; +import SafeHtml from "components/SafeHtml"; +import * as DateUtils from "utils/DateUtils"; +import { GREY_CONTAINED_BUTTON_SX, HEADER_BACKGROUND_COLOR, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; + +const TITLE_IDS = { + expiring: "brPopupTitleExpiring", + expired: "brPopupTitleExpired", + pending: "brPopupTitlePendingVerify", + submitNotExpired: "brPopupTitlePendingVerify", + submitExpired: "brPopupTitlePendingVerify", +}; + +const MESSAGE_IDS = { + expiring: "brPopupExpiring", + expired: "brPopupExpired", + pending: "brPopupPendingVerify", + submitNotExpired: "brSubmitSuccessNotExpired", + submitExpired: "brSubmitSuccessExpired", +}; + +const TITLE_PREFIX = /^(?:)?\s*(Reminder|Action Required(?:\s*:[^<]*)?|Notice|【\s*溫馨提示\s*】|【\s*温馨提示\s*】|系統提示(?:[::][^<]*)?|系统提示(?:[::][^<]*)?)\s*(?:<\/strong>)?\s*[::]?\s*/i; + +function stripTags(html) { + return (html || "") + .replace(/<[^>]+>/g, " ") + .replace(/ /gi, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function splitTitleFromHtml(html, fallbackTitle) { + const raw = (html || "").trim(); + if (!raw) { + return { title: fallbackTitle, bodyHtml: "" }; + } + + const inner = raw + .replace(/^]*>/i, "") + .replace(/<\/p>\s*$/i, "") + .trim(); + + const brMatch = inner.match(//i); + if (brMatch && brMatch.index >= 0) { + const title = stripTags(inner.slice(0, brMatch.index)).replace(/[::]\s*$/, "").trim(); + const body = inner.slice(brMatch.index + brMatch[0].length).trim(); + return { + title: title || fallbackTitle, + bodyHtml: body ? `

${body}

` : "" + }; + } + + const prefix = inner.match(TITLE_PREFIX); + if (prefix) { + const title = stripTags(prefix[1]).replace(/[::]\s*$/, "").trim(); + const rest = inner.slice(prefix[0].length).trim(); + return { + title: title || fallbackTitle, + bodyHtml: rest ? `

${rest}

` : raw + }; + } + + return { title: fallbackTitle, bodyHtml: raw.startsWith("<") ? raw : `

${raw}

` }; +} + +export default function BrStatusDialog({ open, variant, expiryDate, onClose }) { + const intl = useIntl(); + const navigate = useNavigate(); + const messageId = MESSAGE_IDS[variant] || MESSAGE_IDS.expiring; + const titleId = TITLE_IDS[variant] || TITLE_IDS.expiring; + const formattedDate = expiryDate ? DateUtils.dateStr(expiryDate) : ""; + const html = (intl.formatMessage({ id: messageId, defaultMessage: "" }) || "") + .replaceAll("[BR_EXPIRY_DATE]", formattedDate); + const fallbackTitle = intl.formatMessage({ id: titleId, defaultMessage: "" }); + const { title, bodyHtml } = splitTitleFromHtml(html, fallbackTitle); + const showGo = variant === "expiring" || variant === "expired"; + + const goToSubmit = () => { + if (onClose) { + onClose(); + } + navigate("/org/submit-br"); + }; + + return ( + + + + {title} + + + + + + + {showGo ? + + : null} + + + + ); +} diff --git a/src/pages/Organization/DetailPage/OrganizationCard.js b/src/pages/Organization/DetailPage/OrganizationCard.js index ea398d78..706c4545 100644 --- a/src/pages/Organization/DetailPage/OrganizationCard.js +++ b/src/pages/Organization/DetailPage/OrganizationCard.js @@ -22,8 +22,10 @@ import Loadable from 'components/Loadable'; import { notifySaveSuccess } from 'utils/CommonFunction'; import { useIntl } from "react-intl"; import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; +import { GREY_CONTAINED_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; import { ThemeProvider } from "@emotion/react"; import { isGrantedAny } from "auth/utils"; +import FileList from "components/FileList"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import dayjs from "dayjs"; @@ -36,6 +38,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); const [afterSendPopUp, setAfterSendPopUp] = React.useState(false); + const [confirmBrPopUp, setConfirmBrPopUp] = React.useState(false); const [currentUserData, setCurrentUserData] = useState({}); const [overduePublicNotice, setOverduePublicNotice] = useState(0); @@ -246,6 +249,17 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { }); } + const confirmNewBr = () => { + setConfirmBrPopUp(false); + HttpUtils.post({ + url: UrlUtils.POST_ORG_CONFIRM_BR + "/" + id + "/confirm-br", + onSuccess: () => { + notifySaveSuccess(); + loadDataFun(); + } + }); + } + return ( { + {currentUserData.newBrSubmitted ? + + + + + + : null} { currentUserData.creditor ? @@ -324,7 +351,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { + + + + ); }; diff --git a/src/pages/Organization/DetailPage/OrganizationPubCard.js b/src/pages/Organization/DetailPage/OrganizationPubCard.js index 8368bbeb..fe5a460e 100644 --- a/src/pages/Organization/DetailPage/OrganizationPubCard.js +++ b/src/pages/Organization/DetailPage/OrganizationPubCard.js @@ -23,12 +23,14 @@ import { notifySaveSuccess } from 'utils/CommonFunction'; import { FormattedMessage, useIntl } from "react-intl"; import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; import { ThemeProvider } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; // ==============================|| DASHBOARD - DEFAULT ||============================== // const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { const intl = useIntl(); + const navigate = useNavigate(); const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); @@ -200,6 +202,19 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { + {currentUserData.canSubmitBr ? + + + + + + : null} } @@ -235,13 +250,15 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'expiryDate' }) + ":"), valueName: "brExpiryDate", disabled: true, - form: formik + form: formik, + displayValue: (!editMode && !createMode && currentUserData.brStatus) + ? `${formik.values.brExpiryDate || ""} (${currentUserData.brStatus === "Invalid" + ? intl.formatMessage({ id: "brStatusInvalid" }) + : intl.formatMessage({ id: "brStatusValid" })})`.trim() + : undefined })} - - - {FieldUtils.getTextField({ label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'nameEng' }) + ":"), diff --git a/src/pages/Organization/DetailPage/index.js b/src/pages/Organization/DetailPage/index.js index 36efc035..c1beec7e 100644 --- a/src/pages/Organization/DetailPage/index.js +++ b/src/pages/Organization/DetailPage/index.js @@ -90,6 +90,12 @@ const OrganizationDetailPage = () => { response.data["brExpiryDate"] = response.data.brExpiryDate ? DateUtils.dateValue(response.data.brExpiryDate) : ""; response.data["orgShortName"] = response.data.orgShortName ? response.data.orgShortName : "N/A" ; + response.data["newAddressLine1"] = response.data.newBrAddress?.addressLine1; + response.data["newAddressLine2"] = response.data.newBrAddress?.addressLine2; + response.data["newAddressLine3"] = response.data.newBrAddress?.addressLine3; + response.data["newDistrict"] = getObjectByType(ComboData.district, "type", response.data.newBrAddress?.district); + response.data["newCountry"] = getObjectByType(ComboData.country, "type", response.data.newBrAddress?.country); + response.data["brExpiryDateTemp"] = response.data.brExpiryDateTemp ? DateUtils.dateStr(response.data.brExpiryDateTemp) : ""; setFormData(response.data) setList(response.historyList) } diff --git a/src/pages/Organization/SearchPage/OrganizationTable.js b/src/pages/Organization/SearchPage/OrganizationTable.js index ace715ae..1064db6b 100644 --- a/src/pages/Organization/SearchPage/OrganizationTable.js +++ b/src/pages/Organization/SearchPage/OrganizationTable.js @@ -9,11 +9,13 @@ import { useNavigate } from "react-router-dom"; import * as DateUtils from "utils/DateUtils"; import { clickableLink} from 'utils/CommonFunction'; import {GET_ORG_PATH} from "utils/ApiPathConst"; +import { useIntl } from "react-intl"; // ==============================|| EVENT TABLE ||============================== // export default function OrganizationTable({ searchCriteria, applyGridOnReady, applySearch}) { const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); const navigate = useNavigate() + const intl = useIntl(); React.useEffect(() => { set_searchCriteria(searchCriteria); @@ -91,6 +93,34 @@ export default function OrganizationTable({ searchCriteria, applyGridOnReady, ap return DateUtils.dateValue(params?.value); } }, + { + id: 'brStatus', + field: 'brStatus', + headerName: 'BR Status', + flex: 1, + minWidth: 110, + valueGetter: (params) => { + const status = params?.row?.brStatus; + if (status === 'Invalid') { + return intl.formatMessage({ id: 'brStatusInvalid' }); + } + if (status === 'Valid') { + return intl.formatMessage({ id: 'brStatusValid' }); + } + return status || ''; + } + }, + { + id: 'newBrSubmitted', + field: 'newBrSubmitted', + headerName: intl.formatMessage({ id: 'newBr' }), + width: 120, + minWidth: 120, + valueGetter: (params) => { + const value = params?.value; + return value === true || value === 1 || value === '1' ? intl.formatMessage({ id: 'newBr' }) : ''; + } + }, { id: 'creditor', field: 'creditor', diff --git a/src/pages/Organization/SubmitBrPage/index.js b/src/pages/Organization/SubmitBrPage/index.js new file mode 100644 index 00000000..e2c791ba --- /dev/null +++ b/src/pages/Organization/SubmitBrPage/index.js @@ -0,0 +1,388 @@ +import { Grid, Typography, Stack, Box, Button, FormHelperText, CircularProgress } from '@mui/material'; +import * as React from "react"; +import { useFormik } from 'formik'; +import * as yup from 'yup'; +import * as HttpUtils from "utils/HttpUtils"; +import * as UrlUtils from "utils/ApiPathConst"; +import * as DateUtils from "utils/DateUtils"; +import * as FieldUtils from "utils/FieldUtils"; +import * as ComboData from "utils/ComboData"; +import { getObjectByType } from "utils/CommonFunction"; +import { isORGLoggedIn, isPrimaryLoggedIn } from "utils/Utils"; +import Loadable from "components/Loadable"; +import { lazy } from "react"; +import MainCard from "components/MainCard"; +import ForwardIcon from "@mui/icons-material/Forward"; +import { useNavigate } from "react-router-dom"; +import titleBackgroundImg from "assets/images/dashboard/gazette-bar.png"; +import { FormattedMessage, useIntl } from "react-intl"; +import usePageTitle from "components/usePageTitle"; +import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; +import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; +import { ThemeProvider } from "@emotion/react"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import dayjs from "dayjs"; +import { DemoItem } from "@mui/x-date-pickers/internals/demo"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material"; +import BrStatusDialog from "components/BrStatusDialog"; + +const LoadingComponent = Loadable(lazy(() => import("pages/extra-pages/LoadingComponent"))); +const UploadFileTable = Loadable(lazy(() => import("pages/Proof/Reply_Public/UploadFileTable"))); + +const BackgroundHead = { + backgroundImage: `url(${titleBackgroundImg})`, + width: "100%", + height: "100%", + backgroundSize: "contain", + backgroundRepeat: "no-repeat", + backgroundColor: "#0C489E", + backgroundPosition: "right" +}; + +const SubmitBrPage = () => { + usePageTitle("submitBrCertificate"); + const intl = useIntl(); + const navigate = useNavigate(); + const [formData, setFormData] = React.useState({}); + const [isLoading, setLoading] = React.useState(true); + const [errorMsg, setErrorMsg] = React.useState(""); + const [attachments, setAttachments] = React.useState([]); + const attachmentsRef = React.useRef([]); + attachmentsRef.current = attachments; + const [warningText, setWarningText] = React.useState(""); + const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); + const [showSubmittedDialog, setShowSubmittedDialog] = React.useState(false); + const fileInputRef = React.useRef(null); + const minDate = React.useMemo(() => new Date().setDate(new Date().getDate() + 1), []); + + React.useEffect(() => { + if (!isORGLoggedIn()) { + navigate("/dashboard"); + return; + } + HttpUtils.get({ + url: UrlUtils.GET_PUB_ORG_PATH, + onSuccess: (response) => { + const data = response.data || {}; + if (!data.canSubmitBr) { + navigate(isPrimaryLoggedIn() ? "/org" : "/dashboard"); + return; + } + data.country = getObjectByType(ComboData.country, "type", data.addressTemp?.country); + data.district = getObjectByType(ComboData.district, "type", data.addressTemp?.district); + data.addressLine1 = data.addressTemp?.addressLine1; + data.addressLine2 = data.addressTemp?.addressLine2; + data.addressLine3 = data.addressTemp?.addressLine3; + setFormData(data); + setLoading(false); + }, + onFail: () => { + navigate("/dashboard"); + }, + onError: () => { + navigate("/dashboard"); + } + }); + }, [navigate]); + + const displayErrorMsg = (msg) => {msg}; + + const getFileExtension = (fileName) => { + const name = (fileName || "").toLowerCase(); + const dot = name.lastIndexOf("."); + if (dot <= 0 || dot === name.length - 1) { + return ""; + } + return name.substring(dot + 1); + }; + + const isAcceptedFile = (fileName) => ["pdf", "jpg", "jpeg", "png"].includes(getFileExtension(fileName)); + + const initialValues = React.useMemo(() => ({ + ...formData, + brExpiryDate: null, + certificateFile: null, + }), [formData]); + + const formik = useFormik({ + enableReinitialize: true, + initialValues, + validationSchema: yup.object().shape({ + enCompanyName: yup.string().trim().max(150).required(displayErrorMsg(intl.formatMessage({ id: "userRequireEnglishName" }))), + chCompanyName: yup.string().max(150).nullable(), + addressLine1: yup.string().trim().max(40).required(displayErrorMsg(intl.formatMessage({ id: "validateAddressLine1" }))), + addressLine2: yup.string().max(40, displayErrorMsg(intl.formatMessage({ id: "noMoreThen40Words" }))), + addressLine3: yup.string().max(40, displayErrorMsg(intl.formatMessage({ id: "noMoreThen40Words" }))), + brExpiryDate: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "pleaseFillInBusinessRegCertValidityDate" }))), + certificateFile: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "requireValidFileWithProofReplyFormat" }))), + country: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "pleaseFillInCountry" }))), + district: yup.mixed().nullable().test( + "hk-district", + displayErrorMsg(intl.formatMessage({ id: "pleaseFillInDistrict" })), + function (value) { + const country = this.parent.country; + if (country && country.type === "hongKong") { + return value != null; + } + return true; + } + ), + }), + onSubmit: (values, { setSubmitting }) => { + setErrorMsg(""); + const files = attachmentsRef.current; + if (!values.brExpiryDate || !files || files.length === 0 || values.country == null + || (values.country.type === "hongKong" && values.district == null)) { + setSubmitting(false); + return; + } + return new Promise((resolve, reject) => { + HttpUtils.postWithFiles({ + url: UrlUtils.POST_PUB_ORG_SUBMIT_BR, + params: { + enCompanyName: values.enCompanyName, + chCompanyName: values.chCompanyName, + brExpiryDate: DateUtils.dateValue(values.brExpiryDate), + address: { + country: values.country.type, + district: values.district?.type, + addressLine1: values.addressLine1, + addressLine2: values.addressLine2, + addressLine3: values.addressLine3, + }, + }, + files: files, + onSuccess: (response) => { + if (response?.msg) { + setErrorMsg(intl.formatMessage({ id: response.msg, defaultMessage: response.msg })); + reject(); + return; + } + setShowSubmittedDialog(true); + resolve(); + }, + onFail: () => reject(), + onError: () => reject(), + }); + }); + } + }); + + const setCertificateFiles = (files) => { + const next = files || []; + setAttachments(next); + formik.setFieldValue("certificateFile", next[0] || null, true); + }; + + const handleSelectCertificate = (event) => { + const file = event.target.files[0]; + if (!file) { + return; + } + if (!isAcceptedFile(file.name)) { + setWarningText(intl.formatMessage({ id: "requireValidFileWithProofReplyFormat" })); + setIsWarningPopUp(true); + event.target.value = ""; + return; + } + if (file.size >= (10 * 1024 * 1034)) { + setWarningText(intl.formatMessage({ id: "fileSizeWarning" })); + setIsWarningPopUp(true); + event.target.value = ""; + return; + } + file.id = 0; + setCertificateFiles([file]); + event.target.value = ""; + }; + + return ( + isLoading ? + + + + : + + +
+ + + + + +
+
+ + + + + + +
+ + + + {errorMsg} + + + + {FieldUtils.getTextField({ + label: intl.formatMessage({ id: "brNo" }) + ":", + valueName: "brNo", + disabled: true, + form: formik + })} + + + + + {FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "newBrExpiryDate" }) + ":")} + + + + + 0)), + helperText: (formik.touched.brExpiryDate || formik.submitCount > 0) ? formik.errors.brExpiryDate : "", + }, + }} + format="DD/MM/YYYY" + value={formik.values.brExpiryDate == null ? null : dayjs(formik.values.brExpiryDate)} + minDate={minDate == null ? null : dayjs(minDate)} + onChange={(newValue) => { + if (DateUtils.dateValue(newValue) > DateUtils.dateValue(new Date())) { + formik.setFieldValue("brExpiryDate", newValue, true); + formik.setFieldTouched("brExpiryDate", true, false); + } + }} + /> + + + + + + + + {FieldUtils.getTextField({ + label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "nameEng" }) + ":"), + valueName: "enCompanyName", + form: formik + })} + + + {FieldUtils.getTextField({ + label: intl.formatMessage({ id: "nameChi" }) + ":", + valueName: "chCompanyName", + form: formik + })} + + + + {FieldUtils.getAddressField({ + label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "formAddress" }) + ":"), + valueName: ["addressLine1", "addressLine2", "addressLine3"], + form: formik + })} + + + {FieldUtils.getProfileComboField({ + label: "", + valueName: "district", + dataList: ComboData.district, + getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", + form: formik + })} + + + {FieldUtils.getProfileComboField({ + label: "", + valueName: "country", + disabled: true, + dataList: ComboData.country, + getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", + form: formik + })} + + + + + + + + + + + + {(formik.touched.certificateFile || formik.submitCount > 0) && formik.errors.certificateFile ? + {formik.errors.certificateFile} + : null} + + {attachments.length > 0 ? + + + + : null} + + + + + + +
+
+
+
+ setIsWarningPopUp(false)}> + + {warningText} + + + + + { + setShowSubmittedDialog(false); + navigate(isPrimaryLoggedIn() ? "/org" : "/dashboard"); + }} + /> +
+ ); +}; + +export default SubmitBrPage; diff --git a/src/pages/PublicNotice/ApplyForm/index.js b/src/pages/PublicNotice/ApplyForm/index.js index 76593ed5..ad79f667 100644 --- a/src/pages/PublicNotice/ApplyForm/index.js +++ b/src/pages/PublicNotice/ApplyForm/index.js @@ -5,6 +5,7 @@ import * as UrlUtils from "utils/ApiPathConst"; import * as FormatUtils from "utils/FormatUtils"; import * as DateUtils from "utils/DateUtils"; import { useIntl } from "react-intl"; +import { useNavigate } from "react-router-dom"; import { Grid, @@ -24,6 +25,7 @@ import { // checkIsOnlyOnlinePayment // isCreditorLoggedIn } from "utils/Utils"; +import { fetchOrgBrData, applyBlockVariant } from "utils/orgBrUtils"; // ==============================|| DASHBOARD - DEFAULT ||============================== // const ApplyForm = () => { @@ -35,12 +37,23 @@ const ApplyForm = () => { const [selections, setSelection] = React.useState([]); const [isLoading, setLoding] = React.useState(true); + const [orgBrReady, setOrgBrReady] = React.useState(false); const intl = useIntl(); + const navigate = useNavigate(); const { locale } = intl; React.useEffect(() => { loadUserData(); + fetchOrgBrData({ + onSuccess: (org) => { + if (applyBlockVariant(org)) { + navigate("/dashboard", { replace: true }); + return; + } + setOrgBrReady(true); + } + }); }, []); const loadUserData = () => { @@ -112,11 +125,10 @@ const ApplyForm = () => { React.useEffect(() => { - if (userData !== null){ + if (userData !== null && orgBrReady){ setLoding(false); - // console.log(isOnlyOnlinePayment) } - }, [userData]); + }, [userData, orgBrReady]); return ( isLoading ? diff --git a/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js b/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js index 859ad5c4..0c375a67 100644 --- a/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js +++ b/src/pages/PublicNotice/ListPanel/PendingPaymentTab.js @@ -398,9 +398,11 @@ export default function SubmittedTab({ setCount, url }) { <>
{isORGLoggedIn() ? - - - : + + + + : + { @@ -44,6 +46,8 @@ const PublicNotice = () => { const [selectedTab, setSelectedTab] = useState("1"); const navigate = useNavigate(); const intl = useIntl(); + const [orgBrData, setOrgBrData] = useState(null); + const [brDialog, setBrDialog] = useState({ open: false, variant: null, afterClose: null }); const _sx = { padding: "4 2 4 2", @@ -76,6 +80,9 @@ const PublicNotice = () => { useEffect(() => { loadData(); + fetchOrgBrData({ + onSuccess: (org) => setOrgBrData(org) + }); }, []); const loadData = () => { @@ -99,6 +106,15 @@ const PublicNotice = () => { } const onBtnClick = () => { + const variant = applyClickVariant(orgBrData); + if (variant) { + setBrDialog({ + open: true, + variant, + afterClose: applyBlockVariant(orgBrData) ? "stay" : "apply", + }); + return; + } navigate('/publicNotice/apply') } @@ -209,6 +225,18 @@ const PublicNotice = () => { ) } + { + const afterClose = brDialog.afterClose; + setBrDialog({ open: false, variant: null, afterClose: null }); + if (afterClose === "apply") { + navigate("/publicNotice/apply"); + } + }} + /> ); }; diff --git a/src/pages/Setting/SystemSetting/Table.js b/src/pages/Setting/SystemSetting/Table.js index 6b43df4b..45e82793 100644 --- a/src/pages/Setting/SystemSetting/Table.js +++ b/src/pages/Setting/SystemSetting/Table.js @@ -1,22 +1,51 @@ // material-ui import { - // Box, + Autocomplete, + TextField, Typography } from '@mui/material'; import MainCard from "components/MainCard"; import * as React from "react"; import { FiDataGrid } from "components/FiDataGrid"; -import { GET_SYS_PARAMS } from "utils/ApiPathConst"; +import { GET_SYS_PARAMS, GET_SYS_PARAM_NAMES } from "utils/ApiPathConst"; import SafeHtml from 'components/SafeHtml'; +import * as HttpUtils from "utils/HttpUtils"; +import { useIntl } from "react-intl"; +const LANG_SUFFIX = /\.(en|zh|cn)$/i; + +function toSearchKey(name) { + return String(name || "").replace(LANG_SUFFIX, ""); +} // ==============================|| DASHBOARD - DEFAULT ||============================== // const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { + const intl = useIntl(); const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); + const [nameOptions, setNameOptions] = React.useState([]); + const [selectedName, setSelectedName] = React.useState(""); React.useEffect(() => { - set_searchCriteria(searchCriteria); - }, [searchCriteria]); + HttpUtils.get({ + url: GET_SYS_PARAM_NAMES, + onSuccess: (responseData) => { + const keys = (Array.isArray(responseData) ? responseData : []) + .map((item) => toSearchKey(item?.label ?? item)) + .filter(Boolean); + setNameOptions([...new Set(keys)].sort((a, b) => a.localeCompare(b))); + } + }); + }, []); + + React.useEffect(() => { + const next = { ...searchCriteria }; + if (selectedName) { + next.name = selectedName; + } else { + delete next.name; + } + set_searchCriteria(next); + }, [searchCriteria, selectedName]); const columns = [ { @@ -61,6 +90,37 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { System Params + { + setSelectedName(newValue || ""); + }} + getOptionLabel={(option) => (option != null ? String(option) : "")} + isOptionEqualToValue={(option, value) => String(option) === String(value)} + sx={{ + mt: 2, + ml: 3, + mr: 3, + maxWidth: 480, + '& .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" })} + /> +
{/* */} import('./Message'))); const Notice = Loadable(React.lazy(() => import('./Notice'))); @@ -56,10 +58,21 @@ const DashboardDefault = () => { const [itemList, setItemList] = React.useState([]); const [listData, setListData] = React.useState([]); const [isPopUp, setIsPopUp] = React.useState(false); + const [orgBrData, setOrgBrData] = React.useState(null); + const [brDialog, setBrDialog] = React.useState({ open: false, variant: null }); React.useEffect(() => { loadMessageData() loadNoticeData() + fetchOrgBrData({ + onSuccess: (org) => { + setOrgBrData(org); + const variant = loginPopupVariant(org); + if (variant) { + setBrDialog({ open: true, variant, afterClose: "stay" }); + } + } + }); localStorage.setItem('searchCriteria',"") }, []); @@ -199,7 +212,18 @@ const DashboardDefault = () => {
+ { + const afterClose = brDialog.afterClose; + setBrDialog({ open: false, variant: null, afterClose: null }); + if (afterClose === "apply") { + navigate("/publicNotice/apply"); + } + }} + />
); }; diff --git a/src/routes/PublicUserRoutes.js b/src/routes/PublicUserRoutes.js index c3f614d9..bb1c049c 100644 --- a/src/routes/PublicUserRoutes.js +++ b/src/routes/PublicUserRoutes.js @@ -27,6 +27,7 @@ const DemandNote_Public = Loadable(lazy(() => import('pages/DemandNote/Search_Pu const UserMaintainPage_Individual = Loadable(lazy(() => import('pages/User/DetailsPage_Individual'))); const UserMaintainPage_Organization = Loadable(lazy(() => import('pages/User/DetailsPage_Organization'))); const OrganizationDetailPage = Loadable(lazy(() => import('pages/Organization/DetailPage'))); +const SubmitBrPage = Loadable(lazy(() => import('pages/Organization/SubmitBrPage'))); const Msg_Details = Loadable(lazy(() => import('pages/Message/Details'))); const Msg_Search = Loadable(lazy(() => import('pages/Message/Search'))); const AnnouncementSearch = Loadable(lazy(() => import('pages/Announcement/Search_Public'))); @@ -126,6 +127,10 @@ const PublicDashboard = { path: '/orgUser', element: }, + { + path: '/org/submit-br', + element: + }, { path: '/org', element: diff --git a/src/themes/colorConst.js b/src/themes/colorConst.js index 32074a43..cca92510 100644 --- a/src/themes/colorConst.js +++ b/src/themes/colorConst.js @@ -69,6 +69,21 @@ export const ERROR_CONTAINED_BUTTON_SX = { }, }; +/** WCAG 2.2 AA contained grey — matches PNSPS containedCancel (#616161 + white, ~5.7:1). */ +export const CONTAINED_NEUTRAL_GREY = '#616161'; + +export const GREY_CONTAINED_BUTTON_SX = { + backgroundColor: CONTAINED_NEUTRAL_GREY, + color: '#FFFFFF', + '&:hover': { + backgroundColor: '#545454', + }, + '&:focus-visible': { + outline: '2px solid #616161', + outlineOffset: '2px', + }, +}; + export const PRIMARY_CONTAINED_BUTTON_SX = { backgroundColor: CONTAINED_PRIMARY_BLUE, color: '#FFFFFF', diff --git a/src/translations/en.json b/src/translations/en.json index edb8a832..2943bd2d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -61,7 +61,7 @@ "MSG.registerIAmSmart": "You may click the \"iAM Smart\" button to fill the personal information automatically or enter the information manually to activate the PNSPS account now.
If you want to use \"iAM Smart\" to fill the personal information, please download the \"iAM Smart\" mobile app and register as an \"iAM Smart\" user first.", "MSG.registerPersonal": "To complete the online application, you need to upload digital copies of identification documents.
e.g. Hong Kong Identity Card, Passport, Mainland China Identity Card, Professional Practicing Certificate, etc.", - "MSG.registerOrg": "You need to upload the proof documents for the online application.
e.g. Business Registration Certificate, Professional Practicing Certificate, etc.", + "MSG.registerOrg": "You need to upload the Business Registration Certificate for the online application.", "MSG.paymentMsg": "Your application and payment have been received", "MSG.expiredApp": "Public Notice application has expired", @@ -286,10 +286,10 @@ "sameAsBusinessRegistrationCert": "Same as Business Registration Certificate", "businessRegCert": "Business Registration Certificate", "businessRegCertNumber": "Hong Kong Business Reg Cert Number", - "businessRegCertAndDoc":"Business Registration Certificate and other documents", + "businessRegCertAndDoc":"Business Registration Certificate", "businessRegCertExpiryDate": "Business registration certificate expiry date", - "pleaseUploadDoc": "Please upload a digital file of your valid business registration certificate and other documents to verify your identity.", - "uploadFile": "Upload business registration certificate and other documents", + "pleaseUploadDoc": "Please upload a digital file of your valid business registration certificate to verify your identity.", + "uploadFile": "Upload business registration certificate", "pleaseUploadIdDoc": "Please upload a digital file of your valid identity document to verify your identity.", "pleaseUploadIdDocSubTitle": "Such as: Hong Kong ID card; passport; Mainland China ID card; professional practice certificate, etc.", "uploadIdDoc": "Upload identity document", @@ -321,7 +321,7 @@ "pleaseFillInBusinessRegCertNumber": "Please fill in Business Registration Certificate Number", "pleaseFillInValidBusinessRegCertNumber": "Please fill in valid Business Registration Certificate Number", "businessRegCertValidityDate": "Business Reg Cert validity date", - "pleaseFillInBusinessRegCertValidityDate": "Please fill in Business Reg Cert validity date", + "pleaseFillInBusinessRegCertValidityDate": "Please fill in BR certificate validity date", "formAddress": "Address", "addressLine1": "First line of address", "addressLine2": "Second line of address", @@ -606,6 +606,24 @@ "nameEng": "Name (Eng)", "nameChi": "Name (Chi)", "expiryDate": "Expiry Date", + "brStatusValid": "Valid", + "brStatusInvalid": "Invalid", + "submitBrCertificate": "Submit Business Registration Certificate (BR)", + "goToSubmitBr": "Go to submit BR", + "brPopupTitleExpiring": "Reminder", + "brPopupTitleExpired": "Action Required", + "brPopupTitlePendingVerify": "Notice", + "brSubmitSuccessNotExpired": "Thank you for your BR's submission. Please wait for official approval, you may submit new public notice application before the expiry date.", + "brSubmitSuccessExpired": "Thank you for your BR's submission. Please wait for official approval before making any new application submissions.", + "selectCertificateFile": "Select Certificate File", + "newBrExpiryDate": "New BR Expiry Date", + "newBr": "New BR", + "newBrInformation": "New BR Information", + "confirmNewBr": "Confirm New BR", + "confirmNewBrMessage": "Confirm the new BR information and overwrite the existing organisation details?", + "brExpiredMsg": "Your company’s business registration has expired. Please upload a valid BR certificate.", + "brPendingVerifyMsg": "Your company has already submitted the BR certificate for verification.", + "uploadedFiles": "Uploaded Files", "create": "Create", "confirmTo": "Confirm to ", @@ -648,6 +666,8 @@ "connectionError": "Connection error. Please try again.", "downloadFailed": "Download failed. Please try again.", + "systemSettingName": "Name", + "muiClear": "Clear", "muiClose": "Close", "muiOpen": "Open", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 76f3ac2f..6079a6f9 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -102,7 +102,7 @@ "MSG.registerIAmSmart": "你可点击「智方便」按钮,系统会自动输入个人资料,或自行输入个人资料,以即时启动 公共启事提交及缴费系统 帐户。
如欲使用「智方便」提供个人资料,请先下载「智方便」流动应用程式并登记成为「智方便」用户。", "MSG.registerPersonal": "需上载身份证明文件数码档案以进行网上申请。
如:香港身份证; 护照; 中国内地身份证; 专业执业证书等", - "MSG.registerOrg": "需上载以下任何一份证明文件以进行网上申请。
如:商业登记证;专业执业证书", + "MSG.registerOrg": "需上载商业登记证以进行网上申请。", "MSG.paymentMsg": "你的申请和付款已收到", "MSG.expiredApp": "公共启事申请已过期", @@ -329,10 +329,10 @@ "sameAsBusinessRegistrationCert": "与商业登记证相同", "businessRegCert": "商业登记证", "businessRegCertNumber": "香港商业登记证号码", - "businessRegCertAndDoc":"商业登记证及其他文件", + "businessRegCertAndDoc":"商业登记证", "businessRegCertExpiryDate": "商业登记证有效期届满日期", - "pleaseUploadDoc": "请上传你的 有效商业登记证及其他文件 的数码档案,以验证你的身份。", - "uploadFile": "上传商业登记证及其他文件", + "pleaseUploadDoc": "请上传你的 有效商业登记证 的数码档案,以验证你的身份。", + "uploadFile": "上传商业登记证", "pleaseUploadIdDoc": "请上传你的 有效身份证明文件 的数码档案,以验证你的身份。", "pleaseUploadIdDocSubTitle": "如: 香港身份证; 护照; 中国内地身份证; 专业执业证书等", "uploadIdDoc": "上传身份证明文件", @@ -602,6 +602,24 @@ "nameEng": "名称 (英文)", "nameChi": "名称 (中文)", "expiryDate": "屆滿日期", + "brStatusValid": "有效", + "brStatusInvalid": "无效", + "submitBrCertificate": "提交商业登记证 (BR)", + "goToSubmitBr": "去提交证书", + "brPopupTitleExpiring": "温馨提示", + "brPopupTitleExpired": "系统提示", + "brPopupTitlePendingVerify": "系统提示", + "brSubmitSuccessNotExpired": "感谢您提交商业登记证,请等待官方审批通过,您仍可以在旧有商业登记证有效期限届满前提交新的公共启事申请。", + "brSubmitSuccessExpired": "感谢您提交商业登记证,请等待官方审批通过后,再提交新的公共启事申请。", + "selectCertificateFile": "选择证书档案", + "newBrExpiryDate": "新商业登记证届满日期", + "newBr": "新商业登记证", + "newBrInformation": "新商业登记证资料", + "confirmNewBr": "确认新商业登记证", + "confirmNewBrMessage": "确定以新的商业登记证资料覆盖现有机构资料?", + "brExpiredMsg": "贵司的商业登记证已过期。请上传有效的商业登记证。", + "brPendingVerifyMsg": "贵司已上传最新的商业登记证,请等待官方审批。", + "uploadedFiles": "已上传档案", "create": "创建", "confirmTo": "确定", @@ -644,6 +662,8 @@ "connectionError": "连接错误,请稍后再试。", "downloadFailed": "下载失败,请稍后再试。", + "systemSettingName": "名称", + "muiClear": "清除", "muiClose": "关闭", "muiOpen": "打开", diff --git a/src/translations/zh-HK.json b/src/translations/zh-HK.json index 11c20259..3cb8917c 100644 --- a/src/translations/zh-HK.json +++ b/src/translations/zh-HK.json @@ -102,7 +102,7 @@ "MSG.registerIAmSmart": "你可點擊「智方便」按鈕,系統會自動輸入個人資料,或自行輸入個人資料,以即時啟動 公共啟事提交及繳費系統 帳戶。
如欲使用「智方便」提供個人資料,請先下載「智方便」流動應用程式並登記成為「智方便」用戶。", "MSG.registerPersonal": "需上載身份證明文件數碼檔案以進行網上申請。
如:香港身份證; 護照; 中國內地身份證; 專業執業証書等", - "MSG.registerOrg": "需上載以下任何一份證明文件以進行網上申請。
如:商業登記證;專業執業證書", + "MSG.registerOrg": "需上載商業登記證以進行網上申請。", "MSG.paymentMsg": "你的申請和付款已收到", "MSG.expiredApp": "公共啟事申請已過期", @@ -324,13 +324,13 @@ "sameAsBusinessRegistrationCert": "與商業登記證相同", "businessRegCert": "商業登記證", "businessRegCertNumber": "香港商業登記證號碼", - "businessRegCertAndDoc":"商業登記證及其他文件", + "businessRegCertAndDoc":"商業登記證", "businessRegCertExpiryDate": "商業登記證有效期屆滿日期", - "pleaseUploadDoc": "請上傳你的 有效商業登記證及其他文件 的數碼檔案,以驗證你的身份。", + "pleaseUploadDoc": "請上傳你的 有效商業登記證 的數碼檔案,以驗證你的身份。", "pleaseUploadIdDoc": "請上傳你的 有效身份證明文件 的數碼檔案,以驗證你的身份。", "pleaseUploadIdDocSubTitle": "如: 香港身份證; 護照; 中國內地身份證; 專業執業証書等", "uploadIdDoc": "上傳身份證明文件", - "uploadFile": "上傳商業登記證及其他文件", + "uploadFile": "上傳商業登記證", "fileName": "檔案名稱", "forOrgUser": "機構/公司用戶", "forIndUser": "個人用戶", @@ -603,6 +603,24 @@ "nameEng": "名稱 (英文)", "nameChi": "名稱 (中文)", "expiryDate": "屆滿日期", + "brStatusValid": "有效", + "brStatusInvalid": "無效", + "submitBrCertificate": "提交商業登記證 (BR)", + "goToSubmitBr": "去提交證書", + "brPopupTitleExpiring": "溫馨提示", + "brPopupTitleExpired": "系統提示", + "brPopupTitlePendingVerify": "系統提示", + "brSubmitSuccessNotExpired": "感謝您提交商業登記證,請等待官方審批通過,您仍可以在舊有商業登記證有效期限屆滿前提交新的公共啟事申請。", + "brSubmitSuccessExpired": "感謝您提交商業登記證,請等待官方審批通過後,再提交新的公共啟事申請。", + "selectCertificateFile": "選擇證書檔案", + "newBrExpiryDate": "新商業登記證屆滿日期", + "newBr": "新商業登記證", + "newBrInformation": "新商業登記證資料", + "confirmNewBr": "確認新商業登記證", + "confirmNewBrMessage": "確定以新的商業登記證資料覆寫現有機構資料?", + "brExpiredMsg": "貴司的商業登記證已過期。請上傳有效的商業登記證。", + "brPendingVerifyMsg": "貴司已上傳最新的商業登記證,請等待官方審批。", + "uploadedFiles": "已上載檔案", "create": "創建", "confirmTo": "確定", @@ -645,6 +663,8 @@ "connectionError": "連線錯誤,請稍後再試。", "downloadFailed": "下載失敗,請稍後再試。", + "systemSettingName": "名稱", + "muiClear": "清除", "muiClose": "關閉", "muiOpen": "開啟", diff --git a/src/utils/ApiPathConst.js b/src/utils/ApiPathConst.js index 4a1059f7..7cb9b5f5 100644 --- a/src/utils/ApiPathConst.js +++ b/src/utils/ApiPathConst.js @@ -9,6 +9,7 @@ export const LOGOUT = "/logout" export const CHANGE_PASSWORD_PATH = "/user/change-password" export const GET_SYS_PARAMS = apiPath+'/settings'; +export const GET_SYS_PARAM_NAMES = apiPath+'/settings/combo-name'; export const PRIVACY_POLICY_PATH = apiPath+'/privacyPolicy'; export const UPDATE_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/update-payment-suspension'; export const GET_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/get-payment-suspension'; @@ -59,6 +60,8 @@ export const GET_SEND_OVERDUE_CREDITOR_LIST = apiPath+'/org/sendDn_OverdueCredit //public export const GET_PUB_ORG_PATH = apiPath+'/org/pub'; export const POST_PUB_ORG_SAVE_PATH = apiPath+'/org/pub/save'; +export const POST_PUB_ORG_SUBMIT_BR = apiPath+'/org/pub/submit-br'; +export const POST_ORG_CONFIRM_BR = apiPath+'/org'; export const GET_PUB_ORG_MARK_AS_CREDITOR = apiPath+'/org/pub/mark-as-creditor'; export const GET_PUB_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/pub/mark-as-non-creditor'; diff --git a/src/utils/FieldUtils.js b/src/utils/FieldUtils.js index 762d71a8..5ab15eab 100644 --- a/src/utils/FieldUtils.js +++ b/src/utils/FieldUtils.js @@ -26,7 +26,7 @@ export const getDateField = ({ label, valueName, form, disabled }) => {
; } -export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => { +export const getTextField = ({ label, valueName, form, disabled, autoFocus, displayValue }) => { return @@ -40,7 +40,8 @@ export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => valueName: valueName, form: form, disabled: disabled, - autoFocus:autoFocus + autoFocus:autoFocus, + displayValue: displayValue })} @@ -233,7 +234,7 @@ export const getProfileComboField = ({ label, dataList, valueName, form, disable ; } -export const initField = ({ type, valueName, form, disabled, autoFocus, multiline, handleChange, placeholder, inputProps, InputProps, width, ...props }) => { +export const initField = ({ type, valueName, form, disabled, autoFocus, multiline, handleChange, placeholder, inputProps, InputProps, width, displayValue, ...props }) => { let err = Boolean(form.errors[valueName]); return { + if (!isORGLoggedIn()) { + onSuccess(null); + return; + } + HttpUtils.get({ + url: GET_PUB_ORG_PATH, + onSuccess: (response) => onSuccess(response?.data || null), + onFail: () => onSuccess(null), + onError: () => onSuccess(null), + }); +}; + +export const applyBlockVariant = (org) => { + if (!org) { + return null; + } + if (org.applyBlockState === "expired") { + return "expired"; + } + if (org.applyBlockState === "pending") { + return "pending"; + } + return null; +}; + +export const applyClickVariant = (org) => { + if (!org) { + return null; + } + const blocked = applyBlockVariant(org); + if (blocked) { + return blocked; + } + if (org.showExpiringPopup) { + return "expiring"; + } + return null; +}; + +export const loginPopupVariant = (org) => { + if (!org) { + return null; + } + if (org.applyBlockState === "expired") { + return "expired"; + } + if (org.showExpiringPopup) { + return "expiring"; + } + return null; +};