| @@ -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 = /^(?:<strong>)?\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(/^<p[^>]*>/i, "") | |||
| .replace(/<\/p>\s*$/i, "") | |||
| .trim(); | |||
| const brMatch = inner.match(/<br\s*\/?>/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 ? `<p>${body}</p>` : "" | |||
| }; | |||
| } | |||
| 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 ? `<p>${rest}</p>` : raw | |||
| }; | |||
| } | |||
| return { title: fallbackTitle, bodyHtml: raw.startsWith("<") ? raw : `<p>${raw}</p>` }; | |||
| } | |||
| 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 ( | |||
| <Dialog | |||
| open={Boolean(open && variant)} | |||
| onClose={onClose} | |||
| maxWidth={false} | |||
| PaperProps={{ | |||
| sx: { | |||
| width: { xs: "90vw", md: "40vw" }, | |||
| maxWidth: { xs: "90vw", md: "40vw" }, | |||
| maxHeight: { xs: "90vh", md: "70vh" }, | |||
| overflow: "hidden" | |||
| } | |||
| }} | |||
| > | |||
| <DialogTitle | |||
| sx={{ | |||
| bgcolor: HEADER_BACKGROUND_COLOR, | |||
| color: "#FFFFFF", | |||
| py: 1, | |||
| px: 3 | |||
| }} | |||
| > | |||
| <Typography component="h2" variant="h6" sx={{ color: "#FFFFFF", fontWeight: 600 }}> | |||
| {title} | |||
| </Typography> | |||
| </DialogTitle> | |||
| <DialogContent sx={{ px: 3, pt: 2, pb: 2, "&.MuiDialogContent-root": { paddingTop: 2 }, "& p:first-of-type": { mt: 0 } }}> | |||
| <SafeHtml html={bodyHtml} /> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| {showGo ? | |||
| <Button variant="contained" onClick={goToSubmit} sx={PRIMARY_CONTAINED_BUTTON_SX}> | |||
| <Typography variant="h5"><FormattedMessage id="goToSubmitBr" /></Typography> | |||
| </Button> | |||
| : null} | |||
| <Button variant="contained" onClick={onClose} sx={GREY_CONTAINED_BUTTON_SX}> | |||
| <Typography variant="h5"><FormattedMessage id="close" /></Typography> | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| ); | |||
| } | |||
| @@ -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 ( | |||
| <MainCard elevation={0} | |||
| border={false} | |||
| @@ -316,6 +330,19 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| {currentUserData.newBrSubmitted ? | |||
| <Grid item sx={{ ml: 3, mr: 3 }}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| onClick={() => setConfirmBrPopUp(true)} | |||
| > | |||
| {intl.formatMessage({ id: "confirmNewBr" })} | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| : null} | |||
| { | |||
| currentUserData.creditor ? | |||
| @@ -324,7 +351,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="error" | |||
| color="success" | |||
| onClick={() => setNonCreditorConfirmPopUp(true)} | |||
| > | |||
| Mark as Non-Credit Client | |||
| @@ -338,7 +365,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="orange" | |||
| color="success" | |||
| onClick={() => setCreditorConfirmPopUp(true)} | |||
| > | |||
| Mark as Credit Client | |||
| @@ -351,7 +378,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| color="success" | |||
| onClick={() => sendDn_Overdue(true)} | |||
| > | |||
| Generate O/S DN List | |||
| @@ -440,10 +467,16 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <TextField | |||
| fullWidth | |||
| id="currentExDate" | |||
| // error={(fromDate===null)} | |||
| // type="date" | |||
| name="currentExDate" | |||
| value={fromDate != null ? DateUtils.dateStr(fromDate) : DateUtils.dateStr(currentFromDate)} | |||
| value={(() => { | |||
| const dateStr = fromDate != null ? DateUtils.dateStr(fromDate) : DateUtils.dateStr(currentFromDate); | |||
| const statusLabel = currentUserData.brStatus === "Invalid" | |||
| ? intl.formatMessage({ id: "brStatusInvalid" }) | |||
| : currentUserData.brStatus === "Valid" | |||
| ? intl.formatMessage({ id: "brStatusValid" }) | |||
| : ""; | |||
| return statusLabel ? `${dateStr} (${statusLabel})` : dateStr; | |||
| })()} | |||
| disabled={true} | |||
| /> : | |||
| <LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}> | |||
| @@ -480,9 +513,6 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| { | |||
| fromDate == null ? | |||
| (!editMode && !createMode) ? | |||
| // <FormHelperText error id="helper-text-date"> | |||
| // Please select a date after today. | |||
| // </FormHelperText> | |||
| null | |||
| : | |||
| <FormHelperText error id="helper-text-date"> | |||
| @@ -575,6 +605,81 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| })} | |||
| </Grid> | |||
| {currentUserData.newBrSubmitted ? | |||
| <> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h4" sx={{ mb: 2, mr: 3, mt: 2, borderBottom: "1px solid black" }}> | |||
| {intl.formatMessage({ id: "newBrInformation" })} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel("Name (Eng):"), | |||
| valueName: "enCompanyNameTemp", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: "Name (Ch):", | |||
| valueName: "chCompanyNameTemp", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel("Expiry Date:"), | |||
| valueName: "brExpiryDateTemp", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getAddressField({ | |||
| label: FieldUtils.notNullFieldLabel("Address:"), | |||
| valueName: ["newAddressLine1", "newAddressLine2", "newAddressLine3"], | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "newDistrict", | |||
| disabled: true, | |||
| dataList: ComboData.district, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "newCountry", | |||
| disabled: true, | |||
| dataList: ComboData.country, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| </> | |||
| : null} | |||
| {id > 0 ? | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| <Typography variant="h4" sx={{ mb: 2, mr: 3, borderBottom: "1px solid black" }}> | |||
| {intl.formatMessage({ id: "uploadedFiles" })} | |||
| </Typography> | |||
| <FileList | |||
| key={`${id}-${currentUserData.newBrSubmitted}-${currentUserData.brExpiryDateTemp || ""}`} | |||
| refType={"orgBrFile"} | |||
| refId={id} | |||
| /> | |||
| </Grid> | |||
| : null} | |||
| <Grid item lg={12} ></Grid> | |||
| </Grid> | |||
| @@ -668,6 +773,32 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| </DialogActions> | |||
| </Dialog> | |||
| </div> | |||
| <div> | |||
| <Dialog | |||
| open={confirmBrPopUp} | |||
| onClose={() => setConfirmBrPopUp(false)} | |||
| PaperProps={{ | |||
| sx: { | |||
| minWidth: '40vw', | |||
| maxWidth: { xs: '90vw', md: '70vw', lg: '70vw' }, | |||
| maxHeight: { xs: '90vh', md: '70vh', lg: '60vh' } | |||
| } | |||
| }} | |||
| > | |||
| <DialogTitle><Typography variant="h3">{intl.formatMessage({ id: "confirmNewBr" })}</Typography></DialogTitle> | |||
| <DialogContent style={{ display: 'flex' }}> | |||
| <Typography variant="h4" style={{ padding: '16px' }}>{intl.formatMessage({ id: "confirmNewBrMessage" })}</Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button variant="contained" onClick={() => setConfirmBrPopUp(false)} sx={GREY_CONTAINED_BUTTON_SX}> | |||
| <Typography variant="h5">{intl.formatMessage({ id: "close" })}</Typography> | |||
| </Button> | |||
| <Button variant="contained" onClick={() => confirmNewBr()} sx={PRIMARY_CONTAINED_BUTTON_SX}> | |||
| <Typography variant="h5">{intl.formatMessage({ id: "confirmNewBr" })}</Typography> | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| </div> | |||
| </MainCard> | |||
| ); | |||
| }; | |||
| @@ -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 }) => { | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| {currentUserData.canSubmitBr ? | |||
| <Grid item sx={{ ml: 3, mr: 3 }}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => navigate("/org/submit-br")} | |||
| color="success" | |||
| > | |||
| <FormattedMessage id="submitBrCertificate" /> | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| : null} | |||
| </> | |||
| } | |||
| </Grid> | |||
| @@ -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 | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4} ></Grid> | |||
| <Grid item xs={12} lg={4} > | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'nameEng' }) + ":"), | |||
| @@ -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) | |||
| } | |||
| @@ -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', | |||
| @@ -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) => <Typography variant="errorMessage1">{msg}</Typography>; | |||
| 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 ? | |||
| <Grid container sx={{ minHeight: "87vh", mb: 3 }} direction="column" justifyContent="center" alignItems="center"> | |||
| <Grid item><LoadingComponent /></Grid> | |||
| </Grid> | |||
| : | |||
| <Grid container direction="column" sx={{ minHeight: "87vh", backgroundColor: "#ffffff" }}> | |||
| <Grid item xs={12}> | |||
| <div style={BackgroundHead}> | |||
| <Stack direction="row" height="70px" justifyContent="flex-start" alignItems="center"> | |||
| <Typography component="h1" ml={15} color="#FFF" variant="h4" sx={{ display: { xs: "none", md: "block" } }}> | |||
| <FormattedMessage id="submitBrCertificate" /> | |||
| </Typography> | |||
| </Stack> | |||
| </div> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Button aria-label={intl.formatMessage({ id: "back" })} title="Back" sx={{ ml: 3.5, mt: 2 }} style={{ border: "2px solid" }} variant="outlined" onClick={() => navigate(-1)}> | |||
| <ForwardIcon style={{ height: 30, width: 50, transform: "rotate(180deg)" }} /> | |||
| </Button> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ p: 1, borderRadius: "10px" }}> | |||
| <MainCard elevation={0} border={false} content={false}> | |||
| <form onSubmit={formik.handleSubmit} noValidate style={{ padding: 24 }}> | |||
| <Grid container spacing={1}> | |||
| <Grid item xs={12}> | |||
| <FormHelperText error> | |||
| <Typography variant="errorMessage1">{errorMsg}</Typography> | |||
| </FormHelperText> | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: intl.formatMessage({ id: "brNo" }) + ":", | |||
| valueName: "brNo", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| <Grid container alignItems={"center"}> | |||
| <Grid item xs={12} md={3} lg={3} sx={{ display: "flex", alignItems: "center" }}> | |||
| <Typography variant="pnspsFormParagraphBold">{FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "newBrExpiryDate" }) + ":")}</Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6} lg={6}> | |||
| <LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}> | |||
| <DemoItem components={["DatePicker"]}> | |||
| <DatePicker | |||
| id="brExpiryDate" | |||
| name="brExpiryDate" | |||
| slotProps={{ | |||
| field: { readOnly: true }, | |||
| textField: { | |||
| error: Boolean(formik.errors.brExpiryDate && (formik.touched.brExpiryDate || formik.submitCount > 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); | |||
| } | |||
| }} | |||
| /> | |||
| </DemoItem> | |||
| </LocalizationProvider> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}></Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "nameEng" }) + ":"), | |||
| valueName: "enCompanyName", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: intl.formatMessage({ id: "nameChi" }) + ":", | |||
| valueName: "chCompanyName", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}></Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getAddressField({ | |||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "formAddress" }) + ":"), | |||
| valueName: ["addressLine1", "addressLine2", "addressLine3"], | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "district", | |||
| dataList: ComboData.district, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "country", | |||
| disabled: true, | |||
| dataList: ComboData.country, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Typography variant="subtitle1" sx={{ color: "primary.primary" }}> | |||
| <FormattedMessage id="acceptProofReplyFileFormats" /> | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <input | |||
| ref={fileInputRef} | |||
| id="uploadFileBtn" | |||
| type="file" | |||
| accept=".pdf,.jpg,.jpeg,.png,image/png,image/jpeg,application/pdf" | |||
| hidden | |||
| onChange={handleSelectCertificate} | |||
| /> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| type="button" | |||
| sx={PRIMARY_CONTAINED_BUTTON_SX} | |||
| onClick={() => fileInputRef.current && fileInputRef.current.click()} | |||
| > | |||
| <FormattedMessage id="selectCertificateFile" /> | |||
| </Button> | |||
| </ThemeProvider> | |||
| {(formik.touched.certificateFile || formik.submitCount > 0) && formik.errors.certificateFile ? | |||
| <FormHelperText error>{formik.errors.certificateFile}</FormHelperText> | |||
| : null} | |||
| </Grid> | |||
| {attachments.length > 0 ? | |||
| <Grid item xs={12} md={8}> | |||
| <UploadFileTable recordList={attachments} setRecordList={setCertificateFiles} /> | |||
| </Grid> | |||
| : null} | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| type="submit" | |||
| color="success" | |||
| disabled={formik.isSubmitting || showSubmittedDialog} | |||
| startIcon={formik.isSubmitting ? <CircularProgress color="inherit" size={18} /> : null} | |||
| > | |||
| <FormattedMessage id="submit" /> | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| </Grid> | |||
| </form> | |||
| </MainCard> | |||
| </Box> | |||
| </Grid> | |||
| <Dialog open={isWarningPopUp} onClose={() => setIsWarningPopUp(false)}> | |||
| <DialogTitle><FormattedMessage id="attention" /></DialogTitle> | |||
| <DialogContent><Typography>{warningText}</Typography></DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={() => setIsWarningPopUp(false)}><FormattedMessage id="close" /></Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| <BrStatusDialog | |||
| open={showSubmittedDialog} | |||
| variant={formData.brStatus === "Invalid" || formData.applyBlockState === "expired" | |||
| ? "submitExpired" | |||
| : "submitNotExpired"} | |||
| expiryDate={formData.brExpiryDate} | |||
| onClose={() => { | |||
| setShowSubmittedDialog(false); | |||
| navigate(isPrimaryLoggedIn() ? "/org" : "/dashboard"); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default SubmitBrPage; | |||
| @@ -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 ? | |||
| @@ -398,9 +398,11 @@ export default function SubmittedTab({ setCount, url }) { | |||
| <> | |||
| <div style={{ minHeight: 400, width: '100%', padding: 4 }}> | |||
| {isORGLoggedIn() ? | |||
| <Grid container direction="row" justifyContent="flex-start" alignItems="center" > | |||
| <Grid item xs={3} md={1}> | |||
| <Typography variant="h5"><FormattedMessage id="careOf" />:</Typography> | |||
| <Grid container direction="row" justifyContent="flex-start" alignItems="center" columnSpacing={1} sx={{ mb: 1 }}> | |||
| <Grid item> | |||
| <Typography variant="h5" sx={{ whiteSpace: 'nowrap' }}> | |||
| <FormattedMessage id="careOf" />: | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={8} md={2}> | |||
| <Autocomplete | |||
| @@ -32,6 +32,8 @@ import { PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import usePageTitle from 'components/usePageTitle'; | |||
| import { fetchOrgBrData, applyBlockVariant, applyClickVariant } from "utils/orgBrUtils"; | |||
| import BrStatusDialog from "components/BrStatusDialog"; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const PublicNotice = () => { | |||
| @@ -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 = () => { | |||
| ) | |||
| } | |||
| <BrStatusDialog | |||
| open={brDialog.open} | |||
| variant={brDialog.variant} | |||
| expiryDate={orgBrData?.brExpiryDate} | |||
| onClose={() => { | |||
| const afterClose = brDialog.afterClose; | |||
| setBrDialog({ open: false, variant: null, afterClose: null }); | |||
| if (afterClose === "apply") { | |||
| navigate("/publicNotice/apply"); | |||
| } | |||
| }} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| @@ -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 | |||
| </Typography> | |||
| <Autocomplete | |||
| disablePortal | |||
| options={nameOptions} | |||
| value={selectedName || null} | |||
| onChange={(event, newValue) => { | |||
| 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) => ( | |||
| <TextField | |||
| {...params} | |||
| label={intl.formatMessage({ id: "systemSettingName" })} | |||
| InputLabelProps={{ shrink: true }} | |||
| /> | |||
| )} | |||
| clearText={intl.formatMessage({ id: "muiClear" })} | |||
| closeText={intl.formatMessage({ id: "muiClose" })} | |||
| openText={intl.formatMessage({ id: "muiOpen" })} | |||
| noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} | |||
| /> | |||
| <div style={{ width: { xs: '92vw', sm: '96.5vw', md: "auto" }, }}> | |||
| {/* <Box width= '100%' sx={{ backgroundColor: "#fff", ml: 2 }} height='100%'> */} | |||
| <FiDataGrid | |||
| @@ -20,6 +20,8 @@ import * as HttpUtils from "utils/HttpUtils"; | |||
| import * as UrlUtils from "utils/ApiPathConst"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import { checkPaymentSuspension } from "utils/Utils"; | |||
| import { fetchOrgBrData, applyBlockVariant, applyClickVariant, loginPopupVariant } from "utils/orgBrUtils"; | |||
| import BrStatusDialog from "components/BrStatusDialog"; | |||
| const Message = Loadable(React.lazy(() => 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 = () => { | |||
| <Grid item xs={12} lg={5} sx={{ pt: 2 }} style={{ height: '100%' }}> | |||
| <Button | |||
| xs={12} | |||
| onClick={() => { navigate("/publicNotice/apply"); }} | |||
| onClick={() => { | |||
| const variant = applyClickVariant(orgBrData); | |||
| if (variant) { | |||
| setBrDialog({ | |||
| open: true, | |||
| variant, | |||
| afterClose: applyBlockVariant(orgBrData) ? "stay" : "apply", | |||
| }); | |||
| return; | |||
| } | |||
| navigate("/publicNotice/apply"); | |||
| }} | |||
| aria-label={intl.formatMessage({ id: 'submitApplication' })} | |||
| sx={{ | |||
| width: "100%", | |||
| @@ -306,6 +330,18 @@ const DashboardDefault = () => { | |||
| </DialogActions> | |||
| </Dialog> | |||
| </div> | |||
| <BrStatusDialog | |||
| open={brDialog.open} | |||
| variant={brDialog.variant} | |||
| expiryDate={orgBrData?.brExpiryDate} | |||
| onClose={() => { | |||
| const afterClose = brDialog.afterClose; | |||
| setBrDialog({ open: false, variant: null, afterClose: null }); | |||
| if (afterClose === "apply") { | |||
| navigate("/publicNotice/apply"); | |||
| } | |||
| }} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| @@ -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: <UserMaintainPage_Organization /> | |||
| }, | |||
| { | |||
| path: '/org/submit-br', | |||
| element: <SubmitBrPage /> | |||
| }, | |||
| { | |||
| path: '/org', | |||
| element: <OrganizationDetailPage /> | |||
| @@ -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', | |||
| @@ -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.<br/>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.<br/>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.<br/>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", | |||
| @@ -102,7 +102,7 @@ | |||
| "MSG.registerIAmSmart": "你可点击「智方便」按钮,系统会自动输入个人资料,或自行输入个人资料,以即时启动 公共启事提交及缴费系统 帐户。<br/>如欲使用「智方便」提供个人资料,请先下载「智方便」流动应用程式并登记成为「智方便」用户。", | |||
| "MSG.registerPersonal": "需上载身份证明文件数码档案以进行网上申请。 <br/>如:香港身份证; 护照; 中国内地身份证; 专业执业证书等", | |||
| "MSG.registerOrg": "需上载以下任何一份证明文件以进行网上申请。 <br/>如:商业登记证;专业执业证书", | |||
| "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": "打开", | |||
| @@ -102,7 +102,7 @@ | |||
| "MSG.registerIAmSmart": "你可點擊「智方便」按鈕,系統會自動輸入個人資料,或自行輸入個人資料,以即時啟動 公共啟事提交及繳費系統 帳戶。<br/>如欲使用「智方便」提供個人資料,請先下載「智方便」流動應用程式並登記成為「智方便」用戶。", | |||
| "MSG.registerPersonal": "需上載身份證明文件數碼檔案以進行網上申請。<br/>如:香港身份證; 護照; 中國內地身份證; 專業執業証書等", | |||
| "MSG.registerOrg": "需上載以下任何一份證明文件以進行網上申請。<br/>如:商業登記證;專業執業證書", | |||
| "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": "開啟", | |||
| @@ -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'; | |||
| @@ -26,7 +26,7 @@ export const getDateField = ({ label, valueName, form, disabled }) => { | |||
| </Grid>; | |||
| } | |||
| export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => { | |||
| export const getTextField = ({ label, valueName, form, disabled, autoFocus, displayValue }) => { | |||
| return <Grid container alignItems={"center"} sx={{mb:2}}> | |||
| <Grid item xs={12} sm={12} md={12} lg={12}> | |||
| <Grid container spacing={1}> | |||
| @@ -40,7 +40,8 @@ export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => | |||
| valueName: valueName, | |||
| form: form, | |||
| disabled: disabled, | |||
| autoFocus:autoFocus | |||
| autoFocus:autoFocus, | |||
| displayValue: displayValue | |||
| })} | |||
| </Grid> | |||
| </Grid> | |||
| @@ -233,7 +234,7 @@ export const getProfileComboField = ({ label, dataList, valueName, form, disable | |||
| </Grid>; | |||
| } | |||
| 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 <TextField | |||
| @@ -248,7 +249,7 @@ export const initField = ({ type, valueName, form, disabled, autoFocus, multilin | |||
| error={err} | |||
| helperText={form.errors[valueName] ? form.errors[valueName] : ''} | |||
| onChange={handleChange ? handleChange : form.handleChange} | |||
| value={form.values[valueName]} | |||
| value={displayValue !== undefined ? displayValue : form.values[valueName]} | |||
| disabled={disabled} | |||
| autoFocus={autoFocus} | |||
| sx={{ | |||
| @@ -0,0 +1,56 @@ | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import { GET_PUB_ORG_PATH } from "utils/ApiPathConst"; | |||
| import { isORGLoggedIn } from "utils/Utils"; | |||
| export const fetchOrgBrData = ({ onSuccess }) => { | |||
| 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; | |||
| }; | |||