比较提交

...

34 次代码提交

作者 SHA1 备注 提交日期
  Jason Chuang 27e5282614 permission policy 1 天前
  Jason Chuang e4a654916a upgrade axios to 1.20.0 3 天前
  Jason Chuang cb053eab3e hide pay button after expiry date 3 天前
  Jason Chuang 2171c19d5b hide payment method when over limit 3 天前
  Jason Chuang a0545e8715 CADD 2026 #18 GET mapping that write data 1周前
  Jason Chuang f35c51b281 CADD 2026 #17 fix XSS via raw html 1周前
  Jason Chuang bd74b6b36b CADD 2026 #12 fix refresh token are not revocable 1周前
  Jason Chuang 84bee6182f CADD 2026 #1 forgot password 1周前
  Jason Chuang 6e7d104757 Merge branch 'CR023' of https://git.2fi-solutions.com/alex/PNSPS-frontend-MaterialUI into CR023 1周前
  Jason Chuang fe574f6fbf fix payment limit check 1周前
  Jason Chuang 6ef6bec05d support dev message 1周前
  Jason Chuang a03aede9b1 useless code 2 周前
  Jason Chuang a128b528a9 fix user group save 2 周前
  Jason Chuang ba188996ec application remove blank line 2 周前
  Jason Chuang 783abe980f GFMIS UI fine tune 3 周前
  Jason Chuang f6b4b06adf add favicon 3 周前
  Jason Chuang 167cf5a3a4 applied auto row height 3 周前
  Jason Chuang d2509e5ff4 pay button protection 3 周前
  Jason Chuang effab06b27 Revert "CR023 - update user search" 3 周前
  Jason Chuang c91b818abe Merge branch 'master' into CR023 3 周前
  Jason Chuang 13c681e226 CR023 - online payment search 1 个月前
  Jason Chuang b6091ac9d2 CR023 - show record count in application and proof search with gazette issue 1 个月前
  Jason Chuang d1e78cda0c fix search doesnt have loading for paging 1 个月前
  Jason Chuang fdd595c5ce CR023 - search proof support no paging. 1 个月前
  Jason Chuang e5fb950f46 CR023 - update user search 1 个月前
  Jason Chuang 6d73b74803 CR023 - update gazette issue import 1 个月前
  Jason Chuang c033d4faf4 update label 1 个月前
  Jason Chuang e201609414 update gazette issue import 1 个月前
  Jason Chuang ed56964c3f CR023 - update holiday import 1 个月前
  Jason Chuang 65657708fb CR023 - proof search server-side sorting 1 个月前
  Jason Chuang c4e7d92cf5 CR023 - application search with issues, no paging 1 个月前
  Jason Chuang 741d0093dd CR023 - application search criteria control 1 个月前
  Jason Chuang 7a77ec00a7 CR023 - application server-side sorting 1 个月前
  Jason Chuang fbb8a01ae0 CR023 - fix group title search from application and proof 1 个月前
