| @@ -29,9 +29,10 @@ | |||||
| "@testing-library/user-event": "^14.4.3", | "@testing-library/user-event": "^14.4.3", | ||||
| "@types/react-input-mask": "^3.0.2", | "@types/react-input-mask": "^3.0.2", | ||||
| "apexcharts": "^3.35.5", | "apexcharts": "^3.35.5", | ||||
| "axios": "^1.12.2", | |||||
| "axios": "^1.20.0", | |||||
| "date-fns": "^3.0.6", | "date-fns": "^3.0.6", | ||||
| "dayjs": "^1.11.9", | "dayjs": "^1.11.9", | ||||
| "dompurify": "^3.4.15", | |||||
| "formik": "^2.2.9", | "formik": "^2.2.9", | ||||
| "framer-motion": "^7.3.6", | "framer-motion": "^7.3.6", | ||||
| "history": "^5.3.0", | "history": "^5.3.0", | ||||
| @@ -6548,13 +6549,14 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/axios": { | "node_modules/axios": { | ||||
| "version": "1.15.0", | |||||
| "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", | |||||
| "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", | |||||
| "version": "1.20.0", | |||||
| "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", | |||||
| "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", | |||||
| "license": "MIT", | "license": "MIT", | ||||
| "dependencies": { | "dependencies": { | ||||
| "follow-redirects": "^1.15.11", | |||||
| "form-data": "^4.0.5", | |||||
| "follow-redirects": "^1.16.0", | |||||
| "form-data": "^4.0.6", | |||||
| "https-proxy-agent": "^5.0.1", | |||||
| "proxy-from-env": "^2.1.0" | "proxy-from-env": "^2.1.0" | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -8419,6 +8421,15 @@ | |||||
| } | } | ||||
| ] | ] | ||||
| }, | }, | ||||
| "node_modules/dompurify": { | |||||
| "version": "3.4.15", | |||||
| "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", | |||||
| "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", | |||||
| "license": "(MPL-2.0 OR Apache-2.0)", | |||||
| "optionalDependencies": { | |||||
| "@types/trusted-types": "^2.0.7" | |||||
| } | |||||
| }, | |||||
| "node_modules/domutils": { | "node_modules/domutils": { | ||||
| "version": "1.7.0", | "version": "1.7.0", | ||||
| "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", | ||||
| @@ -9916,15 +9927,16 @@ | |||||
| "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" | "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" | ||||
| }, | }, | ||||
| "node_modules/follow-redirects": { | "node_modules/follow-redirects": { | ||||
| "version": "1.15.11", | |||||
| "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", | |||||
| "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", | |||||
| "version": "1.16.0", | |||||
| "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", | |||||
| "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", | |||||
| "funding": [ | "funding": [ | ||||
| { | { | ||||
| "type": "individual", | "type": "individual", | ||||
| "url": "https://github.com/sponsors/RubenVerborgh" | "url": "https://github.com/sponsors/RubenVerborgh" | ||||
| } | } | ||||
| ], | ], | ||||
| "license": "MIT", | |||||
| "engines": { | "engines": { | ||||
| "node": ">=4.0" | "node": ">=4.0" | ||||
| }, | }, | ||||
| @@ -10090,16 +10102,16 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/form-data": { | "node_modules/form-data": { | ||||
| "version": "4.0.5", | |||||
| "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", | |||||
| "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", | |||||
| "version": "4.0.6", | |||||
| "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", | |||||
| "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", | |||||
| "license": "MIT", | "license": "MIT", | ||||
| "dependencies": { | "dependencies": { | ||||
| "asynckit": "^0.4.0", | "asynckit": "^0.4.0", | ||||
| "combined-stream": "^1.0.8", | "combined-stream": "^1.0.8", | ||||
| "es-set-tostringtag": "^2.1.0", | "es-set-tostringtag": "^2.1.0", | ||||
| "hasown": "^2.0.2", | |||||
| "mime-types": "^2.1.12" | |||||
| "hasown": "^2.0.4", | |||||
| "mime-types": "^2.1.35" | |||||
| }, | }, | ||||
| "engines": { | "engines": { | ||||
| "node": ">= 6" | "node": ">= 6" | ||||
| @@ -10598,9 +10610,10 @@ | |||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/hasown": { | "node_modules/hasown": { | ||||
| "version": "2.0.2", | |||||
| "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", | |||||
| "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", | |||||
| "version": "2.0.4", | |||||
| "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", | |||||
| "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", | |||||
| "license": "MIT", | |||||
| "dependencies": { | "dependencies": { | ||||
| "function-bind": "^1.1.2" | "function-bind": "^1.1.2" | ||||
| }, | }, | ||||
| @@ -25,9 +25,10 @@ | |||||
| "@testing-library/user-event": "^14.4.3", | "@testing-library/user-event": "^14.4.3", | ||||
| "@types/react-input-mask": "^3.0.2", | "@types/react-input-mask": "^3.0.2", | ||||
| "apexcharts": "^3.35.5", | "apexcharts": "^3.35.5", | ||||
| "axios": "^1.12.2", | |||||
| "axios": "^1.20.0", | |||||
| "date-fns": "^3.0.6", | "date-fns": "^3.0.6", | ||||
| "dayjs": "^1.11.9", | "dayjs": "^1.11.9", | ||||
| "dompurify": "^3.4.15", | |||||
| "formik": "^2.2.9", | "formik": "^2.2.9", | ||||
| "framer-motion": "^7.3.6", | "framer-motion": "^7.3.6", | ||||
| "history": "^5.3.0", | "history": "^5.3.0", | ||||
| @@ -12,7 +12,7 @@ export const predictUsageCount = 'predictUsageCount' | |||||
| export const windowCount = 'windowCount' | export const windowCount = 'windowCount' | ||||
| import {useNavigate} from "react-router-dom"; | import {useNavigate} from "react-router-dom"; | ||||
| import {useDispatch} from "react-redux"; | import {useDispatch} from "react-redux"; | ||||
| import { REFRESH_TOKEN } from 'utils/ApiPathConst'; | |||||
| import { LOGOUT, REFRESH_TOKEN } from 'utils/ApiPathConst'; | |||||
| import { getMessage } from 'utils/getI18nMessage'; | import { getMessage } from 'utils/getI18nMessage'; | ||||
| // Guard so we only register interceptors once (ThemeRoutes re-renders add duplicate handlers otherwise) | // Guard so we only register interceptors once (ThemeRoutes re-renders add duplicate handlers otherwise) | ||||
| @@ -22,7 +22,7 @@ let expiredAlertShownInMemory = false; | |||||
| /** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */ | /** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */ | ||||
| const isPublicAuthRequest = (reqUrl) => | const isPublicAuthRequest = (reqUrl) => | ||||
| reqUrl.includes('/login'); | |||||
| reqUrl.includes('/login') || reqUrl.includes('/refresh-token') || reqUrl.includes('/logout'); | |||||
| /** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */ | /** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */ | ||||
| export const clearExpiredSessionAlert = () => { | export const clearExpiredSessionAlert = () => { | ||||
| @@ -96,6 +96,11 @@ export const isTokenValid = () =>{ | |||||
| // ** Handle User Logout | // ** Handle User Logout | ||||
| export const handleLogoutFunction = () => { | export const handleLogoutFunction = () => { | ||||
| return dispatch => { | return dispatch => { | ||||
| const refreshToken = localStorage.getItem('refreshToken') | |||||
| if (refreshToken) { | |||||
| axios.post(`${apiPath}${LOGOUT}`, { refreshToken }).catch(() => {}) | |||||
| } | |||||
| dispatch({ | dispatch({ | ||||
| type: 'LOGOUT', | type: 'LOGOUT', | ||||
| accessToken: null, | accessToken: null, | ||||
| @@ -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> | |||||
| ); | |||||
| } | |||||
| @@ -9,12 +9,19 @@ import { FormattedMessage, useIntl } from "react-intl"; | |||||
| import { TablePagination, Typography } from '@mui/material'; | import { TablePagination, Typography } from '@mui/material'; | ||||
| import { getSearchCriteria, checkSearchCriteriaPath } from "auth/utils"; | import { getSearchCriteria, checkSearchCriteriaPath } from "auth/utils"; | ||||
| /** When Gazette Issue No. is selected, load all matching rows (no start/limit). */ | |||||
| function hasGazetteIssueFilter(params) { | |||||
| const issueId = params?.issueId; | |||||
| return issueId != null && issueId !== ""; | |||||
| } | |||||
| // ==============================|| EVENT TABLE ||============================== // | // ==============================|| EVENT TABLE ||============================== // | ||||
| export function FiDataGrid({ rows, columns, sx, autoHeight = true, | export function FiDataGrid({ rows, columns, sx, autoHeight = true, | ||||
| hideFooterSelectedRowCount, rowModesModel, editMode, | hideFooterSelectedRowCount, rowModesModel, editMode, | ||||
| pageSizeOptions, filterItems, customPageSize, doLoad, applyGridOnReady, applySearch, | pageSizeOptions, filterItems, customPageSize, doLoad, applyGridOnReady, applySearch, | ||||
| tab, height, maxHeight, pagination = true, ...props }) { | |||||
| tab, height, maxHeight, pagination = true, serverSorting = false, | |||||
| disablePagingOnGazetteIssue = true, hideRowsPerPage = false, ...props }) { | |||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| const [_rows, set_rows] = useState([]); | const [_rows, set_rows] = useState([]); | ||||
| const [_doLoad, set_doLoad] = useState({}); | const [_doLoad, set_doLoad] = useState({}); | ||||
| @@ -27,6 +34,7 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| const [page, setPage] = useState(0); | const [page, setPage] = useState(0); | ||||
| const [pageSize, setPageSize] = useState(10); | const [pageSize, setPageSize] = useState(10); | ||||
| const [sortModel, setSortModel] = useState([]); | |||||
| // const [_autoHeight, set_autoHeight] = useState(true); | // const [_autoHeight, set_autoHeight] = useState(true); | ||||
| const [myHideFooterSelectedRowCount, setMyHideFooterSelectedRowCount] = useState(true); | const [myHideFooterSelectedRowCount, setMyHideFooterSelectedRowCount] = useState(true); | ||||
| const [_sx, set_sx] = useState({ | const [_sx, set_sx] = useState({ | ||||
| @@ -65,7 +73,9 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (doLoad !== undefined && Object.keys(doLoad).length>0 ){ | if (doLoad !== undefined && Object.keys(doLoad).length>0 ){ | ||||
| if(applySearch!=undefined){ | |||||
| if (disablePagingOnGazetteIssue && hasGazetteIssueFilter(doLoad.params)) { | |||||
| setPage(0); | |||||
| } else if(applySearch!=undefined){ | |||||
| if (Object.keys(getSearchCriteria(window.location.pathname)).length>0){ | if (Object.keys(getSearchCriteria(window.location.pathname)).length>0){ | ||||
| const localStorageSearchCriteria = getSearchCriteria(window.location.pathname) | const localStorageSearchCriteria = getSearchCriteria(window.location.pathname) | ||||
| // console.log(localStorageSearchCriteria) | // console.log(localStorageSearchCriteria) | ||||
| @@ -78,14 +88,58 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| setPage(0); | setPage(0); | ||||
| setPageSize(parseInt(event.target.value, 10)); | setPageSize(parseInt(event.target.value, 10)); | ||||
| } | } | ||||
| if (serverSorting) { | |||||
| if (doLoad.params?.sort && doLoad.params?.direction) { | |||||
| setSortModel([{ | |||||
| field: doLoad.params.sort, | |||||
| sort: String(doLoad.params.direction).toLowerCase() | |||||
| }]); | |||||
| } else { | |||||
| setSortModel([]); | |||||
| } | |||||
| } | |||||
| set_doLoad(doLoad); | set_doLoad(doLoad); | ||||
| setLoading(true) | setLoading(true) | ||||
| } | } | ||||
| }, [doLoad]); | }, [doLoad]); | ||||
| const handleSortModelChange = (newModel) => { | |||||
| if (!serverSorting) return; | |||||
| setSortModel(newModel); | |||||
| setPage(0); | |||||
| const params = { ...(_doLoad.params || {}) }; | |||||
| if (newModel?.length > 0 && newModel[0].field && newModel[0].sort) { | |||||
| params.sort = newModel[0].field; | |||||
| params.direction = newModel[0].sort; | |||||
| } else { | |||||
| delete params.sort; | |||||
| delete params.direction; | |||||
| } | |||||
| if (!pagination || (disablePagingOnGazetteIssue && hasGazetteIssueFilter(params))) { | |||||
| delete params.start; | |||||
| delete params.limit; | |||||
| setPage(0); | |||||
| } else { | |||||
| params.start = 0; | |||||
| params.limit = pageSize; | |||||
| } | |||||
| if (applySearch != undefined) { | |||||
| applySearch(params); | |||||
| } else { | |||||
| set_doLoad({ ..._doLoad, params }); | |||||
| setLoading(true); | |||||
| } | |||||
| }; | |||||
| const loadAllOnGazetteIssue = disablePagingOnGazetteIssue && hasGazetteIssueFilter(_doLoad?.params); | |||||
| const ignorePagingParams = !pagination || loadAllOnGazetteIssue; | |||||
| const showPaginationBar = pagination; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| getDataList(); | getDataList(); | ||||
| }, [_doLoad, page]); | |||||
| }, [_doLoad, page, pageSize]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -166,8 +220,13 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| if (_doLoad.params == undefined) return; | if (_doLoad.params == undefined) return; | ||||
| if (_doLoad.params.searchCriteria !== undefined) return; | if (_doLoad.params.searchCriteria !== undefined) return; | ||||
| if (_doLoad.params == null) _doLoad.params = {}; | if (_doLoad.params == null) _doLoad.params = {}; | ||||
| _doLoad.params.start = page * pageSize; | |||||
| _doLoad.params.limit = pageSize; | |||||
| if (ignorePagingParams) { | |||||
| delete _doLoad.params.start; | |||||
| delete _doLoad.params.limit; | |||||
| } else { | |||||
| _doLoad.params.start = page * pageSize; | |||||
| _doLoad.params.limit = pageSize; | |||||
| } | |||||
| if(checkSearchCriteriaPath(window.location.pathname)){ | if(checkSearchCriteriaPath(window.location.pathname)){ | ||||
| if(window.location.pathname === "/publicNotice"){ | if(window.location.pathname === "/publicNotice"){ | ||||
| if (tab != undefined && tab ==="application"){ | if (tab != undefined && tab ==="application"){ | ||||
| @@ -177,7 +236,12 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| localStorage.setItem('searchCriteria', JSON.stringify({path:window.location.pathname,data:_doLoad.params})) | localStorage.setItem('searchCriteria', JSON.stringify({path:window.location.pathname,data:_doLoad.params})) | ||||
| } | } | ||||
| } | } | ||||
| setLoading(true); | |||||
| if (applyGridOnReady !== undefined) { | |||||
| applyGridOnReady(true); | |||||
| } | |||||
| HttpUtils.get({ | HttpUtils.get({ | ||||
| url: _doLoad.url, | url: _doLoad.url, | ||||
| params: _doLoad.params, | params: _doLoad.params, | ||||
| @@ -254,21 +318,28 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| disableColumnMenu | disableColumnMenu | ||||
| shrinkWrap | shrinkWrap | ||||
| rowModesModel={_rowModesModel} | rowModesModel={_rowModesModel} | ||||
| pageSizeOptions={pagination ? _pageSizeOptions : []} | |||||
| pageSizeOptions={showPaginationBar ? _pageSizeOptions : []} | |||||
| editMode={_editMode} | editMode={_editMode} | ||||
| autoHeight={effectiveAutoHeight} | autoHeight={effectiveAutoHeight} | ||||
| hideFooterSelectedRowCount={myHideFooterSelectedRowCount} | hideFooterSelectedRowCount={myHideFooterSelectedRowCount} | ||||
| filterModel={{ items: _filterItems }} | filterModel={{ items: _filterItems }} | ||||
| loading={loading} | loading={loading} | ||||
| paginationMode={pagination ? "server" : undefined} | |||||
| paginationMode={showPaginationBar ? "server" : undefined} | |||||
| sortingMode={serverSorting ? "server" : undefined} | |||||
| sortModel={serverSorting ? sortModel : undefined} | |||||
| onSortModelChange={serverSorting ? handleSortModelChange : undefined} | |||||
| sx={{ | sx={{ | ||||
| ..._sx, | ..._sx, | ||||
| '& .MuiDataGrid-virtualScroller': { | '& .MuiDataGrid-virtualScroller': { | ||||
| overflowY: height || maxHeight ? 'auto' : 'visible', | overflowY: height || maxHeight ? 'auto' : 'visible', | ||||
| overflowX: height || maxHeight ? 'auto' : 'visible', | overflowX: height || maxHeight ? 'auto' : 'visible', | ||||
| ...(loading && { filter: 'blur(2px)' }), | |||||
| }, | |||||
| '& .MuiDataGrid-overlay': { | |||||
| backgroundColor: 'rgba(255, 255, 255, 0.55)', | |||||
| }, | }, | ||||
| // 👇 completely hide the footer when pagination is off | |||||
| ...(pagination === false && { | |||||
| // Hide the footer only when pagination is disabled for the grid | |||||
| ...(!showPaginationBar && { | |||||
| '& .MuiDataGrid-footerContainer': { | '& .MuiDataGrid-footerContainer': { | ||||
| display: 'none', | display: 'none', | ||||
| }, | }, | ||||
| @@ -276,19 +347,30 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| }} | }} | ||||
| components={{ | components={{ | ||||
| NoRowsOverlay: CustomNoRowsOverlay, | NoRowsOverlay: CustomNoRowsOverlay, | ||||
| ...(pagination | |||||
| ...(showPaginationBar | |||||
| ? { | ? { | ||||
| Pagination: () => ( | |||||
| Pagination: () => { | |||||
| const total = rowCount || 0; | |||||
| const pagerPage = loadAllOnGazetteIssue ? 0 : page; | |||||
| const pagerPageSize = loadAllOnGazetteIssue ? Math.max(total, 1) : pageSize; | |||||
| return ( | |||||
| <TablePagination | <TablePagination | ||||
| component="div" | component="div" | ||||
| count={rowCount || 0} | |||||
| page={page} | |||||
| rowsPerPage={pageSize} | |||||
| rowsPerPageOptions={_pageSizeOptions} | |||||
| labelDisplayedRows={() => | |||||
| `${(_rows?.length ? page * pageSize + 1 : 0)}-${page * pageSize + (_rows?.length ?? 0)} ${intl.formatMessage({ id: "of" })} ${rowCount}` | |||||
| } | |||||
| labelRowsPerPage={intl.formatMessage({ id: "rowsPerPage" }) + ":"} | |||||
| count={total} | |||||
| page={pagerPage} | |||||
| rowsPerPage={pagerPageSize} | |||||
| rowsPerPageOptions={(loadAllOnGazetteIssue || hideRowsPerPage) ? [] : _pageSizeOptions} | |||||
| labelDisplayedRows={() => { | |||||
| if (!_rows?.length || total === 0) { | |||||
| return `0-0 ${intl.formatMessage({ id: "of" })} ${total}`; | |||||
| } | |||||
| const from = loadAllOnGazetteIssue ? 1 : page * pageSize + 1; | |||||
| const to = loadAllOnGazetteIssue | |||||
| ? _rows.length | |||||
| : Math.min(page * pageSize + _rows.length, total); | |||||
| return `${from}-${to} ${intl.formatMessage({ id: "of" })} ${total}`; | |||||
| }} | |||||
| labelRowsPerPage={hideRowsPerPage ? () => "" : intl.formatMessage({ id: "rowsPerPage" }) + ":"} | |||||
| getItemAriaLabel={(type) => { | getItemAriaLabel={(type) => { | ||||
| if (type === 'previous') { | if (type === 'previous') { | ||||
| return intl.formatMessage({ id: 'paginationPrev' }); | return intl.formatMessage({ id: 'paginationPrev' }); | ||||
| @@ -298,10 +380,11 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||||
| } | } | ||||
| return ''; | return ''; | ||||
| }} | }} | ||||
| onPageChange={handleChangePage} | |||||
| onRowsPerPageChange={handleChangePageSize} | |||||
| onPageChange={loadAllOnGazetteIssue ? () => {} : handleChangePage} | |||||
| onRowsPerPageChange={(loadAllOnGazetteIssue || hideRowsPerPage) ? () => {} : handleChangePageSize} | |||||
| /> | /> | ||||
| ), | |||||
| ); | |||||
| }, | |||||
| } | } | ||||
| : {}), | : {}), | ||||
| }} | }} | ||||
| @@ -28,14 +28,9 @@ export default function FileList({ refType, refId, allowDelete, sx, dateHideable | |||||
| }, []); | }, []); | ||||
| const onDeleteClick = (fileId, skey, filename) => () => { | const onDeleteClick = (fileId, skey, filename) => () => { | ||||
| HttpUtils.get( | |||||
| HttpUtils.del( | |||||
| { | { | ||||
| url: UrlUtils.GET_FILE_DELETE, | |||||
| params: { | |||||
| fileId: fileId, | |||||
| skey: skey, | |||||
| filename: filename | |||||
| }, | |||||
| url: `${UrlUtils.GET_FILE_DELETE}/${fileId}/${skey}/${encodeURIComponent(filename)}`, | |||||
| onSuccess: function () { | onSuccess: function () { | ||||
| loadData(); | loadData(); | ||||
| } | } | ||||
| @@ -0,0 +1,17 @@ | |||||
| import PropTypes from 'prop-types'; | |||||
| import { sanitizeHtml } from 'utils/sanitizeHtml'; | |||||
| /** | |||||
| * Renders HTML after {@link sanitizeHtml}. Use instead of raw | |||||
| * dangerouslySetInnerHTML for messages, settings, CMS, and i18n HTML. | |||||
| */ | |||||
| const SafeHtml = ({ html, component: Component = 'div', ...rest }) => { | |||||
| return <Component {...rest} dangerouslySetInnerHTML={{ __html: sanitizeHtml(html) }} />; | |||||
| }; | |||||
| SafeHtml.propTypes = { | |||||
| html: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | |||||
| component: PropTypes.elementType | |||||
| }; | |||||
| export default SafeHtml; | |||||
| @@ -53,7 +53,7 @@ export default function SearchTable({ previewSearchCriteria, onPreviewGridOnRead | |||||
| id: 'paymentMethod', | id: 'paymentMethod', | ||||
| field: 'paymentMethod', | field: 'paymentMethod', | ||||
| headerName: 'GFMIS Payment Method', | headerName: 'GFMIS Payment Method', | ||||
| flex: 4, | |||||
| flex: 2, | |||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| let paymentMethod = params.row.paymentMethod; | let paymentMethod = params.row.paymentMethod; | ||||
| return <div style={{ margin: 4 }}>{paymentMethod}</div> | return <div style={{ margin: 4 }}>{paymentMethod}</div> | ||||
| @@ -63,11 +63,27 @@ export default function SearchTable({ previewSearchCriteria, onPreviewGridOnRead | |||||
| id: 'payAmount', | id: 'payAmount', | ||||
| field: 'payAmount', | field: 'payAmount', | ||||
| headerName: 'Amount ($)', | headerName: 'Amount ($)', | ||||
| flex: 5, | |||||
| valueGetter: (params) => { | |||||
| flex: 2, | |||||
| align: 'right', | |||||
| headerAlign: 'right', | |||||
| // keep the raw number as the grid value so sorting is numeric; | |||||
| // valueFormatter is used for display only | |||||
| valueFormatter: (params) => { | |||||
| return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | ||||
| }, | |||||
| sortComparator: (v1, v2) => { | |||||
| const n1 = (v1 == null || isNaN(v1)) ? -Infinity : Number(v1); | |||||
| const n2 = (v2 == null || isNaN(v2)) ? -Infinity : Number(v2); | |||||
| return n1 - n2; | |||||
| } | } | ||||
| }, | }, | ||||
| { | |||||
| id: 'spare', | |||||
| field: 'spare', | |||||
| headerName: '', | |||||
| flex: 4, | |||||
| disableColumnMenu: true, | |||||
| }, | |||||
| ]; | ]; | ||||
| @@ -76,7 +92,7 @@ export default function SearchTable({ previewSearchCriteria, onPreviewGridOnRead | |||||
| <FiDataGrid | <FiDataGrid | ||||
| key={previewToken} | key={previewToken} | ||||
| sx={_sx} | sx={_sx} | ||||
| rowHeight={80} | |||||
| getRowHeight={() => 'auto'} | |||||
| columns={columns} | columns={columns} | ||||
| customPageSize={10} | customPageSize={10} | ||||
| onRowDoubleClick={handleEditClick} | onRowDoubleClick={handleEditClick} | ||||
| @@ -65,6 +65,14 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||||
| flex: 1, | flex: 1, | ||||
| minWidth: 200, | minWidth: 200, | ||||
| cellClassName: 'actions', | cellClassName: 'actions', | ||||
| // sorting/filtering uses the raw payment no. (transNo), not the link | |||||
| valueGetter: (params) => params?.row?.transNo ?? '', | |||||
| sortComparator: (v1, v2) => { | |||||
| const n1 = Number(v1); | |||||
| const n2 = Number(v2); | |||||
| if (!isNaN(n1) && !isNaN(n2)) return n1 - n2; | |||||
| return String(v1 ?? '').localeCompare(String(v2 ?? '')); | |||||
| }, | |||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| return clickableLink('/paymentPage/details/' + params.row.id, params.row.transNo); | return clickableLink('/paymentPage/details/' + params.row.id, params.row.transNo); | ||||
| }, | }, | ||||
| @@ -107,17 +115,33 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||||
| field: 'payAmount', | field: 'payAmount', | ||||
| headerName: 'Amount ($)', | headerName: 'Amount ($)', | ||||
| width: 150, | width: 150, | ||||
| valueGetter: (params) => { | |||||
| align: 'right', | |||||
| headerAlign: 'right', | |||||
| // keep the raw number as the grid value so sorting is numeric; | |||||
| // valueFormatter is used for display only | |||||
| valueFormatter: (params) => { | |||||
| return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | ||||
| }, | |||||
| sortComparator: (v1, v2) => { | |||||
| const n1 = (v1 == null || isNaN(v1)) ? -Infinity : Number(v1); | |||||
| const n2 = (v2 == null || isNaN(v2)) ? -Infinity : Number(v2); | |||||
| return n1 - n2; | |||||
| } | } | ||||
| }, | }, | ||||
| { | |||||
| id: 'spare', | |||||
| field: 'spare', | |||||
| headerName: '', | |||||
| width: 30, | |||||
| disableColumnMenu: true, | |||||
| }, | |||||
| ]; | ]; | ||||
| return ( | return ( | ||||
| <div style={{ width: '100%' }}> | <div style={{ width: '100%' }}> | ||||
| <FiDataGrid | <FiDataGrid | ||||
| sx={_sx} | sx={_sx} | ||||
| rowHeight={60} | |||||
| getRowHeight={() => "auto"} | |||||
| columns={columns} | columns={columns} | ||||
| height={500} | height={500} | ||||
| autoHeight={false} | autoHeight={false} | ||||
| @@ -68,18 +68,6 @@ export default function GazetteIssueTable({ searchCriteria, applyGridOnReady }) | |||||
| return <div style={{ margin: 4 }}>{dateStr(closingDate)}</div> | return <div style={{ margin: 4 }}>{dateStr(closingDate)}</div> | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | |||||
| id: 'closingDateOff', | |||||
| field: 'closingDateOff', | |||||
| headerName: 'Closing Date Off', | |||||
| flex: 2, | |||||
| minWidth: 150, | |||||
| renderCell: (params) => { | |||||
| // console.log(params) | |||||
| let closingDateOff = params.row.closingDateOff; | |||||
| return <div style={{ margin: 4 }}>{dateStr(closingDateOff)}</div> | |||||
| }, | |||||
| }, | |||||
| { | { | ||||
| id: 'issueDesc', | id: 'issueDesc', | ||||
| field: 'issueDesc', | field: 'issueDesc', | ||||
| @@ -97,7 +85,7 @@ export default function GazetteIssueTable({ searchCriteria, applyGridOnReady }) | |||||
| <div style={{ height: "fit-content", width: '100%' }}> | <div style={{ height: "fit-content", width: '100%' }}> | ||||
| <FiDataGrid | <FiDataGrid | ||||
| columns={columns} | columns={columns} | ||||
| customPageSize={10} | |||||
| pagination={false} | |||||
| // onRowDoubleClick={handleRowDoubleClick} | // onRowDoubleClick={handleRowDoubleClick} | ||||
| getRowHeight={() => 'auto'} | getRowHeight={() => 'auto'} | ||||
| applyGridOnReady={applyGridOnReady} | applyGridOnReady={applyGridOnReady} | ||||
| @@ -67,8 +67,8 @@ const SearchGazetteIssueForm = ({ applyExport, comboData, waitDownload}) => { | |||||
| </Grid> | </Grid> | ||||
| {/*row 2*/} | {/*row 2*/} | ||||
| <Grid container display="flex" alignItems={"center"}> | |||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}> | |||||
| <Grid container display="flex" alignItems={"center"} sx={{ mb: 1 }}> | |||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3 }}> | |||||
| <Autocomplete | <Autocomplete | ||||
| disablePortal | disablePortal | ||||
| id="year-combo" | id="year-combo" | ||||
| @@ -93,34 +93,11 @@ const SearchGazetteIssueForm = ({ applyExport, comboData, waitDownload}) => { | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}> | |||||
| {/* <TextField | |||||
| fullWidth | |||||
| InputLabelProps={{ | |||||
| shrink: true | |||||
| }} | |||||
| {...register("dateTo")} | |||||
| InputProps={{ inputProps: { min: minDate } }} | |||||
| onChange={(newValue) => { | |||||
| setMaxDate(DateUtils.dateValue(newValue)); | |||||
| }} | |||||
| id="dateTo" | |||||
| type="date" | |||||
| label="To" | |||||
| defaultValue={searchCriteria.dateTo} | |||||
| /> */} | |||||
| </Grid> | |||||
| {/* <Grid item xs={9} s={6} md={4} lg={3}> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| <Grid container justifyContent="center" direction="row" alignItems="center" spacing={3}> | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| <Grid item sx={{ ml: 3, mb: 3, }} > | |||||
| <Grid item sx={{ mr: 3 }}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| type="submit" | type="submit" | ||||
| size="large" | |||||
| disabled={waitDownload} | disabled={waitDownload} | ||||
| > | > | ||||
| Export | Export | ||||
| @@ -12,14 +12,17 @@ import * as React from "react"; | |||||
| // import * as DateUtils from "utils/DateUtils"; | // import * as DateUtils from "utils/DateUtils"; | ||||
| import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; | import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; | ||||
| import {ThemeProvider} from "@emotion/react"; | import {ThemeProvider} from "@emotion/react"; | ||||
| import { GeneralConfirmWindow } from "utils/CommonFunction"; | |||||
| import { isGrantedAny } from "auth/utils"; | |||||
| // import * as ComboData from "utils/ComboData"; | // import * as ComboData from "utils/ComboData"; | ||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| const [selectedYear, setSelectedYear] = React.useState([]); | |||||
| const SearchGazetteIssueForm = ({ applySearch, comboData, searchCriteria, onGridReady, onDeleteYear, waitDelete }) => { | |||||
| const [selectedYear, setSelectedYear] = React.useState(null); | |||||
| // const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year); | // const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year); | ||||
| const [comboList, setComboList] = React.useState([]); | const [comboList, setComboList] = React.useState([]); | ||||
| const [isConfirmOpen, setIsConfirmOpen] = React.useState(false); | |||||
| // const [onReady, setOnReady] = React.useState(false); | // const [onReady, setOnReady] = React.useState(false); | ||||
| const { | const { | ||||
| @@ -27,7 +30,7 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| handleSubmit } = useForm() | handleSubmit } = useForm() | ||||
| const onSubmit = () => { | const onSubmit = () => { | ||||
| if (selectedYear !=null){ | |||||
| if (selectedYear != null) { | |||||
| const temp = { | const temp = { | ||||
| year: selectedYear.label, | year: selectedYear.label, | ||||
| }; | }; | ||||
| @@ -36,17 +39,28 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| }; | }; | ||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| if (comboData && comboData.length > 0) { | |||||
| // console.log(comboData) | |||||
| // const labelValue = comboData.find(obj => obj.label === searchCriteria.year); | |||||
| // console.log(labelValue) | |||||
| if(selectedYear.length == 0){ | |||||
| setSelectedYear(comboData[0]) | |||||
| } | |||||
| setComboList(comboData) | |||||
| // setSelectedYear(searchCriteria.dateFrom) | |||||
| const nextCombo = Array.isArray(comboData) ? [...comboData] : []; | |||||
| setComboList(nextCombo); | |||||
| if (nextCombo.length === 0) { | |||||
| setSelectedYear(null); | |||||
| return; | |||||
| } | |||||
| const criteriaYear = searchCriteria?.year; | |||||
| const matchedByCriteria = criteriaYear != null | |||||
| ? nextCombo.find((obj) => String(obj.label) === String(criteriaYear)) | |||||
| : null; | |||||
| if (matchedByCriteria) { | |||||
| setSelectedYear(matchedByCriteria); | |||||
| return; | |||||
| } | } | ||||
| }, [comboData]); | |||||
| const matchedBySelected = selectedYear | |||||
| ? nextCombo.find((obj) => String(obj.label) === String(selectedYear.label)) | |||||
| : null; | |||||
| setSelectedYear(matchedBySelected || nextCombo[0]); | |||||
| }, [comboData, searchCriteria]); | |||||
| return ( | return ( | ||||
| @@ -65,8 +79,8 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| </Grid> | </Grid> | ||||
| {/*row 2*/} | {/*row 2*/} | ||||
| <Grid container display="flex" alignItems={"center"}> | |||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}> | |||||
| <Grid container display="flex" alignItems={"center"} sx={{ mb: 1 }}> | |||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3 }}> | |||||
| <Autocomplete | <Autocomplete | ||||
| disablePortal | disablePortal | ||||
| id="year-combo" | id="year-combo" | ||||
| @@ -79,6 +93,9 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| ? String(option.label) | ? String(option.label) | ||||
| : "" | : "" | ||||
| } | } | ||||
| isOptionEqualToValue={(option, value) => | |||||
| String(option?.label) === String(value?.label) | |||||
| } | |||||
| onChange={(event, newValue) => { | onChange={(event, newValue) => { | ||||
| setSelectedYear(newValue); | setSelectedYear(newValue); | ||||
| }} | }} | ||||
| @@ -91,30 +108,8 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}> | |||||
| {/* <TextField | |||||
| fullWidth | |||||
| InputLabelProps={{ | |||||
| shrink: true | |||||
| }} | |||||
| {...register("dateTo")} | |||||
| InputProps={{ inputProps: { min: minDate } }} | |||||
| onChange={(newValue) => { | |||||
| setMaxDate(DateUtils.dateValue(newValue)); | |||||
| }} | |||||
| id="dateTo" | |||||
| type="date" | |||||
| label="To" | |||||
| defaultValue={searchCriteria.dateTo} | |||||
| /> */} | |||||
| </Grid> | |||||
| {/* <Grid item xs={9} s={6} md={4} lg={3}> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| <Grid container justifyContent="flex-end" direction="row" alignItems="center" spacing={3}> | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| <Grid item sx={{ ml: 3, mb: 3, }} > | |||||
| <Grid item sx={{ mr: 3 }}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| type="submit" | type="submit" | ||||
| @@ -124,9 +119,40 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| </ThemeProvider> | </ThemeProvider> | ||||
| {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ? | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||||
| <Grid item sx={{ mr: 3 }}> | |||||
| <Button | |||||
| color="delete" | |||||
| variant="contained" | |||||
| disabled={!selectedYear || waitDelete || onGridReady} | |||||
| onClick={() => setIsConfirmOpen(true)} | |||||
| > | |||||
| Delete | |||||
| </Button> | |||||
| </Grid> | |||||
| </ThemeProvider> | |||||
| : null | |||||
| } | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| </form> | </form> | ||||
| <GeneralConfirmWindow | |||||
| isWindowOpen={isConfirmOpen} | |||||
| title="Confirm Delete" | |||||
| content={selectedYear | |||||
| ? `Confirm to delete all gazette issues of year ${selectedYear.label}?` | |||||
| : "Confirm to delete all gazette issues of the selected year?"} | |||||
| onNormalClose={() => setIsConfirmOpen(false)} | |||||
| onConfirmClose={() => { | |||||
| setIsConfirmOpen(false); | |||||
| if (selectedYear != null && onDeleteYear) { | |||||
| onDeleteYear(selectedYear.label); | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </MainCard> | </MainCard> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -8,6 +8,7 @@ import { | |||||
| import * as UrlUtils from "utils/ApiPathConst"; | import * as UrlUtils from "utils/ApiPathConst"; | ||||
| import * as React from "react"; | import * as React from "react"; | ||||
| import * as HttpUtils from "utils/HttpUtils"; | import * as HttpUtils from "utils/HttpUtils"; | ||||
| import axios from "axios"; | |||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | ||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | ||||
| @@ -26,10 +27,10 @@ const BackgroundHead = { | |||||
| backgroundColor: '#0C489E', | backgroundColor: '#0C489E', | ||||
| backgroundPosition: 'right' | backgroundPosition: 'right' | ||||
| }; | }; | ||||
| import { PNSPS_LONG_BUTTON_THEME } from "themes/buttonConst"; | |||||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||||
| import { ThemeProvider } from "@emotion/react"; | import { ThemeProvider } from "@emotion/react"; | ||||
| import { dateStr_Year } from "utils/DateUtils"; | import { dateStr_Year } from "utils/DateUtils"; | ||||
| import { notifySaveSuccess } from 'utils/CommonFunction'; | |||||
| import { notifyDeleteSuccess, notifySaveSuccess } from 'utils/CommonFunction'; | |||||
| import { isGrantedAny } from "auth/utils"; | import { isGrantedAny } from "auth/utils"; | ||||
| import { useIntl } from 'react-intl'; | import { useIntl } from 'react-intl'; | ||||
| @@ -50,6 +51,7 @@ const Index = () => { | |||||
| const [attachments, setAttachments] = React.useState([]); | const [attachments, setAttachments] = React.useState([]); | ||||
| const [waitImport, setWaitImport] = React.useState(false); | const [waitImport, setWaitImport] = React.useState(false); | ||||
| const [waitDownload, setWaitDownload] = React.useState(false); | const [waitDownload, setWaitDownload] = React.useState(false); | ||||
| const [waitDelete, setWaitDelete] = React.useState(false); | |||||
| const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); | const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); | ||||
| const [warningText, setWarningText] = React.useState(""); | const [warningText, setWarningText] = React.useState(""); | ||||
| const fileInputRef = React.useRef(null); | const fileInputRef = React.useRef(null); | ||||
| @@ -96,6 +98,43 @@ const Index = () => { | |||||
| setGridOnReady(input); | setGridOnReady(input); | ||||
| } | } | ||||
| function deleteYear(year) { | |||||
| if (year == null || year === "") { | |||||
| setWarningText("Please select a year."); | |||||
| setIsWarningPopUp(true); | |||||
| return; | |||||
| } | |||||
| setWaitDelete(true); | |||||
| setOnSearchReady(false); | |||||
| axios.delete(`${UrlUtils.DELETE_ISSUE_YEAR}/${year}`) | |||||
| .then(() => { | |||||
| notifyDeleteSuccess(); | |||||
| HttpUtils.get({ | |||||
| url: UrlUtils.GET_ISSUE_YEAR_COMBO, | |||||
| onSuccess: (responseData) => { | |||||
| const combo = Array.isArray(responseData) ? [...responseData] : []; | |||||
| setComboData(combo); | |||||
| setWaitDelete(false); | |||||
| const nextYear = combo.length > 0 ? combo[0].label : year; | |||||
| applySearch({ year: nextYear }); | |||||
| }, | |||||
| onError: () => { | |||||
| setComboData([]); | |||||
| setWaitDelete(false); | |||||
| applySearch({ year }); | |||||
| } | |||||
| }); | |||||
| }) | |||||
| .catch((error) => { | |||||
| const msg = error?.response?.data?.error | |||||
| || "Delete failed. Please try again."; | |||||
| setWarningText(msg); | |||||
| setIsWarningPopUp(true); | |||||
| setWaitDelete(false); | |||||
| setOnSearchReady(true); | |||||
| }); | |||||
| } | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| if (Object.keys(exportCriteria).length > 0) { | if (Object.keys(exportCriteria).length > 0) { | ||||
| doExport(); | doExport(); | ||||
| @@ -138,6 +177,7 @@ const Index = () => { | |||||
| setWaitImport(true); | setWaitImport(true); | ||||
| if (!attachments || attachments.length <= 0) { | if (!attachments || attachments.length <= 0) { | ||||
| setWarningText("Please upload file."); | setWarningText("Please upload file."); | ||||
| setIsWarningPopUp(true); | |||||
| setWaitImport(false); | setWaitImport(false); | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -149,6 +189,14 @@ const Index = () => { | |||||
| setWaitImport(false); | setWaitImport(false); | ||||
| setAttachments([]); | setAttachments([]); | ||||
| loadCombo(); | loadCombo(); | ||||
| }, | |||||
| onError: (error) => { | |||||
| const msg = error?.response?.data?.error | |||||
| || "Import failed. Please try again."; | |||||
| setWarningText(msg); | |||||
| setIsWarningPopUp(true); | |||||
| setWaitImport(false); | |||||
| setAttachments([]); | |||||
| } | } | ||||
| }); | }); | ||||
| }; | }; | ||||
| @@ -185,10 +233,9 @@ const Index = () => { | |||||
| {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) && | {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) && | ||||
| <Grid item xs={12} md={12} lg={6} width="100%"> | <Grid item xs={12} md={12} lg={6} width="100%"> | ||||
| <Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }}> | <Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }}> | ||||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="large" | |||||
| disabled={waitImport} | disabled={waitImport} | ||||
| type="button" | type="button" | ||||
| onClick={() => { | onClick={() => { | ||||
| @@ -205,7 +252,7 @@ const Index = () => { | |||||
| } | } | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography variant="h5">Upload Files</Typography> | |||||
| Upload Gazette Issue | |||||
| </Button> | </Button> | ||||
| <input | <input | ||||
| id="uploadFileBtn" | id="uploadFileBtn" | ||||
| @@ -227,8 +274,11 @@ const Index = () => { | |||||
| <Grid item xs={12} md={12} lg={12} width="100%"> | <Grid item xs={12} md={12} lg={12} width="100%"> | ||||
| <SearchForm | <SearchForm | ||||
| applySearch={applySearch} | applySearch={applySearch} | ||||
| searchCriteria={searchCriteria} | |||||
| comboData={comboData} | comboData={comboData} | ||||
| onGridReady={onGridReady} | onGridReady={onGridReady} | ||||
| onDeleteYear={deleteYear} | |||||
| waitDelete={waitDelete} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| @@ -260,7 +310,7 @@ const Index = () => { | |||||
| <Typography variant="h3">Warning</Typography> | <Typography variant="h3">Warning</Typography> | ||||
| </DialogTitle> | </DialogTitle> | ||||
| <DialogContent style={{ display: 'flex' }}> | <DialogContent style={{ display: 'flex' }}> | ||||
| <Typography variant="h4" style={{ padding: '16px' }}>{warningText}</Typography> | |||||
| <Typography variant="h4" style={{ padding: '16px', whiteSpace: 'pre-line' }}>{warningText}</Typography> | |||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| <Button onClick={() => setIsWarningPopUp(false)}> | <Button onClick={() => setIsWarningPopUp(false)}> | ||||
| @@ -12,14 +12,17 @@ import * as React from "react"; | |||||
| // import * as DateUtils from "utils/DateUtils"; | // import * as DateUtils from "utils/DateUtils"; | ||||
| import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; | import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; | ||||
| import {ThemeProvider} from "@emotion/react"; | import {ThemeProvider} from "@emotion/react"; | ||||
| import { GeneralConfirmWindow } from "utils/CommonFunction"; | |||||
| import { isGrantedAny } from "auth/utils"; | |||||
| // import * as ComboData from "utils/ComboData"; | // import * as ComboData from "utils/ComboData"; | ||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| const SearchHolidayForm = ({ applySearch, comboData, searchCriteria, onGridReady, onDeleteYear, waitDelete }) => { | |||||
| const [selectedYear, setSelectedYear] = React.useState(null); | const [selectedYear, setSelectedYear] = React.useState(null); | ||||
| // const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year); | // const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year); | ||||
| const [comboList, setComboList] = React.useState([]); | const [comboList, setComboList] = React.useState([]); | ||||
| const [isConfirmOpen, setIsConfirmOpen] = React.useState(false); | |||||
| // const [onReady, setOnReady] = React.useState(false); | // const [onReady, setOnReady] = React.useState(false); | ||||
| const { | const { | ||||
| @@ -36,17 +39,28 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| }; | }; | ||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| if (comboData && comboData.length > 0) { | |||||
| // console.log(comboData) | |||||
| // const labelValue = comboData.find(obj => obj.label === searchCriteria.year); | |||||
| // console.log(labelValue) | |||||
| if (!selectedYear) { | |||||
| setSelectedYear(comboData[0]); | |||||
| } | |||||
| setComboList(comboData) | |||||
| // setSelectedYear(searchCriteria.dateFrom) | |||||
| const nextCombo = Array.isArray(comboData) ? [...comboData] : []; | |||||
| setComboList(nextCombo); | |||||
| if (nextCombo.length === 0) { | |||||
| setSelectedYear(null); | |||||
| return; | |||||
| } | |||||
| const criteriaYear = searchCriteria?.year; | |||||
| const matchedByCriteria = criteriaYear != null | |||||
| ? nextCombo.find((obj) => String(obj.label) === String(criteriaYear)) | |||||
| : null; | |||||
| if (matchedByCriteria) { | |||||
| setSelectedYear(matchedByCriteria); | |||||
| return; | |||||
| } | } | ||||
| }, [comboData]); | |||||
| const matchedBySelected = selectedYear | |||||
| ? nextCombo.find((obj) => String(obj.label) === String(selectedYear.label)) | |||||
| : null; | |||||
| setSelectedYear(matchedBySelected || nextCombo[0]); | |||||
| }, [comboData, searchCriteria]); | |||||
| return ( | return ( | ||||
| @@ -65,8 +79,8 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| </Grid> | </Grid> | ||||
| {/*row 2*/} | {/*row 2*/} | ||||
| <Grid container display="flex" alignItems={"center"}> | |||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}> | |||||
| <Grid container display="flex" alignItems={"center"} sx={{ mb: 1 }}> | |||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3 }}> | |||||
| <Autocomplete | <Autocomplete | ||||
| disablePortal | disablePortal | ||||
| id="year-combo" | id="year-combo" | ||||
| @@ -79,6 +93,9 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| ? String(option.label) | ? String(option.label) | ||||
| : "" | : "" | ||||
| } | } | ||||
| isOptionEqualToValue={(option, value) => | |||||
| String(option?.label) === String(value?.label) | |||||
| } | |||||
| onChange={(event, newValue) => { | onChange={(event, newValue) => { | ||||
| setSelectedYear(newValue); | setSelectedYear(newValue); | ||||
| }} | }} | ||||
| @@ -91,30 +108,8 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}> | |||||
| {/* <TextField | |||||
| fullWidth | |||||
| InputLabelProps={{ | |||||
| shrink: true | |||||
| }} | |||||
| {...register("dateTo")} | |||||
| InputProps={{ inputProps: { min: minDate } }} | |||||
| onChange={(newValue) => { | |||||
| setMaxDate(DateUtils.dateValue(newValue)); | |||||
| }} | |||||
| id="dateTo" | |||||
| type="date" | |||||
| label="To" | |||||
| defaultValue={searchCriteria.dateTo} | |||||
| /> */} | |||||
| </Grid> | |||||
| {/* <Grid item xs={9} s={6} md={4} lg={3}> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| <Grid container justifyContent="flex-end" direction="row" alignItems="center" spacing={3}> | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| <Grid item sx={{ ml: 3, mb: 3, }} > | |||||
| <Grid item sx={{ mr: 3 }}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| type="submit" | type="submit" | ||||
| @@ -124,9 +119,40 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| </ThemeProvider> | </ThemeProvider> | ||||
| {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ? | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||||
| <Grid item sx={{ mr: 3 }}> | |||||
| <Button | |||||
| color="delete" | |||||
| variant="contained" | |||||
| disabled={!selectedYear || waitDelete || onGridReady} | |||||
| onClick={() => setIsConfirmOpen(true)} | |||||
| > | |||||
| Delete | |||||
| </Button> | |||||
| </Grid> | |||||
| </ThemeProvider> | |||||
| : null | |||||
| } | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| </form> | </form> | ||||
| <GeneralConfirmWindow | |||||
| isWindowOpen={isConfirmOpen} | |||||
| title="Confirm Delete" | |||||
| content={selectedYear | |||||
| ? `Confirm to delete all holidays of year ${selectedYear.label}?` | |||||
| : "Confirm to delete all holidays of the selected year?"} | |||||
| onNormalClose={() => setIsConfirmOpen(false)} | |||||
| onConfirmClose={() => { | |||||
| setIsConfirmOpen(false); | |||||
| if (selectedYear != null && onDeleteYear) { | |||||
| onDeleteYear(selectedYear.label); | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </MainCard> | </MainCard> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -8,6 +8,7 @@ import { | |||||
| import * as UrlUtils from "utils/ApiPathConst"; | import * as UrlUtils from "utils/ApiPathConst"; | ||||
| import * as React from "react"; | import * as React from "react"; | ||||
| import * as HttpUtils from "utils/HttpUtils"; | import * as HttpUtils from "utils/HttpUtils"; | ||||
| import axios from "axios"; | |||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | ||||
| @@ -26,10 +27,10 @@ const BackgroundHead = { | |||||
| backgroundPosition: 'right' | backgroundPosition: 'right' | ||||
| }; | }; | ||||
| import { PNSPS_LONG_BUTTON_THEME } from "themes/buttonConst"; | |||||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||||
| import { ThemeProvider } from "@emotion/react"; | import { ThemeProvider } from "@emotion/react"; | ||||
| import { dateStr_Year } from "utils/DateUtils"; | import { dateStr_Year } from "utils/DateUtils"; | ||||
| import { notifySaveSuccess } from 'utils/CommonFunction'; | |||||
| import { notifyDeleteSuccess, notifySaveSuccess } from 'utils/CommonFunction'; | |||||
| import { isGrantedAny } from "auth/utils"; | import { isGrantedAny } from "auth/utils"; | ||||
| import { useIntl } from 'react-intl'; | import { useIntl } from 'react-intl'; | ||||
| @@ -49,6 +50,7 @@ const Index = () => { | |||||
| const [attachments, setAttachments] = React.useState([]); | const [attachments, setAttachments] = React.useState([]); | ||||
| const [waitImport, setWaitImport] = React.useState(false); | const [waitImport, setWaitImport] = React.useState(false); | ||||
| const [waitDownload, setWaitDownload] = React.useState(false); | const [waitDownload, setWaitDownload] = React.useState(false); | ||||
| const [waitDelete, setWaitDelete] = React.useState(false); | |||||
| const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); | const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); | ||||
| const [warningText, setWarningText] = React.useState(""); | const [warningText, setWarningText] = React.useState(""); | ||||
| const fileInputRef = React.useRef(null); | const fileInputRef = React.useRef(null); | ||||
| @@ -58,13 +60,17 @@ const Index = () => { | |||||
| loadForm(); | loadForm(); | ||||
| }, [searchCriteria]); | }, [searchCriteria]); | ||||
| function loadForm() { | |||||
| function loadForm(refreshCombo = false, criteria = null) { | |||||
| const params = criteria || searchCriteria; | |||||
| HttpUtils.get({ | HttpUtils.get({ | ||||
| url: UrlUtils.GET_HOLIDAY, | url: UrlUtils.GET_HOLIDAY, | ||||
| params: searchCriteria, | |||||
| params, | |||||
| onSuccess: (responseData) => { | onSuccess: (responseData) => { | ||||
| setRecord(responseData); | setRecord(responseData); | ||||
| if (comboData.length === 0) { | |||||
| if (criteria) { | |||||
| setSearchCriteria(criteria); | |||||
| } | |||||
| if (refreshCombo || comboData.length === 0) { | |||||
| loadCombo(); | loadCombo(); | ||||
| } else { | } else { | ||||
| setOnSearchReady(true); | setOnSearchReady(true); | ||||
| @@ -77,8 +83,7 @@ const Index = () => { | |||||
| HttpUtils.get({ | HttpUtils.get({ | ||||
| url: UrlUtils.GET_HOLIDAY_COMBO, | url: UrlUtils.GET_HOLIDAY_COMBO, | ||||
| onSuccess: (responseData) => { | onSuccess: (responseData) => { | ||||
| let combo = responseData; | |||||
| setComboData(combo); | |||||
| setComboData(Array.isArray(responseData) ? [...responseData] : []); | |||||
| setOnReady(true); | setOnReady(true); | ||||
| setOnSearchReady(true); | setOnSearchReady(true); | ||||
| } | } | ||||
| @@ -94,6 +99,43 @@ const Index = () => { | |||||
| setGridOnReady(input); | setGridOnReady(input); | ||||
| } | } | ||||
| function deleteYear(year) { | |||||
| if (year == null || year === "") { | |||||
| setWarningText("Please select a year."); | |||||
| setIsWarningPopUp(true); | |||||
| return; | |||||
| } | |||||
| setWaitDelete(true); | |||||
| setOnSearchReady(false); | |||||
| axios.delete(`${UrlUtils.DELETE_HOLIDAY_YEAR}/${year}`) | |||||
| .then(() => { | |||||
| notifyDeleteSuccess(); | |||||
| HttpUtils.get({ | |||||
| url: UrlUtils.GET_HOLIDAY_COMBO, | |||||
| onSuccess: (responseData) => { | |||||
| const combo = Array.isArray(responseData) ? [...responseData] : []; | |||||
| setComboData(combo); | |||||
| setWaitDelete(false); | |||||
| const nextYear = combo.length > 0 ? combo[0].label : year; | |||||
| applySearch({ year: nextYear }); | |||||
| }, | |||||
| onError: () => { | |||||
| setComboData([]); | |||||
| setWaitDelete(false); | |||||
| applySearch({ year }); | |||||
| } | |||||
| }); | |||||
| }) | |||||
| .catch((error) => { | |||||
| const msg = error?.response?.data?.error | |||||
| || "Delete failed. Please try again."; | |||||
| setWarningText(msg); | |||||
| setIsWarningPopUp(true); | |||||
| setWaitDelete(false); | |||||
| setOnSearchReady(true); | |||||
| }); | |||||
| } | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| if (attachments.length > 0) { | if (attachments.length > 0) { | ||||
| importHoliday(); | importHoliday(); | ||||
| @@ -132,7 +174,9 @@ const Index = () => { | |||||
| setOnSearchReady(false); | setOnSearchReady(false); | ||||
| if (!attachments || attachments.length <= 0) { | if (!attachments || attachments.length <= 0) { | ||||
| setWarningText("Please upload file."); | setWarningText("Please upload file."); | ||||
| setIsWarningPopUp(true); | |||||
| setWaitImport(false); | setWaitImport(false); | ||||
| setOnSearchReady(true); | |||||
| return; | return; | ||||
| } | } | ||||
| HttpUtils.postWithFiles({ | HttpUtils.postWithFiles({ | ||||
| @@ -142,7 +186,16 @@ const Index = () => { | |||||
| notifySaveSuccess(); | notifySaveSuccess(); | ||||
| setWaitImport(false); | setWaitImport(false); | ||||
| setAttachments([]); | setAttachments([]); | ||||
| loadForm(); | |||||
| loadForm(true); | |||||
| }, | |||||
| onError: (error) => { | |||||
| const msg = error?.response?.data?.error | |||||
| || "Import failed. Please try again."; | |||||
| setWarningText(msg); | |||||
| setIsWarningPopUp(true); | |||||
| setWaitImport(false); | |||||
| setAttachments([]); | |||||
| setOnSearchReady(true); | |||||
| } | } | ||||
| }); | }); | ||||
| }; | }; | ||||
| @@ -168,22 +221,20 @@ const Index = () => { | |||||
| <Grid item xs={12} md={12} lg={6} width="100%"> | <Grid item xs={12} md={12} lg={6} width="100%"> | ||||
| <Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }} > | <Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }} > | ||||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="large" | |||||
| disabled={waitDownload} | disabled={waitDownload} | ||||
| onClick={doExport} | onClick={doExport} | ||||
| aria-label={intl.formatMessage({ id: 'ariaExportHolidayTemplate' })} | aria-label={intl.formatMessage({ id: 'ariaExportHolidayTemplate' })} | ||||
| > | > | ||||
| <Typography variant="h5">Export</Typography> | |||||
| Download Template | |||||
| </Button> | </Button> | ||||
| </ThemeProvider> | </ThemeProvider> | ||||
| {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ? | {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ? | ||||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| size="large" | |||||
| disabled={waitImport} | disabled={waitImport} | ||||
| type="button" | type="button" | ||||
| onClick={() => { | onClick={() => { | ||||
| @@ -200,7 +251,7 @@ const Index = () => { | |||||
| } | } | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography variant="h5">Upload Files</Typography> | |||||
| Upload Holiday | |||||
| </Button> | </Button> | ||||
| <input | <input | ||||
| id="uploadFileBtn" | id="uploadFileBtn" | ||||
| @@ -226,6 +277,8 @@ const Index = () => { | |||||
| searchCriteria={searchCriteria} | searchCriteria={searchCriteria} | ||||
| comboData={comboData} | comboData={comboData} | ||||
| onGridReady={onGridReady} | onGridReady={onGridReady} | ||||
| onDeleteYear={deleteYear} | |||||
| waitDelete={waitDelete} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| @@ -255,7 +308,7 @@ const Index = () => { | |||||
| > | > | ||||
| <DialogTitle><Typography variant="h3">Warning</Typography></DialogTitle> | <DialogTitle><Typography variant="h3">Warning</Typography></DialogTitle> | ||||
| <DialogContent style={{ display: 'flex' }}> | <DialogContent style={{ display: 'flex' }}> | ||||
| <Typography variant="h4" style={{ padding: '16px' }}>{warningText}</Typography> | |||||
| <Typography variant="h4" style={{ padding: '16px', whiteSpace: 'pre-line' }}>{warningText}</Typography> | |||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| <Button onClick={() => setIsWarningPopUp(false)}><Typography variant="h5">OK</Typography></Button> | <Button onClick={() => setIsWarningPopUp(false)}><Typography variant="h5">OK</Typography></Button> | ||||
| @@ -19,6 +19,7 @@ import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||||
| import { FormattedMessage } from "react-intl"; | import { FormattedMessage } from "react-intl"; | ||||
| import usePageTitle from 'components/usePageTitle'; | import usePageTitle from 'components/usePageTitle'; | ||||
| import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst'; | import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| backgroundImage: `url(${titleBackgroundImg})`, | backgroundImage: `url(${titleBackgroundImg})`, | ||||
| @@ -93,7 +94,7 @@ const Index = () => { | |||||
| </Typography> | </Typography> | ||||
| <Typography sx={{p:1}} align="justify">{DateUtils.datetimeStr(record?.sentDate)}</Typography> | <Typography sx={{p:1}} align="justify">{DateUtils.datetimeStr(record?.sentDate)}</Typography> | ||||
| <Typography component="div" variant="body1" sx={{ p: 4, textAlign: "left", bgcolor: "#f8f8f8" }} align="justify"> | <Typography component="div" variant="body1" sx={{ p: 4, textAlign: "left", bgcolor: "#f8f8f8" }} align="justify"> | ||||
| <div dangerouslySetInnerHTML={{ __html: record?.content }} /> | |||||
| <SafeHtml html={record?.content} /> | |||||
| </Typography> | </Typography> | ||||
| <Typography component="h3" variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> | <Typography component="h3" variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> | ||||
| @@ -22,8 +22,10 @@ import Loadable from 'components/Loadable'; | |||||
| import { notifySaveSuccess } from 'utils/CommonFunction'; | import { notifySaveSuccess } from 'utils/CommonFunction'; | ||||
| import { useIntl } from "react-intl"; | import { useIntl } from "react-intl"; | ||||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | 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 { ThemeProvider } from "@emotion/react"; | ||||
| import { isGrantedAny } from "auth/utils"; | import { isGrantedAny } from "auth/utils"; | ||||
| import FileList from "components/FileList"; | |||||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| @@ -36,6 +38,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); | const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); | ||||
| const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); | const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); | ||||
| const [afterSendPopUp, setAfterSendPopUp] = React.useState(false); | const [afterSendPopUp, setAfterSendPopUp] = React.useState(false); | ||||
| const [confirmBrPopUp, setConfirmBrPopUp] = React.useState(false); | |||||
| const [currentUserData, setCurrentUserData] = useState({}); | const [currentUserData, setCurrentUserData] = useState({}); | ||||
| const [overduePublicNotice, setOverduePublicNotice] = useState(0); | const [overduePublicNotice, setOverduePublicNotice] = useState(0); | ||||
| @@ -227,7 +230,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| const markAsNonCreditor = () => { | const markAsNonCreditor = () => { | ||||
| setNonCreditorConfirmPopUp(false); | setNonCreditorConfirmPopUp(false); | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: UrlUtils.GET_ORG_MARK_AS_NON_CREDITOR + "/" + id, | url: UrlUtils.GET_ORG_MARK_AS_NON_CREDITOR + "/" + id, | ||||
| onSuccess: () => { | onSuccess: () => { | ||||
| loadDataFun(); | loadDataFun(); | ||||
| @@ -237,7 +240,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| const sendDn_Overdue = () => { | const sendDn_Overdue = () => { | ||||
| setNonCreditorConfirmPopUp(false); | setNonCreditorConfirmPopUp(false); | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: UrlUtils.GET_SEND_OVERDUE_CREDITOR_LIST + "/" + id, | url: UrlUtils.GET_SEND_OVERDUE_CREDITOR_LIST + "/" + id, | ||||
| onSuccess: (responseData) => { | onSuccess: (responseData) => { | ||||
| setOverduePublicNotice(responseData.overduePublicNotice); | setOverduePublicNotice(responseData.overduePublicNotice); | ||||
| @@ -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 ( | return ( | ||||
| <MainCard elevation={0} | <MainCard elevation={0} | ||||
| border={false} | border={false} | ||||
| @@ -316,6 +330,19 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| </Button> | </Button> | ||||
| </ThemeProvider> | </ThemeProvider> | ||||
| </Grid> | </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 ? | currentUserData.creditor ? | ||||
| @@ -324,7 +351,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| color="error" | |||||
| color="success" | |||||
| onClick={() => setNonCreditorConfirmPopUp(true)} | onClick={() => setNonCreditorConfirmPopUp(true)} | ||||
| > | > | ||||
| Mark as Non-Credit Client | Mark as Non-Credit Client | ||||
| @@ -338,7 +365,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| color="orange" | |||||
| color="success" | |||||
| onClick={() => setCreditorConfirmPopUp(true)} | onClick={() => setCreditorConfirmPopUp(true)} | ||||
| > | > | ||||
| Mark as Credit Client | Mark as Credit Client | ||||
| @@ -351,7 +378,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| color="primary" | |||||
| color="success" | |||||
| onClick={() => sendDn_Overdue(true)} | onClick={() => sendDn_Overdue(true)} | ||||
| > | > | ||||
| Generate O/S DN List | Generate O/S DN List | ||||
| @@ -440,10 +467,16 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| id="currentExDate" | id="currentExDate" | ||||
| // error={(fromDate===null)} | |||||
| // type="date" | |||||
| name="currentExDate" | 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} | disabled={true} | ||||
| /> : | /> : | ||||
| <LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}> | <LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}> | ||||
| @@ -480,9 +513,6 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| { | { | ||||
| fromDate == null ? | fromDate == null ? | ||||
| (!editMode && !createMode) ? | (!editMode && !createMode) ? | ||||
| // <FormHelperText error id="helper-text-date"> | |||||
| // Please select a date after today. | |||||
| // </FormHelperText> | |||||
| null | null | ||||
| : | : | ||||
| <FormHelperText error id="helper-text-date"> | <FormHelperText error id="helper-text-date"> | ||||
| @@ -575,6 +605,81 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| })} | })} | ||||
| </Grid> | </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 item lg={12} ></Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -668,6 +773,32 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| </DialogActions> | </DialogActions> | ||||
| </Dialog> | </Dialog> | ||||
| </div> | </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> | </MainCard> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -23,12 +23,14 @@ import { notifySaveSuccess } from 'utils/CommonFunction'; | |||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | ||||
| import { ThemeProvider } from "@emotion/react"; | import { ThemeProvider } from "@emotion/react"; | ||||
| import { useNavigate } from "react-router-dom"; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | ||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| const navigate = useNavigate(); | |||||
| const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); | const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); | ||||
| const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); | const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); | ||||
| @@ -200,6 +202,19 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| </Button> | </Button> | ||||
| </ThemeProvider> | </ThemeProvider> | ||||
| </Grid> | </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> | </Grid> | ||||
| @@ -235,13 +250,15 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'expiryDate' }) + ":"), | label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'expiryDate' }) + ":"), | ||||
| valueName: "brExpiryDate", | valueName: "brExpiryDate", | ||||
| disabled: true, | 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> | ||||
| <Grid item xs={12} lg={4} ></Grid> | |||||
| <Grid item xs={12} lg={4} > | <Grid item xs={12} lg={4} > | ||||
| {FieldUtils.getTextField({ | {FieldUtils.getTextField({ | ||||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'nameEng' }) + ":"), | 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["brExpiryDate"] = response.data.brExpiryDate ? DateUtils.dateValue(response.data.brExpiryDate) : ""; | ||||
| response.data["orgShortName"] = response.data.orgShortName ? response.data.orgShortName : "N/A" ; | 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) | setFormData(response.data) | ||||
| setList(response.historyList) | setList(response.historyList) | ||||
| } | } | ||||
| @@ -9,11 +9,13 @@ import { useNavigate } from "react-router-dom"; | |||||
| import * as DateUtils from "utils/DateUtils"; | import * as DateUtils from "utils/DateUtils"; | ||||
| import { clickableLink} from 'utils/CommonFunction'; | import { clickableLink} from 'utils/CommonFunction'; | ||||
| import {GET_ORG_PATH} from "utils/ApiPathConst"; | import {GET_ORG_PATH} from "utils/ApiPathConst"; | ||||
| import { useIntl } from "react-intl"; | |||||
| // ==============================|| EVENT TABLE ||============================== // | // ==============================|| EVENT TABLE ||============================== // | ||||
| export default function OrganizationTable({ searchCriteria, applyGridOnReady, applySearch}) { | export default function OrganizationTable({ searchCriteria, applyGridOnReady, applySearch}) { | ||||
| const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); | const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); | ||||
| const navigate = useNavigate() | const navigate = useNavigate() | ||||
| const intl = useIntl(); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| set_searchCriteria(searchCriteria); | set_searchCriteria(searchCriteria); | ||||
| @@ -91,6 +93,34 @@ export default function OrganizationTable({ searchCriteria, applyGridOnReady, ap | |||||
| return DateUtils.dateValue(params?.value); | 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', | id: 'creditor', | ||||
| field: '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; | |||||
| @@ -25,6 +25,7 @@ import Loadable from 'components/Loadable'; | |||||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | ||||
| import * as FormatUtils from "utils/FormatUtils"; | import * as FormatUtils from "utils/FormatUtils"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import VisaIcon from "assets/images/icons/visacard.svg"; | import VisaIcon from "assets/images/icons/visacard.svg"; | ||||
| import MasterIcon from "assets/images/icons/mastercard.svg"; | import MasterIcon from "assets/images/icons/mastercard.svg"; | ||||
| import JcbIcon from "assets/images/icons/jcb.svg"; | import JcbIcon from "assets/images/icons/jcb.svg"; | ||||
| @@ -58,6 +59,8 @@ const MultiPaymentWindow = (props) => { | |||||
| const [paymentHoldedErrText, setPaymentHoldedErrText] = React.useState(""); | const [paymentHoldedErrText, setPaymentHoldedErrText] = React.useState(""); | ||||
| const [paymentHoldedErr, setPaymentHoldedErr] = React.useState(false); | const [paymentHoldedErr, setPaymentHoldedErr] = React.useState(false); | ||||
| const [isPaying, setIsPaying] = React.useState(false); | |||||
| const payingRef = React.useRef(false); | |||||
| const mobileBrowser = "Mobile"; | const mobileBrowser = "Mobile"; | ||||
| @@ -70,10 +73,7 @@ const MultiPaymentWindow = (props) => { | |||||
| }, [props.transactionData]); | }, [props.transactionData]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| // console.log(props.availableMethods) | |||||
| if(props.availableMethods.length > 0){ | |||||
| setLoadAvailableMethodData(props.availableMethods) | |||||
| } | |||||
| setLoadAvailableMethodData(props.availableMethods || []) | |||||
| }, [props.availableMethods]); | }, [props.availableMethods]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -89,9 +89,7 @@ const MultiPaymentWindow = (props) => { | |||||
| }, [transactionData]); | }, [transactionData]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if(loadAvailableMethodData.length > 0){ | |||||
| setAvailableMethodData(loadAvailableMethodData) | |||||
| } | |||||
| setAvailableMethodData(loadAvailableMethodData || []) | |||||
| }, [loadAvailableMethodData]); | }, [loadAvailableMethodData]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -144,7 +142,13 @@ const MultiPaymentWindow = (props) => { | |||||
| }, [paymentMethod]); | }, [paymentMethod]); | ||||
| const releasePayLock = () => { | |||||
| payingRef.current = false; | |||||
| setIsPaying(false); | |||||
| }; | |||||
| const selectedPaymentMethodHandle = (method) => () =>{ | const selectedPaymentMethodHandle = (method) => () =>{ | ||||
| if (payingRef.current) return; | |||||
| if (method != paymentMethod){ | if (method != paymentMethod){ | ||||
| resetForm() | resetForm() | ||||
| let totalAmount = props.totalAmount; | let totalAmount = props.totalAmount; | ||||
| @@ -198,6 +202,10 @@ const MultiPaymentWindow = (props) => { | |||||
| }; | }; | ||||
| const handlePaymentCheck = () => { | const handlePaymentCheck = () => { | ||||
| if (payingRef.current) return; | |||||
| payingRef.current = true; | |||||
| setIsPaying(true); | |||||
| let appIdList = props.appIds | let appIdList = props.appIds | ||||
| // console.log(props.appIds) | // console.log(props.appIds) | ||||
| // console.log(appIdList) | // console.log(appIdList) | ||||
| @@ -207,6 +215,8 @@ const MultiPaymentWindow = (props) => { | |||||
| appIds: appIdList | appIds: appIdList | ||||
| }, | }, | ||||
| onSuccess: (responseData) => { | onSuccess: (responseData) => { | ||||
| if (!payingRef.current) return; | |||||
| const latestData = {}; | const latestData = {}; | ||||
| responseData.forEach(item => { | responseData.forEach(item => { | ||||
| @@ -235,13 +245,21 @@ const MultiPaymentWindow = (props) => { | |||||
| const resultString = HoldingApplication.map(item => item.appNo).join(' , '); | const resultString = HoldingApplication.map(item => item.appNo).join(' , '); | ||||
| setPaymentHoldedErrText(resultString); | setPaymentHoldedErrText(resultString); | ||||
| // setPaymentHoldedErrText(intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: record.appNo })); | // setPaymentHoldedErrText(intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: record.appNo })); | ||||
| releasePayLock(); | |||||
| setPaymentHoldedErr(true); | setPaymentHoldedErr(true); | ||||
| } | } | ||||
| }, | |||||
| onFail: () => { | |||||
| releasePayLock(); | |||||
| }, | |||||
| onError: () => { | |||||
| releasePayLock(); | |||||
| } | } | ||||
| }); | }); | ||||
| }; | }; | ||||
| const closeHandle = () => () =>{ | const closeHandle = () => () =>{ | ||||
| releasePayLock(); | |||||
| resetForm() | resetForm() | ||||
| props.setOpen(false) | props.setOpen(false) | ||||
| }; | }; | ||||
| @@ -261,6 +279,12 @@ const MultiPaymentWindow = (props) => { | |||||
| } | } | ||||
| }, [availableMethodData]); | }, [availableMethodData]); | ||||
| useEffect(() => { | |||||
| if (!props.confirmPayment) { | |||||
| releasePayLock(); | |||||
| } | |||||
| }, [props.confirmPayment]); | |||||
| const formik = useFormik({ | const formik = useFormik({ | ||||
| initialValues: ({ | initialValues: ({ | ||||
| username: '', | username: '', | ||||
| @@ -269,10 +293,16 @@ const MultiPaymentWindow = (props) => { | |||||
| }), | }), | ||||
| }); | }); | ||||
| const showFps = props.fpsStatus?.active === "Y"; | |||||
| const showCreditCard = props.creditCardStatus?.active === "Y"; | |||||
| const showUnionPay = props.unionPayStatus?.active === "Y"; | |||||
| const showPps = props.ppsStatus?.active === "Y" && props.browserType !== mobileBrowser; | |||||
| const hasVisiblePaymentMethod = showFps || showCreditCard || showUnionPay || showPps; | |||||
| return ( | return ( | ||||
| <Dialog | <Dialog | ||||
| open={props.open} | open={props.open} | ||||
| onClose={() => props.setOpen(false)} | |||||
| onClose={closeHandle()} | |||||
| fullWidth={true} | fullWidth={true} | ||||
| maxWidth={'xl'} | maxWidth={'xl'} | ||||
| fullScreen={props.isFullScreen} | fullScreen={props.isFullScreen} | ||||
| @@ -307,7 +337,7 @@ const MultiPaymentWindow = (props) => { | |||||
| </Typography> */} | </Typography> */} | ||||
| {!props.onReady ? | {!props.onReady ? | ||||
| <LoadingComponent /> | <LoadingComponent /> | ||||
| :availableMethodData.length>0? | |||||
| :hasVisiblePaymentMethod? | |||||
| <Grid container spacing={2} direction="column" justifyContent="space-between" alignItems="flex-start"> | <Grid container spacing={2} direction="column" justifyContent="space-between" alignItems="flex-start"> | ||||
| <Grid item xs={12} md={12}> | <Grid item xs={12} md={12}> | ||||
| <Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="center"> | <Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="center"> | ||||
| @@ -317,40 +347,48 @@ const MultiPaymentWindow = (props) => { | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item sx={{display: { sm: 'block', md: 'none' }}}></Grid> | <Grid item sx={{display: { sm: 'block', md: 'none' }}}></Grid> | ||||
| {showFps ? | |||||
| <Grid item> | <Grid item> | ||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("FPS")} disabled={props.fpsStatus.active === "N"}> | |||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("FPS")} disabled={isPaying}> | |||||
| <img className={fpsClass} src={FpsIcon} width="80" height="80" alt="FPS"></img> | <img className={fpsClass} src={FpsIcon} width="80" height="80" alt="FPS"></img> | ||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| : null} | |||||
| {showCreditCard ? | |||||
| <Grid item> | <Grid item> | ||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("Visa")} disabled={props.creditCardStatus.active === "N"}> | |||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("Visa")} disabled={isPaying}> | |||||
| <img className={visaClass} src={VisaIcon} width="80" height="80" alt="Visa"></img> | <img className={visaClass} src={VisaIcon} width="80" height="80" alt="Visa"></img> | ||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| : null} | |||||
| {showCreditCard ? | |||||
| <Grid item> | <Grid item> | ||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("MasterCard")} disabled={props.creditCardStatus.active === "N"}> | |||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("MasterCard")} disabled={isPaying}> | |||||
| <img className={mastercardClass} src={MasterIcon} width="80" height="80" alt="MasterCard"></img> | <img className={mastercardClass} src={MasterIcon} width="80" height="80" alt="MasterCard"></img> | ||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| : null} | |||||
| {showUnionPay ? | |||||
| <Grid item> | <Grid item> | ||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("UnionPay")} disabled={props.unionPayStatus.active === "N"}> | |||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("UnionPay")} disabled={isPaying}> | |||||
| <img className={unionPayClass} src={UnionPayIcon} width="80" height="80" alt="UnionPay"></img> | <img className={unionPayClass} src={UnionPayIcon} width="80" height="80" alt="UnionPay"></img> | ||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| : null} | |||||
| {showCreditCard ? | |||||
| <Grid item> | <Grid item> | ||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("JCB")} disabled={props.unionPayStatus.active === "N"}> | |||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("JCB")} disabled={isPaying}> | |||||
| <img className={jCBClass} src={JcbIcon} width="80" height="80" alt="JCB"></img> | <img className={jCBClass} src={JcbIcon} width="80" height="80" alt="JCB"></img> | ||||
| </Button> | </Button> | ||||
| </Grid> | </Grid> | ||||
| : null} | |||||
| {showPps ? | |||||
| <Grid item> | <Grid item> | ||||
| {props.browserType==mobileBrowser? | |||||
| null | |||||
| : | |||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("PPS")} disabled={props.ppsStatus.active === "N"}> | |||||
| <Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("PPS")} disabled={isPaying}> | |||||
| <img className={pPSClass} src={PpsIcon} width="80" height="80" alt="PPS"></img> | <img className={pPSClass} src={PpsIcon} width="80" height="80" alt="PPS"></img> | ||||
| </Button> | </Button> | ||||
| } | |||||
| </Grid> | </Grid> | ||||
| : null} | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| {paymentMethod !=""? | {paymentMethod !=""? | ||||
| @@ -447,10 +485,10 @@ const MultiPaymentWindow = (props) => { | |||||
| </DialogActions> | </DialogActions> | ||||
| <DialogActions> | <DialogActions> | ||||
| { | { | ||||
| props.onPayment? | |||||
| props.onPayment || isPaying? | |||||
| <LoadingComponent disableText={true} alignItems="flex-start"/> | <LoadingComponent disableText={true} alignItems="flex-start"/> | ||||
| : | : | ||||
| <Button variant="contained" onClick={confirmPaymentHandle()} disabled={paymentMethod === "" || isLimit || isPPSLimit} sx={PAY_CONTAINED_BUTTON_SX}> | |||||
| <Button variant="contained" onClick={confirmPaymentHandle()} disabled={paymentMethod === "" || isLimit || isPPSLimit || isPaying} sx={PAY_CONTAINED_BUTTON_SX}> | |||||
| <FormattedMessage id="pay"/> | <FormattedMessage id="pay"/> | ||||
| </Button> | </Button> | ||||
| } | } | ||||
| @@ -472,7 +510,7 @@ const MultiPaymentWindow = (props) => { | |||||
| <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | ||||
| <DialogContent style={{ display: 'flex', }}> | <DialogContent style={{ display: 'flex', }}> | ||||
| <Stack direction="column" justifyContent="space-between"> | <Stack direction="column" justifyContent="space-between"> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} /> | |||||
| </Stack> | </Stack> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -194,7 +194,7 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||||
| <div style={{ width: '100%' }}> | <div style={{ width: '100%' }}> | ||||
| <FiDataGrid | <FiDataGrid | ||||
| sx={_sx} | sx={_sx} | ||||
| rowHeight={80} | |||||
| getRowHeight={() => 'auto'} | |||||
| columns={columns} | columns={columns} | ||||
| customPageSize={10} | customPageSize={10} | ||||
| onRowDoubleClick={handleEditClick} | onRowDoubleClick={handleEditClick} | ||||
| @@ -18,6 +18,21 @@ import dayjs from "dayjs"; | |||||
| import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | ||||
| import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | ||||
| import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14)); | |||||
| const getDefaultDateTo = () => DateUtils.dateValue(new Date()); | |||||
| const isSubmitDateEmpty = (value) => | |||||
| value == null || value === "" || value === "dd / mm / yyyy"; | |||||
| const isBlank = (value) => value == null || String(value).trim() === ""; | |||||
| const toDayjsOrNull = (value) => { | |||||
| if (isSubmitDateEmpty(value)) return null; | |||||
| const d = dayjs(value); | |||||
| return d.isValid() ? d : null; | |||||
| }; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | ||||
| @@ -25,12 +40,11 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | ||||
| const [status, setStatus] = React.useState(ComboData.paymentStatus[0]); | const [status, setStatus] = React.useState(ComboData.paymentStatus[0]); | ||||
| const [payMethod, setPayMethod] = React.useState(ComboData.payMethod[0]); | const [payMethod, setPayMethod] = React.useState(ComboData.payMethod[0]); | ||||
| const { reset, register, handleSubmit } = useForm() | |||||
| const marginBottom = 2.5; | const marginBottom = 2.5; | ||||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const prevHasOtherRef = React.useRef(null); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| if(searchCriteria.status!=undefined){ | if(searchCriteria.status!=undefined){ | ||||
| @@ -72,20 +86,77 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| setToDateValue(maxDate); | setToDateValue(maxDate); | ||||
| }, [maxDate]); | }, [maxDate]); | ||||
| const { reset, register, handleSubmit, watch } = useForm({ | |||||
| defaultValues: { | |||||
| code: searchCriteria.code || "", | |||||
| transNo: searchCriteria.transNo || "" | |||||
| } | |||||
| }); | |||||
| const code = watch("code"); | |||||
| const transNo = watch("transNo"); | |||||
| // add near the top inside the component (after useState for payMethod) | // add near the top inside the component (after useState for payMethod) | ||||
| const toPayMethodArray = (opt) => { | const toPayMethodArray = (opt) => { | ||||
| if (!opt || opt.type === 'all') return []; | if (!opt || opt.type === 'all') return []; | ||||
| return Array.isArray(opt.type) ? opt.type : [opt.type]; | return Array.isArray(opt.type) ? opt.type : [opt.type]; | ||||
| }; | }; | ||||
| const clearSubmitDates = () => { | |||||
| setMinDate(null); | |||||
| setMaxDate(null); | |||||
| }; | |||||
| const restoreDefaultSubmitDates = () => { | |||||
| setMinDate(getDefaultDateFrom()); | |||||
| setMaxDate(getDefaultDateTo()); | |||||
| }; | |||||
| const hasOtherCriteria = (textFields = {}) => { | |||||
| if (!isBlank(textFields.code)) return true; | |||||
| if (!isBlank(textFields.transNo)) return true; | |||||
| if (status?.type && status.type !== "all" && status.type !== "") return true; | |||||
| if (payMethod?.type && payMethod.type !== "all") return true; | |||||
| return false; | |||||
| }; | |||||
| React.useEffect(() => { | |||||
| const hasOther = hasOtherCriteria({ code, transNo }); | |||||
| if (prevHasOtherRef.current === null) { | |||||
| prevHasOtherRef.current = hasOther; | |||||
| return; | |||||
| } | |||||
| if (hasOther && !prevHasOtherRef.current) { | |||||
| clearSubmitDates(); | |||||
| } else if (!hasOther && prevHasOtherRef.current) { | |||||
| // Only refill defaults when From or To is empty; keep user-entered dates otherwise | |||||
| if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) { | |||||
| restoreDefaultSubmitDates(); | |||||
| } | |||||
| } | |||||
| prevHasOtherRef.current = hasOther; | |||||
| }, [code, transNo, status, payMethod]); | |||||
| const onSubmit = (data) => { | const onSubmit = (data) => { | ||||
| let sentDateFrom = ""; | let sentDateFrom = ""; | ||||
| let sentDateTo = ""; | let sentDateTo = ""; | ||||
| if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue) | |||||
| sentDateTo = DateUtils.dateValue(toDateValue) | |||||
| const hasOther = hasOtherCriteria({ | |||||
| code: data.code, | |||||
| transNo: data.transNo | |||||
| }); | |||||
| const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue) | |||||
| || minDate == null || maxDate == null; | |||||
| if (!hasOther && datesEmpty) { | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| sentDateFrom = dateFrom; | |||||
| sentDateTo = dateTo; | |||||
| } else if (!datesEmpty) { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue); | |||||
| sentDateTo = DateUtils.dateValue(toDateValue); | |||||
| } | } | ||||
| const temp = { | const temp = { | ||||
| @@ -98,18 +169,36 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| start:0, | start:0, | ||||
| limit:10 | limit:10 | ||||
| }; | }; | ||||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||||
| temp.sort = searchCriteria.sort; | |||||
| temp.direction = searchCriteria.direction; | |||||
| } | |||||
| applySearch(temp); | applySearch(temp); | ||||
| }; | }; | ||||
| function resetForm() { | function resetForm() { | ||||
| setStatus(ComboData.paymentStatus[0]); | setStatus(ComboData.paymentStatus[0]); | ||||
| setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14))) | |||||
| setMaxDate(DateUtils.dateValue(new Date())) | |||||
| setPayMethod(ComboData.payMethod[0]); | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| reset({ | reset({ | ||||
| code:"", | code:"", | ||||
| transNo:"" | transNo:"" | ||||
| }); | }); | ||||
| localStorage.setItem('searchCriteria',"") | |||||
| prevHasOtherRef.current = false; | |||||
| localStorage.setItem('searchCriteria',""); | |||||
| applySearch({ | |||||
| code: "", | |||||
| transNo: "", | |||||
| dateFrom, | |||||
| dateTo, | |||||
| status: "", | |||||
| payMethod: [], | |||||
| start: 0, | |||||
| limit: 10 | |||||
| }); | |||||
| } | } | ||||
| @@ -150,22 +239,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateFrom" | id="dateFrom" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label="Payment Date (From)" | label="Payment Date (From)" | ||||
| value={minDate === null ? null : dayjs(minDate)} | |||||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||||
| value={toDayjsOrNull(minDate)} | |||||
| maxDate={toDayjsOrNull(maxDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMinDate(newValue); | |||||
| } | |||||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -176,22 +264,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateTo" | id="dateTo" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label="Payment Date (To)" | label="Payment Date (To)" | ||||
| value={maxDate === null ? null : dayjs(maxDate)} | |||||
| minDate={minDate === null ? null : dayjs(minDate)} | |||||
| value={toDayjsOrNull(maxDate)} | |||||
| minDate={toDayjsOrNull(minDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMaxDate(newValue); | |||||
| } | |||||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -119,7 +119,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| <div style={{ minHeight: 400, width: '100%' }}> | <div style={{ minHeight: 400, width: '100%' }}> | ||||
| <FiDataGrid | <FiDataGrid | ||||
| sx={_sx} | sx={_sx} | ||||
| rowHeight={80} | |||||
| getRowHeight={() => 'auto'} | |||||
| columns={columns} | columns={columns} | ||||
| customPageSize={10} | customPageSize={10} | ||||
| onRowDoubleClick={handleEditDoubleClick} | onRowDoubleClick={handleEditDoubleClick} | ||||
| @@ -20,6 +20,21 @@ import dayjs from "dayjs"; | |||||
| import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | ||||
| import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | ||||
| import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14)); | |||||
| const getDefaultDateTo = () => DateUtils.dateValue(new Date()); | |||||
| const isSubmitDateEmpty = (value) => | |||||
| value == null || value === "" || value === "dd / mm / yyyy"; | |||||
| const isBlank = (value) => value == null || String(value).trim() === ""; | |||||
| const toDayjsOrNull = (value) => { | |||||
| if (isSubmitDateEmpty(value)) return null; | |||||
| const d = dayjs(value); | |||||
| return d.isValid() ? d : null; | |||||
| }; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | ||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| @@ -29,9 +44,9 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const prevHasOtherRef = React.useRef(null); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| // console.log(minDate) | |||||
| setFromDateValue(minDate); | setFromDateValue(minDate); | ||||
| }, [minDate]); | }, [minDate]); | ||||
| @@ -55,15 +70,72 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| } | } | ||||
| } | } | ||||
| const { reset, register, handleSubmit } = useForm() | |||||
| const { reset, register, handleSubmit, watch } = useForm({ | |||||
| defaultValues: { | |||||
| code: searchCriteria.code || "", | |||||
| transNo: searchCriteria.transNo || "" | |||||
| } | |||||
| }); | |||||
| const code = watch("code"); | |||||
| const transNo = watch("transNo"); | |||||
| const clearSubmitDates = () => { | |||||
| setMinDate(null); | |||||
| setMaxDate(null); | |||||
| }; | |||||
| const restoreDefaultSubmitDates = () => { | |||||
| setMinDate(getDefaultDateFrom()); | |||||
| setMaxDate(getDefaultDateTo()); | |||||
| }; | |||||
| const hasOtherCriteria = (textFields = {}) => { | |||||
| if (!isBlank(textFields.code)) return true; | |||||
| if (!isBlank(textFields.transNo)) return true; | |||||
| if (status?.type && status.type !== "all" && status.type !== "") return true; | |||||
| return false; | |||||
| }; | |||||
| React.useEffect(() => { | |||||
| const hasOther = hasOtherCriteria({ code, transNo }); | |||||
| if (prevHasOtherRef.current === null) { | |||||
| prevHasOtherRef.current = hasOther; | |||||
| return; | |||||
| } | |||||
| if (hasOther && !prevHasOtherRef.current) { | |||||
| clearSubmitDates(); | |||||
| } else if (!hasOther && prevHasOtherRef.current) { | |||||
| // Only refill defaults when From or To is empty; keep user-entered dates otherwise | |||||
| if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) { | |||||
| restoreDefaultSubmitDates(); | |||||
| } | |||||
| } | |||||
| prevHasOtherRef.current = hasOther; | |||||
| }, [code, transNo, status]); | |||||
| const onSubmit = (data) => { | const onSubmit = (data) => { | ||||
| let sentDateFrom = ""; | let sentDateFrom = ""; | ||||
| let sentDateTo = ""; | let sentDateTo = ""; | ||||
| if( fromDateValue!="dd / mm / yyyy"&&toDateValue!="dd / mm / yyyy"){ | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue) | |||||
| sentDateTo = DateUtils.dateValue(toDateValue) | |||||
| const hasOther = hasOtherCriteria({ | |||||
| code: data.code, | |||||
| transNo: data.transNo | |||||
| }); | |||||
| const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue) | |||||
| || minDate == null || maxDate == null; | |||||
| if (!hasOther && datesEmpty) { | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| sentDateFrom = dateFrom; | |||||
| sentDateTo = dateTo; | |||||
| } else if (!datesEmpty) { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue); | |||||
| sentDateTo = DateUtils.dateValue(toDateValue); | |||||
| } | } | ||||
| const temp = { | const temp = { | ||||
| code: data.code, | code: data.code, | ||||
| transNo: data.transNo, | transNo: data.transNo, | ||||
| @@ -73,18 +145,34 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| start:0, | start:0, | ||||
| limit:10 | limit:10 | ||||
| }; | }; | ||||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||||
| temp.sort = searchCriteria.sort; | |||||
| temp.direction = searchCriteria.direction; | |||||
| } | |||||
| applySearch(temp); | applySearch(temp); | ||||
| }; | }; | ||||
| function resetForm() { | function resetForm() { | ||||
| setStatus(ComboData.paymentStatus[0]); | setStatus(ComboData.paymentStatus[0]); | ||||
| setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14))) | |||||
| setMaxDate(DateUtils.dateValue(new Date())) | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| reset({ | reset({ | ||||
| code:"", | code:"", | ||||
| transNo:"" | transNo:"" | ||||
| }); | }); | ||||
| localStorage.setItem('searchCriteria',"") | |||||
| prevHasOtherRef.current = false; | |||||
| localStorage.setItem('searchCriteria',""); | |||||
| applySearch({ | |||||
| code: "", | |||||
| transNo: "", | |||||
| dateFrom, | |||||
| dateTo, | |||||
| status: "", | |||||
| start: 0, | |||||
| limit: 10 | |||||
| }); | |||||
| } | } | ||||
| @@ -128,23 +216,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateFrom" | id="dateFrom" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label={intl.formatMessage({id: 'payDateFrom'})} | label={intl.formatMessage({id: 'payDateFrom'})} | ||||
| // defaultValue={searchCriteria.dateFrom} | |||||
| value={minDate === null ? null : dayjs(minDate)} | |||||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||||
| value={toDayjsOrNull(minDate)} | |||||
| maxDate={toDayjsOrNull(maxDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMinDate(newValue); | |||||
| } | |||||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -156,23 +242,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateTo" | id="dateTo" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label={intl.formatMessage({id: 'payDateTo'})} | label={intl.formatMessage({id: 'payDateTo'})} | ||||
| // defaultValue={searchCriteria.dateTo} | |||||
| value={maxDate === null ? null : dayjs(maxDate)} | |||||
| minDate={minDate === null ? null : dayjs(minDate)} | |||||
| value={toDayjsOrNull(maxDate)} | |||||
| minDate={toDayjsOrNull(minDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMaxDate(newValue); | |||||
| } | |||||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -106,6 +106,12 @@ const Index = () => { | |||||
| setTotalAmount(totalAmount); | setTotalAmount(totalAmount); | ||||
| setSelectedPaymentMethod("") | setSelectedPaymentMethod("") | ||||
| setConfirmPayment(false) | setConfirmPayment(false) | ||||
| setOnReady(false) | |||||
| setAvailableMethods([]) | |||||
| setFPSStatus({}) | |||||
| setCreditCardStatus({}) | |||||
| setUnionPayStatus({}) | |||||
| setPPSStatus({}) | |||||
| if (totalAmount > 0) { | if (totalAmount > 0) { | ||||
| getAvailablePayment() | getAvailablePayment() | ||||
| getTransactionId() | getTransactionId() | ||||
| @@ -190,48 +196,97 @@ const Index = () => { | |||||
| } | } | ||||
| }, [afterConfirmPayment]); | }, [afterConfirmPayment]); | ||||
| const getAvailablePayment = () =>{ | |||||
| const filterPreferPaymentMethodsByLimit = (methods, amount, limits) => { | |||||
| if (!methods) { | |||||
| return []; | |||||
| } | |||||
| if (!limits) { | |||||
| return methods; | |||||
| } | |||||
| return methods.filter((method) => { | |||||
| const key = (method || "").toLowerCase(); | |||||
| let maxLimit; | |||||
| if (key === "fps") { | |||||
| maxLimit = limits.fpsLimitRecord?.maxLimit; | |||||
| } else if (key === "pps" || key === "ppsb") { | |||||
| maxLimit = limits.ppsbLimitRecord?.maxLimit; | |||||
| } else if (key === "unionpay") { | |||||
| maxLimit = limits.unionPlayLimitRecord?.maxLimit; | |||||
| } else if (key === "visa" || key === "mastercard" || key === "jcb" || key === "creditcard") { | |||||
| maxLimit = limits.creditCardLimitRecord?.maxLimit; | |||||
| } else { | |||||
| return true; | |||||
| } | |||||
| return maxLimit == null || amount <= maxLimit; | |||||
| }); | |||||
| }; | |||||
| const filterAvailableMethodsByLimit = (methods, amount, limits) => { | |||||
| if (!methods) { | |||||
| return []; | |||||
| } | |||||
| if (!limits) { | |||||
| return methods; | |||||
| } | |||||
| return methods.filter((method) => { | |||||
| if (method.subtype === "FPS") { | |||||
| return limits.fpsLimitRecord?.maxLimit == null || amount <= limits.fpsLimitRecord.maxLimit; | |||||
| } | |||||
| if (method.subtype === "PPS") { | |||||
| return limits.ppsbLimitRecord?.maxLimit == null || amount <= limits.ppsbLimitRecord.maxLimit; | |||||
| } | |||||
| if (method.subtype === "CreditCard") { | |||||
| const isUnionPay = method.supportedcard?.some((card) => card === "UnionPay"); | |||||
| if (isUnionPay) { | |||||
| return limits.unionPlayLimitRecord?.maxLimit == null || amount <= limits.unionPlayLimitRecord.maxLimit; | |||||
| } | |||||
| return limits.creditCardLimitRecord?.maxLimit == null || amount <= limits.creditCardLimitRecord.maxLimit; | |||||
| } | |||||
| return true; | |||||
| }); | |||||
| }; | |||||
| const requestPaymentAvailability = (methods, limits) => { | |||||
| HttpUtils.post({ | HttpUtils.post({ | ||||
| url: UrlUtils.PAYMENT_AVAILABLE_PAYMENT, | url: UrlUtils.PAYMENT_AVAILABLE_PAYMENT, | ||||
| params: { | params: { | ||||
| "locale": locale === 'en' ?local.en:locale === 'zh-HK' ?local.zh:local.cn, | "locale": locale === 'en' ?local.en:locale === 'zh-HK' ?local.zh:local.cn, | ||||
| "amount": totalAmount, | "amount": totalAmount, | ||||
| // "eserviceids": [ | |||||
| // "<eserviceid>", "<eserviceid>" | |||||
| // ], | |||||
| "preferpaymentmethods": preferpaymentmethods | |||||
| "preferpaymentmethods": methods | |||||
| }, | }, | ||||
| onSuccess: (responseData) => { | onSuccess: (responseData) => { | ||||
| let availableMethods = responseData.availablepaymentmethods; | |||||
| let availableMethods = filterAvailableMethodsByLimit( | |||||
| responseData.availablepaymentmethods || [], totalAmount, limits | |||||
| ); | |||||
| setAvailableMethods(availableMethods); | setAvailableMethods(availableMethods); | ||||
| HttpUtils.get({ | |||||
| url: UrlUtils.PAYMENT_LIMIT_SETTING_LIST, | |||||
| params: {}, | |||||
| onSuccess: (responseData) => { | |||||
| // console.log(responseData) | |||||
| setPaymentLimit(responseData) | |||||
| }, | |||||
| onError: () =>{ | |||||
| // setOnReady(true) | |||||
| } | |||||
| }); | |||||
| if (availableMethods.length === 0) { | |||||
| setOnReady(true) | |||||
| } | |||||
| }, | }, | ||||
| onError: () =>{ | onError: () =>{ | ||||
| setOnReady(true) | setOnReady(true) | ||||
| } | } | ||||
| }); | }); | ||||
| }; | |||||
| const getAvailablePayment = () =>{ | |||||
| HttpUtils.get({ | HttpUtils.get({ | ||||
| url: UrlUtils.PAYMENT_LIMIT_SETTING_LIST, | url: UrlUtils.PAYMENT_LIMIT_SETTING_LIST, | ||||
| params: {}, | params: {}, | ||||
| onSuccess: (responseData) => { | onSuccess: (responseData) => { | ||||
| // console.log(responseData) | |||||
| setPaymentLimit(responseData) | setPaymentLimit(responseData) | ||||
| const methodsWithinLimit = filterPreferPaymentMethodsByLimit( | |||||
| preferpaymentmethods, totalAmount, responseData | |||||
| ); | |||||
| if (methodsWithinLimit.length === 0) { | |||||
| setAvailableMethods([]) | |||||
| setOnReady(true) | |||||
| return; | |||||
| } | |||||
| requestPaymentAvailability(methodsWithinLimit, responseData); | |||||
| }, | }, | ||||
| onError: () =>{ | onError: () =>{ | ||||
| // setOnReady(true) | |||||
| requestPaymentAvailability(preferpaymentmethods); | |||||
| } | } | ||||
| }); | }); | ||||
| @@ -383,7 +438,7 @@ const Index = () => { | |||||
| setFPSStatus(method) | setFPSStatus(method) | ||||
| } | } | ||||
| } else if (method.subtype === "CreditCard") { | } else if (method.subtype === "CreditCard") { | ||||
| method.supportedcard.forEach((supportedcard) => { | |||||
| (method.supportedcard || []).forEach((supportedcard) => { | |||||
| if (supportedcard === "JCB" || supportedcard === "MasterCard" || supportedcard === "Visa") { | if (supportedcard === "JCB" || supportedcard === "MasterCard" || supportedcard === "Visa") { | ||||
| setCreditCardStatus(method) | setCreditCardStatus(method) | ||||
| } else { | } else { | ||||
| @@ -435,6 +490,7 @@ const Index = () => { | |||||
| // const confirmPaymentHandle = () => () => { | // const confirmPaymentHandle = () => () => { | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (confirmPayment){ | if (confirmPayment){ | ||||
| setOnPayment(true); | |||||
| HttpUtils.post({ | HttpUtils.post({ | ||||
| url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, | url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, | ||||
| params: { | params: { | ||||
| @@ -445,12 +501,22 @@ const Index = () => { | |||||
| setAfterConfirmPayment(true); | setAfterConfirmPayment(true); | ||||
| return; | return; | ||||
| } | } | ||||
| setOnPayment(false); | |||||
| setConfirmPayment(false); | |||||
| let str = ""; | let str = ""; | ||||
| responData.msg.forEach((item) => { | responData.msg.forEach((item) => { | ||||
| str += "App: " + item.appNo + ", 到期日: " + DateUtils.datetimeStr_Cht(item.expiryDate) + "\n"; | str += "App: " + item.appNo + ", 到期日: " + DateUtils.datetimeStr_Cht(item.expiryDate) + "\n"; | ||||
| }); | }); | ||||
| setExpiryDateErrText(str.split('\n').map(str => <>{str}<br/></>)); | setExpiryDateErrText(str.split('\n').map(str => <>{str}<br/></>)); | ||||
| setExpiryDateErr(true); | setExpiryDateErr(true); | ||||
| }, | |||||
| onFail: () => { | |||||
| setOnPayment(false); | |||||
| setConfirmPayment(false); | |||||
| }, | |||||
| onError: () => { | |||||
| setOnPayment(false); | |||||
| setConfirmPayment(false); | |||||
| } | } | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -551,6 +617,7 @@ const Index = () => { | |||||
| setSelectedPaymentMethod={setSelectedPaymentMethod} | setSelectedPaymentMethod={setSelectedPaymentMethod} | ||||
| selectedPaymentMethod={selectedPaymentMethod} | selectedPaymentMethod={selectedPaymentMethod} | ||||
| setConfirmPayment={setConfirmPayment} | setConfirmPayment={setConfirmPayment} | ||||
| confirmPayment={confirmPayment} | |||||
| getMethodImgClass = {getMethodImgClass} | getMethodImgClass = {getMethodImgClass} | ||||
| onReady = {onReady} | onReady = {onReady} | ||||
| locale = {locale} | locale = {locale} | ||||
| @@ -307,7 +307,7 @@ const FormPanel = ({ formData }) => { | |||||
| } | } | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography variant="h5">Upload Files</Typography> | |||||
| <Typography variant="h5">Upload File</Typography> | |||||
| </Button> | </Button> | ||||
| <input | <input | ||||
| id="uploadFileBtn" | id="uploadFileBtn" | ||||
| @@ -28,6 +28,7 @@ const BackgroundHead = { | |||||
| import { PNSPS_BUTTON_THEME, PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; | import { PNSPS_BUTTON_THEME, PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; | ||||
| import { ThemeProvider } from "@emotion/react"; | import { ThemeProvider } from "@emotion/react"; | ||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const Index = ({ record }) => { | const Index = ({ record }) => { | ||||
| @@ -107,7 +108,7 @@ const Index = ({ record }) => { | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | <Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.payMsg1' }, { appNo: record.appNo }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.payMsg1' }, { appNo: record.appNo })} /> | |||||
| <br /> | <br /> | ||||
| <FormattedMessage id="MSG.payMsg2_1" /> | <FormattedMessage id="MSG.payMsg2_1" /> | ||||
| <span style={{ color: "red" }}> | <span style={{ color: "red" }}> | ||||
| @@ -116,14 +117,12 @@ const Index = ({ record }) => { | |||||
| <FormattedMessage id="MSG.payMsg2_2" /> | <FormattedMessage id="MSG.payMsg2_2" /> | ||||
| <br /> | <br /> | ||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage({ id: 'MSG.payMsg3' }, | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.payMsg3' }, | |||||
| { | { | ||||
| issueYear: record?.issueYear, | issueYear: record?.issueYear, | ||||
| issueVolume: record?.issueVolume, | issueVolume: record?.issueVolume, | ||||
| issueNo: record?.issueNo, | issueNo: record?.issueNo, | ||||
| }) | |||||
| }} /> | |||||
| })} /> | |||||
| </Typography> | </Typography> | ||||
| <Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | <Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | ||||
| @@ -213,7 +212,7 @@ const Index = ({ record }) => { | |||||
| <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | ||||
| <DialogContent style={{ display: 'flex', }}> | <DialogContent style={{ display: 'flex', }}> | ||||
| <Stack direction="column" justifyContent="space-between"> | <Stack direction="column" justifyContent="space-between"> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} /> | |||||
| </Stack> | </Stack> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils"; | |||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | ||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| backgroundImage: `url(${titleBackgroundImg})`, | backgroundImage: `url(${titleBackgroundImg})`, | ||||
| width: '100%', | width: '100%', | ||||
| @@ -76,19 +77,16 @@ const isAfter = checkIsOnlyOnlinePayment(); | |||||
| )} | )} | ||||
| <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_demandNote' | id: 'proofPaymentBody_demandNote' | ||||
| }, | }, | ||||
| { | { | ||||
| appNo: record?.appNo, | appNo: record?.appNo, | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_demandNote2' | id: 'proofPaymentBody_demandNote2' | ||||
| }, | }, | ||||
| @@ -96,27 +94,22 @@ const isAfter = checkIsOnlyOnlinePayment(); | |||||
| closingDateOff: DateUtils.dateFormat(record?.closingDateOff, intl.formatMessage({id: "dateStrFormat"})), | closingDateOff: DateUtils.dateFormat(record?.closingDateOff, intl.formatMessage({id: "dateStrFormat"})), | ||||
| email: record?.mail, | email: record?.mail, | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_demandNote3' | id: 'proofPaymentBody_demandNote3' | ||||
| }, | }, | ||||
| { | { | ||||
| paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), | paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_demandNote4' | id: 'proofPaymentBody_demandNote4' | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| </Typography> | </Typography> | ||||
| @@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils"; | |||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | ||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| backgroundImage: `url(${titleBackgroundImg})`, | backgroundImage: `url(${titleBackgroundImg})`, | ||||
| width: '100%', | width: '100%', | ||||
| @@ -78,59 +79,47 @@ const isAfter = checkIsOnlyOnlinePayment(); | |||||
| <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_office' | id: 'proofPaymentBody_office' | ||||
| }, | }, | ||||
| { | { | ||||
| appNo: record?.appNo, | appNo: record?.appNo, | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_office2' | id: 'proofPaymentBody_office2' | ||||
| }, | }, | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_office3' | id: 'proofPaymentBody_office3' | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_office4' | id: 'proofPaymentBody_office4' | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_office5' | id: 'proofPaymentBody_office5' | ||||
| }, | }, | ||||
| { | { | ||||
| paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), | paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_office6' | id: 'proofPaymentBody_office6' | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| </Typography> | </Typography> | ||||
| @@ -22,6 +22,7 @@ import Loadable from 'components/Loadable'; | |||||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | ||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| backgroundImage: `url(${titleBackgroundImg})`, | backgroundImage: `url(${titleBackgroundImg})`, | ||||
| width: '100%', | width: '100%', | ||||
| @@ -195,32 +196,28 @@ const Index = () => { | |||||
| )} | )} | ||||
| <Typography variant="h5" sx={bodySx}> | <Typography variant="h5" sx={bodySx}> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_online' | id: 'proofPaymentBody_online' | ||||
| }, | }, | ||||
| { | { | ||||
| appNo: record?.appNo, | appNo: record?.appNo, | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_online2' | id: 'proofPaymentBody_online2' | ||||
| }, | }, | ||||
| { | { | ||||
| paymentDeadline: DateUtils.formatDateForLocale(record?.expiryDate, intl, locale), | paymentDeadline: DateUtils.formatDateForLocale(record?.expiryDate, intl, locale), | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| {checkPaymentSuspension()? | {checkPaymentSuspension()? | ||||
| <div> | <div> | ||||
| <Typography style={{ textAlign: "flex-start", color: "red" }}> | <Typography style={{ textAlign: "flex-start", color: "red" }}> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} /> | |||||
| </Typography> | </Typography> | ||||
| <br /> | <br /> | ||||
| </div>:null | </div>:null | ||||
| @@ -272,13 +269,11 @@ const Index = () => { | |||||
| } | } | ||||
| <Typography variant="h4" sx={bodySx}> | <Typography variant="h4" sx={bodySx}> | ||||
| <div dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage( | |||||
| <SafeHtml html={intl.formatMessage( | |||||
| { | { | ||||
| id: 'proofPaymentBody_online3' | id: 'proofPaymentBody_online3' | ||||
| } | } | ||||
| ) | |||||
| }} /> | |||||
| )} /> | |||||
| <br /> | <br /> | ||||
| </Typography> | </Typography> | ||||
| @@ -338,7 +333,7 @@ const Index = () => { | |||||
| <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | ||||
| <DialogContent style={{ display: 'flex', }}> | <DialogContent style={{ display: 'flex', }}> | ||||
| <Stack direction="column" justifyContent="space-between"> | <Stack direction="column" justifyContent="space-between"> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} /> | |||||
| </Stack> | </Stack> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -104,7 +104,7 @@ const ApplicationDetailCard = ({ | |||||
| if (cancellingRef.current) return; | if (cancellingRef.current) return; | ||||
| cancellingRef.current = true; | cancellingRef.current = true; | ||||
| setCancelLoading(true); | setCancelLoading(true); | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: UrlUtils.CANCEL_PROOF + "/" + params.id, | url: UrlUtils.CANCEL_PROOF + "/" + params.id, | ||||
| onSuccess: function (responseData) { | onSuccess: function (responseData) { | ||||
| cancellingRef.current = false; | cancellingRef.current = false; | ||||
| @@ -23,6 +23,7 @@ import { useNavigate } from "react-router-dom"; | |||||
| import * as DateUtils from "utils/DateUtils" | import * as DateUtils from "utils/DateUtils" | ||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| import { notifyActionSuccess } from 'utils/CommonFunction'; | import { notifyActionSuccess } from 'utils/CommonFunction'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | ||||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | ||||
| import { ThemeProvider } from "@emotion/react"; | import { ThemeProvider } from "@emotion/react"; | ||||
| @@ -184,12 +185,25 @@ const FormPanel = ({ formData }) => { | |||||
| } | } | ||||
| }); | }); | ||||
| 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 isAcceptedProofReplyFile = (fileName) => { | |||||
| return ["pdf", "jpg", "jpeg", "png"].includes(getFileExtension(fileName)); | |||||
| } | |||||
| const readFile = (event) => { | const readFile = (event) => { | ||||
| let file = event.target.files[0]; | let file = event.target.files[0]; | ||||
| if (file) { | if (file) { | ||||
| if (!file.name.toLowerCase().substr(file.name.length - 4).includes(".pdf")) { | |||||
| if (!isAcceptedProofReplyFile(file.name)) { | |||||
| setWarningTitle(intl.formatMessage({ id: "attention" })) | setWarningTitle(intl.formatMessage({ id: "attention" })) | ||||
| setWarningText(intl.formatMessage({ id: 'requireValidFileWithPdfFormat' })); | |||||
| setWarningText(intl.formatMessage({ id: 'requireValidFileWithProofReplyFormat' })); | |||||
| setIsWarningPopUp(true); | setIsWarningPopUp(true); | ||||
| document.getElementById("uploadFileBtn").value = ""; | document.getElementById("uploadFileBtn").value = ""; | ||||
| return; | return; | ||||
| @@ -382,7 +396,7 @@ const FormPanel = ({ formData }) => { | |||||
| <li><FormattedMessage id="post" /></li> | <li><FormattedMessage id="post" /></li> | ||||
| </ul> | </ul> | ||||
| <Typography variant="h6" component="span"> | <Typography variant="h6" component="span"> | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} /> | |||||
| </Typography> | </Typography> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -468,7 +482,7 @@ const FormPanel = ({ formData }) => { | |||||
| <FormattedMessage id="payOnline" /> | <FormattedMessage id="payOnline" /> | ||||
| {checkPaymentSuspension()? | {checkPaymentSuspension()? | ||||
| <Typography style={{ padding: '16px', color: "red" }}> | <Typography style={{ padding: '16px', color: "red" }}> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} /> | |||||
| </Typography>:null | </Typography>:null | ||||
| } | } | ||||
| <br /><a href="#payOnlineDetails" color='#fff' onClick={() => { | <br /><a href="#payOnlineDetails" color='#fff' onClick={() => { | ||||
| @@ -536,7 +550,7 @@ const FormPanel = ({ formData }) => { | |||||
| <li><FormattedMessage id="post" /></li> | <li><FormattedMessage id="post" /></li> | ||||
| </ul> | </ul> | ||||
| <Typography variant="h6" component="span"> | <Typography variant="h6" component="span"> | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} /> | |||||
| </Typography> | </Typography> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -599,13 +613,13 @@ const FormPanel = ({ formData }) => { | |||||
| } | } | ||||
| {/* <Grid item xs={12}> | {/* <Grid item xs={12}> | ||||
| <Typography variant="h6" component="span" height="100%" > | <Typography variant="h6" component="span" height="100%" > | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> */} | </Grid> */} | ||||
| {!isOnlyOnlinePayment? | {!isOnlyOnlinePayment? | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Typography variant="h6" component="span" height="100%" > | <Typography variant="h6" component="span" height="100%" > | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofImportant" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "proofImportant" })} style={{ padding: 12 }} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid>:null | </Grid>:null | ||||
| } | } | ||||
| @@ -629,6 +643,11 @@ const FormPanel = ({ formData }) => { | |||||
| <FormattedMessage id="requiredUploadFix" />: | <FormattedMessage id="requiredUploadFix" />: | ||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12} md={12} textAlign="left"> | |||||
| <Typography variant="subtitle1" component="span" sx={{ color: 'primary.primary' }}> | |||||
| <FormattedMessage id="acceptProofReplyFileFormats" /> | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12} md={12} textAlign="left"> | <Grid item xs={12} md={12} textAlign="left"> | ||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| @@ -659,7 +678,7 @@ const FormPanel = ({ formData }) => { | |||||
| id="uploadFileBtn" | id="uploadFileBtn" | ||||
| name="file" | name="file" | ||||
| type="file" | type="file" | ||||
| accept=".pdf" | |||||
| accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" | |||||
| style={{ display: 'none' }} | style={{ display: 'none' }} | ||||
| disabled={attachments.length >= (formik.values.groupType === "Private Bill" ? 2 : 1)} | disabled={attachments.length >= (formik.values.groupType === "Private Bill" ? 2 : 1)} | ||||
| onChange={(event) => { | onChange={(event) => { | ||||
| @@ -21,9 +21,10 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| const columns = [ | const columns = [ | ||||
| { | { | ||||
| field: 'actions', | |||||
| field: 'refNo', | |||||
| headerName: 'Proof No.', | headerName: 'Proof No.', | ||||
| width: 170, | width: 170, | ||||
| sortable: true, | |||||
| cellClassName: 'actions', | cellClassName: 'actions', | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| return clickableLink('/proof/reply/' + params.row.id, params.row.refNo); | return clickableLink('/proof/reply/' + params.row.id, params.row.refNo); | ||||
| @@ -34,6 +35,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| field: 'appId', | field: 'appId', | ||||
| headerName: 'Application No./ Gazette Code/ Gazette Issue No.', | headerName: 'Application No./ Gazette Code/ Gazette Issue No.', | ||||
| width: 400, | width: 400, | ||||
| sortable: true, | |||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| let appNo = params.row.appNo; | let appNo = params.row.appNo; | ||||
| let code = params.row.groupNo; | let code = params.row.groupNo; | ||||
| @@ -51,7 +53,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| headerName: 'Status', | headerName: 'Status', | ||||
| flex: 1, | flex: 1, | ||||
| minWidth: 150, | minWidth: 150, | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| filterable: false, | filterable: false, | ||||
| valueGetter: () => '', | valueGetter: () => '', | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| @@ -64,23 +66,14 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| headerName: 'Proof Issue Date', | headerName: 'Proof Issue Date', | ||||
| flex: 1, | flex: 1, | ||||
| minWidth: 200, | minWidth: 200, | ||||
| // sorting/filtering uses this value | |||||
| sortable: true, | |||||
| valueGetter: (params) => DateUtils.toDate(params?.value), | valueGetter: (params) => DateUtils.toDate(params?.value), | ||||
| // display uses this (params.value is the *Date* returned above) | |||||
| valueFormatter: (params) => { | valueFormatter: (params) => { | ||||
| const d = params.value; // Date or Invalid Date | |||||
| const d = params.value; | |||||
| return d instanceof Date && !isNaN(d.getTime()) | return d instanceof Date && !isNaN(d.getTime()) | ||||
| ? DateUtils.dateStr(d) | ? DateUtils.dateStr(d) | ||||
| : ""; | : ""; | ||||
| }, | }, | ||||
| // make sorting 100% deterministic | |||||
| sortComparator: (v1, v2) => { | |||||
| const t1 = v1 instanceof Date && !isNaN(v1.getTime()) ? v1.getTime() : -Infinity; | |||||
| const t2 = v2 instanceof Date && !isNaN(v2.getTime()) ? v2.getTime() : -Infinity; | |||||
| return t1 - t2; | |||||
| } | |||||
| }, | }, | ||||
| { | { | ||||
| id: 'replyDate', | id: 'replyDate', | ||||
| @@ -88,23 +81,14 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| headerName: 'Confirmed/ Return Date', | headerName: 'Confirmed/ Return Date', | ||||
| flex: 1, | flex: 1, | ||||
| minWidth: 200, | minWidth: 200, | ||||
| // sorting/filtering uses this value | |||||
| sortable: true, | |||||
| valueGetter: (params) => DateUtils.toDate(params?.value), | valueGetter: (params) => DateUtils.toDate(params?.value), | ||||
| // display uses this (params.value is the *Date* returned above) | |||||
| valueFormatter: (params) => { | valueFormatter: (params) => { | ||||
| const d = params.value; // Date or Invalid Date | |||||
| const d = params.value; | |||||
| return d instanceof Date && !isNaN(d.getTime()) | return d instanceof Date && !isNaN(d.getTime()) | ||||
| ? DateUtils.dateStr(d) | ? DateUtils.dateStr(d) | ||||
| : ""; | : ""; | ||||
| }, | }, | ||||
| // make sorting 100% deterministic | |||||
| sortComparator: (v1, v2) => { | |||||
| const t1 = v1 instanceof Date && !isNaN(v1.getTime()) ? v1.getTime() : -Infinity; | |||||
| const t2 = v2 instanceof Date && !isNaN(v2.getTime()) ? v2.getTime() : -Infinity; | |||||
| return t1 - t2; | |||||
| } | |||||
| }, | }, | ||||
| { | { | ||||
| id: 'contactPerson', | id: 'contactPerson', | ||||
| @@ -112,6 +96,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| headerName: 'Client', | headerName: 'Client', | ||||
| flex: 1, | flex: 1, | ||||
| minWidth: 200, | minWidth: 200, | ||||
| sortable: true, | |||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| let company = params.row.enCompanyName != null?params.row.enCompanyName: params.row.chCompanyName; | let company = params.row.enCompanyName != null?params.row.enCompanyName: params.row.chCompanyName; | ||||
| company = company != null ? company : ""; | company = company != null ? company : ""; | ||||
| @@ -129,6 +114,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| headerName: 'Gazette Group', | headerName: 'Gazette Group', | ||||
| flex: 1, | flex: 1, | ||||
| minWidth: 200, | minWidth: 200, | ||||
| sortable: true, | |||||
| valueGetter: (params) => { | valueGetter: (params) => { | ||||
| return (params?.value) ? (params?.value) : ""; | return (params?.value) ? (params?.value) : ""; | ||||
| } | } | ||||
| @@ -139,6 +125,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| headerName: 'Amount ($)', | headerName: 'Amount ($)', | ||||
| flex: 1, | flex: 1, | ||||
| minWidth: 200, | minWidth: 200, | ||||
| sortable: true, | |||||
| valueGetter: (params) => { | valueGetter: (params) => { | ||||
| return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | ||||
| } | } | ||||
| @@ -158,10 +145,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||||
| onRowDoubleClick={handleRowDoubleClick} | onRowDoubleClick={handleRowDoubleClick} | ||||
| applyGridOnReady={applyGridOnReady} | applyGridOnReady={applyGridOnReady} | ||||
| applySearch = {applySearch} | applySearch = {applySearch} | ||||
| // doLoad={{ | |||||
| // url: LIST_PROOF, | |||||
| // params: _searchCriteria, | |||||
| // }} | |||||
| serverSorting | |||||
| doLoad={React.useMemo(() => ({ | doLoad={React.useMemo(() => ({ | ||||
| url: LIST_PROOF, | url: LIST_PROOF, | ||||
| params: _searchCriteria, | params: _searchCriteria, | ||||
| @@ -21,6 +21,21 @@ import dayjs from "dayjs"; | |||||
| import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | ||||
| import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | ||||
| import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14)); | |||||
| const getDefaultDateTo = () => DateUtils.dateValue(new Date()); | |||||
| const isSubmitDateEmpty = (value) => | |||||
| value == null || value === "" || value === "dd / mm / yyyy"; | |||||
| const isBlank = (value) => value == null || String(value).trim() === ""; | |||||
| const toDayjsOrNull = (value) => { | |||||
| if (isSubmitDateEmpty(value)) return null; | |||||
| const d = dayjs(value); | |||||
| return d.isValid() ? d : null; | |||||
| }; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | ||||
| }) => { | }) => { | ||||
| @@ -32,7 +47,7 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| const [issueCombo, setIssueCombo] = React.useState([]); | const [issueCombo, setIssueCombo] = React.useState([]); | ||||
| const [groupSelected, setGroupSelected] = React.useState( | const [groupSelected, setGroupSelected] = React.useState( | ||||
| searchCriteria.gazettGroup != undefined | searchCriteria.gazettGroup != undefined | ||||
| ? ComboData.groupTitle.find(item => item.code === searchCriteria.gazettGroup) ?? null | |||||
| ? ComboData.groupTitle.find(item => item.title === searchCriteria.gazettGroup) ?? null | |||||
| : null | : null | ||||
| ); | ); | ||||
| @@ -40,19 +55,8 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | ||||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const prevHasOtherRef = React.useRef(null); | |||||
| // React.useEffect(() => { | |||||
| // if(searchCriteria.status!=undefined){ | |||||
| // if(searchCriteria.status === ""){ | |||||
| // ComboData.proofStatus_GLD[0] | |||||
| // }else{ | |||||
| // setSelectedStatus(ComboData.proofStatus_GLD.find(item => item.type === searchCriteria.status)) | |||||
| // } | |||||
| // }else{ | |||||
| // setSelectedStatus(ComboData.proofStatus_GLD[0]) | |||||
| // } | |||||
| // }, [searchCriteria]); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| setFromDateValue(minDate); | setFromDateValue(minDate); | ||||
| }, [minDate]); | }, [minDate]); | ||||
| @@ -65,7 +69,55 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| const { locale } = intl; | const { locale } = intl; | ||||
| const marginBottom = 2.5; | const marginBottom = 2.5; | ||||
| const { reset, register, handleSubmit } = useForm() | |||||
| const { reset, register, handleSubmit, watch } = useForm({ | |||||
| defaultValues: { | |||||
| refNo: searchCriteria.refNo || "", | |||||
| code: searchCriteria.code || "", | |||||
| contact: searchCriteria.contact || "" | |||||
| } | |||||
| }); | |||||
| const refNo = watch("refNo"); | |||||
| const code = watch("code"); | |||||
| const contact = watch("contact"); | |||||
| const clearSubmitDates = () => { | |||||
| setMinDate(null); | |||||
| setMaxDate(null); | |||||
| }; | |||||
| const restoreDefaultSubmitDates = () => { | |||||
| setMinDate(getDefaultDateFrom()); | |||||
| setMaxDate(getDefaultDateTo()); | |||||
| }; | |||||
| const hasOtherCriteria = (textFields = {}) => { | |||||
| if (!isBlank(textFields.refNo)) return true; | |||||
| if (!isBlank(textFields.code)) return true; | |||||
| if (!isBlank(textFields.contact)) return true; | |||||
| if (groupSelected?.title) return true; | |||||
| if (orgSelected?.key && orgSelected.key > 0) return true; | |||||
| if (issueSelected?.id) return true; | |||||
| if (status?.type && status.type !== "all" && status.type !== "") return true; | |||||
| return false; | |||||
| }; | |||||
| React.useEffect(() => { | |||||
| const hasOther = hasOtherCriteria({ refNo, code, contact }); | |||||
| if (prevHasOtherRef.current === null) { | |||||
| prevHasOtherRef.current = hasOther; | |||||
| return; | |||||
| } | |||||
| if (hasOther && !prevHasOtherRef.current) { | |||||
| clearSubmitDates(); | |||||
| } else if (!hasOther && prevHasOtherRef.current) { | |||||
| // Only refill defaults when From or To is empty; keep user-entered dates otherwise | |||||
| if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) { | |||||
| restoreDefaultSubmitDates(); | |||||
| } | |||||
| } | |||||
| prevHasOtherRef.current = hasOther; | |||||
| }, [refNo, code, contact, groupSelected, orgSelected, issueSelected, status]); | |||||
| const onSubmit = (data) => { | const onSubmit = (data) => { | ||||
| let typeArray = []; | let typeArray = []; | ||||
| let sentDateFrom = ""; | let sentDateFrom = ""; | ||||
| @@ -75,24 +127,49 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| typeArray.push(type[i].label); | typeArray.push(type[i].label); | ||||
| } | } | ||||
| if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue) | |||||
| sentDateTo = DateUtils.dateValue(toDateValue) | |||||
| const hasOther = hasOtherCriteria({ | |||||
| refNo: data.refNo, | |||||
| code: data.code, | |||||
| contact: data.contact | |||||
| }); | |||||
| const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue) | |||||
| || minDate == null || maxDate == null; | |||||
| if (!hasOther && datesEmpty) { | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| sentDateFrom = dateFrom; | |||||
| sentDateTo = dateTo; | |||||
| } else if (!datesEmpty) { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue); | |||||
| sentDateTo = DateUtils.dateValue(toDateValue); | |||||
| } | } | ||||
| const temp = { | const temp = { | ||||
| refNo: data.refNo, | refNo: data.refNo, | ||||
| code: data.code, | code: data.code, | ||||
| issueId: issueSelected?.id, | issueId: issueSelected?.id, | ||||
| gazettGroup: groupSelected?.code, | |||||
| gazettGroup: groupSelected?.title, | |||||
| dateFrom: sentDateFrom, | dateFrom: sentDateFrom, | ||||
| dateTo: sentDateTo, | dateTo: sentDateTo, | ||||
| contact: data.contact, | contact: data.contact, | ||||
| orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "", | orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "", | ||||
| statusKey:status?.key, | statusKey:status?.key, | ||||
| start: 0, | |||||
| limit: 10, | |||||
| }; | }; | ||||
| if (issueSelected?.id) { | |||||
| delete temp.start; | |||||
| delete temp.limit; | |||||
| } | |||||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||||
| temp.sort = searchCriteria.sort; | |||||
| temp.direction = searchCriteria.direction; | |||||
| } | |||||
| if(status?.type && status?.type != 'all'){ | if(status?.type && status?.type != 'all'){ | ||||
| if (status?.type == "Confirmed"){ | if (status?.type == "Confirmed"){ | ||||
| @@ -130,18 +207,34 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| function resetForm() { | function resetForm() { | ||||
| setType([]); | setType([]); | ||||
| setStatus(ComboData.proofStatus[0]); | |||||
| setStatus(ComboData.proofStatus_GLD[0]); | |||||
| setOrgSelected(null); | setOrgSelected(null); | ||||
| setIssueSelected(null); | setIssueSelected(null); | ||||
| setGroupSelected(null); | setGroupSelected(null); | ||||
| setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14))) | |||||
| setMaxDate(DateUtils.dateValue(new Date())) | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| reset({ | reset({ | ||||
| refNo:"", | refNo:"", | ||||
| code:"", | code:"", | ||||
| contact:"" | contact:"" | ||||
| }); | }); | ||||
| localStorage.setItem('searchCriteria',"") | |||||
| prevHasOtherRef.current = false; | |||||
| localStorage.setItem('searchCriteria',""); | |||||
| applySearch({ | |||||
| refNo: "", | |||||
| code: "", | |||||
| issueId: "", | |||||
| gazettGroup: "", | |||||
| dateFrom, | |||||
| dateTo, | |||||
| contact: "", | |||||
| orgId: "", | |||||
| statusKey: 0, | |||||
| start: 0, | |||||
| limit: 10 | |||||
| }); | |||||
| } | } | ||||
| function getIssueLabel(data) { | function getIssueLabel(data) { | ||||
| @@ -279,22 +372,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateFrom" | id="dateFrom" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label="Proof Issue Date (From)" | label="Proof Issue Date (From)" | ||||
| value={minDate === null ? null : dayjs(minDate)} | |||||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||||
| value={toDayjsOrNull(minDate)} | |||||
| maxDate={toDayjsOrNull(maxDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMinDate(newValue); | |||||
| } | |||||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -306,22 +398,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateTo" | id="dateTo" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label="Proof Issue Date (To)" | label="Proof Issue Date (To)" | ||||
| value={maxDate === null ? null : dayjs(maxDate)} | |||||
| minDate={minDate === null ? null : dayjs(minDate)} | |||||
| value={toDayjsOrNull(maxDate)} | |||||
| minDate={toDayjsOrNull(minDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMaxDate(newValue); | |||||
| } | |||||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -106,10 +106,11 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| const columns = [ | const columns = [ | ||||
| { | { | ||||
| field: 'actions', | |||||
| field: 'refNo', | |||||
| headerName: intl.formatMessage({ id: 'proofId' }), | headerName: intl.formatMessage({ id: 'proofId' }), | ||||
| width: isMdOrLg ? 'auto' : 200, | width: isMdOrLg ? 'auto' : 200, | ||||
| flex: isMdOrLg ? 1.5 : undefined, | flex: isMdOrLg ? 1.5 : undefined, | ||||
| sortable: true, | |||||
| cellClassName: 'actions', | cellClassName: 'actions', | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| @@ -122,6 +123,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount3' }) : intl.formatMessage({ id: 'gazetteCount2' }), | headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount3' }) : intl.formatMessage({ id: 'gazetteCount2' }), | ||||
| width: isMdOrLg ? 'auto' : 330, | width: isMdOrLg ? 'auto' : 330, | ||||
| flex: isMdOrLg ? 2 : undefined, | flex: isMdOrLg ? 2 : undefined, | ||||
| sortable: true, | |||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| // let appNo = params.row.appNo; | // let appNo = params.row.appNo; | ||||
| @@ -138,6 +140,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| headerName: intl.formatMessage({ id: 'proofDate' }), | headerName: intl.formatMessage({ id: 'proofDate' }), | ||||
| width: isMdOrLg ? 'auto' : 200, | width: isMdOrLg ? 'auto' : 200, | ||||
| flex: isMdOrLg ? 1.5 : undefined, | flex: isMdOrLg ? 1.5 : undefined, | ||||
| sortable: true, | |||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| valueGetter: (params) => { | valueGetter: (params) => { | ||||
| return DateUtils.datetimeStr(params?.value); | return DateUtils.datetimeStr(params?.value); | ||||
| @@ -149,6 +152,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| headerName: intl.formatMessage({ id: 'replyBefore' }), | headerName: intl.formatMessage({ id: 'replyBefore' }), | ||||
| width: isMdOrLg ? 'auto' : 200, | width: isMdOrLg ? 'auto' : 200, | ||||
| flex: isMdOrLg ? 1.5 : undefined, | flex: isMdOrLg ? 1.5 : undefined, | ||||
| sortable: true, | |||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| valueGetter: (params) => { | valueGetter: (params) => { | ||||
| const proofPaymentDeadline = DateUtils.convertToDate(params?.value); | const proofPaymentDeadline = DateUtils.convertToDate(params?.value); | ||||
| @@ -164,6 +168,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| headerName: intl.formatMessage({ id: 'replyDate' }), | headerName: intl.formatMessage({ id: 'replyDate' }), | ||||
| width: isMdOrLg ? 'auto' : 200, | width: isMdOrLg ? 'auto' : 200, | ||||
| flex: isMdOrLg ? 1.5 : undefined, | flex: isMdOrLg ? 1.5 : undefined, | ||||
| sortable: true, | |||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| valueGetter: (params) => { | valueGetter: (params) => { | ||||
| return params?.value ? DateUtils.datetimeStr(params?.value) : ""; | return params?.value ? DateUtils.datetimeStr(params?.value) : ""; | ||||
| @@ -174,6 +179,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| headerName: intl.formatMessage({ id: 'status' }), | headerName: intl.formatMessage({ id: 'status' }), | ||||
| width: isMdOrLg ? 'auto' : 160, | width: isMdOrLg ? 'auto' : 160, | ||||
| flex: isMdOrLg ? 1 : undefined, | flex: isMdOrLg ? 1 : undefined, | ||||
| sortable: true, | |||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| return locale === 'en' ? ProofStatus.getStatus_Eng(params) : locale === 'zh-HK' ? ProofStatus.getStatus_Cht(params) : ProofStatus.getStatus_Cn(params); | return locale === 'en' ? ProofStatus.getStatus_Eng(params) : locale === 'zh-HK' ? ProofStatus.getStatus_Cht(params) : ProofStatus.getStatus_Cn(params); | ||||
| @@ -185,6 +191,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| headerName: intl.formatMessage({ id: 'fee' }), | headerName: intl.formatMessage({ id: 'fee' }), | ||||
| width: isMdOrLg ? 'auto' : 160, | width: isMdOrLg ? 'auto' : 160, | ||||
| flex: isMdOrLg ? 1 : undefined, | flex: isMdOrLg ? 1 : undefined, | ||||
| sortable: true, | |||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| valueGetter: (params) => { | valueGetter: (params) => { | ||||
| return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | ||||
| @@ -207,6 +214,8 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| onRowDoubleClick={handleRowDoubleClick} | onRowDoubleClick={handleRowDoubleClick} | ||||
| applyGridOnReady={applyGridOnReady} | applyGridOnReady={applyGridOnReady} | ||||
| applySearch={applySearch} | applySearch={applySearch} | ||||
| serverSorting | |||||
| disablePagingOnGazetteIssue={false} | |||||
| doLoad={React.useMemo(() => ({ | doLoad={React.useMemo(() => ({ | ||||
| url: LIST_PROOF, | url: LIST_PROOF, | ||||
| params: _searchCriteria, | params: _searchCriteria, | ||||
| @@ -214,4 +223,4 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | |||||
| } | |||||
| @@ -21,6 +21,21 @@ import dayjs from "dayjs"; | |||||
| import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | ||||
| import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | ||||
| import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14)); | |||||
| const getDefaultDateTo = () => DateUtils.dateValue(new Date()); | |||||
| const isSubmitDateEmpty = (value) => | |||||
| value == null || value === "" || value === "dd / mm / yyyy"; | |||||
| const isBlank = (value) => value == null || String(value).trim() === ""; | |||||
| const toDayjsOrNull = (value) => { | |||||
| if (isSubmitDateEmpty(value)) return null; | |||||
| const d = dayjs(value); | |||||
| return d.isValid() ? d : null; | |||||
| }; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | ||||
| }) => { | }) => { | ||||
| @@ -31,12 +46,13 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| const [status, setStatus] = React.useState(searchCriteria.statusKey!=undefined?ComboData.proofStatusFull[searchCriteria.statusKey]:ComboData.proofStatusFull[0]); | const [status, setStatus] = React.useState(searchCriteria.statusKey!=undefined?ComboData.proofStatusFull[searchCriteria.statusKey]:ComboData.proofStatusFull[0]); | ||||
| const [issueSelected, setIssueSelected] = React.useState(null); | const [issueSelected, setIssueSelected] = React.useState(null); | ||||
| const [issueCombo, setIssueCombo] = React.useState([]); | const [issueCombo, setIssueCombo] = React.useState([]); | ||||
| const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.code === searchCriteria.gazettGroup):{}); | |||||
| const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.title === searchCriteria.gazettGroup):{}); | |||||
| const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom); | const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom); | ||||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | ||||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const prevHasOtherRef = React.useRef(null); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| setFromDateValue(minDate); | setFromDateValue(minDate); | ||||
| @@ -62,7 +78,50 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| } | } | ||||
| } | } | ||||
| const { reset, register, handleSubmit } = useForm() | |||||
| const { reset, register, handleSubmit, watch } = useForm({ | |||||
| defaultValues: { | |||||
| refNo: searchCriteria.refNo || "", | |||||
| code: searchCriteria.code || "" | |||||
| } | |||||
| }); | |||||
| const refNo = watch("refNo"); | |||||
| const code = watch("code"); | |||||
| const clearSubmitDates = () => { | |||||
| setMinDate(null); | |||||
| setMaxDate(null); | |||||
| }; | |||||
| const restoreDefaultSubmitDates = () => { | |||||
| setMinDate(getDefaultDateFrom()); | |||||
| setMaxDate(getDefaultDateTo()); | |||||
| }; | |||||
| const hasOtherCriteria = (textFields = {}) => { | |||||
| if (!isBlank(textFields.refNo)) return true; | |||||
| if (!isBlank(textFields.code)) return true; | |||||
| if (issueSelected?.id) return true; | |||||
| if (status?.type && status.type !== "all" && status.type !== "") return true; | |||||
| return false; | |||||
| }; | |||||
| React.useEffect(() => { | |||||
| const hasOther = hasOtherCriteria({ refNo, code }); | |||||
| if (prevHasOtherRef.current === null) { | |||||
| prevHasOtherRef.current = hasOther; | |||||
| return; | |||||
| } | |||||
| if (hasOther && !prevHasOtherRef.current) { | |||||
| clearSubmitDates(); | |||||
| } else if (!hasOther && prevHasOtherRef.current) { | |||||
| // Only refill defaults when From or To is empty; keep user-entered dates otherwise | |||||
| if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) { | |||||
| restoreDefaultSubmitDates(); | |||||
| } | |||||
| } | |||||
| prevHasOtherRef.current = hasOther; | |||||
| }, [refNo, code, issueSelected, status]); | |||||
| const onSubmit = (data) => { | const onSubmit = (data) => { | ||||
| let typeArray = []; | let typeArray = []; | ||||
| let sentDateFrom = ""; | let sentDateFrom = ""; | ||||
| @@ -72,20 +131,42 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| typeArray.push(type[i].label); | typeArray.push(type[i].label); | ||||
| } | } | ||||
| if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue) | |||||
| sentDateTo = DateUtils.dateValue(toDateValue) | |||||
| const hasOther = hasOtherCriteria({ | |||||
| refNo: data.refNo, | |||||
| code: data.code | |||||
| }); | |||||
| const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue) | |||||
| || minDate == null || maxDate == null; | |||||
| if (!hasOther && datesEmpty) { | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| sentDateFrom = dateFrom; | |||||
| sentDateTo = dateTo; | |||||
| } else if (!datesEmpty) { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue); | |||||
| sentDateTo = DateUtils.dateValue(toDateValue); | |||||
| } | } | ||||
| const temp = { | const temp = { | ||||
| refNo: data.refNo, | refNo: data.refNo, | ||||
| code: data.code, | code: data.code, | ||||
| issueId: issueSelected?.id, | issueId: issueSelected?.id, | ||||
| gazettGroup: groupSelected?.type, | |||||
| gazettGroup: groupSelected?.title, | |||||
| dateFrom: sentDateFrom, | dateFrom: sentDateFrom, | ||||
| dateTo: sentDateTo, | dateTo: sentDateTo, | ||||
| statusKey:status?.key, | statusKey:status?.key, | ||||
| start: 0, | |||||
| limit: 10, | |||||
| }; | }; | ||||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||||
| temp.sort = searchCriteria.sort; | |||||
| temp.direction = searchCriteria.direction; | |||||
| } | |||||
| if(status?.type && status?.type != 'all'){ | if(status?.type && status?.type != 'all'){ | ||||
| if (status?.type == "Confirmed"){ | if (status?.type == "Confirmed"){ | ||||
| temp["replyed"] = "T"; | temp["replyed"] = "T"; | ||||
| @@ -120,13 +201,27 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| setStatus(ComboData.proofStatusFull[0]); | setStatus(ComboData.proofStatusFull[0]); | ||||
| setIssueSelected(null); | setIssueSelected(null); | ||||
| setGroupSelected({}); | setGroupSelected({}); | ||||
| setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14))) | |||||
| setMaxDate(DateUtils.dateValue(new Date())) | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| reset({ | reset({ | ||||
| refNo:"", | refNo:"", | ||||
| code:"", | code:"", | ||||
| }); | }); | ||||
| localStorage.setItem('searchCriteria',"") | |||||
| prevHasOtherRef.current = false; | |||||
| localStorage.setItem('searchCriteria',""); | |||||
| applySearch({ | |||||
| refNo: "", | |||||
| code: "", | |||||
| issueId: "", | |||||
| gazettGroup: "", | |||||
| dateFrom, | |||||
| dateTo, | |||||
| statusKey: 0, | |||||
| start: 0, | |||||
| limit: 10 | |||||
| }); | |||||
| } | } | ||||
| function getIssueLabel(data) { | function getIssueLabel(data) { | ||||
| @@ -261,22 +356,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateFrom" | id="dateFrom" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label={intl.formatMessage({id: 'proofDateFrom'})} | label={intl.formatMessage({id: 'proofDateFrom'})} | ||||
| value={minDate === null ? null : dayjs(minDate)} | |||||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||||
| value={toDayjsOrNull(minDate)} | |||||
| maxDate={toDayjsOrNull(maxDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMinDate(newValue); | |||||
| } | |||||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -288,22 +382,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateTo" | id="dateTo" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label={intl.formatMessage({id: 'proofDateTo'})} | label={intl.formatMessage({id: 'proofDateTo'})} | ||||
| value={maxDate === null ? null : dayjs(maxDate)} | |||||
| minDate={minDate === null ? null : dayjs(minDate)} | |||||
| value={toDayjsOrNull(maxDate)} | |||||
| minDate={toDayjsOrNull(minDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMaxDate(newValue); | |||||
| } | |||||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -34,6 +34,7 @@ import { FormattedMessage, useIntl } from "react-intl"; | |||||
| import { useState, useEffect, useRef } from 'react'; | import { useState, useEffect, useRef } from 'react'; | ||||
| import { checkPaymentSuspension } from "utils/Utils"; | import { checkPaymentSuspension } from "utils/Utils"; | ||||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| @@ -409,13 +410,12 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||||
| ml:2 | ml:2 | ||||
| }} | }} | ||||
| > | > | ||||
| <span | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage({ | |||||
| id: "suspensionMessageText", | |||||
| defaultMessage: "" | |||||
| }) | |||||
| }} | |||||
| <SafeHtml | |||||
| component="span" | |||||
| html={intl.formatMessage({ | |||||
| id: "suspensionMessageText", | |||||
| defaultMessage: "" | |||||
| })} | |||||
| /> | /> | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| @@ -459,7 +459,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||||
| <li><FormattedMessage id="post" /></li> | <li><FormattedMessage id="post" /></li> | ||||
| </ul> | </ul> | ||||
| <Typography variant="h6" component="span"> | <Typography variant="h6" component="span"> | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} /> | |||||
| </Typography> | </Typography> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -603,14 +603,14 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||||
| <Grid item xs={12} mr={1} mb={2}> | <Grid item xs={12} mr={1} mb={2}> | ||||
| <Typography variant="pnspsFormParagraphBold"> | <Typography variant="pnspsFormParagraphBold"> | ||||
| <span style={{ textAlign: 'justify', }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr0" }) }} /> | |||||
| <SafeHtml component="span" style={{ textAlign: 'justify', }} html={intl.formatMessage({ id: "applyTickUnderStr0" })} /> | |||||
| </Typography> | </Typography> | ||||
| <Typography display="inline" variant="subtitle1" component="span" sx={{ color: 'primary.primary' }} > | <Typography display="inline" variant="subtitle1" component="span" sx={{ color: 'primary.primary' }} > | ||||
| <ol style={{ textAlign: 'justify', }}> | <ol style={{ textAlign: 'justify', }}> | ||||
| <li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr1" }) }} /> | |||||
| <li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr2" }) }} /> | |||||
| <li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr3" }) }} /> | |||||
| <li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "tradeMarkFootnote" }) }} /> | |||||
| <SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr1" })} /> | |||||
| <SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr2" })} /> | |||||
| <SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr3" })} /> | |||||
| <SafeHtml component="li" html={intl.formatMessage({ id: "tradeMarkFootnote" })} /> | |||||
| </ol> | </ol> | ||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| @@ -630,7 +630,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||||
| }} | }} | ||||
| /> | /> | ||||
| <Typography variant="h6" component="span" height="100%" > | <Typography variant="h6" component="span" height="100%" > | ||||
| <div style={{ padding: 12, textAlign: 'justify' }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickStr" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "applyTickStr" })} style={{ padding: 12, textAlign: 'justify' }} /> | |||||
| </Typography> | </Typography> | ||||
| </Stack> | </Stack> | ||||
| </Grid> | </Grid> | ||||
| @@ -653,7 +653,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Typography variant="h6" component="span" height="100%" > | <Typography variant="h6" component="span" height="100%" > | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyPublicNoticeText" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "applyPublicNoticeText" })} style={{ padding: 12 }} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| @@ -5,6 +5,7 @@ import * as UrlUtils from "utils/ApiPathConst"; | |||||
| import * as FormatUtils from "utils/FormatUtils"; | import * as FormatUtils from "utils/FormatUtils"; | ||||
| import * as DateUtils from "utils/DateUtils"; | import * as DateUtils from "utils/DateUtils"; | ||||
| import { useIntl } from "react-intl"; | import { useIntl } from "react-intl"; | ||||
| import { useNavigate } from "react-router-dom"; | |||||
| import { | import { | ||||
| Grid, | Grid, | ||||
| @@ -24,6 +25,7 @@ import { | |||||
| // checkIsOnlyOnlinePayment | // checkIsOnlyOnlinePayment | ||||
| // isCreditorLoggedIn | // isCreditorLoggedIn | ||||
| } from "utils/Utils"; | } from "utils/Utils"; | ||||
| import { fetchOrgBrData, applyBlockVariant } from "utils/orgBrUtils"; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const ApplyForm = () => { | const ApplyForm = () => { | ||||
| @@ -35,12 +37,23 @@ const ApplyForm = () => { | |||||
| const [selections, setSelection] = React.useState([]); | const [selections, setSelection] = React.useState([]); | ||||
| const [isLoading, setLoding] = React.useState(true); | const [isLoading, setLoding] = React.useState(true); | ||||
| const [orgBrReady, setOrgBrReady] = React.useState(false); | |||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| const navigate = useNavigate(); | |||||
| const { locale } = intl; | const { locale } = intl; | ||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| loadUserData(); | loadUserData(); | ||||
| fetchOrgBrData({ | |||||
| onSuccess: (org) => { | |||||
| if (applyBlockVariant(org)) { | |||||
| navigate("/dashboard", { replace: true }); | |||||
| return; | |||||
| } | |||||
| setOrgBrReady(true); | |||||
| } | |||||
| }); | |||||
| }, []); | }, []); | ||||
| const loadUserData = () => { | const loadUserData = () => { | ||||
| @@ -112,11 +125,10 @@ const ApplyForm = () => { | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| if (userData !== null){ | |||||
| if (userData !== null && orgBrReady){ | |||||
| setLoding(false); | setLoding(false); | ||||
| // console.log(isOnlyOnlinePayment) | |||||
| } | } | ||||
| }, [userData]); | |||||
| }, [userData, orgBrReady]); | |||||
| return ( | return ( | ||||
| isLoading ? | isLoading ? | ||||
| @@ -274,7 +274,7 @@ const PublicNoticeDetail_GLD = () => { | |||||
| setStatusWindowAccepted(false); | setStatusWindowAccepted(false); | ||||
| return; | return; | ||||
| } | } | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: `${SET_PUBLIC_NOTICE_STATUS_PUBLISH}/${params.id}`, | url: `${SET_PUBLIC_NOTICE_STATUS_PUBLISH}/${params.id}`, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| setOpen(false); | setOpen(false); | ||||
| @@ -299,7 +299,7 @@ const PublicNoticeDetail_GLD = () => { | |||||
| const onPaidClick = () => { | const onPaidClick = () => { | ||||
| if (params.id > 0) { | if (params.id > 0) { | ||||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_PAID}/${params.id}`) | |||||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_PAID}/${params.id}`) | |||||
| .then((response) => { | .then((response) => { | ||||
| if (response.status === 204) { | if (response.status === 204) { | ||||
| setOpen(false); | setOpen(false); | ||||
| @@ -325,7 +325,7 @@ const PublicNoticeDetail_GLD = () => { | |||||
| const onComplatedClick = () => { | const onComplatedClick = () => { | ||||
| if (params.id > 0) { | if (params.id > 0) { | ||||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_COMPLATED}/${params.id}`) | |||||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_COMPLATED}/${params.id}`) | |||||
| .then((response) => { | .then((response) => { | ||||
| if (response.status === 204) { | if (response.status === 204) { | ||||
| setOpen(false); | setOpen(false); | ||||
| @@ -351,7 +351,7 @@ const PublicNoticeDetail_GLD = () => { | |||||
| const onWithdrawnClick = () => { | const onWithdrawnClick = () => { | ||||
| if (params.id > 0) { | if (params.id > 0) { | ||||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_WITHDRAW}/${params.id}`) | |||||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_WITHDRAW}/${params.id}`) | |||||
| .then((response) => { | .then((response) => { | ||||
| if (response.status === 204) { | if (response.status === 204) { | ||||
| setOpen(false); | setOpen(false); | ||||
| @@ -421,7 +421,7 @@ const PublicNoticeDetail_GLD = () => { | |||||
| setStatusWindowAccepted(false); | setStatusWindowAccepted(false); | ||||
| return; | return; | ||||
| } | } | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: `${SET_PUBLIC_NOTICE_STATUS_REVOKE}/${params.id}`, | url: `${SET_PUBLIC_NOTICE_STATUS_REVOKE}/${params.id}`, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| setOpen(false); | setOpen(false); | ||||
| @@ -447,7 +447,7 @@ const PublicNoticeDetail_GLD = () => { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const status = applicationDetailData.data != undefined ? applicationDetailData.data.status : "" | const status = applicationDetailData.data != undefined ? applicationDetailData.data.status : "" | ||||
| if (status === "submitted" && params.id > 0 && getUploadStatus) { | if (status === "submitted" && params.id > 0 && getUploadStatus) { | ||||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_REVIEWED}/${params.id}`) | |||||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_REVIEWED}/${params.id}`) | |||||
| .then((response) => { | .then((response) => { | ||||
| if (response.status === 204) { | if (response.status === 204) { | ||||
| setUploadStatus(false); | setUploadStatus(false); | ||||
| @@ -41,6 +41,7 @@ import EditNoteIcon from '@mui/icons-material/EditNote'; | |||||
| import DownloadIcon from '@mui/icons-material/Download'; | import DownloadIcon from '@mui/icons-material/Download'; | ||||
| import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; | import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; | ||||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import { ThemeProvider } from "@emotion/react"; | import { ThemeProvider } from "@emotion/react"; | ||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| @@ -114,6 +115,13 @@ const ApplicationDetailCard = ( | |||||
| setStatus("cancel") | setStatus("cancel") | ||||
| }; | }; | ||||
| const isPastPaymentDeadline = (expiryDate) => { | |||||
| if (!expiryDate) return false; | |||||
| const deadline = DateUtils.convertToDate(expiryDate); | |||||
| if (!deadline || Number.isNaN(deadline.getTime())) return false; | |||||
| return Date.now() >= deadline.getTime(); | |||||
| }; | |||||
| const checkExprityDate = () => { | const checkExprityDate = () => { | ||||
| HttpUtils.post({ | HttpUtils.post({ | ||||
| url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, | url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, | ||||
| @@ -202,7 +210,8 @@ const ApplicationDetailCard = ( | |||||
| > | > | ||||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_BUTTON_THEME}> | ||||
| { | { | ||||
| currentApplicationDetailData.status == "confirmed" ? | |||||
| currentApplicationDetailData.status == "confirmed" | |||||
| && !isPastPaymentDeadline(currentApplicationDetailData.expiryDate) ? | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| color="create" | color="create" | ||||
| @@ -834,7 +843,7 @@ const ApplicationDetailCard = ( | |||||
| <Typography variant="h4" component="span" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | <Typography variant="h4" component="span" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | ||||
| <DialogContent style={{ display: 'flex', }}> | <DialogContent style={{ display: 'flex', }}> | ||||
| <Stack direction="column" justifyContent="space-between"> | <Stack direction="column" justifyContent="space-between"> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} /> | |||||
| </Stack> | </Stack> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -145,7 +145,7 @@ const DashboardDefault = () => { | |||||
| if (params.id > 0) { | if (params.id > 0) { | ||||
| cancellingRef.current = true; | cancellingRef.current = true; | ||||
| setCancelLoading(true); | setCancelLoading(true); | ||||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_CANCELLED}/${params.id}`) | |||||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_CANCELLED}/${params.id}`) | |||||
| .then((response) => { | .then((response) => { | ||||
| if (response.status === 204) { | if (response.status === 204) { | ||||
| notifyActionSuccess("取消成功!") | notifyActionSuccess("取消成功!") | ||||
| @@ -28,6 +28,7 @@ import { | |||||
| // import { dateStr } from "utils/DateUtils"; | // import { dateStr } from "utils/DateUtils"; | ||||
| import { ThemeProvider, useTheme } from "@emotion/react"; | import { ThemeProvider, useTheme } from "@emotion/react"; | ||||
| import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; | import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import { PRIMARY_TEXT_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | import { PRIMARY_TEXT_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | ||||
| // ==============================|| EVENT TABLE ||============================== // | // ==============================|| EVENT TABLE ||============================== // | ||||
| @@ -397,9 +398,11 @@ export default function SubmittedTab({ setCount, url }) { | |||||
| <> | <> | ||||
| <div style={{ minHeight: 400, width: '100%', padding: 4 }}> | <div style={{ minHeight: 400, width: '100%', padding: 4 }}> | ||||
| {isORGLoggedIn() ? | {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> | ||||
| <Grid item xs={8} md={2}> | <Grid item xs={8} md={2}> | ||||
| <Autocomplete | <Autocomplete | ||||
| @@ -580,7 +583,7 @@ export default function SubmittedTab({ setCount, url }) { | |||||
| <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | ||||
| <DialogContent style={{ display: 'flex', }}> | <DialogContent style={{ display: 'flex', }}> | ||||
| <Stack direction="column" justifyContent="space-between"> | <Stack direction="column" justifyContent="space-between"> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} /> | |||||
| </Stack> | </Stack> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -23,6 +23,20 @@ import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | |||||
| import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | ||||
| import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14)); | |||||
| const getDefaultDateTo = () => DateUtils.dateValue(new Date()); | |||||
| const isSubmitDateEmpty = (value) => | |||||
| value == null || value === "" || value === "dd / mm / yyyy"; | |||||
| const isBlank = (value) => value == null || String(value).trim() === ""; | |||||
| const toDayjsOrNull = (value) => { | |||||
| if (isSubmitDateEmpty(value)) return null; | |||||
| const d = dayjs(value); | |||||
| return d.isValid() ? d : null; | |||||
| }; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | ||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| @@ -33,9 +47,19 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | ||||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const prevHasOtherRef = React.useRef(null); | |||||
| // const [selectedLabelsString, setSelectedLabelsString] = React.useState(''); | // const [selectedLabelsString, setSelectedLabelsString] = React.useState(''); | ||||
| const { reset, register, handleSubmit } = useForm() | |||||
| const { reset, register, handleSubmit, watch } = useForm({ | |||||
| defaultValues: { | |||||
| appNo: searchCriteria.appNo || "", | |||||
| careOf: searchCriteria.careOf || "", | |||||
| contact: searchCriteria.contact || "" | |||||
| } | |||||
| }); | |||||
| const appNo = watch("appNo"); | |||||
| const careOf = watch("careOf"); | |||||
| const contact = watch("contact"); | |||||
| const marginBottom = 2.5; | const marginBottom = 2.5; | ||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| @@ -70,6 +94,41 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| setToDateValue(maxDate); | setToDateValue(maxDate); | ||||
| }, [maxDate]); | }, [maxDate]); | ||||
| const clearSubmitDates = () => { | |||||
| setMinDate(null); | |||||
| setMaxDate(null); | |||||
| }; | |||||
| const restoreDefaultSubmitDates = () => { | |||||
| setMinDate(getDefaultDateFrom()); | |||||
| setMaxDate(getDefaultDateTo()); | |||||
| }; | |||||
| const hasOtherCriteria = (textFields = {}) => { | |||||
| if (!isBlank(textFields.appNo)) return true; | |||||
| if (!isBlank(textFields.contact)) return true; | |||||
| if (!isBlank(textFields.careOf)) return true; | |||||
| if (status?.type && status.type !== "all" && status.type !== "") return true; | |||||
| return false; | |||||
| }; | |||||
| React.useEffect(() => { | |||||
| const hasOther = hasOtherCriteria({ appNo, contact, careOf }); | |||||
| if (prevHasOtherRef.current === null) { | |||||
| prevHasOtherRef.current = hasOther; | |||||
| return; | |||||
| } | |||||
| if (hasOther && !prevHasOtherRef.current) { | |||||
| clearSubmitDates(); | |||||
| } else if (!hasOther && prevHasOtherRef.current) { | |||||
| // Only refill defaults when From or To is empty; keep user-entered dates otherwise | |||||
| if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) { | |||||
| restoreDefaultSubmitDates(); | |||||
| } | |||||
| } | |||||
| prevHasOtherRef.current = hasOther; | |||||
| }, [appNo, contact, careOf, status]); | |||||
| const onSubmit = (data) => { | const onSubmit = (data) => { | ||||
| data.status = status.type; | data.status = status.type; | ||||
| let typeArray = []; | let typeArray = []; | ||||
| @@ -80,9 +139,24 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| typeArray.push(type[i].label); | typeArray.push(type[i].label); | ||||
| } | } | ||||
| if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue) | |||||
| sentDateTo = DateUtils.dateValue(toDateValue) | |||||
| const hasOther = hasOtherCriteria({ | |||||
| appNo: data.appNo, | |||||
| contact: data.contact, | |||||
| careOf: data.careOf | |||||
| }); | |||||
| const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue) | |||||
| || minDate == null || maxDate == null; | |||||
| if (!hasOther && datesEmpty) { | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| sentDateFrom = dateFrom; | |||||
| sentDateTo = dateTo; | |||||
| } else if (!datesEmpty) { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue); | |||||
| sentDateTo = DateUtils.dateValue(toDateValue); | |||||
| } | } | ||||
| const temp = { | const temp = { | ||||
| @@ -95,18 +169,38 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| start:0, | start:0, | ||||
| limit:10 | limit:10 | ||||
| }; | }; | ||||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||||
| temp.sort = searchCriteria.sort; | |||||
| temp.direction = searchCriteria.direction; | |||||
| } | |||||
| applySearch(temp); | applySearch(temp); | ||||
| }; | }; | ||||
| function resetForm() { | function resetForm() { | ||||
| setType([]); | setType([]); | ||||
| setStatus(localStorage.getItem('userData').creditor?ComboData.publicNoticeStatic_Creditor[0]:ComboData.publicNoticeStatic[0]); | setStatus(localStorage.getItem('userData').creditor?ComboData.publicNoticeStatic_Creditor[0]:ComboData.publicNoticeStatic[0]); | ||||
| setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14))) | |||||
| setMaxDate(DateUtils.dateValue(new Date())) | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| reset({ | reset({ | ||||
| appNo:"" | |||||
| appNo: "", | |||||
| careOf: "", | |||||
| contact: "" | |||||
| }); | |||||
| prevHasOtherRef.current = false; | |||||
| localStorage.setItem('searchCriteria', ""); | |||||
| // Reset form criteria and sorting first, then reload with backend default sort | |||||
| applySearch({ | |||||
| appNo: "", | |||||
| dateFrom, | |||||
| dateTo, | |||||
| contact: "", | |||||
| careOf: "", | |||||
| status: "", | |||||
| start: 0, | |||||
| limit: 10 | |||||
| }); | }); | ||||
| localStorage.setItem('searchCriteria',"") | |||||
| } | } | ||||
| return ( | return ( | ||||
| @@ -142,23 +236,22 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateFrom" | id="dateFrom" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| aria-label={intl.formatMessage({id: 'submitDateFrom'})} | aria-label={intl.formatMessage({id: 'submitDateFrom'})} | ||||
| label={intl.formatMessage({id: 'submitDateFrom'})} | label={intl.formatMessage({id: 'submitDateFrom'})} | ||||
| value={minDate === null ? null : dayjs(minDate)} | |||||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||||
| value={toDayjsOrNull(minDate)} | |||||
| maxDate={toDayjsOrNull(maxDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMinDate(newValue); | |||||
| } | |||||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -170,22 +263,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateTo" | id="dateTo" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label={intl.formatMessage({id: 'submitDateTo'})} | label={intl.formatMessage({id: 'submitDateTo'})} | ||||
| value={maxDate === null ? null : dayjs(maxDate)} | |||||
| minDate={minDate === null ? null : dayjs(minDate)} | |||||
| value={toDayjsOrNull(maxDate)} | |||||
| minDate={toDayjsOrNull(minDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMaxDate(newValue); | |||||
| } | |||||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -45,6 +45,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'appNo', | id: 'appNo', | ||||
| field: 'appNo', | field: 'appNo', | ||||
| headerName: intl.formatMessage({ id: 'applicationId' }), | headerName: intl.formatMessage({ id: 'applicationId' }), | ||||
| sortable: true, | |||||
| width: isMdOrLg ? 'auto' : 160, | width: isMdOrLg ? 'auto' : 160, | ||||
| flex: isMdOrLg ? 1 : undefined, | flex: isMdOrLg ? 1 : undefined, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| @@ -56,6 +57,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'created', | id: 'created', | ||||
| field: 'created', | field: 'created', | ||||
| headerName: intl.formatMessage({ id: 'submitDate' }), | headerName: intl.formatMessage({ id: 'submitDate' }), | ||||
| sortable: true, | |||||
| width: isMdOrLg ? 'auto' : 160, | width: isMdOrLg ? 'auto' : 160, | ||||
| flex: isMdOrLg ? 1 : undefined, | flex: isMdOrLg ? 1 : undefined, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| @@ -63,36 +65,11 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| return DateUtils.datetimeStr(params?.value); | return DateUtils.datetimeStr(params?.value); | ||||
| } | } | ||||
| }, | }, | ||||
| // { | |||||
| // id: 'contactPerson', | |||||
| // field: 'contactPerson', | |||||
| // headerName: '聯絡人', | |||||
| // flex: 2, | |||||
| // renderCell: (params) => { | |||||
| // let phone = JSON.parse(params.row.contactTelNo); | |||||
| // let faxNo = JSON.parse(params.row.contactFaxNo); | |||||
| // let contact = ""; | |||||
| // if (phone) { | |||||
| // contact = "電話: " + phone?.countryCode + " " + phone?.phoneNumber | |||||
| // } | |||||
| // if (faxNo && faxNo?.faxNumber) { | |||||
| // if (contact != "") | |||||
| // contact = contact + ", " | |||||
| // contact = contact + "傳真:" + faxNo?.countryCode + " " + faxNo?.faxNumber | |||||
| // } | |||||
| // return (<> | |||||
| // {params?.value}<br /> | |||||
| // {contact} | |||||
| // </>); | |||||
| // } | |||||
| // }, | |||||
| { | { | ||||
| id: 'remarks', | id: 'remarks', | ||||
| field: 'remarks', | field: 'remarks', | ||||
| headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount2_1' }) : intl.formatMessage({ id: 'myRemarks' }), | headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount2_1' }) : intl.formatMessage({ id: 'myRemarks' }), | ||||
| sortable: true, | |||||
| width: isMdOrLg ? 'auto' : 400, | width: isMdOrLg ? 'auto' : 400, | ||||
| flex: isMdOrLg ? 3 : undefined, | flex: isMdOrLg ? 3 : undefined, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| @@ -124,6 +101,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'status', | id: 'status', | ||||
| field: 'status', | field: 'status', | ||||
| headerName: intl.formatMessage({ id: 'status' }), | headerName: intl.formatMessage({ id: 'status' }), | ||||
| sortable: true, | |||||
| width: 200, | width: 200, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| @@ -134,6 +112,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| field: 'actions', | field: 'actions', | ||||
| type: 'actions', | type: 'actions', | ||||
| headerName: '', | headerName: '', | ||||
| sortable: false, | |||||
| width: 150, | width: 150, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| cellClassName: 'actions', | cellClassName: 'actions', | ||||
| @@ -157,6 +136,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| <FiDataGrid | <FiDataGrid | ||||
| columns={columns} | columns={columns} | ||||
| customPageSize={10} | customPageSize={10} | ||||
| serverSorting | |||||
| getRowHeight={() => 'auto'} | getRowHeight={() => 'auto'} | ||||
| onRowDoubleClick={handleRowDoubleClick} | onRowDoubleClick={handleRowDoubleClick} | ||||
| applyGridOnReady = {applyGridOnReady} | applyGridOnReady = {applyGridOnReady} | ||||
| @@ -32,6 +32,8 @@ import { PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; | |||||
| import { ThemeProvider } from "@emotion/react"; | import { ThemeProvider } from "@emotion/react"; | ||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import usePageTitle from 'components/usePageTitle'; | import usePageTitle from 'components/usePageTitle'; | ||||
| import { fetchOrgBrData, applyBlockVariant, applyClickVariant } from "utils/orgBrUtils"; | |||||
| import BrStatusDialog from "components/BrStatusDialog"; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const PublicNotice = () => { | const PublicNotice = () => { | ||||
| @@ -44,6 +46,8 @@ const PublicNotice = () => { | |||||
| const [selectedTab, setSelectedTab] = useState("1"); | const [selectedTab, setSelectedTab] = useState("1"); | ||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const intl = useIntl(); | const intl = useIntl(); | ||||
| const [orgBrData, setOrgBrData] = useState(null); | |||||
| const [brDialog, setBrDialog] = useState({ open: false, variant: null, afterClose: null }); | |||||
| const _sx = { | const _sx = { | ||||
| padding: "4 2 4 2", | padding: "4 2 4 2", | ||||
| @@ -76,6 +80,9 @@ const PublicNotice = () => { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| loadData(); | loadData(); | ||||
| fetchOrgBrData({ | |||||
| onSuccess: (org) => setOrgBrData(org) | |||||
| }); | |||||
| }, []); | }, []); | ||||
| const loadData = () => { | const loadData = () => { | ||||
| @@ -99,6 +106,15 @@ const PublicNotice = () => { | |||||
| } | } | ||||
| const onBtnClick = () => { | const onBtnClick = () => { | ||||
| const variant = applyClickVariant(orgBrData); | |||||
| if (variant) { | |||||
| setBrDialog({ | |||||
| open: true, | |||||
| variant, | |||||
| afterClose: applyBlockVariant(orgBrData) ? "stay" : "apply", | |||||
| }); | |||||
| return; | |||||
| } | |||||
| navigate('/publicNotice/apply') | 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> | </Grid> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -67,9 +67,9 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| const columns = [ | const columns = [ | ||||
| { | { | ||||
| field: 'actions', | |||||
| field: 'appNo', | |||||
| headerName: 'Application No.', | headerName: 'Application No.', | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| width: 150, | width: 150, | ||||
| cellClassName: 'actions', | cellClassName: 'actions', | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| @@ -81,7 +81,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'mode', | id: 'mode', | ||||
| field: 'mode', | field: 'mode', | ||||
| headerName: 'Mode', | headerName: 'Mode', | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| width: 100, | width: 100, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| @@ -92,7 +92,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'status', | id: 'status', | ||||
| field: 'status', | field: 'status', | ||||
| headerName: 'Status', | headerName: 'Status', | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| width: 240, | width: 240, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| @@ -103,7 +103,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'withProof', | id: 'withProof', | ||||
| field: 'withProof', | field: 'withProof', | ||||
| headerName: 'With Proof', | headerName: 'With Proof', | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| width: 120, | width: 120, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| @@ -114,7 +114,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'created', | id: 'created', | ||||
| field: 'created', | field: 'created', | ||||
| headerName: 'Submit Date', | headerName: 'Submit Date', | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| flex: 1, | flex: 1, | ||||
| minWidth: 200, | minWidth: 200, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| @@ -126,7 +126,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'contactPerson', | id: 'contactPerson', | ||||
| field: 'contactPerson', | field: 'contactPerson', | ||||
| headerName: 'Client / Payment Means', | headerName: 'Client / Payment Means', | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| minWidth: 250, | minWidth: 250, | ||||
| flex: 2, | flex: 2, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| @@ -137,8 +137,14 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| company = company + ": " + params.row.custName | company = company + ": " + params.row.custName | ||||
| } | } | ||||
| let paymentMethod = params.row.paymentMethod!=null?intl.formatMessage({ id: utils.getPaymentMethod(params.row.paymentMethod)}):"" | let paymentMethod = params.row.paymentMethod!=null?intl.formatMessage({ id: utils.getPaymentMethod(params.row.paymentMethod)}):"" | ||||
| const lines = [params?.value, company, paymentMethod].filter(line => line != null && line !== ""); | |||||
| return (<> | return (<> | ||||
| {params?.value}<br />{company} <br/>{paymentMethod} | |||||
| {lines.map((line, index) => ( | |||||
| <React.Fragment key={index}> | |||||
| {index > 0 && <br />} | |||||
| {line} | |||||
| </React.Fragment> | |||||
| ))} | |||||
| </>); | </>); | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -146,7 +152,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| id: 'issueNoAndCode', | id: 'issueNoAndCode', | ||||
| field: 'issueNoAndCode', | field: 'issueNoAndCode', | ||||
| headerName: 'Gazette Issue No. / Gazette Code', | headerName: 'Gazette Issue No. / Gazette Code', | ||||
| sortable: false, | |||||
| sortable: true, | |||||
| flex: 1.5, | flex: 1.5, | ||||
| minWidth: 350, | minWidth: 350, | ||||
| renderHeader: renderHeaderWithAria, | renderHeader: renderHeaderWithAria, | ||||
| @@ -220,6 +226,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||||
| <FiDataGrid | <FiDataGrid | ||||
| checkboxSelection | checkboxSelection | ||||
| disableRowSelectionOnClick | disableRowSelectionOnClick | ||||
| serverSorting | |||||
| onRowSelectionModelChange={(newSelection) => { | onRowSelectionModelChange={(newSelection) => { | ||||
| setSelectedRowItems(newSelection); | setSelectedRowItems(newSelection); | ||||
| }} | }} | ||||
| @@ -21,6 +21,21 @@ import dayjs from "dayjs"; | |||||
| import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | import {DemoItem} from "@mui/x-date-pickers/internals/demo"; | ||||
| import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; | ||||
| import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14)); | |||||
| const getDefaultDateTo = () => DateUtils.dateValue(new Date()); | |||||
| const isSubmitDateEmpty = (value) => | |||||
| value == null || value === "" || value === "dd / mm / yyyy"; | |||||
| const isBlank = (value) => value == null || String(value).trim() === ""; | |||||
| const toDayjsOrNull = (value) => { | |||||
| if (isSubmitDateEmpty(value)) return null; | |||||
| const d = dayjs(value); | |||||
| return d.isValid() ? d : null; | |||||
| }; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | ||||
| }) => { | }) => { | ||||
| @@ -31,12 +46,13 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| const [issueCombo, setIssueCombo] = React.useState([]); | const [issueCombo, setIssueCombo] = React.useState([]); | ||||
| const [selectedStatus, setSelectedStatus] = React.useState({key: 0, label: 'All', type: 'all'}); | const [selectedStatus, setSelectedStatus] = React.useState({key: 0, label: 'All', type: 'all'}); | ||||
| const [selectedMode, setSelectedMode] = React.useState({key: 0, label: 'All', type: 'all'}); | const [selectedMode, setSelectedMode] = React.useState({key: 0, label: 'All', type: 'all'}); | ||||
| const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.code === searchCriteria.gazettGroup):{}); | |||||
| const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.title === searchCriteria.gazettGroup):{}); | |||||
| const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom); | const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom); | ||||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | ||||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | ||||
| const prevHasOtherRef = React.useRef(null); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| if(searchCriteria.status!=undefined){ | if(searchCriteria.status!=undefined){ | ||||
| @@ -64,7 +80,56 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| const { locale } = intl; | const { locale } = intl; | ||||
| const marginBottom = 2.5; | const marginBottom = 2.5; | ||||
| const { reset, register, handleSubmit } = useForm() | |||||
| const { reset, register, handleSubmit, watch } = useForm({ | |||||
| defaultValues: { | |||||
| appNo: searchCriteria.appNo || "", | |||||
| contact: searchCriteria.contact || "", | |||||
| groupNo: searchCriteria.groupNo || "" | |||||
| } | |||||
| }); | |||||
| const appNo = watch("appNo"); | |||||
| const contact = watch("contact"); | |||||
| const groupNo = watch("groupNo"); | |||||
| const clearSubmitDates = () => { | |||||
| setMinDate(null); | |||||
| setMaxDate(null); | |||||
| }; | |||||
| const restoreDefaultSubmitDates = () => { | |||||
| setMinDate(getDefaultDateFrom()); | |||||
| setMaxDate(getDefaultDateTo()); | |||||
| }; | |||||
| const hasOtherCriteria = (textFields = {}) => { | |||||
| if (!isBlank(textFields.appNo)) return true; | |||||
| if (!isBlank(textFields.contact)) return true; | |||||
| if (!isBlank(textFields.groupNo)) return true; | |||||
| if (groupSelected?.title) return true; | |||||
| if (orgSelected?.key && orgSelected.key > 0) return true; | |||||
| if (issueSelected?.id) return true; | |||||
| if (selectedStatus?.type && selectedStatus.type !== "all" && selectedStatus.type !== "") return true; | |||||
| if (selectedMode?.type && selectedMode.type !== "all" && selectedMode.type !== "") return true; | |||||
| return false; | |||||
| }; | |||||
| React.useEffect(() => { | |||||
| const hasOther = hasOtherCriteria({ appNo, contact, groupNo }); | |||||
| if (prevHasOtherRef.current === null) { | |||||
| prevHasOtherRef.current = hasOther; | |||||
| return; | |||||
| } | |||||
| if (hasOther && !prevHasOtherRef.current) { | |||||
| clearSubmitDates(); | |||||
| } else if (!hasOther && prevHasOtherRef.current) { | |||||
| // Only refill defaults when From or To is empty; keep user-entered dates otherwise | |||||
| if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) { | |||||
| restoreDefaultSubmitDates(); | |||||
| } | |||||
| } | |||||
| prevHasOtherRef.current = hasOther; | |||||
| }, [appNo, contact, groupNo, groupSelected, orgSelected, issueSelected, selectedStatus, selectedMode]); | |||||
| const onSubmit = (data) => { | const onSubmit = (data) => { | ||||
| // localStorage.setItem('searchCriteria',"") | // localStorage.setItem('searchCriteria',"") | ||||
| data.status = selectedStatus?.type | data.status = selectedStatus?.type | ||||
| @@ -76,10 +141,25 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| for (let i = 0; i < type.length; i++) { | for (let i = 0; i < type.length; i++) { | ||||
| typeArray.push(type[i].label); | typeArray.push(type[i].label); | ||||
| } | } | ||||
| if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue) | |||||
| sentDateTo = DateUtils.dateValue(toDateValue) | |||||
| const hasOther = hasOtherCriteria({ | |||||
| appNo: data.appNo, | |||||
| contact: data.contact, | |||||
| groupNo: data.groupNo | |||||
| }); | |||||
| const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue) | |||||
| || minDate == null || maxDate == null; | |||||
| if (!hasOther && datesEmpty) { | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| sentDateFrom = dateFrom; | |||||
| sentDateTo = dateTo; | |||||
| } else if (!datesEmpty) { | |||||
| sentDateFrom = DateUtils.dateValue(fromDateValue); | |||||
| sentDateTo = DateUtils.dateValue(toDateValue); | |||||
| } | } | ||||
| const temp = { | const temp = { | ||||
| @@ -91,11 +171,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "", | orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "", | ||||
| issueId: issueSelected?.id, | issueId: issueSelected?.id, | ||||
| groupNo: data.groupNo, | groupNo: data.groupNo, | ||||
| gazettGroup: groupSelected?.code, | |||||
| gazettGroup: groupSelected?.title, | |||||
| mode: (data.mode === '' || data.mode?.includes("all")) ? "" : data.mode, | mode: (data.mode === '' || data.mode?.includes("all")) ? "" : data.mode, | ||||
| start:0, | |||||
| limit:10 | |||||
| }; | }; | ||||
| // Gazette Issue No. search loads all matching rows (no paging) | |||||
| if (issueSelected?.id) { | |||||
| delete temp.start; | |||||
| delete temp.limit; | |||||
| } else { | |||||
| temp.start = 0; | |||||
| temp.limit = 10; | |||||
| } | |||||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||||
| temp.sort = searchCriteria.sort; | |||||
| temp.direction = searchCriteria.direction; | |||||
| } | |||||
| applySearch(temp); | applySearch(temp); | ||||
| // setSearchReady(true) | // setSearchReady(true) | ||||
| }; | }; | ||||
| @@ -117,14 +207,32 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| setGroupSelected({}); | setGroupSelected({}); | ||||
| setSelectedStatus({key: 0, label: 'All', type: 'all'}); | setSelectedStatus({key: 0, label: 'All', type: 'all'}); | ||||
| setSelectedMode({key: 0, label: 'All', type: 'all'}); | setSelectedMode({key: 0, label: 'All', type: 'all'}); | ||||
| setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14))) | |||||
| setMaxDate(DateUtils.dateValue(new Date())) | |||||
| const dateFrom = getDefaultDateFrom(); | |||||
| const dateTo = getDefaultDateTo(); | |||||
| setMinDate(dateFrom); | |||||
| setMaxDate(dateTo); | |||||
| reset({ | reset({ | ||||
| appNo:"", | appNo:"", | ||||
| contact:"", | contact:"", | ||||
| groupNo:"" | groupNo:"" | ||||
| }); | }); | ||||
| localStorage.setItem('searchCriteria',"") | |||||
| prevHasOtherRef.current = false; | |||||
| localStorage.setItem('searchCriteria',""); | |||||
| // Reset form criteria and sorting first, then reload with backend default sort | |||||
| applySearch({ | |||||
| appNo: "", | |||||
| dateFrom, | |||||
| dateTo, | |||||
| contact: "", | |||||
| status: "", | |||||
| orgId: "", | |||||
| issueId: "", | |||||
| groupNo: "", | |||||
| gazettGroup: "", | |||||
| mode: "", | |||||
| start: 0, | |||||
| limit: 10 | |||||
| }); | |||||
| } | } | ||||
| const getIssueLabel=(data)=> { | const getIssueLabel=(data)=> { | ||||
| @@ -188,22 +296,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateFrom" | id="dateFrom" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label={"Submit Date (From)"} | label={"Submit Date (From)"} | ||||
| value={minDate === null ? null : dayjs(minDate)} | |||||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||||
| value={toDayjsOrNull(minDate)} | |||||
| maxDate={toDayjsOrNull(maxDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMinDate(newValue); | |||||
| } | |||||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -215,22 +322,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||||
| <DemoItem components={['DatePicker']}> | <DemoItem components={['DatePicker']}> | ||||
| <DatePicker | <DatePicker | ||||
| id="dateTo" | id="dateTo" | ||||
| // onError={(newError) => setReceiptFromError(newError)} | |||||
| onError={() => {}} | |||||
| slotProps={{ | slotProps={{ | ||||
| field: { readOnly: true, }, | |||||
| // textField: { | |||||
| // helperText: receiptFromErrorMessage, | |||||
| // }, | |||||
| field: { readOnly: true, clearable: true }, | |||||
| textField: { | |||||
| InputLabelProps: { shrink: true }, | |||||
| error: false, | |||||
| helperText: null | |||||
| }, | |||||
| }} | }} | ||||
| format="DD/MM/YYYY" | format="DD/MM/YYYY" | ||||
| label={"Submit Date (To)"} | label={"Submit Date (To)"} | ||||
| value={maxDate === null ? null : dayjs(maxDate)} | |||||
| minDate={minDate === null ? null : dayjs(minDate)} | |||||
| value={toDayjsOrNull(maxDate)} | |||||
| minDate={toDayjsOrNull(minDate)} | |||||
| onChange={(newValue) => { | onChange={(newValue) => { | ||||
| // console.log(newValue) | |||||
| if(newValue!=null){ | |||||
| setMaxDate(newValue); | |||||
| } | |||||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||||
| }} | }} | ||||
| /> | /> | ||||
| </DemoItem > | </DemoItem > | ||||
| @@ -1,21 +1,51 @@ | |||||
| // material-ui | // material-ui | ||||
| import { | import { | ||||
| // Box, | |||||
| Autocomplete, | |||||
| TextField, | |||||
| Typography | Typography | ||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import MainCard from "components/MainCard"; | import MainCard from "components/MainCard"; | ||||
| import * as React from "react"; | import * as React from "react"; | ||||
| import { FiDataGrid } from "components/FiDataGrid"; | 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 ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | ||||
| const intl = useIntl(); | |||||
| const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); | const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); | ||||
| const [nameOptions, setNameOptions] = React.useState([]); | |||||
| const [selectedName, setSelectedName] = React.useState(""); | |||||
| React.useEffect(() => { | 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 = [ | const columns = [ | ||||
| { | { | ||||
| @@ -45,7 +75,7 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | |||||
| flex: 1, | flex: 1, | ||||
| minWidth: 400, | minWidth: 400, | ||||
| renderCell:(params)=>{ | renderCell:(params)=>{ | ||||
| return <div dangerouslySetInnerHTML={{__html: params.value}} /> | |||||
| return <SafeHtml html={params.value} /> | |||||
| } | } | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -60,6 +90,37 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | |||||
| System Params | System Params | ||||
| </Typography> | </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" }, }}> | <div style={{ width: { xs: '92vw', sm: '96.5vw', md: "auto" }, }}> | ||||
| {/* <Box width= '100%' sx={{ backgroundColor: "#fff", ml: 2 }} height='100%'> */} | {/* <Box width= '100%' sx={{ backgroundColor: "#fff", ml: 2 }} height='100%'> */} | ||||
| <FiDataGrid | <FiDataGrid | ||||
| @@ -264,7 +264,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||||
| }; | }; | ||||
| const onVerifiedClick = () => { | const onVerifiedClick = () => { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: UrlUtils.GET_IND_USER_VERIFY + "/" + formData.id, | url: UrlUtils.GET_IND_USER_VERIFY + "/" + formData.id, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| notifyVerifySuccess() | notifyVerifySuccess() | ||||
| @@ -274,7 +274,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||||
| }; | }; | ||||
| const doLock = () => { | const doLock = () => { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: UrlUtils.GET_USER_LOCK + "/" + formData.id, | url: UrlUtils.GET_USER_LOCK + "/" + formData.id, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| notifyLockSuccess() | notifyLockSuccess() | ||||
| @@ -284,7 +284,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||||
| }; | }; | ||||
| const doUnlock = () => { | const doUnlock = () => { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: UrlUtils.GET_USER_UNLOCK + "/" + formData.id, | url: UrlUtils.GET_USER_UNLOCK + "/" + formData.id, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| notifyActiveSuccess() | notifyActiveSuccess() | ||||
| @@ -130,7 +130,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) => | |||||
| const onVerifiedClick = () => { | const onVerifiedClick = () => { | ||||
| if (formik?.values?.orgId) { | if (formik?.values?.orgId) { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: GET_IND_USER_VERIFY + "/" + userData.id, | url: GET_IND_USER_VERIFY + "/" + userData.id, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| notifyVerifySuccess() | notifyVerifySuccess() | ||||
| @@ -148,7 +148,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) => | |||||
| setConfirmText("Confirm to Lock this Account?"); | setConfirmText("Confirm to Lock this Account?"); | ||||
| setConfirmAction({ | setConfirmAction({ | ||||
| function: function () { | function: function () { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: GET_USER_LOCK + "/" + userData.id, | url: GET_USER_LOCK + "/" + userData.id, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| notifyLockSuccess() | notifyLockSuccess() | ||||
| @@ -165,7 +165,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) => | |||||
| setConfirmAction({ | setConfirmAction({ | ||||
| function: function () { | function: function () { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: GET_USER_UNLOCK + "/" + userData.id, | url: GET_USER_UNLOCK + "/" + userData.id, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| notifyActiveSuccess() | notifyActiveSuccess() | ||||
| @@ -77,7 +77,7 @@ const ManageOrgUserPage = () => { | |||||
| function onActiveClick(params) { | function onActiveClick(params) { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: GET_USER_UNLOCK + "/" + params.row.id, | url: GET_USER_UNLOCK + "/" + params.row.id, | ||||
| onSuccess: () => { | onSuccess: () => { | ||||
| setReloadTime(new Date()); | setReloadTime(new Date()); | ||||
| @@ -117,7 +117,7 @@ const ManageOrgUserPage = () => { | |||||
| const setPrimaryUser = () => { | const setPrimaryUser = () => { | ||||
| setIsWarningPopUp(false) | setIsWarningPopUp(false) | ||||
| HttpUtils.get( | |||||
| HttpUtils.post( | |||||
| { | { | ||||
| url: (!selectUser.row.primaryUser ? GET_SET_PRIMARY_USER : GET_SET_UN_PRIMARY_USER) + "/" + selectUser.row.id, | url: (!selectUser.row.primaryUser ? GET_SET_PRIMARY_USER : GET_SET_UN_PRIMARY_USER) + "/" + selectUser.row.id, | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| @@ -37,7 +37,7 @@ export default function UserTable({searchCriteria, applyGridOnReady,applySearch} | |||||
| } | } | ||||
| const doLock = (id) => { | const doLock = (id) => { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: GET_USER_LOCK+"/"+id, | url: GET_USER_LOCK+"/"+id, | ||||
| onSuccess: function(){ | onSuccess: function(){ | ||||
| //setChangeLocked(true) | //setChangeLocked(true) | ||||
| @@ -48,7 +48,7 @@ export default function UserTable({searchCriteria, applyGridOnReady,applySearch} | |||||
| }; | }; | ||||
| const doUnlock = (id) => { | const doUnlock = (id) => { | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: GET_USER_UNLOCK+"/"+id, | url: GET_USER_UNLOCK+"/"+id, | ||||
| onSuccess: function(){ | onSuccess: function(){ | ||||
| //setChangeLocked(true) | //setChangeLocked(true) | ||||
| @@ -61,7 +61,7 @@ const Mail = () => { | |||||
| }); | }); | ||||
| const setReminderDate=()=>{ | const setReminderDate=()=>{ | ||||
| HttpUtils.get({ | |||||
| HttpUtils.post({ | |||||
| url: apiPath + "/demandNote/set-expect-reminder", | url: apiPath + "/demandNote/set-expect-reminder", | ||||
| onSuccess: function () { | onSuccess: function () { | ||||
| setResponsText("Success"); | setResponsText("Success"); | ||||
| @@ -10,6 +10,7 @@ import backbroundImg from 'assets/images/bg_ml.jpg'; | |||||
| import lgceImg from 'assets/images/2025_lgce.jpg'; | import lgceImg from 'assets/images/2025_lgce.jpg'; | ||||
| import 'assets/style/loginStyles.css'; | import 'assets/style/loginStyles.css'; | ||||
| import { SysContext } from 'components/SysSettingProvider'; | import { SysContext } from 'components/SysSettingProvider'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const AuthCard = Loadable(lazy(() => import('./AuthCardCustom'))); | const AuthCard = Loadable(lazy(() => import('./AuthCardCustom'))); | ||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| @@ -175,11 +176,9 @@ const AuthWrapper = ({ children }) => { | |||||
| '& p': { marginBottom: '12px', lineHeight: 1.7 } | '& p': { marginBottom: '12px', lineHeight: 1.7 } | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography | |||||
| component="div" | |||||
| sx={{ fontSize: 18, lineHeight: 1.8 }} | |||||
| dangerouslySetInnerHTML={{ __html: popupHtml }} | |||||
| /> | |||||
| <Typography component="div" sx={{ fontSize: 18, lineHeight: 1.8 }}> | |||||
| <SafeHtml html={popupHtml} /> | |||||
| </Typography> | |||||
| </DialogContent> | </DialogContent> | ||||
| </Dialog> | </Dialog> | ||||
| @@ -207,24 +206,18 @@ const AuthWrapper = ({ children }) => { | |||||
| > | > | ||||
| <Grid item xs={12} sx={{ px: { xs: 2 }, pl: { md: 4 }, pt: { xs: 4, sm: 2, md: 1.5 }, pb: { xs: 4, sm: 0 } }}> | <Grid item xs={12} sx={{ px: { xs: 2 }, pl: { md: 4 }, pt: { xs: 4, sm: 2, md: 1.5 }, pb: { xs: 4, sm: 0 } }}> | ||||
| {checkPaymentSuspension() ? ( | {checkPaymentSuspension() ? ( | ||||
| <Typography | |||||
| component="div" | |||||
| sx={{ color: 'error.main', textAlign: 'start', p: 1.5 }} | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage({ id: 'suspensionMessageText', defaultMessage: '' }) | |||||
| }} | |||||
| /> | |||||
| <Typography component="div" sx={{ color: 'error.main', textAlign: 'start', p: 1.5 }}> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'suspensionMessageText', defaultMessage: '' })} /> | |||||
| </Typography> | |||||
| ) : ( | ) : ( | ||||
| <Typography | |||||
| component="div" | |||||
| sx={{ textAlign: 'start', p: 1.5 }} | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: intl.formatMessage({ | |||||
| <Typography component="div" sx={{ textAlign: 'start', p: 1.5 }}> | |||||
| <SafeHtml | |||||
| html={intl.formatMessage({ | |||||
| id: isOnlyOnline ? 'landingMessage' : 'homePageHeaderMessage', | id: isOnlyOnline ? 'landingMessage' : 'homePageHeaderMessage', | ||||
| defaultMessage: '' | defaultMessage: '' | ||||
| }) | |||||
| }} | |||||
| /> | |||||
| })} | |||||
| /> | |||||
| </Typography> | |||||
| )} | )} | ||||
| </Grid> | </Grid> | ||||
| @@ -246,10 +239,14 @@ const AuthWrapper = ({ children }) => { | |||||
| <Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}> | <Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}> | ||||
| <FormattedMessage id="PNSPS_fullname" /> | <FormattedMessage id="PNSPS_fullname" /> | ||||
| </Typography> | </Typography> | ||||
| {checkSysEnv() !== '' ? ( | |||||
| {checkSysEnv() === 'uat' ? ( | |||||
| <Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}> | <Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}> | ||||
| User Acceptance Test Environment | User Acceptance Test Environment | ||||
| </Typography> | </Typography> | ||||
| ) : checkSysEnv() === 'dev' ? ( | |||||
| <Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}> | |||||
| Development Environment | |||||
| </Typography> | |||||
| ) : ( | ) : ( | ||||
| '' | '' | ||||
| )} | )} | ||||
| @@ -105,7 +105,8 @@ const Index = () => { | |||||
| url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD, | url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD, | ||||
| params:{ | params:{ | ||||
| username: username, | username: username, | ||||
| newPassword: values.password | |||||
| newPassword: values.password, | |||||
| emailVerifyHash: decodeURIComponent(params.verifyCode) | |||||
| }, | }, | ||||
| onSuccess: () => { | onSuccess: () => { | ||||
| useJwt | useJwt | ||||
| @@ -99,7 +99,8 @@ const Index = () => { | |||||
| url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD, | url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD, | ||||
| params:{ | params:{ | ||||
| username: username, | username: username, | ||||
| newPassword: values.password | |||||
| newPassword: values.password, | |||||
| emailVerifyHash: decodeURIComponent(params.verifyCode) | |||||
| }, | }, | ||||
| onSuccess: () => { | onSuccess: () => { | ||||
| useJwt | useJwt | ||||
| @@ -17,6 +17,7 @@ import { I_AM_SMART_PATH } from "utils/ApiPathConst"; | |||||
| import * as React from 'react'; | import * as React from 'react'; | ||||
| import { FormattedMessage, useIntl } from "react-intl"; | import { FormattedMessage, useIntl } from "react-intl"; | ||||
| import usePageTitle from "components/usePageTitle"; | import usePageTitle from "components/usePageTitle"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| // ================================|| LOGIN ||================================ // | // ================================|| LOGIN ||================================ // | ||||
| @@ -127,7 +128,7 @@ const RegisterCustom = () => { | |||||
| /> | /> | ||||
| <Box mt={4} ml={2} mr={2} bgcolor="grey.100" p={1.5} > | <Box mt={4} ml={2} mr={2} bgcolor="grey.100" p={1.5} > | ||||
| <Typography textAlign='justify' variant="body1" display="block" gutterBottom> | <Typography textAlign='justify' variant="body1" display="block" gutterBottom> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerIAmSmart' }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.registerIAmSmart' })} /> | |||||
| </Typography> | </Typography> | ||||
| <Link href={intl.formatMessage({ id: "iamsmartLink" })}> | <Link href={intl.formatMessage({ id: "iamsmartLink" })}> | ||||
| <FormattedMessage id="learnMore" /> | <FormattedMessage id="learnMore" /> | ||||
| @@ -160,7 +161,7 @@ const RegisterCustom = () => { | |||||
| </Button> | </Button> | ||||
| <Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom> | <Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerPersonal' }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.registerPersonal' })} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12} md={6} sx={{ borderLeft: 1, borderColor: 'grey.500' }}> | <Grid item xs={12} md={6} sx={{ borderLeft: 1, borderColor: 'grey.500' }}> | ||||
| @@ -189,7 +190,7 @@ const RegisterCustom = () => { | |||||
| </Typography> | </Typography> | ||||
| </Button> | </Button> | ||||
| <Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom> | <Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerOrg' }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.registerOrg' })} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -35,6 +35,7 @@ import { POST_PUBLIC_USER_REGISTER, POST_CAPTCHA, POST_USERNAME, POST_USER_EMAIL | |||||
| import * as ComboData from "utils/ComboData"; | import * as ComboData from "utils/ComboData"; | ||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import { lazy } from 'react'; | import { lazy } from 'react'; | ||||
| const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); | const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); | ||||
| const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); | ||||
| @@ -1558,6 +1559,9 @@ const BusCustomFormWizard = (props) => { | |||||
| <Typography display="inline" variant="h6" component="span" sx={{ color: 'primary.primary' }}> | <Typography display="inline" variant="h6" component="span" sx={{ color: 'primary.primary' }}> | ||||
| <FormattedMessage id="pleaseUploadDoc"/> | <FormattedMessage id="pleaseUploadDoc"/> | ||||
| </Typography> | </Typography> | ||||
| <Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}> | |||||
| <FormattedMessage id="acceptRegisterUploadFileFormats" /> | |||||
| </Typography> | |||||
| {/* <Typography display="inline" variant="h6" component="span" sx={{ fontSize: 12,color: 'primary.primary'}}>如: 香港身份證; 護照; 中國內地身份證等</Typography> */} | {/* <Typography display="inline" variant="h6" component="span" sx={{ fontSize: 12,color: 'primary.primary'}}>如: 香港身份證; 護照; 中國內地身份證等</Typography> */} | ||||
| <Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}> | <Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}> | ||||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | ||||
| @@ -1583,7 +1587,7 @@ const BusCustomFormWizard = (props) => { | |||||
| <FormattedMessage id="uploadFile" /> | <FormattedMessage id="uploadFile" /> | ||||
| </Button> | </Button> | ||||
| <input | <input | ||||
| accept="image/png, .jpg, .bmp, .pdf" | |||||
| accept=".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp" | |||||
| //className={classes.input} | //className={classes.input} | ||||
| id="contained-button-file" | id="contained-button-file" | ||||
| multiple | multiple | ||||
| @@ -1618,7 +1622,7 @@ const BusCustomFormWizard = (props) => { | |||||
| <Grid container> | <Grid container> | ||||
| <Grid item xs={12} md={12}> | <Grid item xs={12} md={12}> | ||||
| <Typography component="span" variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e", display: "block" }}> | <Typography component="span" variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e", display: "block" }}> | ||||
| <div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} /> | |||||
| <SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -28,6 +28,7 @@ import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER | |||||
| import * as ComboData from "utils/ComboData"; | import * as ComboData from "utils/ComboData"; | ||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import { lazy } from 'react'; | import { lazy } from 'react'; | ||||
| const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); | const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); | ||||
| const PreviewUploadFileTable = Loadable(lazy(() => import('./PreviewUploadFileTable'))); | const PreviewUploadFileTable = Loadable(lazy(() => import('./PreviewUploadFileTable'))); | ||||
| @@ -1887,6 +1888,9 @@ const CustomFormWizard = (props) => { | |||||
| <Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}> | <Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}> | ||||
| <FormattedMessage id="pleaseUploadIdDocSubTitle" /> | <FormattedMessage id="pleaseUploadIdDocSubTitle" /> | ||||
| </Typography> | </Typography> | ||||
| <Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}> | |||||
| <FormattedMessage id="acceptRegisterUploadFileFormats" /> | |||||
| </Typography> | |||||
| <Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}> | <Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}> | ||||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | ||||
| <Button | <Button | ||||
| @@ -1911,7 +1915,7 @@ const CustomFormWizard = (props) => { | |||||
| <FormattedMessage id="uploadIdDoc" /> | <FormattedMessage id="uploadIdDoc" /> | ||||
| </Button> | </Button> | ||||
| <input | <input | ||||
| accept="image/png, .jpg, .bmp, .pdf" | |||||
| accept=".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp" | |||||
| //className={classes.input} | //className={classes.input} | ||||
| id="contained-button-file" | id="contained-button-file" | ||||
| multiple | multiple | ||||
| @@ -1946,7 +1950,7 @@ const CustomFormWizard = (props) => { | |||||
| <Grid container> | <Grid container> | ||||
| <Grid item xs={12} md={12}> | <Grid item xs={12} md={12}> | ||||
| <Typography height="100%" component="span" sx={{ textAlign: "left", display: "block", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}> | <Typography height="100%" component="span" sx={{ textAlign: "left", display: "block", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}> | ||||
| <div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} /> | |||||
| <SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -26,6 +26,7 @@ import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL, POST_CAPTCH | |||||
| import * as ComboData from "utils/ComboData"; | import * as ComboData from "utils/ComboData"; | ||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import { lazy } from 'react'; | import { lazy } from 'react'; | ||||
| const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); | ||||
| @@ -1055,7 +1056,7 @@ const CustomFormWizard = (props) => { | |||||
| <Grid item xs={12} md={12}> | <Grid item xs={12} md={12}> | ||||
| <Typography component="span" sx={{ textAlign: "left", display: "block", borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}> | <Typography component="span" sx={{ textAlign: "left", display: "block", borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}> | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "termsAndCon" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "termsAndCon" })} style={{ padding: 12 }} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -20,6 +20,8 @@ import * as HttpUtils from "utils/HttpUtils"; | |||||
| import * as UrlUtils from "utils/ApiPathConst"; | import * as UrlUtils from "utils/ApiPathConst"; | ||||
| import * as DateUtils from "utils/DateUtils"; | import * as DateUtils from "utils/DateUtils"; | ||||
| import { checkPaymentSuspension } from "utils/Utils"; | 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 Message = Loadable(React.lazy(() => import('./Message'))); | ||||
| const Notice = Loadable(React.lazy(() => import('./Notice'))); | const Notice = Loadable(React.lazy(() => import('./Notice'))); | ||||
| @@ -29,6 +31,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('../../extra-pages/Loa | |||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||
| import usePageTitle from "components/usePageTitle"; | import usePageTitle from "components/usePageTitle"; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | // ==============================|| DASHBOARD - DEFAULT ||============================== // | ||||
| @@ -55,10 +58,21 @@ const DashboardDefault = () => { | |||||
| const [itemList, setItemList] = React.useState([]); | const [itemList, setItemList] = React.useState([]); | ||||
| const [listData, setListData] = React.useState([]); | const [listData, setListData] = React.useState([]); | ||||
| const [isPopUp, setIsPopUp] = React.useState(false); | const [isPopUp, setIsPopUp] = React.useState(false); | ||||
| const [orgBrData, setOrgBrData] = React.useState(null); | |||||
| const [brDialog, setBrDialog] = React.useState({ open: false, variant: null }); | |||||
| React.useEffect(() => { | React.useEffect(() => { | ||||
| loadMessageData() | loadMessageData() | ||||
| loadNoticeData() | loadNoticeData() | ||||
| fetchOrgBrData({ | |||||
| onSuccess: (org) => { | |||||
| setOrgBrData(org); | |||||
| const variant = loginPopupVariant(org); | |||||
| if (variant) { | |||||
| setBrDialog({ open: true, variant, afterClose: "stay" }); | |||||
| } | |||||
| } | |||||
| }); | |||||
| localStorage.setItem('searchCriteria',"") | localStorage.setItem('searchCriteria',"") | ||||
| }, []); | }, []); | ||||
| @@ -198,7 +212,18 @@ const DashboardDefault = () => { | |||||
| <Grid item xs={12} lg={5} sx={{ pt: 2 }} style={{ height: '100%' }}> | <Grid item xs={12} lg={5} sx={{ pt: 2 }} style={{ height: '100%' }}> | ||||
| <Button | <Button | ||||
| xs={12} | 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' })} | aria-label={intl.formatMessage({ id: 'submitApplication' })} | ||||
| sx={{ | sx={{ | ||||
| width: "100%", | width: "100%", | ||||
| @@ -297,7 +322,7 @@ const DashboardDefault = () => { | |||||
| </DialogTitle> | </DialogTitle> | ||||
| <DialogContent style={{ color: '#B00020', display: 'flex', }}> | <DialogContent style={{ color: '#B00020', display: 'flex', }}> | ||||
| <Typography variant="h5" style={{ padding: '16px' }}> | <Typography variant="h5" style={{ padding: '16px' }}> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} /> | |||||
| </Typography> | </Typography> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| @@ -305,6 +330,18 @@ const DashboardDefault = () => { | |||||
| </DialogActions> | </DialogActions> | ||||
| </Dialog> | </Dialog> | ||||
| </div> | </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> | </Grid> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -1,3 +1,4 @@ | |||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const AboutUs = () => { | const AboutUs = () => { | ||||
| @@ -155,7 +156,7 @@ const AboutUs = () => { | |||||
| ` | ` | ||||
| ; | ; | ||||
| return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />); | |||||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||||
| } | } | ||||
| @@ -1,3 +1,4 @@ | |||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const AboutUs = () => { | const AboutUs = () => { | ||||
| @@ -162,7 +163,7 @@ const AboutUs = () => { | |||||
| </div>` | </div>` | ||||
| ; | ; | ||||
| return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />); | |||||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||||
| } | } | ||||
| @@ -1,3 +1,4 @@ | |||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const AboutUs = () => { | const AboutUs = () => { | ||||
| @@ -165,7 +166,7 @@ const AboutUs = () => { | |||||
| ` | ` | ||||
| ; | ; | ||||
| return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />); | |||||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||||
| } | } | ||||
| @@ -3,6 +3,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; | |||||
| import usePageTitle from "components/usePageTitle"; | import usePageTitle from "components/usePageTitle"; | ||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| backgroundImage: `url(${titleBackgroundImg})`, | backgroundImage: `url(${titleBackgroundImg})`, | ||||
| @@ -38,7 +39,7 @@ const ImportantNotice = () => { | |||||
| </div> | </div> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={10} md={8} lg={6}> | <Grid item xs={10} md={8} lg={6}> | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} /> | |||||
| <SafeHtml html={htmlContent} style={{ padding: 12 }} /> | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| ); | ); | ||||
| @@ -1,3 +1,4 @@ | |||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const Page = () => { | const Page = () => { | ||||
| @@ -83,7 +84,7 @@ const Page = () => { | |||||
| </div> | </div> | ||||
| `; | `; | ||||
| return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />); | |||||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||||
| } | } | ||||
| @@ -1,3 +1,5 @@ | |||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const Page = () => { | const Page = () => { | ||||
| const content = ` | const content = ` | ||||
| <style> | <style> | ||||
| @@ -125,7 +127,7 @@ const Page = () => { | |||||
| </div> | </div> | ||||
| `; | `; | ||||
| return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />; | |||||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||||
| }; | }; | ||||
| export default Page; | export default Page; | ||||
| @@ -1,3 +1,5 @@ | |||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const Page = () => { | const Page = () => { | ||||
| const content = ` | const content = ` | ||||
| <style> | <style> | ||||
| @@ -98,7 +100,7 @@ const Page = () => { | |||||
| </div> | </div> | ||||
| `; | `; | ||||
| return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />; | |||||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||||
| }; | }; | ||||
| export default Page; | export default Page; | ||||
| @@ -3,6 +3,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; | |||||
| import usePageTitle from "components/usePageTitle"; | import usePageTitle from "components/usePageTitle"; | ||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| backgroundImage: `url(${titleBackgroundImg})`, | backgroundImage: `url(${titleBackgroundImg})`, | ||||
| @@ -38,7 +39,7 @@ const PrivacyPolicy = () => { | |||||
| </div> | </div> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={10} md={8} lg={6}> | <Grid item xs={10} md={8} lg={6}> | ||||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} /> | |||||
| <SafeHtml html={htmlContent} style={{ padding: 12 }} /> | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| ); | ); | ||||
| @@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom"; | |||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | ||||
| import AccountCircleIcon from '@mui/icons-material/AccountCircle'; | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | ||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| @@ -66,7 +67,7 @@ const Index = () => { | |||||
| <AccountCircleIcon color="secondary" sx={{ width: "200px", height: "200px" }} /> | <AccountCircleIcon color="secondary" sx={{ width: "200px", height: "200px" }} /> | ||||
| <Grid item xs={12} md={12} > | <Grid item xs={12} md={12} > | ||||
| <Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> | <Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> | ||||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'pleaseLloginMessage2' }) }} /> | |||||
| <SafeHtml html={intl.formatMessage({ id: 'pleaseLloginMessage2' })} /> | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| </center> | </center> | ||||
| @@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom"; | |||||
| import Loadable from 'components/Loadable'; | import Loadable from 'components/Loadable'; | ||||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | ||||
| import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; | import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; | ||||
| import SafeHtml from 'components/SafeHtml'; | |||||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | ||||
| const BackgroundHead = { | const BackgroundHead = { | ||||
| @@ -69,8 +70,9 @@ const Index = () => { | |||||
| component="div" | component="div" | ||||
| variant="h3" | variant="h3" | ||||
| sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }} | sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }} | ||||
| dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'loginSuccessMessage2' }) }} | |||||
| /> | |||||
| > | |||||
| <SafeHtml html={intl.formatMessage({ id: 'loginSuccessMessage2' })} /> | |||||
| </Typography> | |||||
| ) : ( | ) : ( | ||||
| <Typography component="div" variant="h3" sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }}> | <Typography component="div" variant="h3" sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }}> | ||||
| <FormattedMessage id="iAmSmartLoginSuccessHeading" /> | <FormattedMessage id="iAmSmartLoginSuccessHeading" /> | ||||
| @@ -15,8 +15,8 @@ const GroupAuthTable = Loadable(lazy(() => import('./GroupAuthTable'))); | |||||
| const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRecord, editMode}) => { | const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRecord, editMode}) => { | ||||
| const [currentAuthData, setCurrentAuthData] = React.useState({}); | const [currentAuthData, setCurrentAuthData] = React.useState({}); | ||||
| const [onReady, setOnReady] = useState(false); | const [onReady, setOnReady] = useState(false); | ||||
| const [selectedRow, setSelectedRow] = useState([]); | |||||
| const [referenceRow, setReferenceRow] = useState([]); | |||||
| const [selectedRow, setSelectedRow] = useState(userGroupData?.authIds || []); | |||||
| const [referenceRow, setReferenceRow] = useState(userGroupData?.authIds || []); | |||||
| const [_editMode, setEditMode] = useState(editMode); | const [_editMode, setEditMode] = useState(editMode); | ||||
| useEffect(()=>{ | useEffect(()=>{ | ||||
| @@ -39,14 +39,22 @@ const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRec | |||||
| } | } | ||||
| }, [currentAuthData]); | }, [currentAuthData]); | ||||
| const toIdList = (ids) => { | |||||
| if (!ids) { | |||||
| return []; | |||||
| } | |||||
| const raw = Array.isArray(ids) ? ids : (ids.ids ? Array.from(ids.ids) : []); | |||||
| return raw.map((id) => Number(id)).filter((id) => !Number.isNaN(id)); | |||||
| }; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| //upload latest data to parent | |||||
| let deletedList = referenceRow.filter(x => !selectedRow.includes(x)); | |||||
| const currentIds = toIdList(selectedRow); | |||||
| const referenceIds = toIdList(referenceRow); | |||||
| updateUserAuthList({ | updateUserAuthList({ | ||||
| "currentList": selectedRow, | |||||
| "deletedList": deletedList | |||||
| currentList: currentIds, | |||||
| deletedList: referenceIds.filter((id) => !currentIds.includes(id)) | |||||
| }); | }); | ||||
| }, [isCollectData]); | |||||
| }, [isCollectData, selectedRow, referenceRow]); | |||||
| return ( | return ( | ||||
| @@ -98,8 +98,10 @@ export default function GroupAuthTable({setSelectedRow, userAuth,isNewRecord, ed | |||||
| rowSelectionModel={currentSelectedRow} | rowSelectionModel={currentSelectedRow} | ||||
| onRowSelectionModelChange={(ids) => { | onRowSelectionModelChange={(ids) => { | ||||
| if(_editMode){ | if(_editMode){ | ||||
| setSelectedRow(ids); | |||||
| setCurrentSelectedRow(ids); | |||||
| const raw = Array.isArray(ids) ? ids : (ids?.ids ? Array.from(ids.ids) : []); | |||||
| const nextIds = raw.map((id) => Number(id)).filter((id) => !Number.isNaN(id)); | |||||
| setSelectedRow(nextIds); | |||||
| setCurrentSelectedRow(nextIds); | |||||
| } | } | ||||
| }} | }} | ||||
| autoHeight | autoHeight | ||||
| @@ -90,12 +90,11 @@ const UserAddCard = ({ isCollectData, updateGroupMember, userGroupData, isNewRec | |||||
| }, [currentUserData]); | }, [currentUserData]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| //upload latest data to parent | |||||
| updateGroupMember({ | updateGroupMember({ | ||||
| "currentList": groupUserData, | |||||
| "deletedList": deletedList | |||||
| currentList: groupUserData, | |||||
| deletedList: deletedList | |||||
| }); | }); | ||||
| }, [isCollectData]); | |||||
| }, [isCollectData, groupUserData, deletedList]); | |||||
| return ( | return ( | ||||
| @@ -47,12 +47,12 @@ const UserMaintainPage = () => { | |||||
| const [isCollectData, setIsCollectData] = useState(false); | const [isCollectData, setIsCollectData] = useState(false); | ||||
| const [editedGroupData, setEditedGroupData] = useState({}); | const [editedGroupData, setEditedGroupData] = useState({}); | ||||
| const [userGroupData, setUserGroupData] = useState([]); | const [userGroupData, setUserGroupData] = useState([]); | ||||
| const [userAuthData, setUserAuthData] = useState([]); | |||||
| const saveInProgressRef = useRef(false); | const saveInProgressRef = useRef(false); | ||||
| const [groupMember, setGroupMember] = useState([]); | |||||
| const userAuthDataRef = useRef([]); | |||||
| const deletedAuthListRef = useRef([]); | |||||
| const groupMemberRef = useRef([]); | |||||
| const deletedUserListRef = useRef([]); | |||||
| const [isNewRecord, setIsNewRecord] = useState(false); | const [isNewRecord, setIsNewRecord] = useState(false); | ||||
| const [deletedUserList, setDeletedUserList] = useState([]); | |||||
| const [deletedAuthList, setDeletedAuthList] = useState([]); | |||||
| const [isWindowOpen, setIsWindowOpen] = useState(false); | const [isWindowOpen, setIsWindowOpen] = useState(false); | ||||
| const handleClose = () => { | const handleClose = () => { | ||||
| @@ -84,13 +84,17 @@ const UserMaintainPage = () => { | |||||
| } | } | ||||
| function updateGroupMember(groupMember) { | function updateGroupMember(groupMember) { | ||||
| setGroupMember(groupMember.currentList); | |||||
| setDeletedUserList(groupMember.deletedList); | |||||
| const currentList = groupMember.currentList || []; | |||||
| const deletedList = groupMember.deletedList || []; | |||||
| groupMemberRef.current = currentList; | |||||
| deletedUserListRef.current = deletedList; | |||||
| } | } | ||||
| function updateUserAuthList(userAuthData) { | |||||
| setUserAuthData(userAuthData.currentList); | |||||
| setDeletedAuthList(userAuthData.deletedList); | |||||
| function updateUserAuthList(authData) { | |||||
| const currentList = authData.currentList || []; | |||||
| const deletedList = authData.deletedList || []; | |||||
| userAuthDataRef.current = currentList; | |||||
| deletedAuthListRef.current = deletedList; | |||||
| } | } | ||||
| const submitData = async () => { | const submitData = async () => { | ||||
| @@ -106,19 +110,36 @@ const UserMaintainPage = () => { | |||||
| return; | return; | ||||
| } | } | ||||
| const latestGroupFormData = getLatestGroupFormData(); | const latestGroupFormData = getLatestGroupFormData(); | ||||
| const finalDeletedUserList = getDeletedRecordWithRefList(deletedUserList, getIdList(groupMember)); | |||||
| const latestGroupMember = groupMemberRef.current; | |||||
| const latestAuthIds = userAuthDataRef.current; | |||||
| const latestDeletedAuthIds = deletedAuthListRef.current; | |||||
| const finalDeletedUserList = getDeletedRecordWithRefList( | |||||
| deletedUserListRef.current, | |||||
| getIdList(latestGroupMember) | |||||
| ); | |||||
| const response = await axios.post(POST_AND_UPDATE_USER_GROUP, { | const response = await axios.post(POST_AND_UPDATE_USER_GROUP, { | ||||
| id: parseInt(params.id) !== -1 ? parseInt(params.id) : null, | id: parseInt(params.id) !== -1 ? parseInt(params.id) : null, | ||||
| name: latestGroupFormData.userGroupName, | name: latestGroupFormData.userGroupName, | ||||
| description: latestGroupFormData.description, | description: latestGroupFormData.description, | ||||
| addUserIds: getIdList(groupMember), | |||||
| addUserIds: getIdList(latestGroupMember), | |||||
| removeUserIds: finalDeletedUserList, | removeUserIds: finalDeletedUserList, | ||||
| addAuthIds: userAuthData, | |||||
| removeAuthIds: deletedAuthList, | |||||
| addAuthIds: latestAuthIds, | |||||
| removeAuthIds: latestDeletedAuthIds, | |||||
| }); | }); | ||||
| if (response.status === 200) { | if (response.status === 200) { | ||||
| navigate('/usergroupSearchview'); | |||||
| notifySaveSuccess(); | notifySaveSuccess(); | ||||
| const savedId = response.data?.id; | |||||
| const currentId = parseInt(params.id); | |||||
| if ((currentId === -1 || Number.isNaN(currentId)) && savedId) { | |||||
| navigate(`/userGroup/${savedId}`, { replace: true }); | |||||
| await loadGroupData(savedId); | |||||
| } else { | |||||
| await loadGroupData(currentId); | |||||
| } | |||||
| setIsNewRecord(false); | |||||
| setEditMode(false); | |||||
| deletedUserListRef.current = []; | |||||
| deletedAuthListRef.current = []; | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.log(error); | console.log(error); | ||||
| @@ -177,14 +198,19 @@ const UserMaintainPage = () => { | |||||
| return true; | return true; | ||||
| }; | }; | ||||
| const loadGroupData = async (groupId) => { | |||||
| const response = await axios.get(`${GET_GROUP_LIST_PATH}/${groupId}`); | |||||
| if (response.status === 200) { | |||||
| setUserGroupData(response.data); | |||||
| const loadedAuthIds = response.data?.authIds || []; | |||||
| userAuthDataRef.current = loadedAuthIds; | |||||
| } | |||||
| return response; | |||||
| }; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (params.id > 0) { | if (params.id > 0) { | ||||
| axios.get(`${GET_GROUP_LIST_PATH}/${params.id}`) | |||||
| .then((response) => { | |||||
| if (response.status === 200) { | |||||
| setUserGroupData(response.data); | |||||
| } | |||||
| }) | |||||
| loadGroupData(params.id) | |||||
| .catch(error => { | .catch(error => { | ||||
| console.log(error); | console.log(error); | ||||
| return false; | return false; | ||||
| @@ -68,7 +68,7 @@ export default function UserGroupTable({searchCriteria, applyGridOnReady,applySe | |||||
| <FiDataGrid | <FiDataGrid | ||||
| columns={columns} | columns={columns} | ||||
| customPageSize={10} | customPageSize={10} | ||||
| pageSizeOptions={[10, 15, 20]} | |||||
| hideRowsPerPage | |||||
| onRowDoubleClick={handleRowDoubleClick} | onRowDoubleClick={handleRowDoubleClick} | ||||
| applyGridOnReady={applyGridOnReady} | applyGridOnReady={applyGridOnReady} | ||||
| applySearch={applySearch} | applySearch={applySearch} | ||||
| @@ -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_Individual = Loadable(lazy(() => import('pages/User/DetailsPage_Individual'))); | ||||
| const UserMaintainPage_Organization = Loadable(lazy(() => import('pages/User/DetailsPage_Organization'))); | const UserMaintainPage_Organization = Loadable(lazy(() => import('pages/User/DetailsPage_Organization'))); | ||||
| const OrganizationDetailPage = Loadable(lazy(() => import('pages/Organization/DetailPage'))); | 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_Details = Loadable(lazy(() => import('pages/Message/Details'))); | ||||
| const Msg_Search = Loadable(lazy(() => import('pages/Message/Search'))); | const Msg_Search = Loadable(lazy(() => import('pages/Message/Search'))); | ||||
| const AnnouncementSearch = Loadable(lazy(() => import('pages/Announcement/Search_Public'))); | const AnnouncementSearch = Loadable(lazy(() => import('pages/Announcement/Search_Public'))); | ||||
| @@ -126,6 +127,10 @@ const PublicDashboard = { | |||||
| path: '/orgUser', | path: '/orgUser', | ||||
| element: <UserMaintainPage_Organization /> | element: <UserMaintainPage_Organization /> | ||||
| }, | }, | ||||
| { | |||||
| path: '/org/submit-br', | |||||
| element: <SubmitBrPage /> | |||||
| }, | |||||
| { | { | ||||
| path: '/org', | path: '/org', | ||||
| element: <OrganizationDetailPage /> | 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 = { | export const PRIMARY_CONTAINED_BUTTON_SX = { | ||||
| backgroundColor: CONTAINED_PRIMARY_BLUE, | backgroundColor: CONTAINED_PRIMARY_BLUE, | ||||
| color: '#FFFFFF', | color: '#FFFFFF', | ||||
| @@ -44,6 +44,8 @@ | |||||
| "requireFile": "Please select a file to upload", | "requireFile": "Please select a file to upload", | ||||
| "requireValidFile": "Please upload a valid file", | "requireValidFile": "Please upload a valid file", | ||||
| "requireValidFileWithPdfFormat": "Please upload a valid file (file format: .pdf)", | "requireValidFileWithPdfFormat": "Please upload a valid file (file format: .pdf)", | ||||
| "requireValidFileWithProofReplyFormat": "Please upload a valid file (file format: .pdf, .jpg, .jpeg, .png)", | |||||
| "acceptProofReplyFileFormats": "Accept file formats: pdf, jpg, jpeg and png.", | |||||
| "requireValidFileWithFormat": "Please upload a valid file (file format: .doc, .docx, .xls, .xlsx)", | "requireValidFileWithFormat": "Please upload a valid file (file format: .doc, .docx, .xls, .xlsx)", | ||||
| "submissionSuccess": "Application submitted successfully", | "submissionSuccess": "Application submitted successfully", | ||||
| "noMoreThen40Words": "Must not exceed 40 characters", | "noMoreThen40Words": "Must not exceed 40 characters", | ||||
| @@ -59,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.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.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.paymentMsg": "Your application and payment have been received", | ||||
| "MSG.expiredApp": "Public Notice application has expired", | "MSG.expiredApp": "Public Notice application has expired", | ||||
| @@ -284,12 +286,13 @@ | |||||
| "sameAsBusinessRegistrationCert": "Same as Business Registration Certificate", | "sameAsBusinessRegistrationCert": "Same as Business Registration Certificate", | ||||
| "businessRegCert": "Business Registration Certificate", | "businessRegCert": "Business Registration Certificate", | ||||
| "businessRegCertNumber": "Hong Kong Business Reg Cert Number", | "businessRegCertNumber": "Hong Kong Business Reg Cert Number", | ||||
| "businessRegCertAndDoc":"Business Registration Certificate and other documents", | |||||
| "businessRegCertAndDoc":"Business Registration Certificate", | |||||
| "businessRegCertExpiryDate": "Business registration certificate expiry date", | "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.", | "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.", | "pleaseUploadIdDocSubTitle": "Such as: Hong Kong ID card; passport; Mainland China ID card; professional practice certificate, etc.", | ||||
| "acceptRegisterUploadFileFormats": "Accept file formats: PDF, JPG, JPEG, PNG, and BMP. Support maximum 5 files.", | |||||
| "uploadIdDoc": "Upload identity document", | "uploadIdDoc": "Upload identity document", | ||||
| "fileName": "File name", | "fileName": "File name", | ||||
| "forOrgUser": "For Organisation / Company Users", | "forOrgUser": "For Organisation / Company Users", | ||||
| @@ -319,7 +322,7 @@ | |||||
| "pleaseFillInBusinessRegCertNumber": "Please fill in Business Registration Certificate Number", | "pleaseFillInBusinessRegCertNumber": "Please fill in Business Registration Certificate Number", | ||||
| "pleaseFillInValidBusinessRegCertNumber": "Please fill in valid Business Registration Certificate Number", | "pleaseFillInValidBusinessRegCertNumber": "Please fill in valid Business Registration Certificate Number", | ||||
| "businessRegCertValidityDate": "Business Reg Cert validity date", | "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", | "formAddress": "Address", | ||||
| "addressLine1": "First line of address", | "addressLine1": "First line of address", | ||||
| "addressLine2": "Second line of address", | "addressLine2": "Second line of address", | ||||
| @@ -604,6 +607,24 @@ | |||||
| "nameEng": "Name (Eng)", | "nameEng": "Name (Eng)", | ||||
| "nameChi": "Name (Chi)", | "nameChi": "Name (Chi)", | ||||
| "expiryDate": "Expiry Date", | "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", | "create": "Create", | ||||
| "confirmTo": "Confirm to ", | "confirmTo": "Confirm to ", | ||||
| @@ -646,6 +667,8 @@ | |||||
| "connectionError": "Connection error. Please try again.", | "connectionError": "Connection error. Please try again.", | ||||
| "downloadFailed": "Download failed. Please try again.", | "downloadFailed": "Download failed. Please try again.", | ||||
| "systemSettingName": "Name", | |||||
| "muiClear": "Clear", | "muiClear": "Clear", | ||||
| "muiClose": "Close", | "muiClose": "Close", | ||||
| "muiOpen": "Open", | "muiOpen": "Open", | ||||
| @@ -44,6 +44,8 @@ | |||||
| "requireFile": "请选择上传档案", | "requireFile": "请选择上传档案", | ||||
| "requireValidFile": "请上传有效档案", | "requireValidFile": "请上传有效档案", | ||||
| "requireValidFileWithPdfFormat": "请上传有效档案 (档案格式: .pdf)", | "requireValidFileWithPdfFormat": "请上传有效档案 (档案格式: .pdf)", | ||||
| "requireValidFileWithProofReplyFormat": "请上传有效档案 (档案格式: .pdf, .jpg, .jpeg, .png)", | |||||
| "acceptProofReplyFileFormats": "接受档案格式:pdf、jpg、jpeg 及 png。", | |||||
| "requireValidFileWithFormat": "请上传有效档案 (档案格式: .doc, .docx, .xls, .xlsx)", | "requireValidFileWithFormat": "请上传有效档案 (档案格式: .doc, .docx, .xls, .xlsx)", | ||||
| "submissionSuccess": "申请提交成功", | "submissionSuccess": "申请提交成功", | ||||
| "noMoreThen40Words": "不得超过 40 个字符", | "noMoreThen40Words": "不得超过 40 个字符", | ||||
| @@ -100,7 +102,7 @@ | |||||
| "MSG.registerIAmSmart": "你可点击「智方便」按钮,系统会自动输入个人资料,或自行输入个人资料,以即时启动 公共启事提交及缴费系统 帐户。<br/>如欲使用「智方便」提供个人资料,请先下载「智方便」流动应用程式并登记成为「智方便」用户。", | "MSG.registerIAmSmart": "你可点击「智方便」按钮,系统会自动输入个人资料,或自行输入个人资料,以即时启动 公共启事提交及缴费系统 帐户。<br/>如欲使用「智方便」提供个人资料,请先下载「智方便」流动应用程式并登记成为「智方便」用户。", | ||||
| "MSG.registerPersonal": "需上载身份证明文件数码档案以进行网上申请。 <br/>如:香港身份证; 护照; 中国内地身份证; 专业执业证书等", | "MSG.registerPersonal": "需上载身份证明文件数码档案以进行网上申请。 <br/>如:香港身份证; 护照; 中国内地身份证; 专业执业证书等", | ||||
| "MSG.registerOrg": "需上载以下任何一份证明文件以进行网上申请。 <br/>如:商业登记证;专业执业证书", | |||||
| "MSG.registerOrg": "需上载商业登记证以进行网上申请。", | |||||
| "MSG.paymentMsg": "你的申请和付款已收到", | "MSG.paymentMsg": "你的申请和付款已收到", | ||||
| "MSG.expiredApp": "公共启事申请已过期", | "MSG.expiredApp": "公共启事申请已过期", | ||||
| @@ -327,12 +329,13 @@ | |||||
| "sameAsBusinessRegistrationCert": "与商业登记证相同", | "sameAsBusinessRegistrationCert": "与商业登记证相同", | ||||
| "businessRegCert": "商业登记证", | "businessRegCert": "商业登记证", | ||||
| "businessRegCertNumber": "香港商业登记证号码", | "businessRegCertNumber": "香港商业登记证号码", | ||||
| "businessRegCertAndDoc":"商业登记证及其他文件", | |||||
| "businessRegCertAndDoc":"商业登记证", | |||||
| "businessRegCertExpiryDate": "商业登记证有效期届满日期", | "businessRegCertExpiryDate": "商业登记证有效期届满日期", | ||||
| "pleaseUploadDoc": "请上传你的 有效商业登记证及其他文件 的数码档案,以验证你的身份。", | |||||
| "uploadFile": "上传商业登记证及其他文件", | |||||
| "pleaseUploadDoc": "请上传你的 有效商业登记证 的数码档案,以验证你的身份。", | |||||
| "uploadFile": "上传商业登记证", | |||||
| "pleaseUploadIdDoc": "请上传你的 有效身份证明文件 的数码档案,以验证你的身份。", | "pleaseUploadIdDoc": "请上传你的 有效身份证明文件 的数码档案,以验证你的身份。", | ||||
| "pleaseUploadIdDocSubTitle": "如: 香港身份证; 护照; 中国内地身份证; 专业执业证书等", | "pleaseUploadIdDocSubTitle": "如: 香港身份证; 护照; 中国内地身份证; 专业执业证书等", | ||||
| "acceptRegisterUploadFileFormats": "接受档案格式:PDF、JPG、JPEG、PNG 及 BMP。最多可上传 5 个档案。", | |||||
| "uploadIdDoc": "上传身份证明文件", | "uploadIdDoc": "上传身份证明文件", | ||||
| "fileName": "档案名称", | "fileName": "档案名称", | ||||
| "forOrgUser": "机构/公司用户", | "forOrgUser": "机构/公司用户", | ||||
| @@ -600,6 +603,24 @@ | |||||
| "nameEng": "名称 (英文)", | "nameEng": "名称 (英文)", | ||||
| "nameChi": "名称 (中文)", | "nameChi": "名称 (中文)", | ||||
| "expiryDate": "屆滿日期", | "expiryDate": "屆滿日期", | ||||
| "brStatusValid": "有效", | |||||
| "brStatusInvalid": "无效", | |||||
| "submitBrCertificate": "提交商业登记证 (BR)", | |||||
| "goToSubmitBr": "去提交证书", | |||||
| "brPopupTitleExpiring": "温馨提示", | |||||
| "brPopupTitleExpired": "系统提示", | |||||
| "brPopupTitlePendingVerify": "系统提示", | |||||
| "brSubmitSuccessNotExpired": "感谢您提交商业登记证,请等待官方审批通过,您仍可以在旧有商业登记证有效期限届满前提交新的公共启事申请。", | |||||
| "brSubmitSuccessExpired": "感谢您提交商业登记证,请等待官方审批通过后,再提交新的公共启事申请。", | |||||
| "selectCertificateFile": "选择证书档案", | |||||
| "newBrExpiryDate": "新商业登记证届满日期", | |||||
| "newBr": "新商业登记证", | |||||
| "newBrInformation": "新商业登记证资料", | |||||
| "confirmNewBr": "确认新商业登记证", | |||||
| "confirmNewBrMessage": "确定以新的商业登记证资料覆盖现有机构资料?", | |||||
| "brExpiredMsg": "贵司的商业登记证已过期。请上传有效的商业登记证。", | |||||
| "brPendingVerifyMsg": "贵司已上传最新的商业登记证,请等待官方审批。", | |||||
| "uploadedFiles": "已上传档案", | |||||
| "create": "创建", | "create": "创建", | ||||
| "confirmTo": "确定", | "confirmTo": "确定", | ||||
| @@ -642,6 +663,8 @@ | |||||
| "connectionError": "连接错误,请稍后再试。", | "connectionError": "连接错误,请稍后再试。", | ||||
| "downloadFailed": "下载失败,请稍后再试。", | "downloadFailed": "下载失败,请稍后再试。", | ||||
| "systemSettingName": "名称", | |||||
| "muiClear": "清除", | "muiClear": "清除", | ||||
| "muiClose": "关闭", | "muiClose": "关闭", | ||||
| "muiOpen": "打开", | "muiOpen": "打开", | ||||
| @@ -44,6 +44,8 @@ | |||||
| "requireFile": "請選擇上傳檔案", | "requireFile": "請選擇上傳檔案", | ||||
| "requireValidFile": "請上傳有效檔案", | "requireValidFile": "請上傳有效檔案", | ||||
| "requireValidFileWithPdfFormat": "請上傳有效檔案 (檔案格式: .pdf)", | "requireValidFileWithPdfFormat": "請上傳有效檔案 (檔案格式: .pdf)", | ||||
| "requireValidFileWithProofReplyFormat": "請上傳有效檔案 (檔案格式: .pdf, .jpg, .jpeg, .png)", | |||||
| "acceptProofReplyFileFormats": "接受檔案格式:pdf、jpg、jpeg 及 png。", | |||||
| "requireValidFileWithFormat": "請上傳有效檔案 (檔案格式: .doc, .docx, .xls, .xlsx)", | "requireValidFileWithFormat": "請上傳有效檔案 (檔案格式: .doc, .docx, .xls, .xlsx)", | ||||
| "submissionSuccess": "申請提交成功", | "submissionSuccess": "申請提交成功", | ||||
| "noMoreThen40Words": "不得超過 40 個字符", | "noMoreThen40Words": "不得超過 40 個字符", | ||||
| @@ -100,7 +102,7 @@ | |||||
| "MSG.registerIAmSmart": "你可點擊「智方便」按鈕,系統會自動輸入個人資料,或自行輸入個人資料,以即時啟動 公共啟事提交及繳費系統 帳戶。<br/>如欲使用「智方便」提供個人資料,請先下載「智方便」流動應用程式並登記成為「智方便」用戶。", | "MSG.registerIAmSmart": "你可點擊「智方便」按鈕,系統會自動輸入個人資料,或自行輸入個人資料,以即時啟動 公共啟事提交及繳費系統 帳戶。<br/>如欲使用「智方便」提供個人資料,請先下載「智方便」流動應用程式並登記成為「智方便」用戶。", | ||||
| "MSG.registerPersonal": "需上載身份證明文件數碼檔案以進行網上申請。<br/>如:香港身份證; 護照; 中國內地身份證; 專業執業証書等", | "MSG.registerPersonal": "需上載身份證明文件數碼檔案以進行網上申請。<br/>如:香港身份證; 護照; 中國內地身份證; 專業執業証書等", | ||||
| "MSG.registerOrg": "需上載以下任何一份證明文件以進行網上申請。<br/>如:商業登記證;專業執業證書", | |||||
| "MSG.registerOrg": "需上載商業登記證以進行網上申請。", | |||||
| "MSG.paymentMsg": "你的申請和付款已收到", | "MSG.paymentMsg": "你的申請和付款已收到", | ||||
| "MSG.expiredApp": "公共啟事申請已過期", | "MSG.expiredApp": "公共啟事申請已過期", | ||||
| @@ -322,13 +324,14 @@ | |||||
| "sameAsBusinessRegistrationCert": "與商業登記證相同", | "sameAsBusinessRegistrationCert": "與商業登記證相同", | ||||
| "businessRegCert": "商業登記證", | "businessRegCert": "商業登記證", | ||||
| "businessRegCertNumber": "香港商業登記證號碼", | "businessRegCertNumber": "香港商業登記證號碼", | ||||
| "businessRegCertAndDoc":"商業登記證及其他文件", | |||||
| "businessRegCertAndDoc":"商業登記證", | |||||
| "businessRegCertExpiryDate": "商業登記證有效期屆滿日期", | "businessRegCertExpiryDate": "商業登記證有效期屆滿日期", | ||||
| "pleaseUploadDoc": "請上傳你的 有效商業登記證及其他文件 的數碼檔案,以驗證你的身份。", | |||||
| "pleaseUploadDoc": "請上傳你的 有效商業登記證 的數碼檔案,以驗證你的身份。", | |||||
| "pleaseUploadIdDoc": "請上傳你的 有效身份證明文件 的數碼檔案,以驗證你的身份。", | "pleaseUploadIdDoc": "請上傳你的 有效身份證明文件 的數碼檔案,以驗證你的身份。", | ||||
| "pleaseUploadIdDocSubTitle": "如: 香港身份證; 護照; 中國內地身份證; 專業執業証書等", | "pleaseUploadIdDocSubTitle": "如: 香港身份證; 護照; 中國內地身份證; 專業執業証書等", | ||||
| "acceptRegisterUploadFileFormats": "接受檔案格式:PDF、JPG、JPEG、PNG 及 BMP。最多可上載 5 個檔案。", | |||||
| "uploadIdDoc": "上傳身份證明文件", | "uploadIdDoc": "上傳身份證明文件", | ||||
| "uploadFile": "上傳商業登記證及其他文件", | |||||
| "uploadFile": "上傳商業登記證", | |||||
| "fileName": "檔案名稱", | "fileName": "檔案名稱", | ||||
| "forOrgUser": "機構/公司用戶", | "forOrgUser": "機構/公司用戶", | ||||
| "forIndUser": "個人用戶", | "forIndUser": "個人用戶", | ||||
| @@ -601,6 +604,24 @@ | |||||
| "nameEng": "名稱 (英文)", | "nameEng": "名稱 (英文)", | ||||
| "nameChi": "名稱 (中文)", | "nameChi": "名稱 (中文)", | ||||
| "expiryDate": "屆滿日期", | "expiryDate": "屆滿日期", | ||||
| "brStatusValid": "有效", | |||||
| "brStatusInvalid": "無效", | |||||
| "submitBrCertificate": "提交商業登記證 (BR)", | |||||
| "goToSubmitBr": "去提交證書", | |||||
| "brPopupTitleExpiring": "溫馨提示", | |||||
| "brPopupTitleExpired": "系統提示", | |||||
| "brPopupTitlePendingVerify": "系統提示", | |||||
| "brSubmitSuccessNotExpired": "感謝您提交商業登記證,請等待官方審批通過,您仍可以在舊有商業登記證有效期限屆滿前提交新的公共啟事申請。", | |||||
| "brSubmitSuccessExpired": "感謝您提交商業登記證,請等待官方審批通過後,再提交新的公共啟事申請。", | |||||
| "selectCertificateFile": "選擇證書檔案", | |||||
| "newBrExpiryDate": "新商業登記證屆滿日期", | |||||
| "newBr": "新商業登記證", | |||||
| "newBrInformation": "新商業登記證資料", | |||||
| "confirmNewBr": "確認新商業登記證", | |||||
| "confirmNewBrMessage": "確定以新的商業登記證資料覆寫現有機構資料?", | |||||
| "brExpiredMsg": "貴司的商業登記證已過期。請上傳有效的商業登記證。", | |||||
| "brPendingVerifyMsg": "貴司已上傳最新的商業登記證,請等待官方審批。", | |||||
| "uploadedFiles": "已上載檔案", | |||||
| "create": "創建", | "create": "創建", | ||||
| "confirmTo": "確定", | "confirmTo": "確定", | ||||
| @@ -643,6 +664,8 @@ | |||||
| "connectionError": "連線錯誤,請稍後再試。", | "connectionError": "連線錯誤,請稍後再試。", | ||||
| "downloadFailed": "下載失敗,請稍後再試。", | "downloadFailed": "下載失敗,請稍後再試。", | ||||
| "systemSettingName": "名稱", | |||||
| "muiClear": "清除", | "muiClear": "清除", | ||||
| "muiClose": "關閉", | "muiClose": "關閉", | ||||
| "muiOpen": "開啟", | "muiOpen": "開啟", | ||||
| @@ -5,9 +5,11 @@ import { | |||||
| // GET request | // GET request | ||||
| export const REFRESH_TOKEN = "/refresh-token" | export const REFRESH_TOKEN = "/refresh-token" | ||||
| export const LOGOUT = "/logout" | |||||
| export const CHANGE_PASSWORD_PATH = "/user/change-password" | export const CHANGE_PASSWORD_PATH = "/user/change-password" | ||||
| export const GET_SYS_PARAMS = apiPath+'/settings'; | 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 PRIVACY_POLICY_PATH = apiPath+'/privacyPolicy'; | ||||
| export const UPDATE_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/update-payment-suspension'; | export const UPDATE_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/update-payment-suspension'; | ||||
| export const GET_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/get-payment-suspension'; | export const GET_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/get-payment-suspension'; | ||||
| @@ -28,11 +30,11 @@ export const GLD_USER_PROFILE_PATH = apiPath+'/user/gldProfile'; | |||||
| export const GET_AUTH_LIST = '/user/auth/combo'; | export const GET_AUTH_LIST = '/user/auth/combo'; | ||||
| export const GET_USER_COMBO_LIST = '/user/combo'; | export const GET_USER_COMBO_LIST = '/user/combo'; | ||||
| export const GET_USER_GLD_COMBO_LIST = '/user/combo/gld'; | export const GET_USER_GLD_COMBO_LIST = '/user/combo/gld'; | ||||
| export const GET_USER_LOCK = apiPath+'/user/lock'; | |||||
| export const GET_USER_UNLOCK = apiPath+'/user/unlock'; | |||||
| export const GET_USER_LOCK = apiPath+'/user/lock'; //POST | |||||
| export const GET_USER_UNLOCK = apiPath+'/user/unlock'; //POST | |||||
| export const GET_IND_USER_PATH = apiPath+'/user/ind'; | export const GET_IND_USER_PATH = apiPath+'/user/ind'; | ||||
| export const GET_IND_USER_VERIFY = apiPath+'/user/verify'; | |||||
| export const GET_IND_USER_VERIFY = apiPath+'/user/verify'; //POST | |||||
| export const POST_IND_USER = apiPath+'/user/ind'; | export const POST_IND_USER = apiPath+'/user/ind'; | ||||
| export const GET_ORG_USER_PATH = apiPath+'/user/org'; | export const GET_ORG_USER_PATH = apiPath+'/user/org'; | ||||
| @@ -50,14 +52,16 @@ export const POST_ORG_SAVE_PATH = apiPath+'/org/save'; | |||||
| export const GET_ORG_COMBO = apiPath+'/org/combo'; | export const GET_ORG_COMBO = apiPath+'/org/combo'; | ||||
| export const GET_ORG_CHECK_CREDITOR = apiPath+'/org/check-creditor'; | export const GET_ORG_CHECK_CREDITOR = apiPath+'/org/check-creditor'; | ||||
| export const GET_ORG_MARK_AS_CREDITOR = apiPath+'/org/mark-as-creditor'; | export const GET_ORG_MARK_AS_CREDITOR = apiPath+'/org/mark-as-creditor'; | ||||
| export const GET_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/mark-as-non-creditor'; | |||||
| export const GET_SEND_TERMINATION_OF_CREDIT = apiPath+'/org/sendDn_terminationOfCredit'; | |||||
| export const GET_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/mark-as-non-creditor'; //POST | |||||
| export const GET_SEND_TERMINATION_OF_CREDIT = apiPath+'/org/sendDn_terminationOfCredit'; //POST | |||||
| export const GET_ORG_EXPORT = apiPath+'/org/export'; | export const GET_ORG_EXPORT = apiPath+'/org/export'; | ||||
| export const GET_SEND_OVERDUE_CREDITOR_LIST = apiPath+'/org/sendDn_OverdueCreditorList'; | |||||
| export const GET_SEND_OVERDUE_CREDITOR_LIST = apiPath+'/org/sendDn_OverdueCreditorList'; //POST | |||||
| //public | //public | ||||
| export const GET_PUB_ORG_PATH = apiPath+'/org/pub'; | 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_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_CREDITOR = apiPath+'/org/pub/mark-as-creditor'; | ||||
| export const GET_PUB_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/pub/mark-as-non-creditor'; | export const GET_PUB_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/pub/mark-as-non-creditor'; | ||||
| @@ -78,7 +82,7 @@ export const CHECK_OVERDUE = apiPath+'/application/check-overdue'; | |||||
| export const FILE_UP_POST = apiPath+'/file/ul'; | export const FILE_UP_POST = apiPath+'/file/ul'; | ||||
| export const FILE_DOWN_GET = apiPath+"/file/dl"; | export const FILE_DOWN_GET = apiPath+"/file/dl"; | ||||
| export const POST_FILE_LIST = apiPath+'/file/list'; | export const POST_FILE_LIST = apiPath+'/file/list'; | ||||
| export const GET_FILE_DELETE = apiPath+'/file/delete'; | |||||
| export const GET_FILE_DELETE = apiPath+'/file/delete'; //DELETE /{fileId}/{skey}/{filename} | |||||
| //export const FILE_DOWN_GET = ({id,skey,filename})=>{ return apiPath+'/file/dl/'+id+'/'+skey+'/'+filename}; | //export const FILE_DOWN_GET = ({id,skey,filename})=>{ return apiPath+'/file/dl/'+id+'/'+skey+'/'+filename}; | ||||
| export const DR_EXPORT = apiPath+'/settings/dr/export'; | export const DR_EXPORT = apiPath+'/settings/dr/export'; | ||||
| @@ -119,8 +123,8 @@ export const PATCH_CHANGE_PASSWORD = apiPath+'/user/change-password'; | |||||
| //Public | //Public | ||||
| export const GET_PUBLIC_ORG_USER_LIST = apiPath+'/user/listOrg'; | export const GET_PUBLIC_ORG_USER_LIST = apiPath+'/user/listOrg'; | ||||
| export const GET_SET_PRIMARY_USER = apiPath+'/user/primary'; | |||||
| export const GET_SET_UN_PRIMARY_USER = apiPath+'/user/un-primary'; | |||||
| export const GET_SET_PRIMARY_USER = apiPath+'/user/primary'; //POST | |||||
| export const GET_SET_UN_PRIMARY_USER = apiPath+'/user/un-primary'; //POST | |||||
| export const GET_PUBLIC_NOTICE_LIST = apiPath+'/application/list'; | export const GET_PUBLIC_NOTICE_LIST = apiPath+'/application/list'; | ||||
| export const GET_PUBLIC_NOTICE_LIST_ListByStatus = apiPath+'/application/status-list'; | export const GET_PUBLIC_NOTICE_LIST_ListByStatus = apiPath+'/application/status-list'; | ||||
| @@ -164,6 +168,7 @@ export const GET_ISSUE_LIST = apiPath+'/gazette-issue/export';//GET | |||||
| export const POST_ISSUE_FILE = apiPath+'/gazette-issue/import';//POST | export const POST_ISSUE_FILE = apiPath+'/gazette-issue/import';//POST | ||||
| export const GET_ISSUE = apiPath+'/gazette-issue/list'; //GET | export const GET_ISSUE = apiPath+'/gazette-issue/list'; //GET | ||||
| export const GET_ISSUE_YEAR_COMBO = apiPath+'/gazette-issue/combo-year'; //GET | export const GET_ISSUE_YEAR_COMBO = apiPath+'/gazette-issue/combo-year'; //GET | ||||
| export const DELETE_ISSUE_YEAR = apiPath+'/gazette-issue/year'; //DELETE /{year} | |||||
| export const CHECK_CREATE_PROOF = apiPath+'/proof/check-create';//GET | export const CHECK_CREATE_PROOF = apiPath+'/proof/check-create';//GET | ||||
| export const LIST_PROOF = apiPath+'/proof/list';//GET | export const LIST_PROOF = apiPath+'/proof/list';//GET | ||||
| @@ -173,7 +178,7 @@ export const GET_PROOF = apiPath+'/proof/details';//GET | |||||
| export const REPLY_PROOF = apiPath+'/proof/reply';//GET | export const REPLY_PROOF = apiPath+'/proof/reply';//GET | ||||
| export const PROOF_CHECK_PRICE = apiPath+'/proof/check-price';//GET | export const PROOF_CHECK_PRICE = apiPath+'/proof/check-price';//GET | ||||
| export const GET_PROOF_PAY = apiPath+'/proof/pay-details';//GET | export const GET_PROOF_PAY = apiPath+'/proof/pay-details';//GET | ||||
| export const CANCEL_PROOF = apiPath+'/proof/cancel';//GET | |||||
| export const CANCEL_PROOF = apiPath+'/proof/cancel';//POST | |||||
| //payment | //payment | ||||
| export const PAYMENT_CREATE = apiPath+'/payment/create';//POST | export const PAYMENT_CREATE = apiPath+'/payment/create';//POST | ||||
| @@ -254,6 +259,7 @@ export const GET_HOLIDAY = apiPath+'/holiday/list'; //GET | |||||
| export const POST_HOLIDAY = apiPath+'/holiday/import'; //POST | export const POST_HOLIDAY = apiPath+'/holiday/import'; //POST | ||||
| export const GET_HOLIDAY_COMBO = apiPath+'/holiday/combo'; //GET | export const GET_HOLIDAY_COMBO = apiPath+'/holiday/combo'; //GET | ||||
| export const GET_HOLIDAY_TEMPLATE = apiPath+'/holiday/export'; //GET | export const GET_HOLIDAY_TEMPLATE = apiPath+'/holiday/export'; //GET | ||||
| export const DELETE_HOLIDAY_YEAR = apiPath+'/holiday/year'; //DELETE /{year} | |||||
| export const GET_JVM_INFO = apiPath+'/jvm-info'; //GET | export const GET_JVM_INFO = apiPath+'/jvm-info'; //GET | ||||
| @@ -26,7 +26,7 @@ export const getDateField = ({ label, valueName, form, disabled }) => { | |||||
| </Grid>; | </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}}> | return <Grid container alignItems={"center"} sx={{mb:2}}> | ||||
| <Grid item xs={12} sm={12} md={12} lg={12}> | <Grid item xs={12} sm={12} md={12} lg={12}> | ||||
| <Grid container spacing={1}> | <Grid container spacing={1}> | ||||
| @@ -40,7 +40,8 @@ export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => | |||||
| valueName: valueName, | valueName: valueName, | ||||
| form: form, | form: form, | ||||
| disabled: disabled, | disabled: disabled, | ||||
| autoFocus:autoFocus | |||||
| autoFocus:autoFocus, | |||||
| displayValue: displayValue | |||||
| })} | })} | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -233,7 +234,7 @@ export const getProfileComboField = ({ label, dataList, valueName, form, disable | |||||
| </Grid>; | </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]); | let err = Boolean(form.errors[valueName]); | ||||
| return <TextField | return <TextField | ||||
| @@ -248,7 +249,7 @@ export const initField = ({ type, valueName, form, disabled, autoFocus, multilin | |||||
| error={err} | error={err} | ||||
| helperText={form.errors[valueName] ? form.errors[valueName] : ''} | helperText={form.errors[valueName] ? form.errors[valueName] : ''} | ||||
| onChange={handleChange ? handleChange : form.handleChange} | onChange={handleChange ? handleChange : form.handleChange} | ||||
| value={form.values[valueName]} | |||||
| value={displayValue !== undefined ? displayValue : form.values[valueName]} | |||||
| disabled={disabled} | disabled={disabled} | ||||
| autoFocus={autoFocus} | autoFocus={autoFocus} | ||||
| sx={{ | sx={{ | ||||
| @@ -1,166 +1,188 @@ | |||||
| import axios from "axios"; | |||||
| import { FILE_UP_POST, FILE_DOWN_GET } from "../utils/ApiPathConst"; | |||||
| import axios from 'axios'; | |||||
| import { FILE_UP_POST, FILE_DOWN_GET } from '../utils/ApiPathConst'; | |||||
| import qs from 'qs'; | import qs from 'qs'; | ||||
| export const get = ({ url, params, onSuccess, onFail, onError, onFinally }) => { | export const get = ({ url, params, onSuccess, onFail, onError, onFinally }) => { | ||||
| axios.get(url, { | |||||
| params, | |||||
| paramsSerializer: (p) => qs.stringify(p, { arrayFormat: 'repeat' }) // <-- FIX | |||||
| }).then( | |||||
| (response) => { onResponse(response, onSuccess, onFail); } | |||||
| ).catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }).finally(() => { | |||||
| if (typeof onFinally === 'function') { | |||||
| onFinally(); | |||||
| } | |||||
| axios | |||||
| .get(url, { | |||||
| params, | |||||
| paramsSerializer: (p) => qs.stringify(p, { arrayFormat: 'repeat' }) // <-- FIX | |||||
| }) | |||||
| .then((response) => { | |||||
| onResponse(response, onSuccess, onFail); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }) | |||||
| .finally(() => { | |||||
| if (typeof onFinally === 'function') { | |||||
| onFinally(); | |||||
| } | |||||
| }); | }); | ||||
| }; | }; | ||||
| //TODO | //TODO | ||||
| export const put = ({ url, params, onSuccess, onFail, onError }) => { | export const put = ({ url, params, onSuccess, onFail, onError }) => { | ||||
| axios.put(url, params).then( | |||||
| (response) => { onResponse(response, onSuccess, onFail); } | |||||
| ).catch(error => { | |||||
| return handleError(error, onError); | |||||
| axios | |||||
| .put(url, params) | |||||
| .then((response) => { | |||||
| onResponse(response, onSuccess, onFail); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }); | }); | ||||
| }; | }; | ||||
| export const patch = ({ url, params, onSuccess, onFail, onError }) => { | export const patch = ({ url, params, onSuccess, onFail, onError }) => { | ||||
| axios.patch(url, params).then( | |||||
| (response) => { onResponse(response, onSuccess, onFail); } | |||||
| ).catch(error => { | |||||
| return handleError(error, onError); | |||||
| axios | |||||
| .patch(url, params) | |||||
| .then((response) => { | |||||
| onResponse(response, onSuccess, onFail); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }); | }); | ||||
| }; | }; | ||||
| export const post = ({ url, params, onSuccess, onFail, onError, headers }) => { | |||||
| headers = headers ? headers : { | |||||
| "Content-Type": "application/json" | |||||
| }; | |||||
| export const del = ({ url, params, onSuccess, onFail, onError }) => { | |||||
| axios | |||||
| .delete(url, { params }) | |||||
| .then((response) => { | |||||
| onResponse(response, onSuccess, onFail); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| }; | |||||
| axios.post(url, params, | |||||
| { | |||||
| headers: headers | |||||
| }).then( | |||||
| (response) => { onResponse(response, onSuccess, onFail); } | |||||
| ).catch(error => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| export const post = ({ url, params, onSuccess, onFail, onError, headers }) => { | |||||
| headers = headers | |||||
| ? headers | |||||
| : { | |||||
| 'Content-Type': 'application/json' | |||||
| }; | |||||
| axios | |||||
| .post(url, params, { | |||||
| headers: headers | |||||
| }) | |||||
| .then((response) => { | |||||
| onResponse(response, onSuccess, onFail); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| }; | }; | ||||
| export const postWithFiles = ({ url, params, files, onSuccess, onFail, onError }) => { | export const postWithFiles = ({ url, params, files, onSuccess, onFail, onError }) => { | ||||
| var formData = new FormData(); | |||||
| for (let i = 0; i < files.length; i++) { | |||||
| const file = files[i] | |||||
| formData.append("multipartFileList", file); | |||||
| var formData = new FormData(); | |||||
| for (let i = 0; i < files.length; i++) { | |||||
| const file = files[i]; | |||||
| formData.append('multipartFileList', file); | |||||
| } | |||||
| if (params) | |||||
| for (var key in params) { | |||||
| if (typeof params[key] === 'object') { | |||||
| formData.append(key, JSON.stringify(params[key])); | |||||
| } else { | |||||
| formData.append(key, params[key]); | |||||
| } | |||||
| } | } | ||||
| if (params) | |||||
| for (var key in params) { | |||||
| if (typeof (params[key]) === 'object') { | |||||
| formData.append(key, JSON.stringify(params[key])); | |||||
| } else { | |||||
| formData.append(key, params[key]); | |||||
| } | |||||
| } | |||||
| axios.post(url, formData, | |||||
| { headers: { "Content-Type": "multipart/form-data" } }) | |||||
| .then( | |||||
| (response) => { onResponse(response, onSuccess, onFail); } | |||||
| ).catch(error => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| axios | |||||
| .post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) | |||||
| .then((response) => { | |||||
| onResponse(response, onSuccess, onFail); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| }; | }; | ||||
| export const fileDownload = ({ url, fileId, skey, params, method, onResponse, onError }) => { | export const fileDownload = ({ url, fileId, skey, params, method, onResponse, onError }) => { | ||||
| if (!url) { | |||||
| url = FILE_DOWN_GET + "/" + fileId + "/" + skey | |||||
| } | |||||
| if (method == 'post') { | |||||
| axios.post(url, params, | |||||
| { | |||||
| responseType: 'blob', | |||||
| headers: { | |||||
| "Content-Type": "application/json" | |||||
| } | |||||
| } | |||||
| ).then( | |||||
| (response) => { | |||||
| fileDownloadResponse(response, onResponse) | |||||
| } | |||||
| ).catch(error => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| } else { | |||||
| axios.get(url, | |||||
| { | |||||
| responseType: 'blob', | |||||
| params: params | |||||
| } | |||||
| ).then( | |||||
| (response) => { | |||||
| fileDownloadResponse(response, onResponse) | |||||
| } | |||||
| ).catch(error => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| } | |||||
| if (!url) { | |||||
| url = FILE_DOWN_GET + '/' + fileId + '/' + skey; | |||||
| } | |||||
| if (method == 'post') { | |||||
| axios | |||||
| .post(url, params, { | |||||
| responseType: 'blob', | |||||
| headers: { | |||||
| 'Content-Type': 'application/json' | |||||
| } | |||||
| }) | |||||
| .then((response) => { | |||||
| fileDownloadResponse(response, onResponse); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| } else { | |||||
| axios | |||||
| .get(url, { | |||||
| responseType: 'blob', | |||||
| params: params | |||||
| }) | |||||
| .then((response) => { | |||||
| fileDownloadResponse(response, onResponse); | |||||
| }) | |||||
| .catch((error) => { | |||||
| return handleError(error, onError); | |||||
| }); | |||||
| } | |||||
| }; | }; | ||||
| const fileDownloadResponse = (response, onResponse) => { | const fileDownloadResponse = (response, onResponse) => { | ||||
| const cd = response.headers?.['content-disposition']; | |||||
| const fn = cd?.split('filename=')[1]?.replaceAll('"','')?.trim() || 'export.xlsx'; | |||||
| const url = URL.createObjectURL(response.data); | |||||
| const a = document.createElement('a'); | |||||
| a.href = url; | |||||
| a.download = fn; | |||||
| document.body.appendChild(a); | |||||
| a.click(); | |||||
| document.body.removeChild(a); | |||||
| URL.revokeObjectURL(url); | |||||
| onResponse?.(); | |||||
| const cd = response.headers?.['content-disposition']; | |||||
| const fn = cd?.split('filename=')[1]?.replaceAll('"', '')?.trim() || 'export.xlsx'; | |||||
| const url = URL.createObjectURL(response.data); | |||||
| const a = document.createElement('a'); | |||||
| a.href = url; | |||||
| a.download = fn; | |||||
| document.body.appendChild(a); | |||||
| a.click(); | |||||
| document.body.removeChild(a); | |||||
| URL.revokeObjectURL(url); | |||||
| onResponse?.(); | |||||
| }; | }; | ||||
| export const fileUpload = ({ refType, refId, files, refCode, onSuccess, onFail, onError }) => { | export const fileUpload = ({ refType, refId, files, refCode, onSuccess, onFail, onError }) => { | ||||
| postWithFiles({ | |||||
| url: FILE_UP_POST, | |||||
| params: { | |||||
| refType: refType, | |||||
| refId: refId, | |||||
| refCode: refCode | |||||
| }, | |||||
| files: files, | |||||
| onSuccess: onSuccess, | |||||
| onFail: onFail, | |||||
| onError: onError | |||||
| }); | |||||
| postWithFiles({ | |||||
| url: FILE_UP_POST, | |||||
| params: { | |||||
| refType: refType, | |||||
| refId: refId, | |||||
| refCode: refCode | |||||
| }, | |||||
| files: files, | |||||
| onSuccess: onSuccess, | |||||
| onFail: onFail, | |||||
| onError: onError | |||||
| }); | |||||
| }; | }; | ||||
| const onResponse = (response, onSuccess, onFail) => { | const onResponse = (response, onSuccess, onFail) => { | ||||
| if (response.status >= 300 || response.status < 200) { | |||||
| console.log("onFail"); | |||||
| if (onFail) { | |||||
| onFail(response); | |||||
| } else { | |||||
| console.log(response); | |||||
| } | |||||
| return; | |||||
| if (response.status >= 300 || response.status < 200) { | |||||
| console.log('onFail'); | |||||
| if (onFail) { | |||||
| onFail(response); | |||||
| } else { | |||||
| console.log(response); | |||||
| } | } | ||||
| return; | |||||
| } | |||||
| if (onSuccess) { | |||||
| onSuccess(response?.data); | |||||
| } | |||||
| } | |||||
| if (onSuccess) { | |||||
| onSuccess(response?.data); | |||||
| } | |||||
| }; | |||||
| const handleError = (error, onError) => { | const handleError = (error, onError) => { | ||||
| if (onError) { | |||||
| return onError(error); | |||||
| } else { | |||||
| // console.log(error); | |||||
| return false; | |||||
| } | |||||
| } | |||||
| if (onError) { | |||||
| return onError(error); | |||||
| } else { | |||||
| // console.log(error); | |||||
| return false; | |||||
| } | |||||
| }; | |||||
| @@ -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; | |||||
| }; | |||||
| @@ -1,18 +1,18 @@ | |||||
| export const ADDRESS_LINE_MAX_LENGTH = 40; | export const ADDRESS_LINE_MAX_LENGTH = 40; | ||||
| export function isAddressLineWithinLimit(line, maxLen = ADDRESS_LINE_MAX_LENGTH) { | export function isAddressLineWithinLimit(line, maxLen = ADDRESS_LINE_MAX_LENGTH) { | ||||
| if (line == null || line === '') return true; | |||||
| return line.length <= maxLen; | |||||
| if (line == null || line === '') return true; | |||||
| return line.length <= maxLen; | |||||
| } | } | ||||
| /** Step-0 gate: line1 required; lines 1–3 each <= maxLen */ | /** Step-0 gate: line1 required; lines 1–3 each <= maxLen */ | ||||
| export function isRegisterAddressValid(data, maxLen = ADDRESS_LINE_MAX_LENGTH) { | export function isRegisterAddressValid(data, maxLen = ADDRESS_LINE_MAX_LENGTH) { | ||||
| return ( | |||||
| data.address1 !== '' && | |||||
| isAddressLineWithinLimit(data.address1, maxLen) && | |||||
| isAddressLineWithinLimit(data.address2, maxLen) && | |||||
| isAddressLineWithinLimit(data.address3, maxLen) | |||||
| ); | |||||
| return ( | |||||
| data.address1 !== '' && | |||||
| isAddressLineWithinLimit(data.address1, maxLen) && | |||||
| isAddressLineWithinLimit(data.address2, maxLen) && | |||||
| isAddressLineWithinLimit(data.address3, maxLen) | |||||
| ); | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -20,48 +20,51 @@ export function isRegisterAddressValid(data, maxLen = ADDRESS_LINE_MAX_LENGTH) { | |||||
| * Packs at comma boundaries where possible. | * Packs at comma boundaries where possible. | ||||
| */ | */ | ||||
| export function splitAddressIntoLines(address, maxLen = ADDRESS_LINE_MAX_LENGTH) { | export function splitAddressIntoLines(address, maxLen = ADDRESS_LINE_MAX_LENGTH) { | ||||
| const empty = { address1: '', address2: '', address3: '' }; | |||||
| if (address == null || address === '') return empty; | |||||
| const empty = { address1: '', address2: '', address3: '' }; | |||||
| if (address == null || address === '') return empty; | |||||
| const trimmed = address.trim(); | |||||
| if (trimmed.length <= maxLen) { | |||||
| return { address1: trimmed, address2: '', address3: '' }; | |||||
| } | |||||
| const trimmed = address.trim(); | |||||
| if (trimmed.length <= maxLen) { | |||||
| return { address1: trimmed, address2: '', address3: '' }; | |||||
| } | |||||
| const segments = trimmed.split(',').map((s) => s.trim()).filter(Boolean); | |||||
| const lines = []; | |||||
| let current = ''; | |||||
| const segments = trimmed | |||||
| .split(',') | |||||
| .map((s) => s.trim()) | |||||
| .filter(Boolean); | |||||
| const lines = []; | |||||
| let current = ''; | |||||
| const pushCurrent = () => { | |||||
| if (current) { | |||||
| lines.push(current); | |||||
| current = ''; | |||||
| } | |||||
| }; | |||||
| const pushCurrent = () => { | |||||
| if (current) { | |||||
| lines.push(current); | |||||
| current = ''; | |||||
| } | |||||
| }; | |||||
| for (const segment of segments) { | |||||
| const candidate = current ? `${current}, ${segment}` : segment; | |||||
| if (candidate.length <= maxLen) { | |||||
| current = candidate; | |||||
| } else if (segment.length <= maxLen) { | |||||
| pushCurrent(); | |||||
| current = segment; | |||||
| } else { | |||||
| pushCurrent(); | |||||
| let remaining = segment; | |||||
| while (remaining.length > maxLen) { | |||||
| lines.push(remaining.slice(0, maxLen)); | |||||
| remaining = remaining.slice(maxLen); | |||||
| } | |||||
| current = remaining; | |||||
| } | |||||
| if (lines.length >= 3) break; | |||||
| for (const segment of segments) { | |||||
| const candidate = current ? `${current}, ${segment}` : segment; | |||||
| if (candidate.length <= maxLen) { | |||||
| current = candidate; | |||||
| } else if (segment.length <= maxLen) { | |||||
| pushCurrent(); | |||||
| current = segment; | |||||
| } else { | |||||
| pushCurrent(); | |||||
| let remaining = segment; | |||||
| while (remaining.length > maxLen) { | |||||
| lines.push(remaining.slice(0, maxLen)); | |||||
| remaining = remaining.slice(maxLen); | |||||
| } | |||||
| current = remaining; | |||||
| } | } | ||||
| pushCurrent(); | |||||
| if (lines.length >= 3) break; | |||||
| } | |||||
| pushCurrent(); | |||||
| return { | |||||
| address1: lines[0] ?? '', | |||||
| address2: lines[1] ?? '', | |||||
| address3: lines[2] ?? '', | |||||
| }; | |||||
| return { | |||||
| address1: lines[0] ?? '', | |||||
| address2: lines[1] ?? '', | |||||
| address3: lines[2] ?? '' | |||||
| }; | |||||
| } | } | ||||
| @@ -0,0 +1,64 @@ | |||||
| import DOMPurify from 'dompurify'; | |||||
| const ALLOWED_TAGS = [ | |||||
| 'a', | |||||
| 'b', | |||||
| 'blockquote', | |||||
| 'br', | |||||
| 'caption', | |||||
| 'col', | |||||
| 'colgroup', | |||||
| 'div', | |||||
| 'em', | |||||
| 'h1', | |||||
| 'h2', | |||||
| 'h3', | |||||
| 'h4', | |||||
| 'h5', | |||||
| 'h6', | |||||
| 'hr', | |||||
| 'i', | |||||
| 'img', | |||||
| 'li', | |||||
| 'ol', | |||||
| 'p', | |||||
| 'span', | |||||
| 'strong', | |||||
| 'style', | |||||
| 'sub', | |||||
| 'sup', | |||||
| 'table', | |||||
| 'tbody', | |||||
| 'td', | |||||
| 'tfoot', | |||||
| 'th', | |||||
| 'thead', | |||||
| 'tr', | |||||
| 'u', | |||||
| 'ul' | |||||
| ]; | |||||
| const ALLOWED_ATTR = ['href', 'title', 'target', 'rel', 'src', 'alt', 'width', 'height', 'colspan', 'rowspan', 'scope', 'class', 'style']; | |||||
| if (typeof window !== 'undefined') { | |||||
| DOMPurify.addHook('afterSanitizeAttributes', (node) => { | |||||
| if (node.tagName === 'A' && node.getAttribute('target') === '_blank') { | |||||
| node.setAttribute('rel', 'noopener noreferrer'); | |||||
| } | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * Strict allow-list sanitize for API / i18n HTML before innerHTML. | |||||
| * Scripts, event handlers, iframes, and javascript: URLs are dropped. | |||||
| */ | |||||
| export function sanitizeHtml(dirty) { | |||||
| if (dirty == null) { | |||||
| return ''; | |||||
| } | |||||
| return DOMPurify.sanitize(String(dirty), { | |||||
| ALLOWED_TAGS, | |||||
| ALLOWED_ATTR, | |||||
| ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i | |||||
| }); | |||||
| } | |||||
| @@ -0,0 +1,53 @@ | |||||
| import { sanitizeHtml } from './sanitizeHtml'; | |||||
| describe('sanitizeHtml', () => { | |||||
| test('strips script tags and event handlers', () => { | |||||
| const clean = sanitizeHtml('<p onclick="alert(1)">Hi<script>alert(1)</script></p>'); | |||||
| expect(clean).toContain('Hi'); | |||||
| expect(clean.toLowerCase()).not.toContain('script'); | |||||
| expect(clean.toLowerCase()).not.toContain('onclick'); | |||||
| expect(clean).not.toContain('alert(1)'); | |||||
| }); | |||||
| test('strips javascript URLs and iframes', () => { | |||||
| const clean = sanitizeHtml( | |||||
| '<a href="javascript:alert(1)">x</a><iframe src="https://evil.example"></iframe>' | |||||
| ); | |||||
| expect(clean.toLowerCase()).not.toContain('javascript'); | |||||
| expect(clean.toLowerCase()).not.toContain('iframe'); | |||||
| expect(clean).not.toContain('evil.example'); | |||||
| }); | |||||
| test('keeps safe formatting, https links, and tables', () => { | |||||
| const clean = sanitizeHtml( | |||||
| '<p>Hello <strong>world</strong></p><a href="https://pnsps.gld.gov.hk/">PNSPS</a><table><tr><td>cell</td></tr></table>' | |||||
| ); | |||||
| expect(clean).toContain('<p>'); | |||||
| expect(clean).toMatch(/<(strong|b)>world<\/(strong|b)>/); | |||||
| expect(clean).toContain('https://pnsps.gld.gov.hk/'); | |||||
| expect(clean).toContain('cell'); | |||||
| }); | |||||
| test('keeps mailto links', () => { | |||||
| const clean = sanitizeHtml('<a href="mailto:[email protected]">mail</a>'); | |||||
| expect(clean).toContain('mailto:[email protected]'); | |||||
| }); | |||||
| test('adds rel on target=_blank links', () => { | |||||
| const clean = sanitizeHtml('<a href="https://egazette.gld.gov.hk/en" target="_blank">Gazette</a>'); | |||||
| expect(clean).toContain('target="_blank"'); | |||||
| expect(clean).toContain('noopener'); | |||||
| }); | |||||
| test('null and empty become empty string', () => { | |||||
| expect(sanitizeHtml(null)).toBe(''); | |||||
| expect(sanitizeHtml(undefined)).toBe(''); | |||||
| expect(sanitizeHtml('')).toBe(''); | |||||
| }); | |||||
| test('img onerror is dropped', () => { | |||||
| const clean = sanitizeHtml('<img src="https://example.com/a.png" onerror="alert(1)">'); | |||||
| expect(clean.toLowerCase()).not.toContain('onerror'); | |||||
| expect(clean).not.toContain('alert(1)'); | |||||
| }); | |||||
| }); | |||||
| @@ -2748,7 +2748,7 @@ | |||||
| dependencies: | dependencies: | ||||
| "@types/jest" "*" | "@types/jest" "*" | ||||
| "@types/trusted-types@^2.0.2": | |||||
| "@types/trusted-types@^2.0.2", "@types/trusted-types@^2.0.7": | |||||
| version "2.0.7" | version "2.0.7" | ||||
| resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" | resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" | ||||
| integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== | integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== | ||||
| @@ -3436,13 +3436,14 @@ axe-core@^4.10.0: | |||||
| resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz" | resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz" | ||||
| integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== | integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== | ||||
| axios@^1.12.2: | |||||
| version "1.15.0" | |||||
| resolved "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz" | |||||
| integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q== | |||||
| axios@^1.20.0: | |||||
| version "1.20.0" | |||||
| resolved "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz" | |||||
| integrity sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg== | |||||
| dependencies: | dependencies: | ||||
| follow-redirects "^1.15.11" | |||||
| form-data "^4.0.5" | |||||
| follow-redirects "^1.16.0" | |||||
| form-data "^4.0.6" | |||||
| https-proxy-agent "^5.0.1" | |||||
| proxy-from-env "^2.1.0" | proxy-from-env "^2.1.0" | ||||
| axobject-query@^4.1.0: | axobject-query@^4.1.0: | ||||
| @@ -4680,6 +4681,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: | |||||
| dependencies: | dependencies: | ||||
| domelementtype "^2.2.0" | domelementtype "^2.2.0" | ||||
| dompurify@^3.4.15: | |||||
| version "3.4.15" | |||||
| resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz" | |||||
| integrity sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw== | |||||
| optionalDependencies: | |||||
| "@types/trusted-types" "^2.0.7" | |||||
| domutils@^1.7.0: | domutils@^1.7.0: | ||||
| version "1.7.0" | version "1.7.0" | ||||
| resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz" | resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz" | ||||
| @@ -5573,10 +5581,10 @@ flatted@^3.2.9: | |||||
| resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" | resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" | ||||
| integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== | integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== | ||||
| follow-redirects@^1.0.0, follow-redirects@^1.15.11: | |||||
| version "1.15.11" | |||||
| resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz" | |||||
| integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== | |||||
| follow-redirects@^1.0.0, follow-redirects@^1.16.0: | |||||
| version "1.16.0" | |||||
| resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz" | |||||
| integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== | |||||
| for-each@^0.3.3, for-each@^0.3.5: | for-each@^0.3.3, for-each@^0.3.5: | ||||
| version "0.3.5" | version "0.3.5" | ||||
| @@ -5623,16 +5631,16 @@ form-data@^3.0.0: | |||||
| hasown "^2.0.2" | hasown "^2.0.2" | ||||
| mime-types "^2.1.35" | mime-types "^2.1.35" | ||||
| form-data@^4.0.5: | |||||
| version "4.0.5" | |||||
| resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz" | |||||
| integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== | |||||
| form-data@^4.0.6: | |||||
| version "4.0.6" | |||||
| resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz" | |||||
| integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== | |||||
| dependencies: | dependencies: | ||||
| asynckit "^0.4.0" | asynckit "^0.4.0" | ||||
| combined-stream "^1.0.8" | combined-stream "^1.0.8" | ||||
| es-set-tostringtag "^2.1.0" | es-set-tostringtag "^2.1.0" | ||||
| hasown "^2.0.2" | |||||
| mime-types "^2.1.12" | |||||
| hasown "^2.0.4" | |||||
| mime-types "^2.1.35" | |||||
| format@^0.2.0: | format@^0.2.0: | ||||
| version "0.2.2" | version "0.2.2" | ||||
| @@ -5718,6 +5726,11 @@ fs.realpath@^1.0.0: | |||||
| resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" | resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" | ||||
| integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== | ||||
| fsevents@^2.3.2, fsevents@~2.3.2: | |||||
| version "2.3.3" | |||||
| resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" | |||||
| integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== | |||||
| function-bind@^1.1.2: | function-bind@^1.1.2: | ||||
| version "1.1.2" | version "1.1.2" | ||||
| resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" | resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" | ||||
| @@ -5964,10 +5977,10 @@ has-tostringtag@^1.0.2: | |||||
| dependencies: | dependencies: | ||||
| has-symbols "^1.0.3" | has-symbols "^1.0.3" | ||||
| hasown@^2.0.2: | |||||
| version "2.0.2" | |||||
| resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" | |||||
| integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== | |||||
| hasown@^2.0.2, hasown@^2.0.4: | |||||
| version "2.0.4" | |||||
| resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz" | |||||
| integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== | |||||
| dependencies: | dependencies: | ||||
| function-bind "^1.1.2" | function-bind "^1.1.2" | ||||
| @@ -6147,7 +6160,7 @@ http-proxy@^1.18.1: | |||||
| follow-redirects "^1.0.0" | follow-redirects "^1.0.0" | ||||
| requires-port "^1.0.0" | requires-port "^1.0.0" | ||||
| https-proxy-agent@^5.0.0: | |||||
| https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: | |||||
| version "5.0.1" | version "5.0.1" | ||||
| resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" | resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" | ||||
| integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== | integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== | ||||
| @@ -7732,7 +7745,7 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: | |||||
| resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" | ||||
| integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== | ||||
| mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: | |||||
| mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: | |||||
| version "2.1.35" | version "2.1.35" | ||||
| resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" | resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" | ||||
| integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== | ||||