| @@ -6,6 +6,7 @@ Adjust `report-uri` if your API base path or host differs (UAT example below; PR | |||
| See also: | |||
| - [permissions-policy-apache.conf.md](./permissions-policy-apache.conf.md) — Permissions-Policy reporting via the same `/api/csp-report` URL | |||
| - [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — latest PROD + UAT report review and add/remove summary | |||
| - [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — earlier PROD report analysis that drove `frame-src` / `media-src` updates | |||
| @@ -0,0 +1,152 @@ | |||
| # Permissions-Policy reporting (Apache) | |||
| The live `Permissions-Policy` header denies features with an empty allowlist | |||
| (`feature=()` — grant to nobody). That header does **not** send reports by | |||
| itself. | |||
| Unlike CSP, Permissions-Policy has **no `report-uri`**. Reporting uses the | |||
| Reporting API: a named endpoint in `Reporting-Endpoints`, plus a per-feature | |||
| `report-to` parameter. | |||
| Reuse the existing CSP collector: | |||
| - UAT: `https://pnspsuat.gld.gov.hk/api/csp-report` | |||
| - PROD: `https://pnsps.gld.gov.hk/api/csp-report` | |||
| Backend: `CspReportController` (`POST /csp-report`) already logs the raw JSON | |||
| and `Content-Type` and returns `204`. No backend change is required. | |||
| See also: [csp-apache.conf.md](./csp-apache.conf.md) | |||
| ## Why not `report-uri` | |||
| This CSP-style trailer does **not** produce Permissions-Policy reports: | |||
| ```apache | |||
| # Wrong — browsers ignore report-uri on Permissions-Policy | |||
| Header always set Permissions-Policy "camera=(), geolocation=(); report-uri https://pnspsuat.gld.gov.hk/api/csp-report" | |||
| ``` | |||
| `report-to` is a **parameter of each feature**, not a global trailing directive. | |||
| ## Does every feature need `report-to`? | |||
| Yes, **if you want a report for that feature**. | |||
| - `feature=()` — still blocked, but silent (no report) | |||
| - `feature=();report-to=csp-endpoint` — blocked **and** reported | |||
| There is no `all=()` reporter and no header-level `report-to` that applies to | |||
| every feature. Add `;report-to=csp-endpoint` only on features you want in the | |||
| log. Leave the rest as `()` if you do not need those reports. | |||
| ```apache | |||
| # camera is reported; geolocation is still blocked, but not reported | |||
| Header always set Permissions-Policy "camera=();report-to=csp-endpoint, geolocation=()" | |||
| ``` | |||
| ## Enforcing + reporting | |||
| Keep the current deny-all policy. Add `Reporting-Endpoints`, then append | |||
| `;report-to=csp-endpoint` on each feature you want to monitor. | |||
| Copy the feature list from the live header; only the `report-to` parameter is | |||
| new. Example (UAT): | |||
| ```apache | |||
| Header always set Reporting-Endpoints "csp-endpoint=\"https://pnspsuat.gld.gov.hk/api/csp-report\"" | |||
| Header always set Permissions-Policy "\ | |||
| accelerometer=();report-to=csp-endpoint, \ | |||
| autoplay=();report-to=csp-endpoint, \ | |||
| camera=();report-to=csp-endpoint, \ | |||
| display-capture=();report-to=csp-endpoint, \ | |||
| encrypted-media=();report-to=csp-endpoint, \ | |||
| fullscreen=();report-to=csp-endpoint, \ | |||
| geolocation=();report-to=csp-endpoint, \ | |||
| gyroscope=();report-to=csp-endpoint, \ | |||
| magnetometer=();report-to=csp-endpoint, \ | |||
| microphone=();report-to=csp-endpoint, \ | |||
| midi=();report-to=csp-endpoint, \ | |||
| payment=();report-to=csp-endpoint, \ | |||
| picture-in-picture=();report-to=csp-endpoint, \ | |||
| publickey-credentials-get=();report-to=csp-endpoint, \ | |||
| screen-wake-lock=();report-to=csp-endpoint, \ | |||
| usb=();report-to=csp-endpoint, \ | |||
| web-share=();report-to=csp-endpoint, \ | |||
| xr-spatial-tracking=();report-to=csp-endpoint" | |||
| ``` | |||
| PROD: same headers, with | |||
| `https://pnsps.gld.gov.hk/api/csp-report` | |||
| Because deny-all is already enforced, put `report-to` on **`Permissions-Policy`** | |||
| (not only Report-Only). Those reports have `disposition: "enforce"`. | |||
| ## Report-Only (observe without blocking) | |||
| Use `Permissions-Policy-Report-Only` only when testing a restriction that is | |||
| **not** already denied by the enforcing header. Reports have | |||
| `disposition: "report"`. | |||
| ```apache | |||
| Header always set Reporting-Endpoints "csp-endpoint=\"https://pnspsuat.gld.gov.hk/api/csp-report\"" | |||
| Header always set Permissions-Policy-Report-Only "geolocation=();report-to=csp-endpoint" | |||
| ``` | |||
| Report-Only cannot re-enable a feature already denied by `Permissions-Policy`. | |||
| ## Same URL, different payload | |||
| Keep CSP on `report-uri` as it is. The collector URL is shared; the body is not. | |||
| | Source | `Content-Type` | Body | | |||
| | --- | --- | --- | | |||
| | CSP `report-uri` | `application/csp-report` | `{ "csp-report": { … } }` | | |||
| | Permissions-Policy | `application/reports+json` | JSON **array**, `type` = `permissions-policy-violation` | | |||
| Example Permissions-Policy report: | |||
| ```json | |||
| [{ | |||
| "type": "permissions-policy-violation", | |||
| "url": "https://pnspsuat.gld.gov.hk/", | |||
| "body": { | |||
| "disposition": "enforce", | |||
| "featureId": "geolocation", | |||
| "message": "Permissions policy violation: geolocation access has been blocked because of a permissions policy applied to the current document." | |||
| } | |||
| }] | |||
| ``` | |||
| Chrome often serializes the feature as `policyId` instead of `featureId`. Filter | |||
| logs on `permissions-policy-violation` vs `csp-report` so the two streams stay | |||
| distinct. | |||
| Optional: also point CSP at the same named endpoint (CSP `report-uri` remains | |||
| for older browsers): | |||
| ```apache | |||
| Header always set Content-Security-Policy-Report-Only "…; report-uri https://pnspsuat.gld.gov.hk/api/csp-report; report-to csp-endpoint" | |||
| ``` | |||
| ## Follow-up checklist | |||
| - [ ] Confirm the live Apache `Permissions-Policy` feature list (deny-all `()`). | |||
| - [ ] Add `Reporting-Endpoints` → existing `/api/csp-report` (UAT vs PROD host). | |||
| - [ ] Add `;report-to=csp-endpoint` only on features you want in the log. | |||
| - [ ] Deploy to UAT first; trigger a blocked API (e.g. `navigator.geolocation`) in Chrome/Edge. | |||
| - [ ] Confirm a `permissions-policy-violation` line in the backend log (may be batched, a few seconds later). | |||
| - [ ] Repeat on PROD with the PROD report URL. | |||
| ## Notes | |||
| - Reporting is Chromium-only (Chrome / Edge). Safari and Firefox still enforce | |||
| `()` and usually send nothing. | |||
| - Reports are batched and may arrive a few seconds after the violation, not on | |||
| the same page request. | |||
| - Same-origin `/api/csp-report` needs no extra CORS setup. The endpoint is | |||
| already unauthenticated and CSRF is disabled. | |||
| - Do not allowlist a feature in `Permissions-Policy` just to silence a report. | |||
| Only grant a feature if the application itself needs it. | |||
| @@ -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, | |||
| @@ -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" }}> | |||
| @@ -227,7 +227,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 +237,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); | |||
| @@ -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"; | |||
| @@ -382,7 +383,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 +469,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 +537,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 +600,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 | |||
| } | |||
| @@ -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> | |||
| @@ -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 ||============================== // | |||
| @@ -580,7 +581,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} | |||
| @@ -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 > | |||
| @@ -7,6 +7,7 @@ import MainCard from "components/MainCard"; | |||
| import * as React from "react"; | |||
| import { FiDataGrid } from "components/FiDataGrid"; | |||
| import { GET_SYS_PARAMS } from "utils/ApiPathConst"; | |||
| import SafeHtml from 'components/SafeHtml'; | |||
| // ==============================|| DASHBOARD - DEFAULT ||============================== // | |||
| @@ -45,7 +46,7 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => { | |||
| flex: 1, | |||
| minWidth: 400, | |||
| renderCell:(params)=>{ | |||
| return <div dangerouslySetInnerHTML={{__html: params.value}} /> | |||
| return <SafeHtml html={params.value} /> | |||
| } | |||
| }, | |||
| ]; | |||
| @@ -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'))); | |||
| @@ -1618,7 +1619,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'))); | |||
| @@ -1946,7 +1947,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> | |||
| @@ -29,6 +29,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 ||============================== // | |||
| @@ -297,7 +298,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> | |||
| @@ -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} | |||
| @@ -5,6 +5,7 @@ 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'; | |||
| @@ -28,11 +29,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,10 +51,10 @@ 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'; | |||
| @@ -78,7 +79,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 +120,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 +165,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 +175,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 +256,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 | |||
| @@ -34,6 +34,14 @@ export const patch = ({ url, params, onSuccess, onFail, onError }) => { | |||
| }); | |||
| }; | |||
| export const del = ({ url, params, onSuccess, onFail, onError }) => { | |||
| axios.delete(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" | |||
| @@ -0,0 +1,78 @@ | |||
| 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== | |||