共有 79 个文件被更改,包括 1982 次插入721 次删除
  1. +1
    -0
      docs/csp-apache.conf.md
  2. +152
    -0
      docs/permissions-policy-apache.conf.md
  3. +30
    -17
      package-lock.json
  4. +2
    -1
      package.json
  5. 二进制
      public/favicon.ico
  6. +7
    -2
      src/auth/index.js
  7. +106
    -23
      src/components/FiDataGrid.js
  8. +2
    -7
      src/components/FileList.js
  9. +17
    -0
      src/components/SafeHtml.js
  10. +20
    -4
      src/pages/GFMIS/DataGrid.js
  11. +26
    -2
      src/pages/GFMIS/TransactionDataGrid.js
  12. +1
    -13
      src/pages/GazetteIssue/DataGrid.js
  13. +3
    -26
      src/pages/GazetteIssue/ExportForm.js
  14. +64
    -38
      src/pages/GazetteIssue/SearchForm.js
  15. +56
    -6
      src/pages/GazetteIssue/index.js
  16. +62
    -36
      src/pages/Holiday/SearchForm.js
  17. +68
    -15
      src/pages/Holiday/index.js
  18. +2
    -1
      src/pages/Message/Details/index.js
  19. +2
    -2
      src/pages/Organization/DetailPage/OrganizationCard.js
  20. +60
    -22
      src/pages/Payment/MultiPaymentWindow.js
  21. +1
    -1
      src/pages/Payment/Search_GLD/DataGrid.js
  22. +117
    -30
      src/pages/Payment/Search_GLD/SearchForm.js
  23. +1
    -1
      src/pages/Payment/Search_Public/DataGrid.js
  24. +116
    -32
      src/pages/Payment/Search_Public/SearchForm.js
  25. +90
    -23
      src/pages/Payment/index.js
  26. +1
    -1
      src/pages/Proof/Create_FromApp/ProofForm.js
  27. +5
    -6
      src/pages/Proof/Payment/Pay.js
  28. +9
    -16
      src/pages/Proof/Payment/Pay_DN.js
  29. +13
    -24
      src/pages/Proof/Payment/Pay_Office.js
  30. +9
    -14
      src/pages/Proof/Payment/Pay_Online.js
  31. +1
    -1
      src/pages/Proof/Reply_GLD/ApplicationDetails.js
  32. +6
    -5
      src/pages/Proof/Reply_Public/ProofForm.js
  33. +12
    -28
      src/pages/Proof/Search_GLD/DataGrid.js
  34. +137
    -46
      src/pages/Proof/Search_GLD/SearchForm.js
  35. +11
    -2
      src/pages/Proof/Search_Public/DataGrid.js
  36. +125
    -32
      src/pages/Proof/Search_Public/SearchForm.js
  37. +15
    -15
      src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js
  38. +6
    -6
      src/pages/PublicNotice/Details_GLD/index.js
  39. +11
    -2
      src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js
  40. +1
    -1
      src/pages/PublicNotice/Details_Public/index.js
  41. +2
    -1
      src/pages/PublicNotice/ListPanel/PendingPaymentTab.js
  42. +122
    -30
      src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js
  43. +6
    -26
      src/pages/PublicNotice/ListPanel/SearchPublicNoticeTable.js
  44. +16
    -9
      src/pages/PublicNotice/Search_GLD/DataGrid.js
  45. +140
    -34
      src/pages/PublicNotice/Search_GLD/SearchForm.js
  46. +2
    -1
      src/pages/Setting/SystemSetting/Table.js
  47. +3
    -3
      src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js
  48. +3
    -3
      src/pages/User/DetailsPage_Organization/UserInformationCard_Organization.js
  49. +2
    -2
      src/pages/User/ManagePage_OrgPublic/index.js
  50. +2
    -2
      src/pages/User/SearchPage/UserTable.js
  51. +1
    -1
      src/pages/_Test/index.js
  52. +18
    -21
      src/pages/authentication/AuthWrapper.js
  53. +2
    -1
      src/pages/authentication/ForgotPassword/AuthCallback/index.js
  54. +2
    -1
      src/pages/authentication/ForgotUsername/AuthCallback/index.js
  55. +4
    -3
      src/pages/authentication/RegisterCustom.js
  56. +2
    -1
      src/pages/authentication/auth-forms/BusCustomFormWizard.js
  57. +2
    -1
      src/pages/authentication/auth-forms/CustomFormWizard.js
  58. +2
    -1
      src/pages/authentication/auth-forms/IAmSmartFormWizard.js
  59. +2
    -1
      src/pages/dashboard/Public/index.js
  60. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_cn.js
  61. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_en.js
  62. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_zh.js
  63. +2
    -1
      src/pages/extra-pages/ImportantNoticePage/index.js
  64. +2
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js
  65. +3
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js
  66. +3
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js
  67. +2
    -1
      src/pages/extra-pages/PrivacyPolicyPage/index.js
  68. +2
    -1
      src/pages/iAmSmart/PleaseLoginCallback/index.js
  69. +4
    -2
      src/pages/iAmSmart/SuccessCallback/index.js
  70. +15
    -7
      src/pages/pnspsUserGroupDetailPage/GroupAuthCard.js
  71. +4
    -2
      src/pages/pnspsUserGroupDetailPage/GroupAuthTable.js
  72. +3
    -4
      src/pages/pnspsUserGroupDetailPage/UserAddCard.js
  73. +46
    -20
      src/pages/pnspsUserGroupDetailPage/index.js
  74. +1
    -1
      src/pages/pnspsUserGroupSearchPage/UserGroupTable.js
  75. +13
    -10
      src/utils/ApiPathConst.js
  76. +8
    -0
      src/utils/HttpUtils.js
  77. +78
    -0
      src/utils/sanitizeHtml.js
  78. +53
    -0
      src/utils/sanitizeHtml.test.js
  79. +36
    -23
      yarn.lock

