| @@ -29,9 +29,10 @@ | |||
| "@testing-library/user-event": "^14.4.3", | |||
| "@types/react-input-mask": "^3.0.2", | |||
| "apexcharts": "^3.35.5", | |||
| "axios": "^1.12.2", | |||
| "axios": "^1.20.0", | |||
| "date-fns": "^3.0.6", | |||
| "dayjs": "^1.11.9", | |||
| "dompurify": "^3.4.15", | |||
| "formik": "^2.2.9", | |||
| "framer-motion": "^7.3.6", | |||
| "history": "^5.3.0", | |||
| @@ -6548,13 +6549,14 @@ | |||
| } | |||
| }, | |||
| "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", | |||
| "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" | |||
| } | |||
| }, | |||
| @@ -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": { | |||
| "version": "1.7.0", | |||
| "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", | |||
| @@ -9916,15 +9927,16 @@ | |||
| "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" | |||
| }, | |||
| "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": [ | |||
| { | |||
| "type": "individual", | |||
| "url": "https://github.com/sponsors/RubenVerborgh" | |||
| } | |||
| ], | |||
| "license": "MIT", | |||
| "engines": { | |||
| "node": ">=4.0" | |||
| }, | |||
| @@ -10090,16 +10102,16 @@ | |||
| } | |||
| }, | |||
| "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", | |||
| "dependencies": { | |||
| "asynckit": "^0.4.0", | |||
| "combined-stream": "^1.0.8", | |||
| "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": { | |||
| "node": ">= 6" | |||
| @@ -10598,9 +10610,10 @@ | |||
| } | |||
| }, | |||
| "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": { | |||
| "function-bind": "^1.1.2" | |||
| }, | |||
| @@ -25,9 +25,10 @@ | |||
| "@testing-library/user-event": "^14.4.3", | |||
| "@types/react-input-mask": "^3.0.2", | |||
| "apexcharts": "^3.35.5", | |||
| "axios": "^1.12.2", | |||
| "axios": "^1.20.0", | |||
| "date-fns": "^3.0.6", | |||
| "dayjs": "^1.11.9", | |||
| "dompurify": "^3.4.15", | |||
| "formik": "^2.2.9", | |||
| "framer-motion": "^7.3.6", | |||
| "history": "^5.3.0", | |||
| @@ -12,7 +12,7 @@ export const predictUsageCount = 'predictUsageCount' | |||
| export const windowCount = 'windowCount' | |||
| import {useNavigate} from "react-router-dom"; | |||
| import {useDispatch} from "react-redux"; | |||
| import { REFRESH_TOKEN } from 'utils/ApiPathConst'; | |||
| import { LOGOUT, REFRESH_TOKEN } from 'utils/ApiPathConst'; | |||
| import { getMessage } from 'utils/getI18nMessage'; | |||
| // 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. */ | |||
| 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. */ | |||
| export const clearExpiredSessionAlert = () => { | |||
| @@ -96,6 +96,11 @@ export const isTokenValid = () =>{ | |||
| // ** Handle User Logout | |||
| export const handleLogoutFunction = () => { | |||
| return dispatch => { | |||
| const refreshToken = localStorage.getItem('refreshToken') | |||
| if (refreshToken) { | |||
| axios.post(`${apiPath}${LOGOUT}`, { refreshToken }).catch(() => {}) | |||
| } | |||
| dispatch({ | |||
| type: 'LOGOUT', | |||
| 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 { 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 ||============================== // | |||
| export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| hideFooterSelectedRowCount, rowModesModel, editMode, | |||
| 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 [_rows, set_rows] = useState([]); | |||
| const [_doLoad, set_doLoad] = useState({}); | |||
| @@ -27,6 +34,7 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| const [page, setPage] = useState(0); | |||
| const [pageSize, setPageSize] = useState(10); | |||
| const [sortModel, setSortModel] = useState([]); | |||
| // const [_autoHeight, set_autoHeight] = useState(true); | |||
| const [myHideFooterSelectedRowCount, setMyHideFooterSelectedRowCount] = useState(true); | |||
| const [_sx, set_sx] = useState({ | |||
| @@ -65,7 +73,9 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| useEffect(() => { | |||
| 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){ | |||
| const localStorageSearchCriteria = getSearchCriteria(window.location.pathname) | |||
| // console.log(localStorageSearchCriteria) | |||
| @@ -78,14 +88,58 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| setPage(0); | |||
| 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); | |||
| setLoading(true) | |||
| } | |||
| }, [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(() => { | |||
| getDataList(); | |||
| }, [_doLoad, page]); | |||
| }, [_doLoad, page, pageSize]); | |||
| useEffect(() => { | |||
| @@ -166,8 +220,13 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| if (_doLoad.params == undefined) return; | |||
| if (_doLoad.params.searchCriteria !== undefined) return; | |||
| 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(window.location.pathname === "/publicNotice"){ | |||
| 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})) | |||
| } | |||
| } | |||
| setLoading(true); | |||
| if (applyGridOnReady !== undefined) { | |||
| applyGridOnReady(true); | |||
| } | |||
| HttpUtils.get({ | |||
| url: _doLoad.url, | |||
| params: _doLoad.params, | |||
| @@ -254,21 +318,28 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| disableColumnMenu | |||
| shrinkWrap | |||
| rowModesModel={_rowModesModel} | |||
| pageSizeOptions={pagination ? _pageSizeOptions : []} | |||
| pageSizeOptions={showPaginationBar ? _pageSizeOptions : []} | |||
| editMode={_editMode} | |||
| autoHeight={effectiveAutoHeight} | |||
| hideFooterSelectedRowCount={myHideFooterSelectedRowCount} | |||
| filterModel={{ items: _filterItems }} | |||
| loading={loading} | |||
| paginationMode={pagination ? "server" : undefined} | |||
| paginationMode={showPaginationBar ? "server" : undefined} | |||
| sortingMode={serverSorting ? "server" : undefined} | |||
| sortModel={serverSorting ? sortModel : undefined} | |||
| onSortModelChange={serverSorting ? handleSortModelChange : undefined} | |||
| sx={{ | |||
| ..._sx, | |||
| '& .MuiDataGrid-virtualScroller': { | |||
| overflowY: 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': { | |||
| display: 'none', | |||
| }, | |||
| @@ -276,19 +347,30 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| }} | |||
| components={{ | |||
| 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 | |||
| 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) => { | |||
| if (type === 'previous') { | |||
| return intl.formatMessage({ id: 'paginationPrev' }); | |||
| @@ -298,10 +380,11 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true, | |||
| } | |||
| 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) => () => { | |||
| 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 () { | |||
| 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', | |||
| field: 'paymentMethod', | |||
| headerName: 'GFMIS Payment Method', | |||
| flex: 4, | |||
| flex: 2, | |||
| renderCell: (params) => { | |||
| let paymentMethod = params.row.paymentMethod; | |||
| return <div style={{ margin: 4 }}>{paymentMethod}</div> | |||
| @@ -63,11 +63,27 @@ export default function SearchTable({ previewSearchCriteria, onPreviewGridOnRead | |||
| id: 'payAmount', | |||
| field: 'payAmount', | |||
| 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) : ""; | |||
| }, | |||
| 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 | |||
| key={previewToken} | |||
| sx={_sx} | |||
| rowHeight={80} | |||
| getRowHeight={() => 'auto'} | |||
| columns={columns} | |||
| customPageSize={10} | |||
| onRowDoubleClick={handleEditClick} | |||
| @@ -65,6 +65,14 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||
| flex: 1, | |||
| minWidth: 200, | |||
| 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) => { | |||
| return clickableLink('/paymentPage/details/' + params.row.id, params.row.transNo); | |||
| }, | |||
| @@ -107,17 +115,33 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||
| field: 'payAmount', | |||
| headerName: 'Amount ($)', | |||
| 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) : ""; | |||
| }, | |||
| 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 ( | |||
| <div style={{ width: '100%' }}> | |||
| <FiDataGrid | |||
| sx={_sx} | |||
| rowHeight={60} | |||
| getRowHeight={() => "auto"} | |||
| columns={columns} | |||
| height={500} | |||
| autoHeight={false} | |||
| @@ -68,18 +68,6 @@ export default function GazetteIssueTable({ searchCriteria, applyGridOnReady }) | |||
| 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', | |||
| field: 'issueDesc', | |||
| @@ -97,7 +85,7 @@ export default function GazetteIssueTable({ searchCriteria, applyGridOnReady }) | |||
| <div style={{ height: "fit-content", width: '100%' }}> | |||
| <FiDataGrid | |||
| columns={columns} | |||
| customPageSize={10} | |||
| pagination={false} | |||
| // onRowDoubleClick={handleRowDoubleClick} | |||
| getRowHeight={() => 'auto'} | |||
| applyGridOnReady={applyGridOnReady} | |||
| @@ -67,8 +67,8 @@ const SearchGazetteIssueForm = ({ applyExport, comboData, waitDownload}) => { | |||
| </Grid> | |||
| {/*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 | |||
| disablePortal | |||
| id="year-combo" | |||
| @@ -93,34 +93,11 @@ const SearchGazetteIssueForm = ({ applyExport, comboData, waitDownload}) => { | |||
| /> | |||
| </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}> | |||
| <Grid item sx={{ ml: 3, mb: 3, }} > | |||
| <Grid item sx={{ mr: 3 }}> | |||
| <Button | |||
| variant="contained" | |||
| type="submit" | |||
| size="large" | |||
| disabled={waitDownload} | |||
| > | |||
| Export | |||
| @@ -12,14 +12,17 @@ import * as React from "react"; | |||
| // import * as DateUtils from "utils/DateUtils"; | |||
| import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; | |||
| import {ThemeProvider} from "@emotion/react"; | |||
| import { GeneralConfirmWindow } from "utils/CommonFunction"; | |||
| import { isGrantedAny } from "auth/utils"; | |||
| // import * as ComboData from "utils/ComboData"; | |||
| // ==============================|| 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 [comboList, setComboList] = React.useState([]); | |||
| const [isConfirmOpen, setIsConfirmOpen] = React.useState(false); | |||
| // const [onReady, setOnReady] = React.useState(false); | |||
| const { | |||
| @@ -27,7 +30,7 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||
| handleSubmit } = useForm() | |||
| const onSubmit = () => { | |||
| if (selectedYear !=null){ | |||
| if (selectedYear != null) { | |||
| const temp = { | |||
| year: selectedYear.label, | |||
| }; | |||
| @@ -36,17 +39,28 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||
| }; | |||
| 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 ( | |||
| @@ -65,8 +79,8 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||
| </Grid> | |||
| {/*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 | |||
| disablePortal | |||
| id="year-combo" | |||
| @@ -79,6 +93,9 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||
| ? String(option.label) | |||
| : "" | |||
| } | |||
| isOptionEqualToValue={(option, value) => | |||
| String(option?.label) === String(value?.label) | |||
| } | |||
| onChange={(event, newValue) => { | |||
| setSelectedYear(newValue); | |||
| }} | |||
| @@ -91,30 +108,8 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||
| /> | |||
| </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}> | |||
| <Grid item sx={{ ml: 3, mb: 3, }} > | |||
| <Grid item sx={{ mr: 3 }}> | |||
| <Button | |||
| variant="contained" | |||
| type="submit" | |||
| @@ -124,9 +119,40 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => { | |||
| </Button> | |||
| </Grid> | |||
| </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> | |||
| </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> | |||
| ); | |||
| }; | |||
| @@ -8,6 +8,7 @@ import { | |||
| import * as UrlUtils from "utils/ApiPathConst"; | |||
| import * as React from "react"; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import axios from "axios"; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import Loadable from 'components/Loadable'; | |||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | |||
| @@ -26,10 +27,10 @@ const BackgroundHead = { | |||
| backgroundColor: '#0C489E', | |||
| backgroundPosition: 'right' | |||
| }; | |||
| import { PNSPS_LONG_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { dateStr_Year } from "utils/DateUtils"; | |||
| import { notifySaveSuccess } from 'utils/CommonFunction'; | |||
| import { notifyDeleteSuccess, notifySaveSuccess } from 'utils/CommonFunction'; | |||
| import { isGrantedAny } from "auth/utils"; | |||
| import { useIntl } from 'react-intl'; | |||
| @@ -50,6 +51,7 @@ const Index = () => { | |||
| const [attachments, setAttachments] = React.useState([]); | |||
| const [waitImport, setWaitImport] = React.useState(false); | |||
| const [waitDownload, setWaitDownload] = React.useState(false); | |||
| const [waitDelete, setWaitDelete] = React.useState(false); | |||
| const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); | |||
| const [warningText, setWarningText] = React.useState(""); | |||
| const fileInputRef = React.useRef(null); | |||
| @@ -96,6 +98,43 @@ const Index = () => { | |||
| 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(() => { | |||
| if (Object.keys(exportCriteria).length > 0) { | |||
| doExport(); | |||
| @@ -138,6 +177,7 @@ const Index = () => { | |||
| setWaitImport(true); | |||
| if (!attachments || attachments.length <= 0) { | |||
| setWarningText("Please upload file."); | |||
| setIsWarningPopUp(true); | |||
| setWaitImport(false); | |||
| return; | |||
| } | |||
| @@ -149,6 +189,14 @@ const Index = () => { | |||
| setWaitImport(false); | |||
| setAttachments([]); | |||
| 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"]) && | |||
| <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 }}> | |||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| size="large" | |||
| disabled={waitImport} | |||
| type="button" | |||
| onClick={() => { | |||
| @@ -205,7 +252,7 @@ const Index = () => { | |||
| } | |||
| }} | |||
| > | |||
| <Typography variant="h5">Upload Files</Typography> | |||
| Upload Gazette Issue | |||
| </Button> | |||
| <input | |||
| id="uploadFileBtn" | |||
| @@ -227,8 +274,11 @@ const Index = () => { | |||
| <Grid item xs={12} md={12} lg={12} width="100%"> | |||
| <SearchForm | |||
| applySearch={applySearch} | |||
| searchCriteria={searchCriteria} | |||
| comboData={comboData} | |||
| onGridReady={onGridReady} | |||
| onDeleteYear={deleteYear} | |||
| waitDelete={waitDelete} | |||
| /> | |||
| </Grid> | |||
| @@ -260,7 +310,7 @@ const Index = () => { | |||
| <Typography variant="h3">Warning</Typography> | |||
| </DialogTitle> | |||
| <DialogContent style={{ display: 'flex' }}> | |||
| <Typography variant="h4" style={{ padding: '16px' }}>{warningText}</Typography> | |||
| <Typography variant="h4" style={{ padding: '16px', whiteSpace: 'pre-line' }}>{warningText}</Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={() => setIsWarningPopUp(false)}> | |||
| @@ -12,14 +12,17 @@ import * as React from "react"; | |||
| // import * as DateUtils from "utils/DateUtils"; | |||
| import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; | |||
| import {ThemeProvider} from "@emotion/react"; | |||
| import { GeneralConfirmWindow } from "utils/CommonFunction"; | |||
| import { isGrantedAny } from "auth/utils"; | |||
| // import * as ComboData from "utils/ComboData"; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||
| const SearchHolidayForm = ({ applySearch, comboData, searchCriteria, onGridReady, onDeleteYear, waitDelete }) => { | |||
| const [selectedYear, setSelectedYear] = React.useState(null); | |||
| // const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year); | |||
| const [comboList, setComboList] = React.useState([]); | |||
| const [isConfirmOpen, setIsConfirmOpen] = React.useState(false); | |||
| // const [onReady, setOnReady] = React.useState(false); | |||
| const { | |||
| @@ -36,17 +39,28 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||
| }; | |||
| 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 ( | |||
| @@ -65,8 +79,8 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||
| </Grid> | |||
| {/*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 | |||
| disablePortal | |||
| id="year-combo" | |||
| @@ -79,6 +93,9 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||
| ? String(option.label) | |||
| : "" | |||
| } | |||
| isOptionEqualToValue={(option, value) => | |||
| String(option?.label) === String(value?.label) | |||
| } | |||
| onChange={(event, newValue) => { | |||
| setSelectedYear(newValue); | |||
| }} | |||
| @@ -91,30 +108,8 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||
| /> | |||
| </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}> | |||
| <Grid item sx={{ ml: 3, mb: 3, }} > | |||
| <Grid item sx={{ mr: 3 }}> | |||
| <Button | |||
| variant="contained" | |||
| type="submit" | |||
| @@ -124,9 +119,40 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => { | |||
| </Button> | |||
| </Grid> | |||
| </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> | |||
| </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> | |||
| ); | |||
| }; | |||
| @@ -8,6 +8,7 @@ import { | |||
| import * as UrlUtils from "utils/ApiPathConst"; | |||
| import * as React from "react"; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import axios from "axios"; | |||
| import Loadable from 'components/Loadable'; | |||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | |||
| @@ -26,10 +27,10 @@ const BackgroundHead = { | |||
| backgroundPosition: 'right' | |||
| }; | |||
| import { PNSPS_LONG_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { dateStr_Year } from "utils/DateUtils"; | |||
| import { notifySaveSuccess } from 'utils/CommonFunction'; | |||
| import { notifyDeleteSuccess, notifySaveSuccess } from 'utils/CommonFunction'; | |||
| import { isGrantedAny } from "auth/utils"; | |||
| import { useIntl } from 'react-intl'; | |||
| @@ -49,6 +50,7 @@ const Index = () => { | |||
| const [attachments, setAttachments] = React.useState([]); | |||
| const [waitImport, setWaitImport] = React.useState(false); | |||
| const [waitDownload, setWaitDownload] = React.useState(false); | |||
| const [waitDelete, setWaitDelete] = React.useState(false); | |||
| const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); | |||
| const [warningText, setWarningText] = React.useState(""); | |||
| const fileInputRef = React.useRef(null); | |||
| @@ -58,13 +60,17 @@ const Index = () => { | |||
| loadForm(); | |||
| }, [searchCriteria]); | |||
| function loadForm() { | |||
| function loadForm(refreshCombo = false, criteria = null) { | |||
| const params = criteria || searchCriteria; | |||
| HttpUtils.get({ | |||
| url: UrlUtils.GET_HOLIDAY, | |||
| params: searchCriteria, | |||
| params, | |||
| onSuccess: (responseData) => { | |||
| setRecord(responseData); | |||
| if (comboData.length === 0) { | |||
| if (criteria) { | |||
| setSearchCriteria(criteria); | |||
| } | |||
| if (refreshCombo || comboData.length === 0) { | |||
| loadCombo(); | |||
| } else { | |||
| setOnSearchReady(true); | |||
| @@ -77,8 +83,7 @@ const Index = () => { | |||
| HttpUtils.get({ | |||
| url: UrlUtils.GET_HOLIDAY_COMBO, | |||
| onSuccess: (responseData) => { | |||
| let combo = responseData; | |||
| setComboData(combo); | |||
| setComboData(Array.isArray(responseData) ? [...responseData] : []); | |||
| setOnReady(true); | |||
| setOnSearchReady(true); | |||
| } | |||
| @@ -94,6 +99,43 @@ const Index = () => { | |||
| 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(() => { | |||
| if (attachments.length > 0) { | |||
| importHoliday(); | |||
| @@ -132,7 +174,9 @@ const Index = () => { | |||
| setOnSearchReady(false); | |||
| if (!attachments || attachments.length <= 0) { | |||
| setWarningText("Please upload file."); | |||
| setIsWarningPopUp(true); | |||
| setWaitImport(false); | |||
| setOnSearchReady(true); | |||
| return; | |||
| } | |||
| HttpUtils.postWithFiles({ | |||
| @@ -142,7 +186,16 @@ const Index = () => { | |||
| notifySaveSuccess(); | |||
| setWaitImport(false); | |||
| 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%"> | |||
| <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 | |||
| variant="contained" | |||
| size="large" | |||
| disabled={waitDownload} | |||
| onClick={doExport} | |||
| aria-label={intl.formatMessage({ id: 'ariaExportHolidayTemplate' })} | |||
| > | |||
| <Typography variant="h5">Export</Typography> | |||
| Download Template | |||
| </Button> | |||
| </ThemeProvider> | |||
| {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ? | |||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| size="large" | |||
| disabled={waitImport} | |||
| type="button" | |||
| onClick={() => { | |||
| @@ -200,7 +251,7 @@ const Index = () => { | |||
| } | |||
| }} | |||
| > | |||
| <Typography variant="h5">Upload Files</Typography> | |||
| Upload Holiday | |||
| </Button> | |||
| <input | |||
| id="uploadFileBtn" | |||
| @@ -226,6 +277,8 @@ const Index = () => { | |||
| searchCriteria={searchCriteria} | |||
| comboData={comboData} | |||
| onGridReady={onGridReady} | |||
| onDeleteYear={deleteYear} | |||
| waitDelete={waitDelete} | |||
| /> | |||
| </Grid> | |||
| @@ -255,7 +308,7 @@ const Index = () => { | |||
| > | |||
| <DialogTitle><Typography variant="h3">Warning</Typography></DialogTitle> | |||
| <DialogContent style={{ display: 'flex' }}> | |||
| <Typography variant="h4" style={{ padding: '16px' }}>{warningText}</Typography> | |||
| <Typography variant="h4" style={{ padding: '16px', whiteSpace: 'pre-line' }}>{warningText}</Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <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 usePageTitle from 'components/usePageTitle'; | |||
| import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| @@ -93,7 +94,7 @@ const Index = () => { | |||
| </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"> | |||
| <div dangerouslySetInnerHTML={{ __html: record?.content }} /> | |||
| <SafeHtml html={record?.content} /> | |||
| </Typography> | |||
| <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 { useIntl } from "react-intl"; | |||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { GREY_CONTAINED_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { isGrantedAny } from "auth/utils"; | |||
| import FileList from "components/FileList"; | |||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import dayjs from "dayjs"; | |||
| @@ -36,6 +38,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); | |||
| const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); | |||
| const [afterSendPopUp, setAfterSendPopUp] = React.useState(false); | |||
| const [confirmBrPopUp, setConfirmBrPopUp] = React.useState(false); | |||
| const [currentUserData, setCurrentUserData] = useState({}); | |||
| const [overduePublicNotice, setOverduePublicNotice] = useState(0); | |||
| @@ -227,7 +230,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| const markAsNonCreditor = () => { | |||
| setNonCreditorConfirmPopUp(false); | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: UrlUtils.GET_ORG_MARK_AS_NON_CREDITOR + "/" + id, | |||
| onSuccess: () => { | |||
| loadDataFun(); | |||
| @@ -237,7 +240,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| const sendDn_Overdue = () => { | |||
| setNonCreditorConfirmPopUp(false); | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: UrlUtils.GET_SEND_OVERDUE_CREDITOR_LIST + "/" + id, | |||
| onSuccess: (responseData) => { | |||
| 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 ( | |||
| <MainCard elevation={0} | |||
| border={false} | |||
| @@ -316,6 +330,19 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| {currentUserData.newBrSubmitted ? | |||
| <Grid item sx={{ ml: 3, mr: 3 }}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="success" | |||
| onClick={() => setConfirmBrPopUp(true)} | |||
| > | |||
| {intl.formatMessage({ id: "confirmNewBr" })} | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| : null} | |||
| { | |||
| currentUserData.creditor ? | |||
| @@ -324,7 +351,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="error" | |||
| color="success" | |||
| onClick={() => setNonCreditorConfirmPopUp(true)} | |||
| > | |||
| Mark as Non-Credit Client | |||
| @@ -338,7 +365,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="orange" | |||
| color="success" | |||
| onClick={() => setCreditorConfirmPopUp(true)} | |||
| > | |||
| Mark as Credit Client | |||
| @@ -351,7 +378,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| color="primary" | |||
| color="success" | |||
| onClick={() => sendDn_Overdue(true)} | |||
| > | |||
| Generate O/S DN List | |||
| @@ -440,10 +467,16 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| <TextField | |||
| fullWidth | |||
| id="currentExDate" | |||
| // error={(fromDate===null)} | |||
| // type="date" | |||
| name="currentExDate" | |||
| value={fromDate != null ? DateUtils.dateStr(fromDate) : DateUtils.dateStr(currentFromDate)} | |||
| value={(() => { | |||
| const dateStr = fromDate != null ? DateUtils.dateStr(fromDate) : DateUtils.dateStr(currentFromDate); | |||
| const statusLabel = currentUserData.brStatus === "Invalid" | |||
| ? intl.formatMessage({ id: "brStatusInvalid" }) | |||
| : currentUserData.brStatus === "Valid" | |||
| ? intl.formatMessage({ id: "brStatusValid" }) | |||
| : ""; | |||
| return statusLabel ? `${dateStr} (${statusLabel})` : dateStr; | |||
| })()} | |||
| disabled={true} | |||
| /> : | |||
| <LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}> | |||
| @@ -480,9 +513,6 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| { | |||
| fromDate == null ? | |||
| (!editMode && !createMode) ? | |||
| // <FormHelperText error id="helper-text-date"> | |||
| // Please select a date after today. | |||
| // </FormHelperText> | |||
| null | |||
| : | |||
| <FormHelperText error id="helper-text-date"> | |||
| @@ -575,6 +605,81 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| })} | |||
| </Grid> | |||
| {currentUserData.newBrSubmitted ? | |||
| <> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h4" sx={{ mb: 2, mr: 3, mt: 2, borderBottom: "1px solid black" }}> | |||
| {intl.formatMessage({ id: "newBrInformation" })} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel("Name (Eng):"), | |||
| valueName: "enCompanyNameTemp", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: "Name (Ch):", | |||
| valueName: "chCompanyNameTemp", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel("Expiry Date:"), | |||
| valueName: "brExpiryDateTemp", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getAddressField({ | |||
| label: FieldUtils.notNullFieldLabel("Address:"), | |||
| valueName: ["newAddressLine1", "newAddressLine2", "newAddressLine3"], | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "newDistrict", | |||
| disabled: true, | |||
| dataList: ComboData.district, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "newCountry", | |||
| disabled: true, | |||
| dataList: ComboData.country, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| </> | |||
| : null} | |||
| {id > 0 ? | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| <Typography variant="h4" sx={{ mb: 2, mr: 3, borderBottom: "1px solid black" }}> | |||
| {intl.formatMessage({ id: "uploadedFiles" })} | |||
| </Typography> | |||
| <FileList | |||
| key={`${id}-${currentUserData.newBrSubmitted}-${currentUserData.brExpiryDateTemp || ""}`} | |||
| refType={"orgBrFile"} | |||
| refId={id} | |||
| /> | |||
| </Grid> | |||
| : null} | |||
| <Grid item lg={12} ></Grid> | |||
| </Grid> | |||
| @@ -668,6 +773,32 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| </DialogActions> | |||
| </Dialog> | |||
| </div> | |||
| <div> | |||
| <Dialog | |||
| open={confirmBrPopUp} | |||
| onClose={() => setConfirmBrPopUp(false)} | |||
| PaperProps={{ | |||
| sx: { | |||
| minWidth: '40vw', | |||
| maxWidth: { xs: '90vw', md: '70vw', lg: '70vw' }, | |||
| maxHeight: { xs: '90vh', md: '70vh', lg: '60vh' } | |||
| } | |||
| }} | |||
| > | |||
| <DialogTitle><Typography variant="h3">{intl.formatMessage({ id: "confirmNewBr" })}</Typography></DialogTitle> | |||
| <DialogContent style={{ display: 'flex' }}> | |||
| <Typography variant="h4" style={{ padding: '16px' }}>{intl.formatMessage({ id: "confirmNewBrMessage" })}</Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button variant="contained" onClick={() => setConfirmBrPopUp(false)} sx={GREY_CONTAINED_BUTTON_SX}> | |||
| <Typography variant="h5">{intl.formatMessage({ id: "close" })}</Typography> | |||
| </Button> | |||
| <Button variant="contained" onClick={() => confirmNewBr()} sx={PRIMARY_CONTAINED_BUTTON_SX}> | |||
| <Typography variant="h5">{intl.formatMessage({ id: "confirmNewBr" })}</Typography> | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| </div> | |||
| </MainCard> | |||
| ); | |||
| }; | |||
| @@ -23,12 +23,14 @@ import { notifySaveSuccess } from 'utils/CommonFunction'; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { useNavigate } from "react-router-dom"; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| const intl = useIntl(); | |||
| const navigate = useNavigate(); | |||
| const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false); | |||
| const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false); | |||
| @@ -200,6 +202,19 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| {currentUserData.canSubmitBr ? | |||
| <Grid item sx={{ ml: 3, mr: 3 }}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => navigate("/org/submit-br")} | |||
| color="success" | |||
| > | |||
| <FormattedMessage id="submitBrCertificate" /> | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| : null} | |||
| </> | |||
| } | |||
| </Grid> | |||
| @@ -235,13 +250,15 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => { | |||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'expiryDate' }) + ":"), | |||
| valueName: "brExpiryDate", | |||
| disabled: true, | |||
| form: formik | |||
| form: formik, | |||
| displayValue: (!editMode && !createMode && currentUserData.brStatus) | |||
| ? `${formik.values.brExpiryDate || ""} (${currentUserData.brStatus === "Invalid" | |||
| ? intl.formatMessage({ id: "brStatusInvalid" }) | |||
| : intl.formatMessage({ id: "brStatusValid" })})`.trim() | |||
| : undefined | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4} ></Grid> | |||
| <Grid item xs={12} lg={4} > | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'nameEng' }) + ":"), | |||
| @@ -90,6 +90,12 @@ const OrganizationDetailPage = () => { | |||
| response.data["brExpiryDate"] = response.data.brExpiryDate ? DateUtils.dateValue(response.data.brExpiryDate) : ""; | |||
| response.data["orgShortName"] = response.data.orgShortName ? response.data.orgShortName : "N/A" ; | |||
| response.data["newAddressLine1"] = response.data.newBrAddress?.addressLine1; | |||
| response.data["newAddressLine2"] = response.data.newBrAddress?.addressLine2; | |||
| response.data["newAddressLine3"] = response.data.newBrAddress?.addressLine3; | |||
| response.data["newDistrict"] = getObjectByType(ComboData.district, "type", response.data.newBrAddress?.district); | |||
| response.data["newCountry"] = getObjectByType(ComboData.country, "type", response.data.newBrAddress?.country); | |||
| response.data["brExpiryDateTemp"] = response.data.brExpiryDateTemp ? DateUtils.dateStr(response.data.brExpiryDateTemp) : ""; | |||
| setFormData(response.data) | |||
| setList(response.historyList) | |||
| } | |||
| @@ -9,11 +9,13 @@ import { useNavigate } from "react-router-dom"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import { clickableLink} from 'utils/CommonFunction'; | |||
| import {GET_ORG_PATH} from "utils/ApiPathConst"; | |||
| import { useIntl } from "react-intl"; | |||
| // ==============================|| EVENT TABLE ||============================== // | |||
| export default function OrganizationTable({ searchCriteria, applyGridOnReady, applySearch}) { | |||
| const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); | |||
| const navigate = useNavigate() | |||
| const intl = useIntl(); | |||
| React.useEffect(() => { | |||
| set_searchCriteria(searchCriteria); | |||
| @@ -91,6 +93,34 @@ export default function OrganizationTable({ searchCriteria, applyGridOnReady, ap | |||
| return DateUtils.dateValue(params?.value); | |||
| } | |||
| }, | |||
| { | |||
| id: 'brStatus', | |||
| field: 'brStatus', | |||
| headerName: 'BR Status', | |||
| flex: 1, | |||
| minWidth: 110, | |||
| valueGetter: (params) => { | |||
| const status = params?.row?.brStatus; | |||
| if (status === 'Invalid') { | |||
| return intl.formatMessage({ id: 'brStatusInvalid' }); | |||
| } | |||
| if (status === 'Valid') { | |||
| return intl.formatMessage({ id: 'brStatusValid' }); | |||
| } | |||
| return status || ''; | |||
| } | |||
| }, | |||
| { | |||
| id: 'newBrSubmitted', | |||
| field: 'newBrSubmitted', | |||
| headerName: intl.formatMessage({ id: 'newBr' }), | |||
| width: 120, | |||
| minWidth: 120, | |||
| valueGetter: (params) => { | |||
| const value = params?.value; | |||
| return value === true || value === 1 || value === '1' ? intl.formatMessage({ id: 'newBr' }) : ''; | |||
| } | |||
| }, | |||
| { | |||
| id: 'creditor', | |||
| field: 'creditor', | |||
| @@ -0,0 +1,388 @@ | |||
| import { Grid, Typography, Stack, Box, Button, FormHelperText, CircularProgress } from '@mui/material'; | |||
| import * as React from "react"; | |||
| import { useFormik } from 'formik'; | |||
| import * as yup from 'yup'; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import * as UrlUtils from "utils/ApiPathConst"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import * as FieldUtils from "utils/FieldUtils"; | |||
| import * as ComboData from "utils/ComboData"; | |||
| import { getObjectByType } from "utils/CommonFunction"; | |||
| import { isORGLoggedIn, isPrimaryLoggedIn } from "utils/Utils"; | |||
| import Loadable from "components/Loadable"; | |||
| import { lazy } from "react"; | |||
| import MainCard from "components/MainCard"; | |||
| import ForwardIcon from "@mui/icons-material/Forward"; | |||
| import { useNavigate } from "react-router-dom"; | |||
| import titleBackgroundImg from "assets/images/dashboard/gazette-bar.png"; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import usePageTitle from "components/usePageTitle"; | |||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||
| import dayjs from "dayjs"; | |||
| import { DemoItem } from "@mui/x-date-pickers/internals/demo"; | |||
| import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material"; | |||
| import BrStatusDialog from "components/BrStatusDialog"; | |||
| const LoadingComponent = Loadable(lazy(() => import("pages/extra-pages/LoadingComponent"))); | |||
| const UploadFileTable = Loadable(lazy(() => import("pages/Proof/Reply_Public/UploadFileTable"))); | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: "100%", | |||
| height: "100%", | |||
| backgroundSize: "contain", | |||
| backgroundRepeat: "no-repeat", | |||
| backgroundColor: "#0C489E", | |||
| backgroundPosition: "right" | |||
| }; | |||
| const SubmitBrPage = () => { | |||
| usePageTitle("submitBrCertificate"); | |||
| const intl = useIntl(); | |||
| const navigate = useNavigate(); | |||
| const [formData, setFormData] = React.useState({}); | |||
| const [isLoading, setLoading] = React.useState(true); | |||
| const [errorMsg, setErrorMsg] = React.useState(""); | |||
| const [attachments, setAttachments] = React.useState([]); | |||
| const attachmentsRef = React.useRef([]); | |||
| attachmentsRef.current = attachments; | |||
| const [warningText, setWarningText] = React.useState(""); | |||
| const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); | |||
| const [showSubmittedDialog, setShowSubmittedDialog] = React.useState(false); | |||
| const fileInputRef = React.useRef(null); | |||
| const minDate = React.useMemo(() => new Date().setDate(new Date().getDate() + 1), []); | |||
| React.useEffect(() => { | |||
| if (!isORGLoggedIn()) { | |||
| navigate("/dashboard"); | |||
| return; | |||
| } | |||
| HttpUtils.get({ | |||
| url: UrlUtils.GET_PUB_ORG_PATH, | |||
| onSuccess: (response) => { | |||
| const data = response.data || {}; | |||
| if (!data.canSubmitBr) { | |||
| navigate(isPrimaryLoggedIn() ? "/org" : "/dashboard"); | |||
| return; | |||
| } | |||
| data.country = getObjectByType(ComboData.country, "type", data.addressTemp?.country); | |||
| data.district = getObjectByType(ComboData.district, "type", data.addressTemp?.district); | |||
| data.addressLine1 = data.addressTemp?.addressLine1; | |||
| data.addressLine2 = data.addressTemp?.addressLine2; | |||
| data.addressLine3 = data.addressTemp?.addressLine3; | |||
| setFormData(data); | |||
| setLoading(false); | |||
| }, | |||
| onFail: () => { | |||
| navigate("/dashboard"); | |||
| }, | |||
| onError: () => { | |||
| navigate("/dashboard"); | |||
| } | |||
| }); | |||
| }, [navigate]); | |||
| const displayErrorMsg = (msg) => <Typography variant="errorMessage1">{msg}</Typography>; | |||
| const getFileExtension = (fileName) => { | |||
| const name = (fileName || "").toLowerCase(); | |||
| const dot = name.lastIndexOf("."); | |||
| if (dot <= 0 || dot === name.length - 1) { | |||
| return ""; | |||
| } | |||
| return name.substring(dot + 1); | |||
| }; | |||
| const isAcceptedFile = (fileName) => ["pdf", "jpg", "jpeg", "png"].includes(getFileExtension(fileName)); | |||
| const initialValues = React.useMemo(() => ({ | |||
| ...formData, | |||
| brExpiryDate: null, | |||
| certificateFile: null, | |||
| }), [formData]); | |||
| const formik = useFormik({ | |||
| enableReinitialize: true, | |||
| initialValues, | |||
| validationSchema: yup.object().shape({ | |||
| enCompanyName: yup.string().trim().max(150).required(displayErrorMsg(intl.formatMessage({ id: "userRequireEnglishName" }))), | |||
| chCompanyName: yup.string().max(150).nullable(), | |||
| addressLine1: yup.string().trim().max(40).required(displayErrorMsg(intl.formatMessage({ id: "validateAddressLine1" }))), | |||
| addressLine2: yup.string().max(40, displayErrorMsg(intl.formatMessage({ id: "noMoreThen40Words" }))), | |||
| addressLine3: yup.string().max(40, displayErrorMsg(intl.formatMessage({ id: "noMoreThen40Words" }))), | |||
| brExpiryDate: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "pleaseFillInBusinessRegCertValidityDate" }))), | |||
| certificateFile: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "requireValidFileWithProofReplyFormat" }))), | |||
| country: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "pleaseFillInCountry" }))), | |||
| district: yup.mixed().nullable().test( | |||
| "hk-district", | |||
| displayErrorMsg(intl.formatMessage({ id: "pleaseFillInDistrict" })), | |||
| function (value) { | |||
| const country = this.parent.country; | |||
| if (country && country.type === "hongKong") { | |||
| return value != null; | |||
| } | |||
| return true; | |||
| } | |||
| ), | |||
| }), | |||
| onSubmit: (values, { setSubmitting }) => { | |||
| setErrorMsg(""); | |||
| const files = attachmentsRef.current; | |||
| if (!values.brExpiryDate || !files || files.length === 0 || values.country == null | |||
| || (values.country.type === "hongKong" && values.district == null)) { | |||
| setSubmitting(false); | |||
| return; | |||
| } | |||
| return new Promise((resolve, reject) => { | |||
| HttpUtils.postWithFiles({ | |||
| url: UrlUtils.POST_PUB_ORG_SUBMIT_BR, | |||
| params: { | |||
| enCompanyName: values.enCompanyName, | |||
| chCompanyName: values.chCompanyName, | |||
| brExpiryDate: DateUtils.dateValue(values.brExpiryDate), | |||
| address: { | |||
| country: values.country.type, | |||
| district: values.district?.type, | |||
| addressLine1: values.addressLine1, | |||
| addressLine2: values.addressLine2, | |||
| addressLine3: values.addressLine3, | |||
| }, | |||
| }, | |||
| files: files, | |||
| onSuccess: (response) => { | |||
| if (response?.msg) { | |||
| setErrorMsg(intl.formatMessage({ id: response.msg, defaultMessage: response.msg })); | |||
| reject(); | |||
| return; | |||
| } | |||
| setShowSubmittedDialog(true); | |||
| resolve(); | |||
| }, | |||
| onFail: () => reject(), | |||
| onError: () => reject(), | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| const setCertificateFiles = (files) => { | |||
| const next = files || []; | |||
| setAttachments(next); | |||
| formik.setFieldValue("certificateFile", next[0] || null, true); | |||
| }; | |||
| const handleSelectCertificate = (event) => { | |||
| const file = event.target.files[0]; | |||
| if (!file) { | |||
| return; | |||
| } | |||
| if (!isAcceptedFile(file.name)) { | |||
| setWarningText(intl.formatMessage({ id: "requireValidFileWithProofReplyFormat" })); | |||
| setIsWarningPopUp(true); | |||
| event.target.value = ""; | |||
| return; | |||
| } | |||
| if (file.size >= (10 * 1024 * 1034)) { | |||
| setWarningText(intl.formatMessage({ id: "fileSizeWarning" })); | |||
| setIsWarningPopUp(true); | |||
| event.target.value = ""; | |||
| return; | |||
| } | |||
| file.id = 0; | |||
| setCertificateFiles([file]); | |||
| event.target.value = ""; | |||
| }; | |||
| return ( | |||
| isLoading ? | |||
| <Grid container sx={{ minHeight: "87vh", mb: 3 }} direction="column" justifyContent="center" alignItems="center"> | |||
| <Grid item><LoadingComponent /></Grid> | |||
| </Grid> | |||
| : | |||
| <Grid container direction="column" sx={{ minHeight: "87vh", backgroundColor: "#ffffff" }}> | |||
| <Grid item xs={12}> | |||
| <div style={BackgroundHead}> | |||
| <Stack direction="row" height="70px" justifyContent="flex-start" alignItems="center"> | |||
| <Typography component="h1" ml={15} color="#FFF" variant="h4" sx={{ display: { xs: "none", md: "block" } }}> | |||
| <FormattedMessage id="submitBrCertificate" /> | |||
| </Typography> | |||
| </Stack> | |||
| </div> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Button aria-label={intl.formatMessage({ id: "back" })} title="Back" sx={{ ml: 3.5, mt: 2 }} style={{ border: "2px solid" }} variant="outlined" onClick={() => navigate(-1)}> | |||
| <ForwardIcon style={{ height: 30, width: 50, transform: "rotate(180deg)" }} /> | |||
| </Button> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ p: 1, borderRadius: "10px" }}> | |||
| <MainCard elevation={0} border={false} content={false}> | |||
| <form onSubmit={formik.handleSubmit} noValidate style={{ padding: 24 }}> | |||
| <Grid container spacing={1}> | |||
| <Grid item xs={12}> | |||
| <FormHelperText error> | |||
| <Typography variant="errorMessage1">{errorMsg}</Typography> | |||
| </FormHelperText> | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: intl.formatMessage({ id: "brNo" }) + ":", | |||
| valueName: "brNo", | |||
| disabled: true, | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| <Grid container alignItems={"center"}> | |||
| <Grid item xs={12} md={3} lg={3} sx={{ display: "flex", alignItems: "center" }}> | |||
| <Typography variant="pnspsFormParagraphBold">{FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "newBrExpiryDate" }) + ":")}</Typography> | |||
| </Grid> | |||
| <Grid item xs={12} md={6} lg={6}> | |||
| <LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}> | |||
| <DemoItem components={["DatePicker"]}> | |||
| <DatePicker | |||
| id="brExpiryDate" | |||
| name="brExpiryDate" | |||
| slotProps={{ | |||
| field: { readOnly: true }, | |||
| textField: { | |||
| error: Boolean(formik.errors.brExpiryDate && (formik.touched.brExpiryDate || formik.submitCount > 0)), | |||
| helperText: (formik.touched.brExpiryDate || formik.submitCount > 0) ? formik.errors.brExpiryDate : "", | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| value={formik.values.brExpiryDate == null ? null : dayjs(formik.values.brExpiryDate)} | |||
| minDate={minDate == null ? null : dayjs(minDate)} | |||
| onChange={(newValue) => { | |||
| if (DateUtils.dateValue(newValue) > DateUtils.dateValue(new Date())) { | |||
| formik.setFieldValue("brExpiryDate", newValue, true); | |||
| formik.setFieldTouched("brExpiryDate", true, false); | |||
| } | |||
| }} | |||
| /> | |||
| </DemoItem> | |||
| </LocalizationProvider> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}></Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "nameEng" }) + ":"), | |||
| valueName: "enCompanyName", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}> | |||
| {FieldUtils.getTextField({ | |||
| label: intl.formatMessage({ id: "nameChi" }) + ":", | |||
| valueName: "chCompanyName", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={4}></Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getAddressField({ | |||
| label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "formAddress" }) + ":"), | |||
| valueName: ["addressLine1", "addressLine2", "addressLine3"], | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "district", | |||
| dataList: ComboData.district, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12} lg={12}> | |||
| {FieldUtils.getProfileComboField({ | |||
| label: "", | |||
| valueName: "country", | |||
| disabled: true, | |||
| dataList: ComboData.country, | |||
| getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "", | |||
| form: formik | |||
| })} | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Typography variant="subtitle1" sx={{ color: "primary.primary" }}> | |||
| <FormattedMessage id="acceptProofReplyFileFormats" /> | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <input | |||
| ref={fileInputRef} | |||
| id="uploadFileBtn" | |||
| type="file" | |||
| accept=".pdf,.jpg,.jpeg,.png,image/png,image/jpeg,application/pdf" | |||
| hidden | |||
| onChange={handleSelectCertificate} | |||
| /> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| type="button" | |||
| sx={PRIMARY_CONTAINED_BUTTON_SX} | |||
| onClick={() => fileInputRef.current && fileInputRef.current.click()} | |||
| > | |||
| <FormattedMessage id="selectCertificateFile" /> | |||
| </Button> | |||
| </ThemeProvider> | |||
| {(formik.touched.certificateFile || formik.submitCount > 0) && formik.errors.certificateFile ? | |||
| <FormHelperText error>{formik.errors.certificateFile}</FormHelperText> | |||
| : null} | |||
| </Grid> | |||
| {attachments.length > 0 ? | |||
| <Grid item xs={12} md={8}> | |||
| <UploadFileTable recordList={attachments} setRecordList={setCertificateFiles} /> | |||
| </Grid> | |||
| : null} | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| <Button | |||
| variant="contained" | |||
| type="submit" | |||
| color="success" | |||
| disabled={formik.isSubmitting || showSubmittedDialog} | |||
| startIcon={formik.isSubmitting ? <CircularProgress color="inherit" size={18} /> : null} | |||
| > | |||
| <FormattedMessage id="submit" /> | |||
| </Button> | |||
| </ThemeProvider> | |||
| </Grid> | |||
| </Grid> | |||
| </form> | |||
| </MainCard> | |||
| </Box> | |||
| </Grid> | |||
| <Dialog open={isWarningPopUp} onClose={() => setIsWarningPopUp(false)}> | |||
| <DialogTitle><FormattedMessage id="attention" /></DialogTitle> | |||
| <DialogContent><Typography>{warningText}</Typography></DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={() => setIsWarningPopUp(false)}><FormattedMessage id="close" /></Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| <BrStatusDialog | |||
| open={showSubmittedDialog} | |||
| variant={formData.brStatus === "Invalid" || formData.applyBlockState === "expired" | |||
| ? "submitExpired" | |||
| : "submitNotExpired"} | |||
| expiryDate={formData.brExpiryDate} | |||
| onClose={() => { | |||
| setShowSubmittedDialog(false); | |||
| navigate(isPrimaryLoggedIn() ? "/org" : "/dashboard"); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default SubmitBrPage; | |||
| @@ -25,6 +25,7 @@ import Loadable from 'components/Loadable'; | |||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | |||
| import * as FormatUtils from "utils/FormatUtils"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import VisaIcon from "assets/images/icons/visacard.svg"; | |||
| import MasterIcon from "assets/images/icons/mastercard.svg"; | |||
| import JcbIcon from "assets/images/icons/jcb.svg"; | |||
| @@ -58,6 +59,8 @@ const MultiPaymentWindow = (props) => { | |||
| const [paymentHoldedErrText, setPaymentHoldedErrText] = React.useState(""); | |||
| const [paymentHoldedErr, setPaymentHoldedErr] = React.useState(false); | |||
| const [isPaying, setIsPaying] = React.useState(false); | |||
| const payingRef = React.useRef(false); | |||
| const mobileBrowser = "Mobile"; | |||
| @@ -70,10 +73,7 @@ const MultiPaymentWindow = (props) => { | |||
| }, [props.transactionData]); | |||
| useEffect(() => { | |||
| // console.log(props.availableMethods) | |||
| if(props.availableMethods.length > 0){ | |||
| setLoadAvailableMethodData(props.availableMethods) | |||
| } | |||
| setLoadAvailableMethodData(props.availableMethods || []) | |||
| }, [props.availableMethods]); | |||
| useEffect(() => { | |||
| @@ -89,9 +89,7 @@ const MultiPaymentWindow = (props) => { | |||
| }, [transactionData]); | |||
| useEffect(() => { | |||
| if(loadAvailableMethodData.length > 0){ | |||
| setAvailableMethodData(loadAvailableMethodData) | |||
| } | |||
| setAvailableMethodData(loadAvailableMethodData || []) | |||
| }, [loadAvailableMethodData]); | |||
| useEffect(() => { | |||
| @@ -144,7 +142,13 @@ const MultiPaymentWindow = (props) => { | |||
| }, [paymentMethod]); | |||
| const releasePayLock = () => { | |||
| payingRef.current = false; | |||
| setIsPaying(false); | |||
| }; | |||
| const selectedPaymentMethodHandle = (method) => () =>{ | |||
| if (payingRef.current) return; | |||
| if (method != paymentMethod){ | |||
| resetForm() | |||
| let totalAmount = props.totalAmount; | |||
| @@ -198,6 +202,10 @@ const MultiPaymentWindow = (props) => { | |||
| }; | |||
| const handlePaymentCheck = () => { | |||
| if (payingRef.current) return; | |||
| payingRef.current = true; | |||
| setIsPaying(true); | |||
| let appIdList = props.appIds | |||
| // console.log(props.appIds) | |||
| // console.log(appIdList) | |||
| @@ -207,6 +215,8 @@ const MultiPaymentWindow = (props) => { | |||
| appIds: appIdList | |||
| }, | |||
| onSuccess: (responseData) => { | |||
| if (!payingRef.current) return; | |||
| const latestData = {}; | |||
| responseData.forEach(item => { | |||
| @@ -235,13 +245,21 @@ const MultiPaymentWindow = (props) => { | |||
| const resultString = HoldingApplication.map(item => item.appNo).join(' , '); | |||
| setPaymentHoldedErrText(resultString); | |||
| // setPaymentHoldedErrText(intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: record.appNo })); | |||
| releasePayLock(); | |||
| setPaymentHoldedErr(true); | |||
| } | |||
| }, | |||
| onFail: () => { | |||
| releasePayLock(); | |||
| }, | |||
| onError: () => { | |||
| releasePayLock(); | |||
| } | |||
| }); | |||
| }; | |||
| const closeHandle = () => () =>{ | |||
| releasePayLock(); | |||
| resetForm() | |||
| props.setOpen(false) | |||
| }; | |||
| @@ -261,6 +279,12 @@ const MultiPaymentWindow = (props) => { | |||
| } | |||
| }, [availableMethodData]); | |||
| useEffect(() => { | |||
| if (!props.confirmPayment) { | |||
| releasePayLock(); | |||
| } | |||
| }, [props.confirmPayment]); | |||
| const formik = useFormik({ | |||
| initialValues: ({ | |||
| 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 ( | |||
| <Dialog | |||
| open={props.open} | |||
| onClose={() => props.setOpen(false)} | |||
| onClose={closeHandle()} | |||
| fullWidth={true} | |||
| maxWidth={'xl'} | |||
| fullScreen={props.isFullScreen} | |||
| @@ -307,7 +337,7 @@ const MultiPaymentWindow = (props) => { | |||
| </Typography> */} | |||
| {!props.onReady ? | |||
| <LoadingComponent /> | |||
| :availableMethodData.length>0? | |||
| :hasVisiblePaymentMethod? | |||
| <Grid container spacing={2} direction="column" justifyContent="space-between" alignItems="flex-start"> | |||
| <Grid item xs={12} md={12}> | |||
| <Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="center"> | |||
| @@ -317,40 +347,48 @@ const MultiPaymentWindow = (props) => { | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item sx={{display: { sm: 'block', md: 'none' }}}></Grid> | |||
| {showFps ? | |||
| <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> | |||
| </Button> | |||
| </Grid> | |||
| : null} | |||
| {showCreditCard ? | |||
| <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> | |||
| </Button> | |||
| </Grid> | |||
| : null} | |||
| {showCreditCard ? | |||
| <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> | |||
| </Button> | |||
| </Grid> | |||
| : null} | |||
| {showUnionPay ? | |||
| <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> | |||
| </Button> | |||
| </Grid> | |||
| : null} | |||
| {showCreditCard ? | |||
| <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> | |||
| </Button> | |||
| </Grid> | |||
| : null} | |||
| {showPps ? | |||
| <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> | |||
| </Button> | |||
| } | |||
| </Grid> | |||
| : null} | |||
| </Grid> | |||
| </Grid> | |||
| {paymentMethod !=""? | |||
| @@ -447,10 +485,10 @@ const MultiPaymentWindow = (props) => { | |||
| </DialogActions> | |||
| <DialogActions> | |||
| { | |||
| props.onPayment? | |||
| props.onPayment || isPaying? | |||
| <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"/> | |||
| </Button> | |||
| } | |||
| @@ -472,7 +510,7 @@ const MultiPaymentWindow = (props) => { | |||
| <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | |||
| <DialogContent style={{ display: 'flex', }}> | |||
| <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> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -194,7 +194,7 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a | |||
| <div style={{ width: '100%' }}> | |||
| <FiDataGrid | |||
| sx={_sx} | |||
| rowHeight={80} | |||
| getRowHeight={() => 'auto'} | |||
| columns={columns} | |||
| customPageSize={10} | |||
| onRowDoubleClick={handleEditClick} | |||
| @@ -18,6 +18,21 @@ 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"; | |||
| 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 ||============================== // | |||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | |||
| @@ -25,12 +40,11 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | |||
| const [status, setStatus] = React.useState(ComboData.paymentStatus[0]); | |||
| const [payMethod, setPayMethod] = React.useState(ComboData.payMethod[0]); | |||
| const { reset, register, handleSubmit } = useForm() | |||
| const marginBottom = 2.5; | |||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | |||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | |||
| const prevHasOtherRef = React.useRef(null); | |||
| React.useEffect(() => { | |||
| if(searchCriteria.status!=undefined){ | |||
| @@ -72,20 +86,77 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| setToDateValue(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) | |||
| const toPayMethodArray = (opt) => { | |||
| if (!opt || opt.type === 'all') return []; | |||
| 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) => { | |||
| let sentDateFrom = ""; | |||
| 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 = { | |||
| @@ -98,18 +169,36 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| start:0, | |||
| limit:10 | |||
| }; | |||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||
| temp.sort = searchCriteria.sort; | |||
| temp.direction = searchCriteria.direction; | |||
| } | |||
| applySearch(temp); | |||
| }; | |||
| function resetForm() { | |||
| 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({ | |||
| code:"", | |||
| 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']}> | |||
| <DatePicker | |||
| id="dateFrom" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| label="Payment Date (From)" | |||
| value={minDate === null ? null : dayjs(minDate)} | |||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||
| value={toDayjsOrNull(minDate)} | |||
| maxDate={toDayjsOrNull(maxDate)} | |||
| onChange={(newValue) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMinDate(newValue); | |||
| } | |||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -176,22 +264,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateTo" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| label="Payment Date (To)" | |||
| value={maxDate === null ? null : dayjs(maxDate)} | |||
| minDate={minDate === null ? null : dayjs(minDate)} | |||
| value={toDayjsOrNull(maxDate)} | |||
| minDate={toDayjsOrNull(minDate)} | |||
| onChange={(newValue) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMaxDate(newValue); | |||
| } | |||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -119,7 +119,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| <div style={{ minHeight: 400, width: '100%' }}> | |||
| <FiDataGrid | |||
| sx={_sx} | |||
| rowHeight={80} | |||
| getRowHeight={() => 'auto'} | |||
| columns={columns} | |||
| customPageSize={10} | |||
| onRowDoubleClick={handleEditDoubleClick} | |||
| @@ -20,6 +20,21 @@ 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"; | |||
| 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 ||============================== // | |||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | |||
| const intl = useIntl(); | |||
| @@ -29,9 +44,9 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | |||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | |||
| const prevHasOtherRef = React.useRef(null); | |||
| React.useEffect(() => { | |||
| // console.log(minDate) | |||
| setFromDateValue(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) => { | |||
| let sentDateFrom = ""; | |||
| 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 = { | |||
| code: data.code, | |||
| transNo: data.transNo, | |||
| @@ -73,18 +145,34 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| start:0, | |||
| limit:10 | |||
| }; | |||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||
| temp.sort = searchCriteria.sort; | |||
| temp.direction = searchCriteria.direction; | |||
| } | |||
| applySearch(temp); | |||
| }; | |||
| function resetForm() { | |||
| 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({ | |||
| code:"", | |||
| 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']}> | |||
| <DatePicker | |||
| id="dateFrom" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| 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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMinDate(newValue); | |||
| } | |||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -156,23 +242,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateTo" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| 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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMaxDate(newValue); | |||
| } | |||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -106,6 +106,12 @@ const Index = () => { | |||
| setTotalAmount(totalAmount); | |||
| setSelectedPaymentMethod("") | |||
| setConfirmPayment(false) | |||
| setOnReady(false) | |||
| setAvailableMethods([]) | |||
| setFPSStatus({}) | |||
| setCreditCardStatus({}) | |||
| setUnionPayStatus({}) | |||
| setPPSStatus({}) | |||
| if (totalAmount > 0) { | |||
| getAvailablePayment() | |||
| getTransactionId() | |||
| @@ -190,48 +196,97 @@ const Index = () => { | |||
| } | |||
| }, [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({ | |||
| url: UrlUtils.PAYMENT_AVAILABLE_PAYMENT, | |||
| params: { | |||
| "locale": locale === 'en' ?local.en:locale === 'zh-HK' ?local.zh:local.cn, | |||
| "amount": totalAmount, | |||
| // "eserviceids": [ | |||
| // "<eserviceid>", "<eserviceid>" | |||
| // ], | |||
| "preferpaymentmethods": preferpaymentmethods | |||
| "preferpaymentmethods": methods | |||
| }, | |||
| onSuccess: (responseData) => { | |||
| let availableMethods = responseData.availablepaymentmethods; | |||
| let availableMethods = filterAvailableMethodsByLimit( | |||
| responseData.availablepaymentmethods || [], totalAmount, limits | |||
| ); | |||
| 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: () =>{ | |||
| setOnReady(true) | |||
| } | |||
| }); | |||
| }; | |||
| const getAvailablePayment = () =>{ | |||
| HttpUtils.get({ | |||
| url: UrlUtils.PAYMENT_LIMIT_SETTING_LIST, | |||
| params: {}, | |||
| onSuccess: (responseData) => { | |||
| // console.log(responseData) | |||
| setPaymentLimit(responseData) | |||
| const methodsWithinLimit = filterPreferPaymentMethodsByLimit( | |||
| preferpaymentmethods, totalAmount, responseData | |||
| ); | |||
| if (methodsWithinLimit.length === 0) { | |||
| setAvailableMethods([]) | |||
| setOnReady(true) | |||
| return; | |||
| } | |||
| requestPaymentAvailability(methodsWithinLimit, responseData); | |||
| }, | |||
| onError: () =>{ | |||
| // setOnReady(true) | |||
| requestPaymentAvailability(preferpaymentmethods); | |||
| } | |||
| }); | |||
| @@ -383,7 +438,7 @@ const Index = () => { | |||
| setFPSStatus(method) | |||
| } | |||
| } else if (method.subtype === "CreditCard") { | |||
| method.supportedcard.forEach((supportedcard) => { | |||
| (method.supportedcard || []).forEach((supportedcard) => { | |||
| if (supportedcard === "JCB" || supportedcard === "MasterCard" || supportedcard === "Visa") { | |||
| setCreditCardStatus(method) | |||
| } else { | |||
| @@ -435,6 +490,7 @@ const Index = () => { | |||
| // const confirmPaymentHandle = () => () => { | |||
| useEffect(() => { | |||
| if (confirmPayment){ | |||
| setOnPayment(true); | |||
| HttpUtils.post({ | |||
| url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, | |||
| params: { | |||
| @@ -445,12 +501,22 @@ const Index = () => { | |||
| setAfterConfirmPayment(true); | |||
| return; | |||
| } | |||
| setOnPayment(false); | |||
| setConfirmPayment(false); | |||
| let str = ""; | |||
| responData.msg.forEach((item) => { | |||
| str += "App: " + item.appNo + ", 到期日: " + DateUtils.datetimeStr_Cht(item.expiryDate) + "\n"; | |||
| }); | |||
| setExpiryDateErrText(str.split('\n').map(str => <>{str}<br/></>)); | |||
| setExpiryDateErr(true); | |||
| }, | |||
| onFail: () => { | |||
| setOnPayment(false); | |||
| setConfirmPayment(false); | |||
| }, | |||
| onError: () => { | |||
| setOnPayment(false); | |||
| setConfirmPayment(false); | |||
| } | |||
| }); | |||
| } | |||
| @@ -551,6 +617,7 @@ const Index = () => { | |||
| setSelectedPaymentMethod={setSelectedPaymentMethod} | |||
| selectedPaymentMethod={selectedPaymentMethod} | |||
| setConfirmPayment={setConfirmPayment} | |||
| confirmPayment={confirmPayment} | |||
| getMethodImgClass = {getMethodImgClass} | |||
| onReady = {onReady} | |||
| locale = {locale} | |||
| @@ -307,7 +307,7 @@ const FormPanel = ({ formData }) => { | |||
| } | |||
| }} | |||
| > | |||
| <Typography variant="h5">Upload Files</Typography> | |||
| <Typography variant="h5">Upload File</Typography> | |||
| </Button> | |||
| <input | |||
| id="uploadFileBtn" | |||
| @@ -28,6 +28,7 @@ const BackgroundHead = { | |||
| import { PNSPS_BUTTON_THEME, PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const Index = ({ record }) => { | |||
| @@ -107,7 +108,7 @@ const Index = ({ record }) => { | |||
| </Typography> | |||
| <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 /> | |||
| <FormattedMessage id="MSG.payMsg2_1" /> | |||
| <span style={{ color: "red" }}> | |||
| @@ -116,14 +117,12 @@ const Index = ({ record }) => { | |||
| <FormattedMessage id="MSG.payMsg2_2" /> | |||
| <br /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage({ id: 'MSG.payMsg3' }, | |||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.payMsg3' }, | |||
| { | |||
| issueYear: record?.issueYear, | |||
| issueVolume: record?.issueVolume, | |||
| issueNo: record?.issueNo, | |||
| }) | |||
| }} /> | |||
| })} /> | |||
| </Typography> | |||
| <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> | |||
| <DialogContent style={{ display: 'flex', }}> | |||
| <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> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils"; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: '100%', | |||
| @@ -76,19 +77,16 @@ const isAfter = checkIsOnlyOnlinePayment(); | |||
| )} | |||
| <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_demandNote' | |||
| }, | |||
| { | |||
| appNo: record?.appNo, | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_demandNote2' | |||
| }, | |||
| @@ -96,27 +94,22 @@ const isAfter = checkIsOnlyOnlinePayment(); | |||
| closingDateOff: DateUtils.dateFormat(record?.closingDateOff, intl.formatMessage({id: "dateStrFormat"})), | |||
| email: record?.mail, | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_demandNote3' | |||
| }, | |||
| { | |||
| paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_demandNote4' | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| </Typography> | |||
| @@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils"; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: '100%', | |||
| @@ -78,59 +79,47 @@ const isAfter = checkIsOnlyOnlinePayment(); | |||
| <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_office' | |||
| }, | |||
| { | |||
| appNo: record?.appNo, | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_office2' | |||
| }, | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_office3' | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_office4' | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_office5' | |||
| }, | |||
| { | |||
| paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_office6' | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| </Typography> | |||
| @@ -22,6 +22,7 @@ import Loadable from 'components/Loadable'; | |||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| width: '100%', | |||
| @@ -195,32 +196,28 @@ const Index = () => { | |||
| )} | |||
| <Typography variant="h5" sx={bodySx}> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_online' | |||
| }, | |||
| { | |||
| appNo: record?.appNo, | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_online2' | |||
| }, | |||
| { | |||
| paymentDeadline: DateUtils.formatDateForLocale(record?.expiryDate, intl, locale), | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| {checkPaymentSuspension()? | |||
| <div> | |||
| <Typography style={{ textAlign: "flex-start", color: "red" }}> | |||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} /> | |||
| <SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} /> | |||
| </Typography> | |||
| <br /> | |||
| </div>:null | |||
| @@ -272,13 +269,11 @@ const Index = () => { | |||
| } | |||
| <Typography variant="h4" sx={bodySx}> | |||
| <div dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage( | |||
| <SafeHtml html={intl.formatMessage( | |||
| { | |||
| id: 'proofPaymentBody_online3' | |||
| } | |||
| ) | |||
| }} /> | |||
| )} /> | |||
| <br /> | |||
| </Typography> | |||
| @@ -338,7 +333,7 @@ const Index = () => { | |||
| <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | |||
| <DialogContent style={{ display: 'flex', }}> | |||
| <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> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -104,7 +104,7 @@ const ApplicationDetailCard = ({ | |||
| if (cancellingRef.current) return; | |||
| cancellingRef.current = true; | |||
| setCancelLoading(true); | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: UrlUtils.CANCEL_PROOF + "/" + params.id, | |||
| onSuccess: function (responseData) { | |||
| cancellingRef.current = false; | |||
| @@ -23,6 +23,7 @@ import { useNavigate } from "react-router-dom"; | |||
| import * as DateUtils from "utils/DateUtils" | |||
| import Loadable from 'components/Loadable'; | |||
| import { notifyActionSuccess } from 'utils/CommonFunction'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; | |||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| 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) => { | |||
| let file = event.target.files[0]; | |||
| if (file) { | |||
| if (!file.name.toLowerCase().substr(file.name.length - 4).includes(".pdf")) { | |||
| if (!isAcceptedProofReplyFile(file.name)) { | |||
| setWarningTitle(intl.formatMessage({ id: "attention" })) | |||
| setWarningText(intl.formatMessage({ id: 'requireValidFileWithPdfFormat' })); | |||
| setWarningText(intl.formatMessage({ id: 'requireValidFileWithProofReplyFormat' })); | |||
| setIsWarningPopUp(true); | |||
| document.getElementById("uploadFileBtn").value = ""; | |||
| return; | |||
| @@ -382,7 +396,7 @@ const FormPanel = ({ formData }) => { | |||
| <li><FormattedMessage id="post" /></li> | |||
| </ul> | |||
| <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> | |||
| </> | |||
| ); | |||
| @@ -468,7 +482,7 @@ const FormPanel = ({ formData }) => { | |||
| <FormattedMessage id="payOnline" /> | |||
| {checkPaymentSuspension()? | |||
| <Typography style={{ padding: '16px', color: "red" }}> | |||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} /> | |||
| <SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} /> | |||
| </Typography>:null | |||
| } | |||
| <br /><a href="#payOnlineDetails" color='#fff' onClick={() => { | |||
| @@ -536,7 +550,7 @@ const FormPanel = ({ formData }) => { | |||
| <li><FormattedMessage id="post" /></li> | |||
| </ul> | |||
| <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> | |||
| </> | |||
| ); | |||
| @@ -599,13 +613,13 @@ const FormPanel = ({ formData }) => { | |||
| } | |||
| {/* <Grid item xs={12}> | |||
| <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> | |||
| </Grid> */} | |||
| {!isOnlyOnlinePayment? | |||
| <Grid item xs={12}> | |||
| <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> | |||
| </Grid>:null | |||
| } | |||
| @@ -629,6 +643,11 @@ const FormPanel = ({ formData }) => { | |||
| <FormattedMessage id="requiredUploadFix" />: | |||
| </Typography> | |||
| </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"> | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| @@ -659,7 +678,7 @@ const FormPanel = ({ formData }) => { | |||
| id="uploadFileBtn" | |||
| name="file" | |||
| type="file" | |||
| accept=".pdf" | |||
| accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png" | |||
| style={{ display: 'none' }} | |||
| disabled={attachments.length >= (formik.values.groupType === "Private Bill" ? 2 : 1)} | |||
| onChange={(event) => { | |||
| @@ -21,9 +21,10 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| const columns = [ | |||
| { | |||
| field: 'actions', | |||
| field: 'refNo', | |||
| headerName: 'Proof No.', | |||
| width: 170, | |||
| sortable: true, | |||
| cellClassName: 'actions', | |||
| renderCell: (params) => { | |||
| return clickableLink('/proof/reply/' + params.row.id, params.row.refNo); | |||
| @@ -34,6 +35,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| field: 'appId', | |||
| headerName: 'Application No./ Gazette Code/ Gazette Issue No.', | |||
| width: 400, | |||
| sortable: true, | |||
| renderCell: (params) => { | |||
| let appNo = params.row.appNo; | |||
| let code = params.row.groupNo; | |||
| @@ -51,7 +53,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| headerName: 'Status', | |||
| flex: 1, | |||
| minWidth: 150, | |||
| sortable: false, | |||
| sortable: true, | |||
| filterable: false, | |||
| valueGetter: () => '', | |||
| renderCell: (params) => { | |||
| @@ -64,23 +66,14 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| headerName: 'Proof Issue Date', | |||
| flex: 1, | |||
| minWidth: 200, | |||
| // sorting/filtering uses this value | |||
| sortable: true, | |||
| valueGetter: (params) => DateUtils.toDate(params?.value), | |||
| // display uses this (params.value is the *Date* returned above) | |||
| valueFormatter: (params) => { | |||
| const d = params.value; // Date or Invalid Date | |||
| const d = params.value; | |||
| return d instanceof Date && !isNaN(d.getTime()) | |||
| ? 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', | |||
| @@ -88,23 +81,14 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| headerName: 'Confirmed/ Return Date', | |||
| flex: 1, | |||
| minWidth: 200, | |||
| // sorting/filtering uses this value | |||
| sortable: true, | |||
| valueGetter: (params) => DateUtils.toDate(params?.value), | |||
| // display uses this (params.value is the *Date* returned above) | |||
| valueFormatter: (params) => { | |||
| const d = params.value; // Date or Invalid Date | |||
| const d = params.value; | |||
| return d instanceof Date && !isNaN(d.getTime()) | |||
| ? 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', | |||
| @@ -112,6 +96,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| headerName: 'Client', | |||
| flex: 1, | |||
| minWidth: 200, | |||
| sortable: true, | |||
| renderCell: (params) => { | |||
| let company = params.row.enCompanyName != null?params.row.enCompanyName: params.row.chCompanyName; | |||
| company = company != null ? company : ""; | |||
| @@ -129,6 +114,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| headerName: 'Gazette Group', | |||
| flex: 1, | |||
| minWidth: 200, | |||
| sortable: true, | |||
| valueGetter: (params) => { | |||
| return (params?.value) ? (params?.value) : ""; | |||
| } | |||
| @@ -139,6 +125,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| headerName: 'Amount ($)', | |||
| flex: 1, | |||
| minWidth: 200, | |||
| sortable: true, | |||
| valueGetter: (params) => { | |||
| return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | |||
| } | |||
| @@ -158,10 +145,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead | |||
| onRowDoubleClick={handleRowDoubleClick} | |||
| applyGridOnReady={applyGridOnReady} | |||
| applySearch = {applySearch} | |||
| // doLoad={{ | |||
| // url: LIST_PROOF, | |||
| // params: _searchCriteria, | |||
| // }} | |||
| serverSorting | |||
| doLoad={React.useMemo(() => ({ | |||
| url: LIST_PROOF, | |||
| params: _searchCriteria, | |||
| @@ -21,6 +21,21 @@ 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"; | |||
| 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 ||============================== // | |||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | |||
| }) => { | |||
| @@ -32,7 +47,7 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| const [issueCombo, setIssueCombo] = React.useState([]); | |||
| const [groupSelected, setGroupSelected] = React.useState( | |||
| searchCriteria.gazettGroup != undefined | |||
| ? ComboData.groupTitle.find(item => item.code === searchCriteria.gazettGroup) ?? null | |||
| ? ComboData.groupTitle.find(item => item.title === searchCriteria.gazettGroup) ?? null | |||
| : null | |||
| ); | |||
| @@ -40,19 +55,8 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | |||
| const [fromDateValue, setFromDateValue] = 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(() => { | |||
| setFromDateValue(minDate); | |||
| }, [minDate]); | |||
| @@ -65,7 +69,55 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| const { locale } = intl; | |||
| 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) => { | |||
| let typeArray = []; | |||
| let sentDateFrom = ""; | |||
| @@ -75,24 +127,49 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| 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 = { | |||
| refNo: data.refNo, | |||
| code: data.code, | |||
| issueId: issueSelected?.id, | |||
| gazettGroup: groupSelected?.code, | |||
| gazettGroup: groupSelected?.title, | |||
| dateFrom: sentDateFrom, | |||
| dateTo: sentDateTo, | |||
| contact: data.contact, | |||
| orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.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 == "Confirmed"){ | |||
| @@ -130,18 +207,34 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| function resetForm() { | |||
| setType([]); | |||
| setStatus(ComboData.proofStatus[0]); | |||
| setStatus(ComboData.proofStatus_GLD[0]); | |||
| setOrgSelected(null); | |||
| setIssueSelected(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({ | |||
| refNo:"", | |||
| code:"", | |||
| 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) { | |||
| @@ -279,22 +372,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateFrom" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| 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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMinDate(newValue); | |||
| } | |||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -306,22 +398,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateTo" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| 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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMaxDate(newValue); | |||
| } | |||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -106,10 +106,11 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| const columns = [ | |||
| { | |||
| field: 'actions', | |||
| field: 'refNo', | |||
| headerName: intl.formatMessage({ id: 'proofId' }), | |||
| width: isMdOrLg ? 'auto' : 200, | |||
| flex: isMdOrLg ? 1.5 : undefined, | |||
| sortable: true, | |||
| cellClassName: 'actions', | |||
| renderHeader: renderHeaderWithAria, | |||
| renderCell: (params) => { | |||
| @@ -122,6 +123,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount3' }) : intl.formatMessage({ id: 'gazetteCount2' }), | |||
| width: isMdOrLg ? 'auto' : 330, | |||
| flex: isMdOrLg ? 2 : undefined, | |||
| sortable: true, | |||
| renderHeader: renderHeaderWithAria, | |||
| renderCell: (params) => { | |||
| // let appNo = params.row.appNo; | |||
| @@ -138,6 +140,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| headerName: intl.formatMessage({ id: 'proofDate' }), | |||
| width: isMdOrLg ? 'auto' : 200, | |||
| flex: isMdOrLg ? 1.5 : undefined, | |||
| sortable: true, | |||
| renderHeader: renderHeaderWithAria, | |||
| valueGetter: (params) => { | |||
| return DateUtils.datetimeStr(params?.value); | |||
| @@ -149,6 +152,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| headerName: intl.formatMessage({ id: 'replyBefore' }), | |||
| width: isMdOrLg ? 'auto' : 200, | |||
| flex: isMdOrLg ? 1.5 : undefined, | |||
| sortable: true, | |||
| renderHeader: renderHeaderWithAria, | |||
| valueGetter: (params) => { | |||
| const proofPaymentDeadline = DateUtils.convertToDate(params?.value); | |||
| @@ -164,6 +168,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| headerName: intl.formatMessage({ id: 'replyDate' }), | |||
| width: isMdOrLg ? 'auto' : 200, | |||
| flex: isMdOrLg ? 1.5 : undefined, | |||
| sortable: true, | |||
| renderHeader: renderHeaderWithAria, | |||
| valueGetter: (params) => { | |||
| return params?.value ? DateUtils.datetimeStr(params?.value) : ""; | |||
| @@ -174,6 +179,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| headerName: intl.formatMessage({ id: 'status' }), | |||
| width: isMdOrLg ? 'auto' : 160, | |||
| flex: isMdOrLg ? 1 : undefined, | |||
| sortable: true, | |||
| renderHeader: renderHeaderWithAria, | |||
| renderCell: (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' }), | |||
| width: isMdOrLg ? 'auto' : 160, | |||
| flex: isMdOrLg ? 1 : undefined, | |||
| sortable: true, | |||
| renderHeader: renderHeaderWithAria, | |||
| valueGetter: (params) => { | |||
| return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; | |||
| @@ -207,6 +214,8 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| onRowDoubleClick={handleRowDoubleClick} | |||
| applyGridOnReady={applyGridOnReady} | |||
| applySearch={applySearch} | |||
| serverSorting | |||
| disablePagingOnGazetteIssue={false} | |||
| doLoad={React.useMemo(() => ({ | |||
| url: LIST_PROOF, | |||
| params: _searchCriteria, | |||
| @@ -214,4 +223,4 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| @@ -21,6 +21,21 @@ 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"; | |||
| 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 ||============================== // | |||
| 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 [issueSelected, setIssueSelected] = React.useState(null); | |||
| 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 [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | |||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | |||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | |||
| const prevHasOtherRef = React.useRef(null); | |||
| React.useEffect(() => { | |||
| 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) => { | |||
| let typeArray = []; | |||
| let sentDateFrom = ""; | |||
| @@ -72,20 +131,42 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| 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 = { | |||
| refNo: data.refNo, | |||
| code: data.code, | |||
| issueId: issueSelected?.id, | |||
| gazettGroup: groupSelected?.type, | |||
| gazettGroup: groupSelected?.title, | |||
| dateFrom: sentDateFrom, | |||
| dateTo: sentDateTo, | |||
| 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 == "Confirmed"){ | |||
| temp["replyed"] = "T"; | |||
| @@ -120,13 +201,27 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| setStatus(ComboData.proofStatusFull[0]); | |||
| setIssueSelected(null); | |||
| 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({ | |||
| refNo:"", | |||
| 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) { | |||
| @@ -261,22 +356,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateFrom" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| 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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMinDate(newValue); | |||
| } | |||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -288,22 +382,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateTo" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| 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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMaxDate(newValue); | |||
| } | |||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -34,6 +34,7 @@ import { FormattedMessage, useIntl } from "react-intl"; | |||
| import { useState, useEffect, useRef } from 'react'; | |||
| import { checkPaymentSuspension } from "utils/Utils"; | |||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| @@ -409,13 +410,12 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||
| ml:2 | |||
| }} | |||
| > | |||
| <span | |||
| dangerouslySetInnerHTML={{ | |||
| __html: intl.formatMessage({ | |||
| id: "suspensionMessageText", | |||
| defaultMessage: "" | |||
| }) | |||
| }} | |||
| <SafeHtml | |||
| component="span" | |||
| html={intl.formatMessage({ | |||
| id: "suspensionMessageText", | |||
| defaultMessage: "" | |||
| })} | |||
| /> | |||
| </Typography> | |||
| )} | |||
| @@ -459,7 +459,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||
| <li><FormattedMessage id="post" /></li> | |||
| </ul> | |||
| <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> | |||
| </> | |||
| ); | |||
| @@ -603,14 +603,14 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||
| <Grid item xs={12} mr={1} mb={2}> | |||
| <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 display="inline" variant="subtitle1" component="span" sx={{ color: 'primary.primary' }} > | |||
| <ol style={{ textAlign: 'justify', }}> | |||
| <li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr1" }) }} /> | |||
| <li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr2" }) }} /> | |||
| <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> | |||
| </Typography> | |||
| </Grid> | |||
| @@ -630,7 +630,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||
| }} | |||
| /> | |||
| <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> | |||
| </Stack> | |||
| </Grid> | |||
| @@ -653,7 +653,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) => | |||
| <Grid item xs={12}> | |||
| <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> | |||
| </Grid> | |||
| @@ -5,6 +5,7 @@ import * as UrlUtils from "utils/ApiPathConst"; | |||
| import * as FormatUtils from "utils/FormatUtils"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import { useIntl } from "react-intl"; | |||
| import { useNavigate } from "react-router-dom"; | |||
| import { | |||
| Grid, | |||
| @@ -24,6 +25,7 @@ import { | |||
| // checkIsOnlyOnlinePayment | |||
| // isCreditorLoggedIn | |||
| } from "utils/Utils"; | |||
| import { fetchOrgBrData, applyBlockVariant } from "utils/orgBrUtils"; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const ApplyForm = () => { | |||
| @@ -35,12 +37,23 @@ const ApplyForm = () => { | |||
| const [selections, setSelection] = React.useState([]); | |||
| const [isLoading, setLoding] = React.useState(true); | |||
| const [orgBrReady, setOrgBrReady] = React.useState(false); | |||
| const intl = useIntl(); | |||
| const navigate = useNavigate(); | |||
| const { locale } = intl; | |||
| React.useEffect(() => { | |||
| loadUserData(); | |||
| fetchOrgBrData({ | |||
| onSuccess: (org) => { | |||
| if (applyBlockVariant(org)) { | |||
| navigate("/dashboard", { replace: true }); | |||
| return; | |||
| } | |||
| setOrgBrReady(true); | |||
| } | |||
| }); | |||
| }, []); | |||
| const loadUserData = () => { | |||
| @@ -112,11 +125,10 @@ const ApplyForm = () => { | |||
| React.useEffect(() => { | |||
| if (userData !== null){ | |||
| if (userData !== null && orgBrReady){ | |||
| setLoding(false); | |||
| // console.log(isOnlyOnlinePayment) | |||
| } | |||
| }, [userData]); | |||
| }, [userData, orgBrReady]); | |||
| return ( | |||
| isLoading ? | |||
| @@ -274,7 +274,7 @@ const PublicNoticeDetail_GLD = () => { | |||
| setStatusWindowAccepted(false); | |||
| return; | |||
| } | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: `${SET_PUBLIC_NOTICE_STATUS_PUBLISH}/${params.id}`, | |||
| onSuccess: function () { | |||
| setOpen(false); | |||
| @@ -299,7 +299,7 @@ const PublicNoticeDetail_GLD = () => { | |||
| const onPaidClick = () => { | |||
| if (params.id > 0) { | |||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_PAID}/${params.id}`) | |||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_PAID}/${params.id}`) | |||
| .then((response) => { | |||
| if (response.status === 204) { | |||
| setOpen(false); | |||
| @@ -325,7 +325,7 @@ const PublicNoticeDetail_GLD = () => { | |||
| const onComplatedClick = () => { | |||
| if (params.id > 0) { | |||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_COMPLATED}/${params.id}`) | |||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_COMPLATED}/${params.id}`) | |||
| .then((response) => { | |||
| if (response.status === 204) { | |||
| setOpen(false); | |||
| @@ -351,7 +351,7 @@ const PublicNoticeDetail_GLD = () => { | |||
| const onWithdrawnClick = () => { | |||
| if (params.id > 0) { | |||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_WITHDRAW}/${params.id}`) | |||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_WITHDRAW}/${params.id}`) | |||
| .then((response) => { | |||
| if (response.status === 204) { | |||
| setOpen(false); | |||
| @@ -421,7 +421,7 @@ const PublicNoticeDetail_GLD = () => { | |||
| setStatusWindowAccepted(false); | |||
| return; | |||
| } | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: `${SET_PUBLIC_NOTICE_STATUS_REVOKE}/${params.id}`, | |||
| onSuccess: function () { | |||
| setOpen(false); | |||
| @@ -447,7 +447,7 @@ const PublicNoticeDetail_GLD = () => { | |||
| useEffect(() => { | |||
| const status = applicationDetailData.data != undefined ? applicationDetailData.data.status : "" | |||
| 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) => { | |||
| if (response.status === 204) { | |||
| setUploadStatus(false); | |||
| @@ -41,6 +41,7 @@ import EditNoteIcon from '@mui/icons-material/EditNote'; | |||
| import DownloadIcon from '@mui/icons-material/Download'; | |||
| import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; | |||
| import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| @@ -114,6 +115,13 @@ const ApplicationDetailCard = ( | |||
| 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 = () => { | |||
| HttpUtils.post({ | |||
| url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, | |||
| @@ -202,7 +210,8 @@ const ApplicationDetailCard = ( | |||
| > | |||
| <ThemeProvider theme={PNSPS_BUTTON_THEME}> | |||
| { | |||
| currentApplicationDetailData.status == "confirmed" ? | |||
| currentApplicationDetailData.status == "confirmed" | |||
| && !isPastPaymentDeadline(currentApplicationDetailData.expiryDate) ? | |||
| <Button | |||
| variant="contained" | |||
| color="create" | |||
| @@ -834,7 +843,7 @@ const ApplicationDetailCard = ( | |||
| <Typography variant="h4" component="span" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | |||
| <DialogContent style={{ display: 'flex', }}> | |||
| <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> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -145,7 +145,7 @@ const DashboardDefault = () => { | |||
| if (params.id > 0) { | |||
| cancellingRef.current = true; | |||
| setCancelLoading(true); | |||
| axios.get(`${SET_PUBLIC_NOTICE_STATUS_CANCELLED}/${params.id}`) | |||
| axios.post(`${SET_PUBLIC_NOTICE_STATUS_CANCELLED}/${params.id}`) | |||
| .then((response) => { | |||
| if (response.status === 204) { | |||
| notifyActionSuccess("取消成功!") | |||
| @@ -28,6 +28,7 @@ import { | |||
| // import { dateStr } from "utils/DateUtils"; | |||
| import { ThemeProvider, useTheme } from "@emotion/react"; | |||
| import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import { PRIMARY_TEXT_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; | |||
| // ==============================|| EVENT TABLE ||============================== // | |||
| @@ -397,9 +398,11 @@ export default function SubmittedTab({ setCount, url }) { | |||
| <> | |||
| <div style={{ minHeight: 400, width: '100%', padding: 4 }}> | |||
| {isORGLoggedIn() ? | |||
| <Grid container direction="row" justifyContent="flex-start" alignItems="center" > | |||
| <Grid item xs={3} md={1}> | |||
| <Typography variant="h5"><FormattedMessage id="careOf" />:</Typography> | |||
| <Grid container direction="row" justifyContent="flex-start" alignItems="center" columnSpacing={1} sx={{ mb: 1 }}> | |||
| <Grid item> | |||
| <Typography variant="h5" sx={{ whiteSpace: 'nowrap' }}> | |||
| <FormattedMessage id="careOf" />: | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={8} md={2}> | |||
| <Autocomplete | |||
| @@ -580,7 +583,7 @@ export default function SubmittedTab({ setCount, url }) { | |||
| <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> | |||
| <DialogContent style={{ display: 'flex', }}> | |||
| <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> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -23,6 +23,20 @@ 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"; | |||
| 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 ||============================== // | |||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { | |||
| const intl = useIntl(); | |||
| @@ -33,9 +47,19 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | |||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | |||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | |||
| const prevHasOtherRef = React.useRef(null); | |||
| // 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; | |||
| React.useEffect(() => { | |||
| @@ -70,6 +94,41 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| setToDateValue(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) => { | |||
| data.status = status.type; | |||
| let typeArray = []; | |||
| @@ -80,9 +139,24 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| 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 = { | |||
| @@ -95,18 +169,38 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| start:0, | |||
| limit:10 | |||
| }; | |||
| if (searchCriteria?.sort && searchCriteria?.direction) { | |||
| temp.sort = searchCriteria.sort; | |||
| temp.direction = searchCriteria.direction; | |||
| } | |||
| applySearch(temp); | |||
| }; | |||
| function resetForm() { | |||
| setType([]); | |||
| 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({ | |||
| 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 ( | |||
| @@ -142,23 +236,22 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateFrom" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| aria-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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMinDate(newValue); | |||
| } | |||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -170,22 +263,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateTo" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| 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) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMaxDate(newValue); | |||
| } | |||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -45,6 +45,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'appNo', | |||
| field: 'appNo', | |||
| headerName: intl.formatMessage({ id: 'applicationId' }), | |||
| sortable: true, | |||
| width: isMdOrLg ? 'auto' : 160, | |||
| flex: isMdOrLg ? 1 : undefined, | |||
| renderHeader: renderHeaderWithAria, | |||
| @@ -56,6 +57,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'created', | |||
| field: 'created', | |||
| headerName: intl.formatMessage({ id: 'submitDate' }), | |||
| sortable: true, | |||
| width: isMdOrLg ? 'auto' : 160, | |||
| flex: isMdOrLg ? 1 : undefined, | |||
| renderHeader: renderHeaderWithAria, | |||
| @@ -63,36 +65,11 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| 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', | |||
| field: 'remarks', | |||
| headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount2_1' }) : intl.formatMessage({ id: 'myRemarks' }), | |||
| sortable: true, | |||
| width: isMdOrLg ? 'auto' : 400, | |||
| flex: isMdOrLg ? 3 : undefined, | |||
| renderHeader: renderHeaderWithAria, | |||
| @@ -124,6 +101,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'status', | |||
| field: 'status', | |||
| headerName: intl.formatMessage({ id: 'status' }), | |||
| sortable: true, | |||
| width: 200, | |||
| renderHeader: renderHeaderWithAria, | |||
| renderCell: (params) => { | |||
| @@ -134,6 +112,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| field: 'actions', | |||
| type: 'actions', | |||
| headerName: '', | |||
| sortable: false, | |||
| width: 150, | |||
| renderHeader: renderHeaderWithAria, | |||
| cellClassName: 'actions', | |||
| @@ -157,6 +136,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| <FiDataGrid | |||
| columns={columns} | |||
| customPageSize={10} | |||
| serverSorting | |||
| getRowHeight={() => 'auto'} | |||
| onRowDoubleClick={handleRowDoubleClick} | |||
| applyGridOnReady = {applyGridOnReady} | |||
| @@ -32,6 +32,8 @@ import { PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst"; | |||
| import { ThemeProvider } from "@emotion/react"; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import usePageTitle from 'components/usePageTitle'; | |||
| import { fetchOrgBrData, applyBlockVariant, applyClickVariant } from "utils/orgBrUtils"; | |||
| import BrStatusDialog from "components/BrStatusDialog"; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const PublicNotice = () => { | |||
| @@ -44,6 +46,8 @@ const PublicNotice = () => { | |||
| const [selectedTab, setSelectedTab] = useState("1"); | |||
| const navigate = useNavigate(); | |||
| const intl = useIntl(); | |||
| const [orgBrData, setOrgBrData] = useState(null); | |||
| const [brDialog, setBrDialog] = useState({ open: false, variant: null, afterClose: null }); | |||
| const _sx = { | |||
| padding: "4 2 4 2", | |||
| @@ -76,6 +80,9 @@ const PublicNotice = () => { | |||
| useEffect(() => { | |||
| loadData(); | |||
| fetchOrgBrData({ | |||
| onSuccess: (org) => setOrgBrData(org) | |||
| }); | |||
| }, []); | |||
| const loadData = () => { | |||
| @@ -99,6 +106,15 @@ const PublicNotice = () => { | |||
| } | |||
| const onBtnClick = () => { | |||
| const variant = applyClickVariant(orgBrData); | |||
| if (variant) { | |||
| setBrDialog({ | |||
| open: true, | |||
| variant, | |||
| afterClose: applyBlockVariant(orgBrData) ? "stay" : "apply", | |||
| }); | |||
| return; | |||
| } | |||
| navigate('/publicNotice/apply') | |||
| } | |||
| @@ -209,6 +225,18 @@ const PublicNotice = () => { | |||
| ) | |||
| } | |||
| <BrStatusDialog | |||
| open={brDialog.open} | |||
| variant={brDialog.variant} | |||
| expiryDate={orgBrData?.brExpiryDate} | |||
| onClose={() => { | |||
| const afterClose = brDialog.afterClose; | |||
| setBrDialog({ open: false, variant: null, afterClose: null }); | |||
| if (afterClose === "apply") { | |||
| navigate("/publicNotice/apply"); | |||
| } | |||
| }} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| @@ -67,9 +67,9 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| const columns = [ | |||
| { | |||
| field: 'actions', | |||
| field: 'appNo', | |||
| headerName: 'Application No.', | |||
| sortable: false, | |||
| sortable: true, | |||
| width: 150, | |||
| cellClassName: 'actions', | |||
| renderHeader: renderHeaderWithAria, | |||
| @@ -81,7 +81,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'mode', | |||
| field: 'mode', | |||
| headerName: 'Mode', | |||
| sortable: false, | |||
| sortable: true, | |||
| width: 100, | |||
| renderHeader: renderHeaderWithAria, | |||
| renderCell: (params) => { | |||
| @@ -92,7 +92,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'status', | |||
| field: 'status', | |||
| headerName: 'Status', | |||
| sortable: false, | |||
| sortable: true, | |||
| width: 240, | |||
| renderHeader: renderHeaderWithAria, | |||
| renderCell: (params) => { | |||
| @@ -103,7 +103,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'withProof', | |||
| field: 'withProof', | |||
| headerName: 'With Proof', | |||
| sortable: false, | |||
| sortable: true, | |||
| width: 120, | |||
| renderHeader: renderHeaderWithAria, | |||
| renderCell: (params) => { | |||
| @@ -114,7 +114,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'created', | |||
| field: 'created', | |||
| headerName: 'Submit Date', | |||
| sortable: false, | |||
| sortable: true, | |||
| flex: 1, | |||
| minWidth: 200, | |||
| renderHeader: renderHeaderWithAria, | |||
| @@ -126,7 +126,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| id: 'contactPerson', | |||
| field: 'contactPerson', | |||
| headerName: 'Client / Payment Means', | |||
| sortable: false, | |||
| sortable: true, | |||
| minWidth: 250, | |||
| flex: 2, | |||
| renderHeader: renderHeaderWithAria, | |||
| @@ -137,8 +137,14 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| company = company + ": " + params.row.custName | |||
| } | |||
| 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 (<> | |||
| {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', | |||
| field: 'issueNoAndCode', | |||
| headerName: 'Gazette Issue No. / Gazette Code', | |||
| sortable: false, | |||
| sortable: true, | |||
| flex: 1.5, | |||
| minWidth: 350, | |||
| renderHeader: renderHeaderWithAria, | |||
| @@ -220,6 +226,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea | |||
| <FiDataGrid | |||
| checkboxSelection | |||
| disableRowSelectionOnClick | |||
| serverSorting | |||
| onRowSelectionModelChange={(newSelection) => { | |||
| setSelectedRowItems(newSelection); | |||
| }} | |||
| @@ -21,6 +21,21 @@ 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"; | |||
| 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 ||============================== // | |||
| const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady | |||
| }) => { | |||
| @@ -31,12 +46,13 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| const [issueCombo, setIssueCombo] = React.useState([]); | |||
| const [selectedStatus, setSelectedStatus] = 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 [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); | |||
| const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); | |||
| const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); | |||
| const prevHasOtherRef = React.useRef(null); | |||
| React.useEffect(() => { | |||
| if(searchCriteria.status!=undefined){ | |||
| @@ -64,7 +80,56 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| const { locale } = intl; | |||
| 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) => { | |||
| // localStorage.setItem('searchCriteria',"") | |||
| data.status = selectedStatus?.type | |||
| @@ -76,10 +141,25 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| for (let i = 0; i < type.length; i++) { | |||
| 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 = { | |||
| @@ -91,11 +171,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "", | |||
| issueId: issueSelected?.id, | |||
| groupNo: data.groupNo, | |||
| gazettGroup: groupSelected?.code, | |||
| gazettGroup: groupSelected?.title, | |||
| 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); | |||
| // setSearchReady(true) | |||
| }; | |||
| @@ -117,14 +207,32 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| setGroupSelected({}); | |||
| setSelectedStatus({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({ | |||
| appNo:"", | |||
| contact:"", | |||
| 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)=> { | |||
| @@ -188,22 +296,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateFrom" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| label={"Submit Date (From)"} | |||
| value={minDate === null ? null : dayjs(minDate)} | |||
| maxDate={maxDate === null ? null : dayjs(maxDate)} | |||
| value={toDayjsOrNull(minDate)} | |||
| maxDate={toDayjsOrNull(maxDate)} | |||
| onChange={(newValue) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMinDate(newValue); | |||
| } | |||
| setMinDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -215,22 +322,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o | |||
| <DemoItem components={['DatePicker']}> | |||
| <DatePicker | |||
| id="dateTo" | |||
| // onError={(newError) => setReceiptFromError(newError)} | |||
| onError={() => {}} | |||
| slotProps={{ | |||
| field: { readOnly: true, }, | |||
| // textField: { | |||
| // helperText: receiptFromErrorMessage, | |||
| // }, | |||
| field: { readOnly: true, clearable: true }, | |||
| textField: { | |||
| InputLabelProps: { shrink: true }, | |||
| error: false, | |||
| helperText: null | |||
| }, | |||
| }} | |||
| format="DD/MM/YYYY" | |||
| label={"Submit Date (To)"} | |||
| value={maxDate === null ? null : dayjs(maxDate)} | |||
| minDate={minDate === null ? null : dayjs(minDate)} | |||
| value={toDayjsOrNull(maxDate)} | |||
| minDate={toDayjsOrNull(minDate)} | |||
| onChange={(newValue) => { | |||
| // console.log(newValue) | |||
| if(newValue!=null){ | |||
| setMaxDate(newValue); | |||
| } | |||
| setMaxDate(newValue && newValue.isValid?.() ? newValue : null); | |||
| }} | |||
| /> | |||
| </DemoItem > | |||
| @@ -1,21 +1,51 @@ | |||
| // material-ui | |||
| import { | |||
| // Box, | |||
| Autocomplete, | |||
| TextField, | |||
| Typography | |||
| } from '@mui/material'; | |||
| import MainCard from "components/MainCard"; | |||
| import * as React from "react"; | |||
| import { FiDataGrid } from "components/FiDataGrid"; | |||
| import { GET_SYS_PARAMS } from "utils/ApiPathConst"; | |||
| import { GET_SYS_PARAMS, GET_SYS_PARAM_NAMES } from "utils/ApiPathConst"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import * as HttpUtils from "utils/HttpUtils"; | |||
| import { useIntl } from "react-intl"; | |||
| const LANG_SUFFIX = /\.(en|zh|cn)$/i; | |||
| function toSearchKey(name) { | |||
| return String(name || "").replace(LANG_SUFFIX, ""); | |||
| } | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | |||
| const intl = useIntl(); | |||
| const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria); | |||
| const [nameOptions, setNameOptions] = React.useState([]); | |||
| const [selectedName, setSelectedName] = React.useState(""); | |||
| React.useEffect(() => { | |||
| set_searchCriteria(searchCriteria); | |||
| }, [searchCriteria]); | |||
| HttpUtils.get({ | |||
| url: GET_SYS_PARAM_NAMES, | |||
| onSuccess: (responseData) => { | |||
| const keys = (Array.isArray(responseData) ? responseData : []) | |||
| .map((item) => toSearchKey(item?.label ?? item)) | |||
| .filter(Boolean); | |||
| setNameOptions([...new Set(keys)].sort((a, b) => a.localeCompare(b))); | |||
| } | |||
| }); | |||
| }, []); | |||
| React.useEffect(() => { | |||
| const next = { ...searchCriteria }; | |||
| if (selectedName) { | |||
| next.name = selectedName; | |||
| } else { | |||
| delete next.name; | |||
| } | |||
| set_searchCriteria(next); | |||
| }, [searchCriteria, selectedName]); | |||
| const columns = [ | |||
| { | |||
| @@ -45,7 +75,7 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | |||
| flex: 1, | |||
| minWidth: 400, | |||
| renderCell:(params)=>{ | |||
| return <div dangerouslySetInnerHTML={{__html: params.value}} /> | |||
| return <SafeHtml html={params.value} /> | |||
| } | |||
| }, | |||
| ]; | |||
| @@ -60,6 +90,37 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | |||
| System Params | |||
| </Typography> | |||
| <Autocomplete | |||
| disablePortal | |||
| options={nameOptions} | |||
| value={selectedName || null} | |||
| onChange={(event, newValue) => { | |||
| setSelectedName(newValue || ""); | |||
| }} | |||
| getOptionLabel={(option) => (option != null ? String(option) : "")} | |||
| isOptionEqualToValue={(option, value) => String(option) === String(value)} | |||
| sx={{ | |||
| mt: 2, | |||
| ml: 3, | |||
| mr: 3, | |||
| maxWidth: 480, | |||
| '& .MuiInputBase-root': { alignItems: 'center' }, | |||
| '& .MuiAutocomplete-endAdornment': { top: '50%', transform: 'translateY(-50%)' }, | |||
| '& .MuiOutlinedInput-root': { height: 40 } | |||
| }} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| label={intl.formatMessage({ id: "systemSettingName" })} | |||
| InputLabelProps={{ shrink: true }} | |||
| /> | |||
| )} | |||
| clearText={intl.formatMessage({ id: "muiClear" })} | |||
| closeText={intl.formatMessage({ id: "muiClose" })} | |||
| openText={intl.formatMessage({ id: "muiOpen" })} | |||
| noOptionsText={intl.formatMessage({ id: "muiNoOptions" })} | |||
| /> | |||
| <div style={{ width: { xs: '92vw', sm: '96.5vw', md: "auto" }, }}> | |||
| {/* <Box width= '100%' sx={{ backgroundColor: "#fff", ml: 2 }} height='100%'> */} | |||
| <FiDataGrid | |||
| @@ -264,7 +264,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| }; | |||
| const onVerifiedClick = () => { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: UrlUtils.GET_IND_USER_VERIFY + "/" + formData.id, | |||
| onSuccess: function () { | |||
| notifyVerifySuccess() | |||
| @@ -274,7 +274,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| }; | |||
| const doLock = () => { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: UrlUtils.GET_USER_LOCK + "/" + formData.id, | |||
| onSuccess: function () { | |||
| notifyLockSuccess() | |||
| @@ -284,7 +284,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => { | |||
| }; | |||
| const doUnlock = () => { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: UrlUtils.GET_USER_UNLOCK + "/" + formData.id, | |||
| onSuccess: function () { | |||
| notifyActiveSuccess() | |||
| @@ -130,7 +130,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) => | |||
| const onVerifiedClick = () => { | |||
| if (formik?.values?.orgId) { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: GET_IND_USER_VERIFY + "/" + userData.id, | |||
| onSuccess: function () { | |||
| notifyVerifySuccess() | |||
| @@ -148,7 +148,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) => | |||
| setConfirmText("Confirm to Lock this Account?"); | |||
| setConfirmAction({ | |||
| function: function () { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: GET_USER_LOCK + "/" + userData.id, | |||
| onSuccess: function () { | |||
| notifyLockSuccess() | |||
| @@ -165,7 +165,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) => | |||
| setConfirmAction({ | |||
| function: function () { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: GET_USER_UNLOCK + "/" + userData.id, | |||
| onSuccess: function () { | |||
| notifyActiveSuccess() | |||
| @@ -77,7 +77,7 @@ const ManageOrgUserPage = () => { | |||
| function onActiveClick(params) { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: GET_USER_UNLOCK + "/" + params.row.id, | |||
| onSuccess: () => { | |||
| setReloadTime(new Date()); | |||
| @@ -117,7 +117,7 @@ const ManageOrgUserPage = () => { | |||
| const setPrimaryUser = () => { | |||
| setIsWarningPopUp(false) | |||
| HttpUtils.get( | |||
| HttpUtils.post( | |||
| { | |||
| url: (!selectUser.row.primaryUser ? GET_SET_PRIMARY_USER : GET_SET_UN_PRIMARY_USER) + "/" + selectUser.row.id, | |||
| onSuccess: function () { | |||
| @@ -37,7 +37,7 @@ export default function UserTable({searchCriteria, applyGridOnReady,applySearch} | |||
| } | |||
| const doLock = (id) => { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: GET_USER_LOCK+"/"+id, | |||
| onSuccess: function(){ | |||
| //setChangeLocked(true) | |||
| @@ -48,7 +48,7 @@ export default function UserTable({searchCriteria, applyGridOnReady,applySearch} | |||
| }; | |||
| const doUnlock = (id) => { | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: GET_USER_UNLOCK+"/"+id, | |||
| onSuccess: function(){ | |||
| //setChangeLocked(true) | |||
| @@ -61,7 +61,7 @@ const Mail = () => { | |||
| }); | |||
| const setReminderDate=()=>{ | |||
| HttpUtils.get({ | |||
| HttpUtils.post({ | |||
| url: apiPath + "/demandNote/set-expect-reminder", | |||
| onSuccess: function () { | |||
| setResponsText("Success"); | |||
| @@ -10,6 +10,7 @@ import backbroundImg from 'assets/images/bg_ml.jpg'; | |||
| import lgceImg from 'assets/images/2025_lgce.jpg'; | |||
| import 'assets/style/loginStyles.css'; | |||
| import { SysContext } from 'components/SysSettingProvider'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const AuthCard = Loadable(lazy(() => import('./AuthCardCustom'))); | |||
| const BackgroundHead = { | |||
| @@ -175,11 +176,9 @@ const AuthWrapper = ({ children }) => { | |||
| '& 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> | |||
| </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 } }}> | |||
| {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', | |||
| defaultMessage: '' | |||
| }) | |||
| }} | |||
| /> | |||
| })} | |||
| /> | |||
| </Typography> | |||
| )} | |||
| </Grid> | |||
| @@ -246,10 +239,14 @@ const AuthWrapper = ({ children }) => { | |||
| <Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}> | |||
| <FormattedMessage id="PNSPS_fullname" /> | |||
| </Typography> | |||
| {checkSysEnv() !== '' ? ( | |||
| {checkSysEnv() === 'uat' ? ( | |||
| <Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}> | |||
| User Acceptance Test Environment | |||
| </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, | |||
| params:{ | |||
| username: username, | |||
| newPassword: values.password | |||
| newPassword: values.password, | |||
| emailVerifyHash: decodeURIComponent(params.verifyCode) | |||
| }, | |||
| onSuccess: () => { | |||
| useJwt | |||
| @@ -99,7 +99,8 @@ const Index = () => { | |||
| url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD, | |||
| params:{ | |||
| username: username, | |||
| newPassword: values.password | |||
| newPassword: values.password, | |||
| emailVerifyHash: decodeURIComponent(params.verifyCode) | |||
| }, | |||
| onSuccess: () => { | |||
| useJwt | |||
| @@ -17,6 +17,7 @@ import { I_AM_SMART_PATH } from "utils/ApiPathConst"; | |||
| import * as React from 'react'; | |||
| import { FormattedMessage, useIntl } from "react-intl"; | |||
| import usePageTitle from "components/usePageTitle"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| // ================================|| LOGIN ||================================ // | |||
| @@ -127,7 +128,7 @@ const RegisterCustom = () => { | |||
| /> | |||
| <Box mt={4} ml={2} mr={2} bgcolor="grey.100" p={1.5} > | |||
| <Typography textAlign='justify' variant="body1" display="block" gutterBottom> | |||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerIAmSmart' }) }} /> | |||
| <SafeHtml html={intl.formatMessage({ id: 'MSG.registerIAmSmart' })} /> | |||
| </Typography> | |||
| <Link href={intl.formatMessage({ id: "iamsmartLink" })}> | |||
| <FormattedMessage id="learnMore" /> | |||
| @@ -160,7 +161,7 @@ const RegisterCustom = () => { | |||
| </Button> | |||
| <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> | |||
| </Grid> | |||
| <Grid item xs={12} md={6} sx={{ borderLeft: 1, borderColor: 'grey.500' }}> | |||
| @@ -189,7 +190,7 @@ const RegisterCustom = () => { | |||
| </Typography> | |||
| </Button> | |||
| <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> | |||
| </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 Loadable from 'components/Loadable'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import { lazy } from 'react'; | |||
| const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); | |||
| 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' }}> | |||
| <FormattedMessage id="pleaseUploadDoc"/> | |||
| </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> */} | |||
| <Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}> | |||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | |||
| @@ -1583,7 +1587,7 @@ const BusCustomFormWizard = (props) => { | |||
| <FormattedMessage id="uploadFile" /> | |||
| </Button> | |||
| <input | |||
| accept="image/png, .jpg, .bmp, .pdf" | |||
| accept=".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp" | |||
| //className={classes.input} | |||
| id="contained-button-file" | |||
| multiple | |||
| @@ -1618,7 +1622,7 @@ const BusCustomFormWizard = (props) => { | |||
| <Grid container> | |||
| <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" }}> | |||
| <div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} /> | |||
| <SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} /> | |||
| </Typography> | |||
| </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 Loadable from 'components/Loadable'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import { lazy } from 'react'; | |||
| const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); | |||
| const PreviewUploadFileTable = Loadable(lazy(() => import('./PreviewUploadFileTable'))); | |||
| @@ -1887,6 +1888,9 @@ const CustomFormWizard = (props) => { | |||
| <Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}> | |||
| <FormattedMessage id="pleaseUploadIdDocSubTitle" /> | |||
| </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}> | |||
| <ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}> | |||
| <Button | |||
| @@ -1911,7 +1915,7 @@ const CustomFormWizard = (props) => { | |||
| <FormattedMessage id="uploadIdDoc" /> | |||
| </Button> | |||
| <input | |||
| accept="image/png, .jpg, .bmp, .pdf" | |||
| accept=".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp" | |||
| //className={classes.input} | |||
| id="contained-button-file" | |||
| multiple | |||
| @@ -1946,7 +1950,7 @@ const CustomFormWizard = (props) => { | |||
| <Grid container> | |||
| <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" }}> | |||
| <div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} /> | |||
| <SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} /> | |||
| </Typography> | |||
| </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 Loadable from 'components/Loadable'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import { lazy } from 'react'; | |||
| const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); | |||
| @@ -1055,7 +1056,7 @@ const CustomFormWizard = (props) => { | |||
| <Grid item xs={12} md={12}> | |||
| <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> | |||
| </Grid> | |||
| </Grid> | |||
| @@ -20,6 +20,8 @@ import * as HttpUtils from "utils/HttpUtils"; | |||
| import * as UrlUtils from "utils/ApiPathConst"; | |||
| import * as DateUtils from "utils/DateUtils"; | |||
| import { checkPaymentSuspension } from "utils/Utils"; | |||
| import { fetchOrgBrData, applyBlockVariant, applyClickVariant, loginPopupVariant } from "utils/orgBrUtils"; | |||
| import BrStatusDialog from "components/BrStatusDialog"; | |||
| const Message = Loadable(React.lazy(() => import('./Message'))); | |||
| const Notice = Loadable(React.lazy(() => import('./Notice'))); | |||
| @@ -29,6 +31,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('../../extra-pages/Loa | |||
| import { useNavigate } from "react-router-dom"; | |||
| import usePageTitle from "components/usePageTitle"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| @@ -55,10 +58,21 @@ const DashboardDefault = () => { | |||
| const [itemList, setItemList] = React.useState([]); | |||
| const [listData, setListData] = React.useState([]); | |||
| const [isPopUp, setIsPopUp] = React.useState(false); | |||
| const [orgBrData, setOrgBrData] = React.useState(null); | |||
| const [brDialog, setBrDialog] = React.useState({ open: false, variant: null }); | |||
| React.useEffect(() => { | |||
| loadMessageData() | |||
| loadNoticeData() | |||
| fetchOrgBrData({ | |||
| onSuccess: (org) => { | |||
| setOrgBrData(org); | |||
| const variant = loginPopupVariant(org); | |||
| if (variant) { | |||
| setBrDialog({ open: true, variant, afterClose: "stay" }); | |||
| } | |||
| } | |||
| }); | |||
| localStorage.setItem('searchCriteria',"") | |||
| }, []); | |||
| @@ -198,7 +212,18 @@ const DashboardDefault = () => { | |||
| <Grid item xs={12} lg={5} sx={{ pt: 2 }} style={{ height: '100%' }}> | |||
| <Button | |||
| xs={12} | |||
| onClick={() => { navigate("/publicNotice/apply"); }} | |||
| onClick={() => { | |||
| const variant = applyClickVariant(orgBrData); | |||
| if (variant) { | |||
| setBrDialog({ | |||
| open: true, | |||
| variant, | |||
| afterClose: applyBlockVariant(orgBrData) ? "stay" : "apply", | |||
| }); | |||
| return; | |||
| } | |||
| navigate("/publicNotice/apply"); | |||
| }} | |||
| aria-label={intl.formatMessage({ id: 'submitApplication' })} | |||
| sx={{ | |||
| width: "100%", | |||
| @@ -297,7 +322,7 @@ const DashboardDefault = () => { | |||
| </DialogTitle> | |||
| <DialogContent style={{ color: '#B00020', display: 'flex', }}> | |||
| <Typography variant="h5" style={{ padding: '16px' }}> | |||
| <div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} /> | |||
| <SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} /> | |||
| </Typography> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| @@ -305,6 +330,18 @@ const DashboardDefault = () => { | |||
| </DialogActions> | |||
| </Dialog> | |||
| </div> | |||
| <BrStatusDialog | |||
| open={brDialog.open} | |||
| variant={brDialog.variant} | |||
| expiryDate={orgBrData?.brExpiryDate} | |||
| onClose={() => { | |||
| const afterClose = brDialog.afterClose; | |||
| setBrDialog({ open: false, variant: null, afterClose: null }); | |||
| if (afterClose === "apply") { | |||
| navigate("/publicNotice/apply"); | |||
| } | |||
| }} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| @@ -1,3 +1,4 @@ | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| 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 = () => { | |||
| @@ -162,7 +163,7 @@ const AboutUs = () => { | |||
| </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 = () => { | |||
| @@ -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 titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| @@ -38,7 +39,7 @@ const ImportantNotice = () => { | |||
| </div> | |||
| </Grid> | |||
| <Grid item xs={10} md={8} lg={6}> | |||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} /> | |||
| <SafeHtml html={htmlContent} style={{ padding: 12 }} /> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| @@ -1,3 +1,4 @@ | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const Page = () => { | |||
| @@ -83,7 +84,7 @@ const Page = () => { | |||
| </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 content = ` | |||
| <style> | |||
| @@ -125,7 +127,7 @@ const Page = () => { | |||
| </div> | |||
| `; | |||
| return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />; | |||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||
| }; | |||
| export default Page; | |||
| @@ -1,3 +1,5 @@ | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const Page = () => { | |||
| const content = ` | |||
| <style> | |||
| @@ -98,7 +100,7 @@ const Page = () => { | |||
| </div> | |||
| `; | |||
| return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />; | |||
| return <SafeHtml html={content} style={{ padding: 12 }} />; | |||
| }; | |||
| export default Page; | |||
| @@ -3,6 +3,7 @@ import { useIntl, FormattedMessage } from 'react-intl'; | |||
| import usePageTitle from "components/usePageTitle"; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| const BackgroundHead = { | |||
| backgroundImage: `url(${titleBackgroundImg})`, | |||
| @@ -38,7 +39,7 @@ const PrivacyPolicy = () => { | |||
| </div> | |||
| </Grid> | |||
| <Grid item xs={10} md={8} lg={6}> | |||
| <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} /> | |||
| <SafeHtml html={htmlContent} style={{ padding: 12 }} /> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| @@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom"; | |||
| import Loadable from 'components/Loadable'; | |||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | |||
| import AccountCircleIcon from '@mui/icons-material/AccountCircle'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| const BackgroundHead = { | |||
| @@ -66,7 +67,7 @@ const Index = () => { | |||
| <AccountCircleIcon color="secondary" sx={{ width: "200px", height: "200px" }} /> | |||
| <Grid item xs={12} md={12} > | |||
| <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> | |||
| </Grid> | |||
| </center> | |||
| @@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom"; | |||
| import Loadable from 'components/Loadable'; | |||
| const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); | |||
| import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' | |||
| const BackgroundHead = { | |||
| @@ -69,8 +70,9 @@ const Index = () => { | |||
| component="div" | |||
| variant="h3" | |||
| 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' }}> | |||
| <FormattedMessage id="iAmSmartLoginSuccessHeading" /> | |||
| @@ -15,8 +15,8 @@ const GroupAuthTable = Loadable(lazy(() => import('./GroupAuthTable'))); | |||
| const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRecord, editMode}) => { | |||
| const [currentAuthData, setCurrentAuthData] = React.useState({}); | |||
| 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); | |||
| useEffect(()=>{ | |||
| @@ -39,14 +39,22 @@ const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRec | |||
| } | |||
| }, [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(() => { | |||
| //upload latest data to parent | |||
| let deletedList = referenceRow.filter(x => !selectedRow.includes(x)); | |||
| const currentIds = toIdList(selectedRow); | |||
| const referenceIds = toIdList(referenceRow); | |||
| updateUserAuthList({ | |||
| "currentList": selectedRow, | |||
| "deletedList": deletedList | |||
| currentList: currentIds, | |||
| deletedList: referenceIds.filter((id) => !currentIds.includes(id)) | |||
| }); | |||
| }, [isCollectData]); | |||
| }, [isCollectData, selectedRow, referenceRow]); | |||
| return ( | |||
| @@ -98,8 +98,10 @@ export default function GroupAuthTable({setSelectedRow, userAuth,isNewRecord, ed | |||
| rowSelectionModel={currentSelectedRow} | |||
| onRowSelectionModelChange={(ids) => { | |||
| 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 | |||
| @@ -90,12 +90,11 @@ const UserAddCard = ({ isCollectData, updateGroupMember, userGroupData, isNewRec | |||
| }, [currentUserData]); | |||
| useEffect(() => { | |||
| //upload latest data to parent | |||
| updateGroupMember({ | |||
| "currentList": groupUserData, | |||
| "deletedList": deletedList | |||
| currentList: groupUserData, | |||
| deletedList: deletedList | |||
| }); | |||
| }, [isCollectData]); | |||
| }, [isCollectData, groupUserData, deletedList]); | |||
| return ( | |||
| @@ -47,12 +47,12 @@ const UserMaintainPage = () => { | |||
| const [isCollectData, setIsCollectData] = useState(false); | |||
| const [editedGroupData, setEditedGroupData] = useState({}); | |||
| const [userGroupData, setUserGroupData] = useState([]); | |||
| const [userAuthData, setUserAuthData] = useState([]); | |||
| 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 [deletedUserList, setDeletedUserList] = useState([]); | |||
| const [deletedAuthList, setDeletedAuthList] = useState([]); | |||
| const [isWindowOpen, setIsWindowOpen] = useState(false); | |||
| const handleClose = () => { | |||
| @@ -84,13 +84,17 @@ const UserMaintainPage = () => { | |||
| } | |||
| 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 () => { | |||
| @@ -106,19 +110,36 @@ const UserMaintainPage = () => { | |||
| return; | |||
| } | |||
| 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, { | |||
| id: parseInt(params.id) !== -1 ? parseInt(params.id) : null, | |||
| name: latestGroupFormData.userGroupName, | |||
| description: latestGroupFormData.description, | |||
| addUserIds: getIdList(groupMember), | |||
| addUserIds: getIdList(latestGroupMember), | |||
| removeUserIds: finalDeletedUserList, | |||
| addAuthIds: userAuthData, | |||
| removeAuthIds: deletedAuthList, | |||
| addAuthIds: latestAuthIds, | |||
| removeAuthIds: latestDeletedAuthIds, | |||
| }); | |||
| if (response.status === 200) { | |||
| navigate('/usergroupSearchview'); | |||
| 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) { | |||
| console.log(error); | |||
| @@ -177,14 +198,19 @@ const UserMaintainPage = () => { | |||
| 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(() => { | |||
| 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 => { | |||
| console.log(error); | |||
| return false; | |||
| @@ -68,7 +68,7 @@ export default function UserGroupTable({searchCriteria, applyGridOnReady,applySe | |||
| <FiDataGrid | |||
| columns={columns} | |||
| customPageSize={10} | |||
| pageSizeOptions={[10, 15, 20]} | |||
| hideRowsPerPage | |||
| onRowDoubleClick={handleRowDoubleClick} | |||
| applyGridOnReady={applyGridOnReady} | |||
| 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_Organization = Loadable(lazy(() => import('pages/User/DetailsPage_Organization'))); | |||
| const OrganizationDetailPage = Loadable(lazy(() => import('pages/Organization/DetailPage'))); | |||
| const SubmitBrPage = Loadable(lazy(() => import('pages/Organization/SubmitBrPage'))); | |||
| const Msg_Details = Loadable(lazy(() => import('pages/Message/Details'))); | |||
| const Msg_Search = Loadable(lazy(() => import('pages/Message/Search'))); | |||
| const AnnouncementSearch = Loadable(lazy(() => import('pages/Announcement/Search_Public'))); | |||
| @@ -126,6 +127,10 @@ const PublicDashboard = { | |||
| path: '/orgUser', | |||
| element: <UserMaintainPage_Organization /> | |||
| }, | |||
| { | |||
| path: '/org/submit-br', | |||
| element: <SubmitBrPage /> | |||
| }, | |||
| { | |||
| path: '/org', | |||
| element: <OrganizationDetailPage /> | |||
| @@ -69,6 +69,21 @@ export const ERROR_CONTAINED_BUTTON_SX = { | |||
| }, | |||
| }; | |||
| /** WCAG 2.2 AA contained grey — matches PNSPS containedCancel (#616161 + white, ~5.7:1). */ | |||
| export const CONTAINED_NEUTRAL_GREY = '#616161'; | |||
| export const GREY_CONTAINED_BUTTON_SX = { | |||
| backgroundColor: CONTAINED_NEUTRAL_GREY, | |||
| color: '#FFFFFF', | |||
| '&:hover': { | |||
| backgroundColor: '#545454', | |||
| }, | |||
| '&:focus-visible': { | |||
| outline: '2px solid #616161', | |||
| outlineOffset: '2px', | |||
| }, | |||
| }; | |||
| export const PRIMARY_CONTAINED_BUTTON_SX = { | |||
| backgroundColor: CONTAINED_PRIMARY_BLUE, | |||
| color: '#FFFFFF', | |||
| @@ -44,6 +44,8 @@ | |||
| "requireFile": "Please select a file to upload", | |||
| "requireValidFile": "Please upload a valid file", | |||
| "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)", | |||
| "submissionSuccess": "Application submitted successfully", | |||
| "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.registerPersonal": "To complete the online application, you need to upload digital copies of identification documents.<br/>e.g. Hong Kong Identity Card, Passport, Mainland China Identity Card, Professional Practicing Certificate, etc.", | |||
| "MSG.registerOrg": "You need to upload the proof documents for the online application.<br/>e.g. Business Registration Certificate, Professional Practicing Certificate, etc.", | |||
| "MSG.registerOrg": "You need to upload the Business Registration Certificate for the online application.", | |||
| "MSG.paymentMsg": "Your application and payment have been received", | |||
| "MSG.expiredApp": "Public Notice application has expired", | |||
| @@ -284,12 +286,13 @@ | |||
| "sameAsBusinessRegistrationCert": "Same as Business Registration Certificate", | |||
| "businessRegCert": "Business Registration Certificate", | |||
| "businessRegCertNumber": "Hong Kong Business Reg Cert Number", | |||
| "businessRegCertAndDoc":"Business Registration Certificate and other documents", | |||
| "businessRegCertAndDoc":"Business Registration Certificate", | |||
| "businessRegCertExpiryDate": "Business registration certificate expiry date", | |||
| "pleaseUploadDoc": "Please upload a digital file of your valid business registration certificate and other documents to verify your identity.", | |||
| "uploadFile": "Upload business registration certificate and other documents", | |||
| "pleaseUploadDoc": "Please upload a digital file of your valid business registration certificate to verify your identity.", | |||
| "uploadFile": "Upload business registration certificate", | |||
| "pleaseUploadIdDoc": "Please upload a digital file of your valid identity document to verify your identity.", | |||
| "pleaseUploadIdDocSubTitle": "Such as: Hong Kong ID card; passport; Mainland China ID card; professional practice certificate, etc.", | |||
| "acceptRegisterUploadFileFormats": "Accept file formats: PDF, JPG, JPEG, PNG, and BMP. Support maximum 5 files.", | |||
| "uploadIdDoc": "Upload identity document", | |||
| "fileName": "File name", | |||
| "forOrgUser": "For Organisation / Company Users", | |||
| @@ -319,7 +322,7 @@ | |||
| "pleaseFillInBusinessRegCertNumber": "Please fill in Business Registration Certificate Number", | |||
| "pleaseFillInValidBusinessRegCertNumber": "Please fill in valid Business Registration Certificate Number", | |||
| "businessRegCertValidityDate": "Business Reg Cert validity date", | |||
| "pleaseFillInBusinessRegCertValidityDate": "Please fill in Business Reg Cert validity date", | |||
| "pleaseFillInBusinessRegCertValidityDate": "Please fill in BR certificate validity date", | |||
| "formAddress": "Address", | |||
| "addressLine1": "First line of address", | |||
| "addressLine2": "Second line of address", | |||
| @@ -604,6 +607,24 @@ | |||
| "nameEng": "Name (Eng)", | |||
| "nameChi": "Name (Chi)", | |||
| "expiryDate": "Expiry Date", | |||
| "brStatusValid": "Valid", | |||
| "brStatusInvalid": "Invalid", | |||
| "submitBrCertificate": "Submit Business Registration Certificate (BR)", | |||
| "goToSubmitBr": "Go to submit BR", | |||
| "brPopupTitleExpiring": "Reminder", | |||
| "brPopupTitleExpired": "Action Required", | |||
| "brPopupTitlePendingVerify": "Notice", | |||
| "brSubmitSuccessNotExpired": "Thank you for your BR's submission. Please wait for official approval, you may submit new public notice application before the expiry date.", | |||
| "brSubmitSuccessExpired": "Thank you for your BR's submission. Please wait for official approval before making any new application submissions.", | |||
| "selectCertificateFile": "Select Certificate File", | |||
| "newBrExpiryDate": "New BR Expiry Date", | |||
| "newBr": "New BR", | |||
| "newBrInformation": "New BR Information", | |||
| "confirmNewBr": "Confirm New BR", | |||
| "confirmNewBrMessage": "Confirm the new BR information and overwrite the existing organisation details?", | |||
| "brExpiredMsg": "Your company’s business registration has expired. Please upload a valid BR certificate.", | |||
| "brPendingVerifyMsg": "Your company has already submitted the BR certificate for verification.", | |||
| "uploadedFiles": "Uploaded Files", | |||
| "create": "Create", | |||
| "confirmTo": "Confirm to ", | |||
| @@ -646,6 +667,8 @@ | |||
| "connectionError": "Connection error. Please try again.", | |||
| "downloadFailed": "Download failed. Please try again.", | |||
| "systemSettingName": "Name", | |||
| "muiClear": "Clear", | |||
| "muiClose": "Close", | |||
| "muiOpen": "Open", | |||
| @@ -44,6 +44,8 @@ | |||
| "requireFile": "请选择上传档案", | |||
| "requireValidFile": "请上传有效档案", | |||
| "requireValidFileWithPdfFormat": "请上传有效档案 (档案格式: .pdf)", | |||
| "requireValidFileWithProofReplyFormat": "请上传有效档案 (档案格式: .pdf, .jpg, .jpeg, .png)", | |||
| "acceptProofReplyFileFormats": "接受档案格式:pdf、jpg、jpeg 及 png。", | |||
| "requireValidFileWithFormat": "请上传有效档案 (档案格式: .doc, .docx, .xls, .xlsx)", | |||
| "submissionSuccess": "申请提交成功", | |||
| "noMoreThen40Words": "不得超过 40 个字符", | |||
| @@ -100,7 +102,7 @@ | |||
| "MSG.registerIAmSmart": "你可点击「智方便」按钮,系统会自动输入个人资料,或自行输入个人资料,以即时启动 公共启事提交及缴费系统 帐户。<br/>如欲使用「智方便」提供个人资料,请先下载「智方便」流动应用程式并登记成为「智方便」用户。", | |||
| "MSG.registerPersonal": "需上载身份证明文件数码档案以进行网上申请。 <br/>如:香港身份证; 护照; 中国内地身份证; 专业执业证书等", | |||
| "MSG.registerOrg": "需上载以下任何一份证明文件以进行网上申请。 <br/>如:商业登记证;专业执业证书", | |||
| "MSG.registerOrg": "需上载商业登记证以进行网上申请。", | |||
| "MSG.paymentMsg": "你的申请和付款已收到", | |||
| "MSG.expiredApp": "公共启事申请已过期", | |||
| @@ -327,12 +329,13 @@ | |||
| "sameAsBusinessRegistrationCert": "与商业登记证相同", | |||
| "businessRegCert": "商业登记证", | |||
| "businessRegCertNumber": "香港商业登记证号码", | |||
| "businessRegCertAndDoc":"商业登记证及其他文件", | |||
| "businessRegCertAndDoc":"商业登记证", | |||
| "businessRegCertExpiryDate": "商业登记证有效期届满日期", | |||
| "pleaseUploadDoc": "请上传你的 有效商业登记证及其他文件 的数码档案,以验证你的身份。", | |||
| "uploadFile": "上传商业登记证及其他文件", | |||
| "pleaseUploadDoc": "请上传你的 有效商业登记证 的数码档案,以验证你的身份。", | |||
| "uploadFile": "上传商业登记证", | |||
| "pleaseUploadIdDoc": "请上传你的 有效身份证明文件 的数码档案,以验证你的身份。", | |||
| "pleaseUploadIdDocSubTitle": "如: 香港身份证; 护照; 中国内地身份证; 专业执业证书等", | |||
| "acceptRegisterUploadFileFormats": "接受档案格式:PDF、JPG、JPEG、PNG 及 BMP。最多可上传 5 个档案。", | |||
| "uploadIdDoc": "上传身份证明文件", | |||
| "fileName": "档案名称", | |||
| "forOrgUser": "机构/公司用户", | |||
| @@ -600,6 +603,24 @@ | |||
| "nameEng": "名称 (英文)", | |||
| "nameChi": "名称 (中文)", | |||
| "expiryDate": "屆滿日期", | |||
| "brStatusValid": "有效", | |||
| "brStatusInvalid": "无效", | |||
| "submitBrCertificate": "提交商业登记证 (BR)", | |||
| "goToSubmitBr": "去提交证书", | |||
| "brPopupTitleExpiring": "温馨提示", | |||
| "brPopupTitleExpired": "系统提示", | |||
| "brPopupTitlePendingVerify": "系统提示", | |||
| "brSubmitSuccessNotExpired": "感谢您提交商业登记证,请等待官方审批通过,您仍可以在旧有商业登记证有效期限届满前提交新的公共启事申请。", | |||
| "brSubmitSuccessExpired": "感谢您提交商业登记证,请等待官方审批通过后,再提交新的公共启事申请。", | |||
| "selectCertificateFile": "选择证书档案", | |||
| "newBrExpiryDate": "新商业登记证届满日期", | |||
| "newBr": "新商业登记证", | |||
| "newBrInformation": "新商业登记证资料", | |||
| "confirmNewBr": "确认新商业登记证", | |||
| "confirmNewBrMessage": "确定以新的商业登记证资料覆盖现有机构资料?", | |||
| "brExpiredMsg": "贵司的商业登记证已过期。请上传有效的商业登记证。", | |||
| "brPendingVerifyMsg": "贵司已上传最新的商业登记证,请等待官方审批。", | |||
| "uploadedFiles": "已上传档案", | |||
| "create": "创建", | |||
| "confirmTo": "确定", | |||
| @@ -642,6 +663,8 @@ | |||
| "connectionError": "连接错误,请稍后再试。", | |||
| "downloadFailed": "下载失败,请稍后再试。", | |||
| "systemSettingName": "名称", | |||
| "muiClear": "清除", | |||
| "muiClose": "关闭", | |||
| "muiOpen": "打开", | |||
| @@ -44,6 +44,8 @@ | |||
| "requireFile": "請選擇上傳檔案", | |||
| "requireValidFile": "請上傳有效檔案", | |||
| "requireValidFileWithPdfFormat": "請上傳有效檔案 (檔案格式: .pdf)", | |||
| "requireValidFileWithProofReplyFormat": "請上傳有效檔案 (檔案格式: .pdf, .jpg, .jpeg, .png)", | |||
| "acceptProofReplyFileFormats": "接受檔案格式:pdf、jpg、jpeg 及 png。", | |||
| "requireValidFileWithFormat": "請上傳有效檔案 (檔案格式: .doc, .docx, .xls, .xlsx)", | |||
| "submissionSuccess": "申請提交成功", | |||
| "noMoreThen40Words": "不得超過 40 個字符", | |||
| @@ -100,7 +102,7 @@ | |||
| "MSG.registerIAmSmart": "你可點擊「智方便」按鈕,系統會自動輸入個人資料,或自行輸入個人資料,以即時啟動 公共啟事提交及繳費系統 帳戶。<br/>如欲使用「智方便」提供個人資料,請先下載「智方便」流動應用程式並登記成為「智方便」用戶。", | |||
| "MSG.registerPersonal": "需上載身份證明文件數碼檔案以進行網上申請。<br/>如:香港身份證; 護照; 中國內地身份證; 專業執業証書等", | |||
| "MSG.registerOrg": "需上載以下任何一份證明文件以進行網上申請。<br/>如:商業登記證;專業執業證書", | |||
| "MSG.registerOrg": "需上載商業登記證以進行網上申請。", | |||
| "MSG.paymentMsg": "你的申請和付款已收到", | |||
| "MSG.expiredApp": "公共啟事申請已過期", | |||
| @@ -322,13 +324,14 @@ | |||
| "sameAsBusinessRegistrationCert": "與商業登記證相同", | |||
| "businessRegCert": "商業登記證", | |||
| "businessRegCertNumber": "香港商業登記證號碼", | |||
| "businessRegCertAndDoc":"商業登記證及其他文件", | |||
| "businessRegCertAndDoc":"商業登記證", | |||
| "businessRegCertExpiryDate": "商業登記證有效期屆滿日期", | |||
| "pleaseUploadDoc": "請上傳你的 有效商業登記證及其他文件 的數碼檔案,以驗證你的身份。", | |||
| "pleaseUploadDoc": "請上傳你的 有效商業登記證 的數碼檔案,以驗證你的身份。", | |||
| "pleaseUploadIdDoc": "請上傳你的 有效身份證明文件 的數碼檔案,以驗證你的身份。", | |||
| "pleaseUploadIdDocSubTitle": "如: 香港身份證; 護照; 中國內地身份證; 專業執業証書等", | |||
| "acceptRegisterUploadFileFormats": "接受檔案格式:PDF、JPG、JPEG、PNG 及 BMP。最多可上載 5 個檔案。", | |||
| "uploadIdDoc": "上傳身份證明文件", | |||
| "uploadFile": "上傳商業登記證及其他文件", | |||
| "uploadFile": "上傳商業登記證", | |||
| "fileName": "檔案名稱", | |||
| "forOrgUser": "機構/公司用戶", | |||
| "forIndUser": "個人用戶", | |||
| @@ -601,6 +604,24 @@ | |||
| "nameEng": "名稱 (英文)", | |||
| "nameChi": "名稱 (中文)", | |||
| "expiryDate": "屆滿日期", | |||
| "brStatusValid": "有效", | |||
| "brStatusInvalid": "無效", | |||
| "submitBrCertificate": "提交商業登記證 (BR)", | |||
| "goToSubmitBr": "去提交證書", | |||
| "brPopupTitleExpiring": "溫馨提示", | |||
| "brPopupTitleExpired": "系統提示", | |||
| "brPopupTitlePendingVerify": "系統提示", | |||
| "brSubmitSuccessNotExpired": "感謝您提交商業登記證,請等待官方審批通過,您仍可以在舊有商業登記證有效期限屆滿前提交新的公共啟事申請。", | |||
| "brSubmitSuccessExpired": "感謝您提交商業登記證,請等待官方審批通過後,再提交新的公共啟事申請。", | |||
| "selectCertificateFile": "選擇證書檔案", | |||
| "newBrExpiryDate": "新商業登記證屆滿日期", | |||
| "newBr": "新商業登記證", | |||
| "newBrInformation": "新商業登記證資料", | |||
| "confirmNewBr": "確認新商業登記證", | |||
| "confirmNewBrMessage": "確定以新的商業登記證資料覆寫現有機構資料?", | |||
| "brExpiredMsg": "貴司的商業登記證已過期。請上傳有效的商業登記證。", | |||
| "brPendingVerifyMsg": "貴司已上傳最新的商業登記證,請等待官方審批。", | |||
| "uploadedFiles": "已上載檔案", | |||
| "create": "創建", | |||
| "confirmTo": "確定", | |||
| @@ -643,6 +664,8 @@ | |||
| "connectionError": "連線錯誤,請稍後再試。", | |||
| "downloadFailed": "下載失敗,請稍後再試。", | |||
| "systemSettingName": "名稱", | |||
| "muiClear": "清除", | |||
| "muiClose": "關閉", | |||
| "muiOpen": "開啟", | |||
| @@ -5,9 +5,11 @@ import { | |||
| // GET request | |||
| export const REFRESH_TOKEN = "/refresh-token" | |||
| export const LOGOUT = "/logout" | |||
| export const CHANGE_PASSWORD_PATH = "/user/change-password" | |||
| export const GET_SYS_PARAMS = apiPath+'/settings'; | |||
| export const GET_SYS_PARAM_NAMES = apiPath+'/settings/combo-name'; | |||
| export const PRIVACY_POLICY_PATH = apiPath+'/privacyPolicy'; | |||
| export const UPDATE_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/update-payment-suspension'; | |||
| export const GET_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/get-payment-suspension'; | |||
| @@ -28,11 +30,11 @@ export const GLD_USER_PROFILE_PATH = apiPath+'/user/gldProfile'; | |||
| export const GET_AUTH_LIST = '/user/auth/combo'; | |||
| export const GET_USER_COMBO_LIST = '/user/combo'; | |||
| 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_VERIFY = apiPath+'/user/verify'; | |||
| export const GET_IND_USER_VERIFY = apiPath+'/user/verify'; //POST | |||
| export const POST_IND_USER = apiPath+'/user/ind'; | |||
| 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_CHECK_CREDITOR = apiPath+'/org/check-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_SEND_OVERDUE_CREDITOR_LIST = apiPath+'/org/sendDn_OverdueCreditorList'; | |||
| export const GET_SEND_OVERDUE_CREDITOR_LIST = apiPath+'/org/sendDn_OverdueCreditorList'; //POST | |||
| //public | |||
| export const GET_PUB_ORG_PATH = apiPath+'/org/pub'; | |||
| export const POST_PUB_ORG_SAVE_PATH = apiPath+'/org/pub/save'; | |||
| export const POST_PUB_ORG_SUBMIT_BR = apiPath+'/org/pub/submit-br'; | |||
| export const POST_ORG_CONFIRM_BR = apiPath+'/org'; | |||
| export const GET_PUB_ORG_MARK_AS_CREDITOR = apiPath+'/org/pub/mark-as-creditor'; | |||
| export const GET_PUB_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/pub/mark-as-non-creditor'; | |||
| @@ -78,7 +82,7 @@ export const CHECK_OVERDUE = apiPath+'/application/check-overdue'; | |||
| export const FILE_UP_POST = apiPath+'/file/ul'; | |||
| export const FILE_DOWN_GET = apiPath+"/file/dl"; | |||
| 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 DR_EXPORT = apiPath+'/settings/dr/export'; | |||
| @@ -119,8 +123,8 @@ export const PATCH_CHANGE_PASSWORD = apiPath+'/user/change-password'; | |||
| //Public | |||
| 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_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 GET_ISSUE = apiPath+'/gazette-issue/list'; //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 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 PROOF_CHECK_PRICE = apiPath+'/proof/check-price';//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 | |||
| 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 GET_HOLIDAY_COMBO = apiPath+'/holiday/combo'; //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 | |||
| @@ -26,7 +26,7 @@ export const getDateField = ({ label, valueName, form, disabled }) => { | |||
| </Grid>; | |||
| } | |||
| export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => { | |||
| export const getTextField = ({ label, valueName, form, disabled, autoFocus, displayValue }) => { | |||
| return <Grid container alignItems={"center"} sx={{mb:2}}> | |||
| <Grid item xs={12} sm={12} md={12} lg={12}> | |||
| <Grid container spacing={1}> | |||
| @@ -40,7 +40,8 @@ export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => | |||
| valueName: valueName, | |||
| form: form, | |||
| disabled: disabled, | |||
| autoFocus:autoFocus | |||
| autoFocus:autoFocus, | |||
| displayValue: displayValue | |||
| })} | |||
| </Grid> | |||
| </Grid> | |||
| @@ -233,7 +234,7 @@ export const getProfileComboField = ({ label, dataList, valueName, form, disable | |||
| </Grid>; | |||
| } | |||
| export const initField = ({ type, valueName, form, disabled, autoFocus, multiline, handleChange, placeholder, inputProps, InputProps, width, ...props }) => { | |||
| export const initField = ({ type, valueName, form, disabled, autoFocus, multiline, handleChange, placeholder, inputProps, InputProps, width, displayValue, ...props }) => { | |||
| let err = Boolean(form.errors[valueName]); | |||
| return <TextField | |||
| @@ -248,7 +249,7 @@ export const initField = ({ type, valueName, form, disabled, autoFocus, multilin | |||
| error={err} | |||
| helperText={form.errors[valueName] ? form.errors[valueName] : ''} | |||
| onChange={handleChange ? handleChange : form.handleChange} | |||
| value={form.values[valueName]} | |||
| value={displayValue !== undefined ? displayValue : form.values[valueName]} | |||
| disabled={disabled} | |||
| autoFocus={autoFocus} | |||
| sx={{ | |||
| @@ -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'; | |||
| 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 | |||
| 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 }) => { | |||
| 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 }) => { | |||
| 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 }) => { | |||
| 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 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 }) => { | |||
| 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) => { | |||
| 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) => { | |||
| 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 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 */ | |||
| 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. | |||
| */ | |||
| 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: | |||
| "@types/jest" "*" | |||
| "@types/trusted-types@^2.0.2": | |||
| "@types/trusted-types@^2.0.2", "@types/trusted-types@^2.0.7": | |||
| version "2.0.7" | |||
| resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" | |||
| 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" | |||
| 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: | |||
| 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" | |||
| axobject-query@^4.1.0: | |||
| @@ -4680,6 +4681,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: | |||
| dependencies: | |||
| 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: | |||
| version "1.7.0" | |||
| 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" | |||
| 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: | |||
| version "0.3.5" | |||
| @@ -5623,16 +5631,16 @@ form-data@^3.0.0: | |||
| hasown "^2.0.2" | |||
| 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: | |||
| asynckit "^0.4.0" | |||
| combined-stream "^1.0.8" | |||
| 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: | |||
| 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" | |||
| 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: | |||
| version "1.1.2" | |||
| resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" | |||
| @@ -5964,10 +5977,10 @@ has-tostringtag@^1.0.2: | |||
| dependencies: | |||
| 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: | |||
| function-bind "^1.1.2" | |||
| @@ -6147,7 +6160,7 @@ http-proxy@^1.18.1: | |||
| follow-redirects "^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" | |||
| resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" | |||
| 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" | |||
| 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" | |||
| resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" | |||
| integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== | |||