+ 1
- 0
docs/csp-apache.conf.md 查看文件

@@ -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



+ 152
- 0
docs/permissions-policy-apache.conf.md 查看文件

@@ -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.

+ 30
- 17
package-lock.json 查看文件

@@ -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"
},


+ 2
- 1
package.json 查看文件

@@ -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",


二进制
public/favicon.ico 查看文件

之前 之后

+ 7
- 2
src/auth/index.js 查看文件

@@ -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,


+ 106
- 23
src/components/FiDataGrid.js 查看文件

@@ -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}
/>
),
);
},
}
: {}),
}}


+ 2
- 7
src/components/FileList.js 查看文件

@@ -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();
}


+ 17
- 0
src/components/SafeHtml.js 查看文件

@@ -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;

+ 20
- 4
src/pages/GFMIS/DataGrid.js 查看文件

@@ -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}


+ 26
- 2
src/pages/GFMIS/TransactionDataGrid.js 查看文件

@@ -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}


+ 1
- 13
src/pages/GazetteIssue/DataGrid.js 查看文件

@@ -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}


+ 3
- 26
src/pages/GazetteIssue/ExportForm.js 查看文件

@@ -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


+ 64
- 38
src/pages/GazetteIssue/SearchForm.js 查看文件

@@ -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>
);
};


+ 56
- 6
src/pages/GazetteIssue/index.js 查看文件

@@ -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)}>


+ 62
- 36
src/pages/Holiday/SearchForm.js 查看文件

@@ -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>
);
};


+ 68
- 15
src/pages/Holiday/index.js 查看文件

@@ -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>


+ 2
- 1
src/pages/Message/Details/index.js 查看文件

@@ -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" }}>


+ 2
- 2
src/pages/Organization/DetailPage/OrganizationCard.js 查看文件

@@ -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);


+ 60
- 22
src/pages/Payment/MultiPaymentWindow.js 查看文件

@@ -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>


+ 1
- 1
src/pages/Payment/Search_GLD/DataGrid.js 查看文件

@@ -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}


+ 117
- 30
src/pages/Payment/Search_GLD/SearchForm.js 查看文件

@@ -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 >


+ 1
- 1
src/pages/Payment/Search_Public/DataGrid.js 查看文件

@@ -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}


+ 116
- 32
src/pages/Payment/Search_Public/SearchForm.js 查看文件

@@ -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 >


+ 90
- 23
src/pages/Payment/index.js 查看文件

@@ -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}


+ 1
- 1
src/pages/Proof/Create_FromApp/ProofForm.js 查看文件

@@ -307,7 +307,7 @@ const FormPanel = ({ formData }) => {
}
}}
>
<Typography variant="h5">Upload Files</Typography>
<Typography variant="h5">Upload File</Typography>
</Button>
<input
id="uploadFileBtn"


+ 5
- 6
src/pages/Proof/Payment/Pay.js 查看文件

@@ -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>


+ 9
- 16
src/pages/Proof/Payment/Pay_DN.js 查看文件

@@ -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>


+ 13
- 24
src/pages/Proof/Payment/Pay_Office.js 查看文件

@@ -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>


+ 9
- 14
src/pages/Proof/Payment/Pay_Online.js 查看文件

@@ -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>


+ 1
- 1
src/pages/Proof/Reply_GLD/ApplicationDetails.js 查看文件

@@ -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;


+ 6
- 5
src/pages/Proof/Reply_Public/ProofForm.js 查看文件

@@ -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
}


+ 12
- 28
src/pages/Proof/Search_GLD/DataGrid.js 查看文件

@@ -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,


+ 137
- 46
src/pages/Proof/Search_GLD/SearchForm.js 查看文件

@@ -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 >


+ 11
- 2
src/pages/Proof/Search_Public/DataGrid.js 查看文件

@@ -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>
);
}
}

+ 125
- 32
src/pages/Proof/Search_Public/SearchForm.js 查看文件

@@ -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 >


+ 15
- 15
src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js 查看文件

@@ -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>



+ 6
- 6
src/pages/PublicNotice/Details_GLD/index.js 查看文件

@@ -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);


+ 11
- 2
src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js 查看文件

@@ -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>


+ 1
- 1
src/pages/PublicNotice/Details_Public/index.js 查看文件

@@ -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("取消成功!")


+ 2
- 1
src/pages/PublicNotice/ListPanel/PendingPaymentTab.js 查看文件

@@ -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>


+ 122
- 30
src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js 查看文件

@@ -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 >


+ 6
- 26
src/pages/PublicNotice/ListPanel/SearchPublicNoticeTable.js 查看文件

@@ -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}


+ 16
- 9
src/pages/PublicNotice/Search_GLD/DataGrid.js 查看文件

@@ -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);
}}


+ 140
- 34
src/pages/PublicNotice/Search_GLD/SearchForm.js 查看文件

@@ -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 >


+ 2
- 1
src/pages/Setting/SystemSetting/Table.js 查看文件

@@ -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} />
}
},
];


+ 3
- 3
src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js 查看文件

@@ -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()


+ 3
- 3
src/pages/User/DetailsPage_Organization/UserInformationCard_Organization.js 查看文件

@@ -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()


+ 2
- 2
src/pages/User/ManagePage_OrgPublic/index.js 查看文件

@@ -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 () {


+ 2
- 2
src/pages/User/SearchPage/UserTable.js 查看文件

@@ -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)


+ 1
- 1
src/pages/_Test/index.js 查看文件

@@ -61,7 +61,7 @@ const Mail = () => {
});

const setReminderDate=()=>{
HttpUtils.get({
HttpUtils.post({
url: apiPath + "/demandNote/set-expect-reminder",
onSuccess: function () {
setResponsText("Success");


+ 18
- 21
src/pages/authentication/AuthWrapper.js 查看文件

@@ -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>
) : (
''
)}


+ 2
- 1
src/pages/authentication/ForgotPassword/AuthCallback/index.js 查看文件

@@ -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


+ 2
- 1
src/pages/authentication/ForgotUsername/AuthCallback/index.js 查看文件

@@ -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


+ 4
- 3
src/pages/authentication/RegisterCustom.js 查看文件

@@ -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>


+ 2
- 1
src/pages/authentication/auth-forms/BusCustomFormWizard.js 查看文件

@@ -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>


+ 2
- 1
src/pages/authentication/auth-forms/CustomFormWizard.js 查看文件

@@ -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>


+ 2
- 1
src/pages/authentication/auth-forms/IAmSmartFormWizard.js 查看文件

@@ -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>


+ 2
- 1
src/pages/dashboard/Public/index.js 查看文件

@@ -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>


+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_cn.js 查看文件

@@ -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 }} />;

}


+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_en.js 查看文件

@@ -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 }} />;

}


+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_zh.js 查看文件

@@ -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 }} />;

}


+ 2
- 1
src/pages/extra-pages/ImportantNoticePage/index.js 查看文件

@@ -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>
);


+ 2
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js 查看文件

@@ -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 }} />;

}


+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js 查看文件

@@ -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;

+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js 查看文件

@@ -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;

+ 2
- 1
src/pages/extra-pages/PrivacyPolicyPage/index.js 查看文件

@@ -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>
);


+ 2
- 1
src/pages/iAmSmart/PleaseLoginCallback/index.js 查看文件

@@ -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>


+ 4
- 2
src/pages/iAmSmart/SuccessCallback/index.js 查看文件

@@ -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
- 7
src/pages/pnspsUserGroupDetailPage/GroupAuthCard.js 查看文件

@@ -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 (


+ 4
- 2
src/pages/pnspsUserGroupDetailPage/GroupAuthTable.js 查看文件

@@ -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


+ 3
- 4
src/pages/pnspsUserGroupDetailPage/UserAddCard.js 查看文件

@@ -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 (


+ 46
- 20
src/pages/pnspsUserGroupDetailPage/index.js 查看文件

@@ -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;


+ 1
- 1
src/pages/pnspsUserGroupSearchPage/UserGroupTable.js 查看文件

@@ -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}


+ 13
- 10
src/utils/ApiPathConst.js 查看文件

@@ -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

+ 8
- 0
src/utils/HttpUtils.js 查看文件

@@ -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"


+ 78
- 0
src/utils/sanitizeHtml.js 查看文件

@@ -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
});
}

+ 53
- 0
src/utils/sanitizeHtml.test.js 查看文件

@@ -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)');
});
});

+ 36
- 23
yarn.lock 查看文件

@@ -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==


正在加载...
取消
保存