Сравнить коммиты

...

34 коммитов

Автор SHA1 Сообщение Дата
  Jason Chuang 27e5282614 permission policy 1 день назад
  Jason Chuang e4a654916a upgrade axios to 1.20.0 2 дней назад
  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: 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-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 - [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", "@testing-library/user-event": "^14.4.3",
"@types/react-input-mask": "^3.0.2", "@types/react-input-mask": "^3.0.2",
"apexcharts": "^3.35.5", "apexcharts": "^3.35.5",
"axios": "^1.12.2",
"axios": "^1.20.0",
"date-fns": "^3.0.6", "date-fns": "^3.0.6",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"dompurify": "^3.4.15",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^7.3.6", "framer-motion": "^7.3.6",
"history": "^5.3.0", "history": "^5.3.0",
@@ -6548,13 +6549,14 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
"integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"follow-redirects": "^1.16.0",
"form-data": "^4.0.6",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0" "proxy-from-env": "^2.1.0"
} }
}, },
@@ -8419,6 +8421,15 @@
} }
] ]
}, },
"node_modules/dompurify": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz",
"integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
@@ -9916,15 +9927,16 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh" "url": "https://github.com/sponsors/RubenVerborgh"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
}, },
@@ -10090,16 +10102,16 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -10598,9 +10610,10 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },


+ 2
- 1
package.json Просмотреть файл

@@ -25,9 +25,10 @@
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/react-input-mask": "^3.0.2", "@types/react-input-mask": "^3.0.2",
"apexcharts": "^3.35.5", "apexcharts": "^3.35.5",
"axios": "^1.12.2",
"axios": "^1.20.0",
"date-fns": "^3.0.6", "date-fns": "^3.0.6",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"dompurify": "^3.4.15",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^7.3.6", "framer-motion": "^7.3.6",
"history": "^5.3.0", "history": "^5.3.0",


Двоичные данные
public/favicon.ico Просмотреть файл

До После

+ 7
- 2
src/auth/index.js Просмотреть файл

@@ -12,7 +12,7 @@ export const predictUsageCount = 'predictUsageCount'
export const windowCount = 'windowCount' export const windowCount = 'windowCount'
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import {useDispatch} from "react-redux"; import {useDispatch} from "react-redux";
import { REFRESH_TOKEN } from 'utils/ApiPathConst';
import { LOGOUT, REFRESH_TOKEN } from 'utils/ApiPathConst';
import { getMessage } from 'utils/getI18nMessage'; import { getMessage } from 'utils/getI18nMessage';


// Guard so we only register interceptors once (ThemeRoutes re-renders add duplicate handlers otherwise) // Guard so we only register interceptors once (ThemeRoutes re-renders add duplicate handlers otherwise)
@@ -22,7 +22,7 @@ let expiredAlertShownInMemory = false;


/** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */ /** Login / public auth endpoints must not trigger token refresh or session-expiry reload. */
const isPublicAuthRequest = (reqUrl) => const isPublicAuthRequest = (reqUrl) =>
reqUrl.includes('/login');
reqUrl.includes('/login') || reqUrl.includes('/refresh-token') || reqUrl.includes('/logout');


/** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */ /** Clear stale session-expiry flag so a new login attempt can show its own error dialog. */
export const clearExpiredSessionAlert = () => { export const clearExpiredSessionAlert = () => {
@@ -96,6 +96,11 @@ export const isTokenValid = () =>{
// ** Handle User Logout // ** Handle User Logout
export const handleLogoutFunction = () => { export const handleLogoutFunction = () => {
return dispatch => { return dispatch => {
const refreshToken = localStorage.getItem('refreshToken')
if (refreshToken) {
axios.post(`${apiPath}${LOGOUT}`, { refreshToken }).catch(() => {})
}

dispatch({ dispatch({
type: 'LOGOUT', type: 'LOGOUT',
accessToken: null, accessToken: null,


+ 106
- 23
src/components/FiDataGrid.js Просмотреть файл

@@ -9,12 +9,19 @@ import { FormattedMessage, useIntl } from "react-intl";
import { TablePagination, Typography } from '@mui/material'; import { TablePagination, Typography } from '@mui/material';
import { getSearchCriteria, checkSearchCriteriaPath } from "auth/utils"; import { getSearchCriteria, checkSearchCriteriaPath } from "auth/utils";


/** When Gazette Issue No. is selected, load all matching rows (no start/limit). */
function hasGazetteIssueFilter(params) {
const issueId = params?.issueId;
return issueId != null && issueId !== "";
}

// ==============================|| EVENT TABLE ||============================== // // ==============================|| EVENT TABLE ||============================== //


export function FiDataGrid({ rows, columns, sx, autoHeight = true, export function FiDataGrid({ rows, columns, sx, autoHeight = true,
hideFooterSelectedRowCount, rowModesModel, editMode, hideFooterSelectedRowCount, rowModesModel, editMode,
pageSizeOptions, filterItems, customPageSize, doLoad, applyGridOnReady, applySearch, pageSizeOptions, filterItems, customPageSize, doLoad, applyGridOnReady, applySearch,
tab, height, maxHeight, pagination = true, ...props }) {
tab, height, maxHeight, pagination = true, serverSorting = false,
disablePagingOnGazetteIssue = true, hideRowsPerPage = false, ...props }) {
const intl = useIntl(); const intl = useIntl();
const [_rows, set_rows] = useState([]); const [_rows, set_rows] = useState([]);
const [_doLoad, set_doLoad] = useState({}); const [_doLoad, set_doLoad] = useState({});
@@ -27,6 +34,7 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,


const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [sortModel, setSortModel] = useState([]);
// const [_autoHeight, set_autoHeight] = useState(true); // const [_autoHeight, set_autoHeight] = useState(true);
const [myHideFooterSelectedRowCount, setMyHideFooterSelectedRowCount] = useState(true); const [myHideFooterSelectedRowCount, setMyHideFooterSelectedRowCount] = useState(true);
const [_sx, set_sx] = useState({ const [_sx, set_sx] = useState({
@@ -65,7 +73,9 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,


useEffect(() => { useEffect(() => {
if (doLoad !== undefined && Object.keys(doLoad).length>0 ){ if (doLoad !== undefined && Object.keys(doLoad).length>0 ){
if(applySearch!=undefined){
if (disablePagingOnGazetteIssue && hasGazetteIssueFilter(doLoad.params)) {
setPage(0);
} else if(applySearch!=undefined){
if (Object.keys(getSearchCriteria(window.location.pathname)).length>0){ if (Object.keys(getSearchCriteria(window.location.pathname)).length>0){
const localStorageSearchCriteria = getSearchCriteria(window.location.pathname) const localStorageSearchCriteria = getSearchCriteria(window.location.pathname)
// console.log(localStorageSearchCriteria) // console.log(localStorageSearchCriteria)
@@ -78,14 +88,58 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,
setPage(0); setPage(0);
setPageSize(parseInt(event.target.value, 10)); setPageSize(parseInt(event.target.value, 10));
} }
if (serverSorting) {
if (doLoad.params?.sort && doLoad.params?.direction) {
setSortModel([{
field: doLoad.params.sort,
sort: String(doLoad.params.direction).toLowerCase()
}]);
} else {
setSortModel([]);
}
}
set_doLoad(doLoad); set_doLoad(doLoad);
setLoading(true) setLoading(true)
} }
}, [doLoad]); }, [doLoad]);


const handleSortModelChange = (newModel) => {
if (!serverSorting) return;
setSortModel(newModel);
setPage(0);

const params = { ...(_doLoad.params || {}) };
if (newModel?.length > 0 && newModel[0].field && newModel[0].sort) {
params.sort = newModel[0].field;
params.direction = newModel[0].sort;
} else {
delete params.sort;
delete params.direction;
}
if (!pagination || (disablePagingOnGazetteIssue && hasGazetteIssueFilter(params))) {
delete params.start;
delete params.limit;
setPage(0);
} else {
params.start = 0;
params.limit = pageSize;
}

if (applySearch != undefined) {
applySearch(params);
} else {
set_doLoad({ ..._doLoad, params });
setLoading(true);
}
};

const loadAllOnGazetteIssue = disablePagingOnGazetteIssue && hasGazetteIssueFilter(_doLoad?.params);
const ignorePagingParams = !pagination || loadAllOnGazetteIssue;
const showPaginationBar = pagination;

useEffect(() => { useEffect(() => {
getDataList(); getDataList();
}, [_doLoad, page]);
}, [_doLoad, page, pageSize]);




useEffect(() => { useEffect(() => {
@@ -166,8 +220,13 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,
if (_doLoad.params == undefined) return; if (_doLoad.params == undefined) return;
if (_doLoad.params.searchCriteria !== undefined) return; if (_doLoad.params.searchCriteria !== undefined) return;
if (_doLoad.params == null) _doLoad.params = {}; if (_doLoad.params == null) _doLoad.params = {};
_doLoad.params.start = page * pageSize;
_doLoad.params.limit = pageSize;
if (ignorePagingParams) {
delete _doLoad.params.start;
delete _doLoad.params.limit;
} else {
_doLoad.params.start = page * pageSize;
_doLoad.params.limit = pageSize;
}
if(checkSearchCriteriaPath(window.location.pathname)){ if(checkSearchCriteriaPath(window.location.pathname)){
if(window.location.pathname === "/publicNotice"){ if(window.location.pathname === "/publicNotice"){
if (tab != undefined && tab ==="application"){ if (tab != undefined && tab ==="application"){
@@ -177,7 +236,12 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,
localStorage.setItem('searchCriteria', JSON.stringify({path:window.location.pathname,data:_doLoad.params})) localStorage.setItem('searchCriteria', JSON.stringify({path:window.location.pathname,data:_doLoad.params}))
} }
} }

setLoading(true);
if (applyGridOnReady !== undefined) {
applyGridOnReady(true);
}

HttpUtils.get({ HttpUtils.get({
url: _doLoad.url, url: _doLoad.url,
params: _doLoad.params, params: _doLoad.params,
@@ -254,21 +318,28 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,
disableColumnMenu disableColumnMenu
shrinkWrap shrinkWrap
rowModesModel={_rowModesModel} rowModesModel={_rowModesModel}
pageSizeOptions={pagination ? _pageSizeOptions : []}
pageSizeOptions={showPaginationBar ? _pageSizeOptions : []}
editMode={_editMode} editMode={_editMode}
autoHeight={effectiveAutoHeight} autoHeight={effectiveAutoHeight}
hideFooterSelectedRowCount={myHideFooterSelectedRowCount} hideFooterSelectedRowCount={myHideFooterSelectedRowCount}
filterModel={{ items: _filterItems }} filterModel={{ items: _filterItems }}
loading={loading} loading={loading}
paginationMode={pagination ? "server" : undefined}
paginationMode={showPaginationBar ? "server" : undefined}
sortingMode={serverSorting ? "server" : undefined}
sortModel={serverSorting ? sortModel : undefined}
onSortModelChange={serverSorting ? handleSortModelChange : undefined}
sx={{ sx={{
..._sx, ..._sx,
'& .MuiDataGrid-virtualScroller': { '& .MuiDataGrid-virtualScroller': {
overflowY: height || maxHeight ? 'auto' : 'visible', overflowY: height || maxHeight ? 'auto' : 'visible',
overflowX: height || maxHeight ? 'auto' : 'visible', overflowX: height || maxHeight ? 'auto' : 'visible',
...(loading && { filter: 'blur(2px)' }),
},
'& .MuiDataGrid-overlay': {
backgroundColor: 'rgba(255, 255, 255, 0.55)',
}, },
// 👇 completely hide the footer when pagination is off
...(pagination === false && {
// Hide the footer only when pagination is disabled for the grid
...(!showPaginationBar && {
'& .MuiDataGrid-footerContainer': { '& .MuiDataGrid-footerContainer': {
display: 'none', display: 'none',
}, },
@@ -276,19 +347,30 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,
}} }}
components={{ components={{
NoRowsOverlay: CustomNoRowsOverlay, NoRowsOverlay: CustomNoRowsOverlay,
...(pagination
...(showPaginationBar
? { ? {
Pagination: () => (
Pagination: () => {
const total = rowCount || 0;
const pagerPage = loadAllOnGazetteIssue ? 0 : page;
const pagerPageSize = loadAllOnGazetteIssue ? Math.max(total, 1) : pageSize;
return (
<TablePagination <TablePagination
component="div" component="div"
count={rowCount || 0}
page={page}
rowsPerPage={pageSize}
rowsPerPageOptions={_pageSizeOptions}
labelDisplayedRows={() =>
`${(_rows?.length ? page * pageSize + 1 : 0)}-${page * pageSize + (_rows?.length ?? 0)} ${intl.formatMessage({ id: "of" })} ${rowCount}`
}
labelRowsPerPage={intl.formatMessage({ id: "rowsPerPage" }) + ":"}
count={total}
page={pagerPage}
rowsPerPage={pagerPageSize}
rowsPerPageOptions={(loadAllOnGazetteIssue || hideRowsPerPage) ? [] : _pageSizeOptions}
labelDisplayedRows={() => {
if (!_rows?.length || total === 0) {
return `0-0 ${intl.formatMessage({ id: "of" })} ${total}`;
}
const from = loadAllOnGazetteIssue ? 1 : page * pageSize + 1;
const to = loadAllOnGazetteIssue
? _rows.length
: Math.min(page * pageSize + _rows.length, total);
return `${from}-${to} ${intl.formatMessage({ id: "of" })} ${total}`;
}}
labelRowsPerPage={hideRowsPerPage ? () => "" : intl.formatMessage({ id: "rowsPerPage" }) + ":"}
getItemAriaLabel={(type) => { getItemAriaLabel={(type) => {
if (type === 'previous') { if (type === 'previous') {
return intl.formatMessage({ id: 'paginationPrev' }); return intl.formatMessage({ id: 'paginationPrev' });
@@ -298,10 +380,11 @@ export function FiDataGrid({ rows, columns, sx, autoHeight = true,
} }
return ''; return '';
}} }}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangePageSize}
onPageChange={loadAllOnGazetteIssue ? () => {} : handleChangePage}
onRowsPerPageChange={(loadAllOnGazetteIssue || hideRowsPerPage) ? () => {} : handleChangePageSize}
/> />
),
);
},
} }
: {}), : {}),
}} }}


+ 2
- 7
src/components/FileList.js Просмотреть файл

@@ -28,14 +28,9 @@ export default function FileList({ refType, refId, allowDelete, sx, dateHideable
}, []); }, []);


const onDeleteClick = (fileId, skey, filename) => () => { const onDeleteClick = (fileId, skey, filename) => () => {
HttpUtils.get(
HttpUtils.del(
{ {
url: UrlUtils.GET_FILE_DELETE,
params: {
fileId: fileId,
skey: skey,
filename: filename
},
url: `${UrlUtils.GET_FILE_DELETE}/${fileId}/${skey}/${encodeURIComponent(filename)}`,
onSuccess: function () { onSuccess: function () {
loadData(); loadData();
} }


+ 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', id: 'paymentMethod',
field: 'paymentMethod', field: 'paymentMethod',
headerName: 'GFMIS Payment Method', headerName: 'GFMIS Payment Method',
flex: 4,
flex: 2,
renderCell: (params) => { renderCell: (params) => {
let paymentMethod = params.row.paymentMethod; let paymentMethod = params.row.paymentMethod;
return <div style={{ margin: 4 }}>{paymentMethod}</div> return <div style={{ margin: 4 }}>{paymentMethod}</div>
@@ -63,11 +63,27 @@ export default function SearchTable({ previewSearchCriteria, onPreviewGridOnRead
id: 'payAmount', id: 'payAmount',
field: 'payAmount', field: 'payAmount',
headerName: 'Amount ($)', headerName: 'Amount ($)',
flex: 5,
valueGetter: (params) => {
flex: 2,
align: 'right',
headerAlign: 'right',
// keep the raw number as the grid value so sorting is numeric;
// valueFormatter is used for display only
valueFormatter: (params) => {
return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : "";
},
sortComparator: (v1, v2) => {
const n1 = (v1 == null || isNaN(v1)) ? -Infinity : Number(v1);
const n2 = (v2 == null || isNaN(v2)) ? -Infinity : Number(v2);
return n1 - n2;
} }
}, },
{
id: 'spare',
field: 'spare',
headerName: '',
flex: 4,
disableColumnMenu: true,
},
]; ];


@@ -76,7 +92,7 @@ export default function SearchTable({ previewSearchCriteria, onPreviewGridOnRead
<FiDataGrid <FiDataGrid
key={previewToken} key={previewToken}
sx={_sx} sx={_sx}
rowHeight={80}
getRowHeight={() => 'auto'}
columns={columns} columns={columns}
customPageSize={10} customPageSize={10}
onRowDoubleClick={handleEditClick} onRowDoubleClick={handleEditClick}


+ 26
- 2
src/pages/GFMIS/TransactionDataGrid.js Просмотреть файл

@@ -65,6 +65,14 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
flex: 1, flex: 1,
minWidth: 200, minWidth: 200,
cellClassName: 'actions', cellClassName: 'actions',
// sorting/filtering uses the raw payment no. (transNo), not the link
valueGetter: (params) => params?.row?.transNo ?? '',
sortComparator: (v1, v2) => {
const n1 = Number(v1);
const n2 = Number(v2);
if (!isNaN(n1) && !isNaN(n2)) return n1 - n2;
return String(v1 ?? '').localeCompare(String(v2 ?? ''));
},
renderCell: (params) => { renderCell: (params) => {
return clickableLink('/paymentPage/details/' + params.row.id, params.row.transNo); return clickableLink('/paymentPage/details/' + params.row.id, params.row.transNo);
}, },
@@ -107,17 +115,33 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
field: 'payAmount', field: 'payAmount',
headerName: 'Amount ($)', headerName: 'Amount ($)',
width: 150, width: 150,
valueGetter: (params) => {
align: 'right',
headerAlign: 'right',
// keep the raw number as the grid value so sorting is numeric;
// valueFormatter is used for display only
valueFormatter: (params) => {
return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : "";
},
sortComparator: (v1, v2) => {
const n1 = (v1 == null || isNaN(v1)) ? -Infinity : Number(v1);
const n2 = (v2 == null || isNaN(v2)) ? -Infinity : Number(v2);
return n1 - n2;
} }
}, },
{
id: 'spare',
field: 'spare',
headerName: '',
width: 30,
disableColumnMenu: true,
},
]; ];


return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<FiDataGrid <FiDataGrid
sx={_sx} sx={_sx}
rowHeight={60}
getRowHeight={() => "auto"}
columns={columns} columns={columns}
height={500} height={500}
autoHeight={false} autoHeight={false}


+ 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> return <div style={{ margin: 4 }}>{dateStr(closingDate)}</div>
}, },
}, },
{
id: 'closingDateOff',
field: 'closingDateOff',
headerName: 'Closing Date Off',
flex: 2,
minWidth: 150,
renderCell: (params) => {
// console.log(params)
let closingDateOff = params.row.closingDateOff;
return <div style={{ margin: 4 }}>{dateStr(closingDateOff)}</div>
},
},
{ {
id: 'issueDesc', id: 'issueDesc',
field: 'issueDesc', field: 'issueDesc',
@@ -97,7 +85,7 @@ export default function GazetteIssueTable({ searchCriteria, applyGridOnReady })
<div style={{ height: "fit-content", width: '100%' }}> <div style={{ height: "fit-content", width: '100%' }}>
<FiDataGrid <FiDataGrid
columns={columns} columns={columns}
customPageSize={10}
pagination={false}
// onRowDoubleClick={handleRowDoubleClick} // onRowDoubleClick={handleRowDoubleClick}
getRowHeight={() => 'auto'} getRowHeight={() => 'auto'}
applyGridOnReady={applyGridOnReady} applyGridOnReady={applyGridOnReady}


+ 3
- 26
src/pages/GazetteIssue/ExportForm.js Просмотреть файл

@@ -67,8 +67,8 @@ const SearchGazetteIssueForm = ({ applyExport, comboData, waitDownload}) => {
</Grid> </Grid>
{/*row 2*/} {/*row 2*/}


<Grid container display="flex" alignItems={"center"}>
<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}>
<Grid container display="flex" alignItems={"center"} sx={{ mb: 1 }}>
<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3 }}>
<Autocomplete <Autocomplete
disablePortal disablePortal
id="year-combo" id="year-combo"
@@ -93,34 +93,11 @@ const SearchGazetteIssueForm = ({ applyExport, comboData, waitDownload}) => {
/> />
</Grid> </Grid>


<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}>
{/* <TextField
fullWidth
InputLabelProps={{
shrink: true
}}
{...register("dateTo")}
InputProps={{ inputProps: { min: minDate } }}
onChange={(newValue) => {
setMaxDate(DateUtils.dateValue(newValue));
}}
id="dateTo"
type="date"
label="To"
defaultValue={searchCriteria.dateTo}
/> */}
</Grid>

{/* <Grid item xs={9} s={6} md={4} lg={3}>
</Grid> */}
</Grid>
<Grid container justifyContent="center" direction="row" alignItems="center" spacing={3}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}> <ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Grid item sx={{ ml: 3, mb: 3, }} >
<Grid item sx={{ mr: 3 }}>
<Button <Button
variant="contained" variant="contained"
type="submit" type="submit"
size="large"
disabled={waitDownload} disabled={waitDownload}
> >
Export Export


+ 64
- 38
src/pages/GazetteIssue/SearchForm.js Просмотреть файл

@@ -12,14 +12,17 @@ import * as React from "react";
// import * as DateUtils from "utils/DateUtils"; // import * as DateUtils from "utils/DateUtils";
import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; import {PNSPS_BUTTON_THEME} from "themes/buttonConst";
import {ThemeProvider} from "@emotion/react"; import {ThemeProvider} from "@emotion/react";
import { GeneralConfirmWindow } from "utils/CommonFunction";
import { isGrantedAny } from "auth/utils";
// import * as ComboData from "utils/ComboData"; // import * as ComboData from "utils/ComboData";
// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //




const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => {
const [selectedYear, setSelectedYear] = React.useState([]);
const SearchGazetteIssueForm = ({ applySearch, comboData, searchCriteria, onGridReady, onDeleteYear, waitDelete }) => {
const [selectedYear, setSelectedYear] = React.useState(null);
// const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year); // const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year);
const [comboList, setComboList] = React.useState([]); const [comboList, setComboList] = React.useState([]);
const [isConfirmOpen, setIsConfirmOpen] = React.useState(false);
// const [onReady, setOnReady] = React.useState(false); // const [onReady, setOnReady] = React.useState(false);


const { const {
@@ -27,7 +30,7 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => {
handleSubmit } = useForm() handleSubmit } = useForm()


const onSubmit = () => { const onSubmit = () => {
if (selectedYear !=null){
if (selectedYear != null) {
const temp = { const temp = {
year: selectedYear.label, year: selectedYear.label,
}; };
@@ -36,17 +39,28 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => {
}; };


React.useEffect(() => { React.useEffect(() => {
if (comboData && comboData.length > 0) {
// console.log(comboData)
// const labelValue = comboData.find(obj => obj.label === searchCriteria.year);
// console.log(labelValue)
if(selectedYear.length == 0){
setSelectedYear(comboData[0])
}
setComboList(comboData)
// setSelectedYear(searchCriteria.dateFrom)
const nextCombo = Array.isArray(comboData) ? [...comboData] : [];
setComboList(nextCombo);

if (nextCombo.length === 0) {
setSelectedYear(null);
return;
}

const criteriaYear = searchCriteria?.year;
const matchedByCriteria = criteriaYear != null
? nextCombo.find((obj) => String(obj.label) === String(criteriaYear))
: null;
if (matchedByCriteria) {
setSelectedYear(matchedByCriteria);
return;
} }
}, [comboData]);

const matchedBySelected = selectedYear
? nextCombo.find((obj) => String(obj.label) === String(selectedYear.label))
: null;
setSelectedYear(matchedBySelected || nextCombo[0]);
}, [comboData, searchCriteria]);




return ( return (
@@ -65,8 +79,8 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => {
</Grid> </Grid>
{/*row 2*/} {/*row 2*/}


<Grid container display="flex" alignItems={"center"}>
<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}>
<Grid container display="flex" alignItems={"center"} sx={{ mb: 1 }}>
<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3 }}>
<Autocomplete <Autocomplete
disablePortal disablePortal
id="year-combo" id="year-combo"
@@ -79,6 +93,9 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => {
? String(option.label) ? String(option.label)
: "" : ""
} }
isOptionEqualToValue={(option, value) =>
String(option?.label) === String(value?.label)
}
onChange={(event, newValue) => { onChange={(event, newValue) => {
setSelectedYear(newValue); setSelectedYear(newValue);
}} }}
@@ -91,30 +108,8 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => {
/> />
</Grid> </Grid>


<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}>
{/* <TextField
fullWidth
InputLabelProps={{
shrink: true
}}
{...register("dateTo")}
InputProps={{ inputProps: { min: minDate } }}
onChange={(newValue) => {
setMaxDate(DateUtils.dateValue(newValue));
}}
id="dateTo"
type="date"
label="To"
defaultValue={searchCriteria.dateTo}
/> */}
</Grid>

{/* <Grid item xs={9} s={6} md={4} lg={3}>
</Grid> */}
</Grid>
<Grid container justifyContent="flex-end" direction="row" alignItems="center" spacing={3}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}> <ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Grid item sx={{ ml: 3, mb: 3, }} >
<Grid item sx={{ mr: 3 }}>
<Button <Button
variant="contained" variant="contained"
type="submit" type="submit"
@@ -124,9 +119,40 @@ const SearchGazetteIssueForm = ({ applySearch, comboData, onGridReady}) => {
</Button> </Button>
</Grid> </Grid>
</ThemeProvider> </ThemeProvider>

{isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ?
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Grid item sx={{ mr: 3 }}>
<Button
color="delete"
variant="contained"
disabled={!selectedYear || waitDelete || onGridReady}
onClick={() => setIsConfirmOpen(true)}
>
Delete
</Button>
</Grid>
</ThemeProvider>
: null
}
</Grid> </Grid>
</Grid> </Grid>
</form> </form>

<GeneralConfirmWindow
isWindowOpen={isConfirmOpen}
title="Confirm Delete"
content={selectedYear
? `Confirm to delete all gazette issues of year ${selectedYear.label}?`
: "Confirm to delete all gazette issues of the selected year?"}
onNormalClose={() => setIsConfirmOpen(false)}
onConfirmClose={() => {
setIsConfirmOpen(false);
if (selectedYear != null && onDeleteYear) {
onDeleteYear(selectedYear.label);
}
}}
/>
</MainCard> </MainCard>
); );
}; };


+ 56
- 6
src/pages/GazetteIssue/index.js Просмотреть файл

@@ -8,6 +8,7 @@ import {
import * as UrlUtils from "utils/ApiPathConst"; import * as UrlUtils from "utils/ApiPathConst";
import * as React from "react"; import * as React from "react";
import * as HttpUtils from "utils/HttpUtils"; import * as HttpUtils from "utils/HttpUtils";
import axios from "axios";
import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
@@ -26,10 +27,10 @@ const BackgroundHead = {
backgroundColor: '#0C489E', backgroundColor: '#0C489E',
backgroundPosition: 'right' backgroundPosition: 'right'
}; };
import { PNSPS_LONG_BUTTON_THEME } from "themes/buttonConst";
import { PNSPS_BUTTON_THEME } from "themes/buttonConst";
import { ThemeProvider } from "@emotion/react"; import { ThemeProvider } from "@emotion/react";
import { dateStr_Year } from "utils/DateUtils"; import { dateStr_Year } from "utils/DateUtils";
import { notifySaveSuccess } from 'utils/CommonFunction';
import { notifyDeleteSuccess, notifySaveSuccess } from 'utils/CommonFunction';
import { isGrantedAny } from "auth/utils"; import { isGrantedAny } from "auth/utils";
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';


@@ -50,6 +51,7 @@ const Index = () => {
const [attachments, setAttachments] = React.useState([]); const [attachments, setAttachments] = React.useState([]);
const [waitImport, setWaitImport] = React.useState(false); const [waitImport, setWaitImport] = React.useState(false);
const [waitDownload, setWaitDownload] = React.useState(false); const [waitDownload, setWaitDownload] = React.useState(false);
const [waitDelete, setWaitDelete] = React.useState(false);
const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); const [isWarningPopUp, setIsWarningPopUp] = React.useState(false);
const [warningText, setWarningText] = React.useState(""); const [warningText, setWarningText] = React.useState("");
const fileInputRef = React.useRef(null); const fileInputRef = React.useRef(null);
@@ -96,6 +98,43 @@ const Index = () => {
setGridOnReady(input); setGridOnReady(input);
} }


function deleteYear(year) {
if (year == null || year === "") {
setWarningText("Please select a year.");
setIsWarningPopUp(true);
return;
}
setWaitDelete(true);
setOnSearchReady(false);
axios.delete(`${UrlUtils.DELETE_ISSUE_YEAR}/${year}`)
.then(() => {
notifyDeleteSuccess();
HttpUtils.get({
url: UrlUtils.GET_ISSUE_YEAR_COMBO,
onSuccess: (responseData) => {
const combo = Array.isArray(responseData) ? [...responseData] : [];
setComboData(combo);
setWaitDelete(false);
const nextYear = combo.length > 0 ? combo[0].label : year;
applySearch({ year: nextYear });
},
onError: () => {
setComboData([]);
setWaitDelete(false);
applySearch({ year });
}
});
})
.catch((error) => {
const msg = error?.response?.data?.error
|| "Delete failed. Please try again.";
setWarningText(msg);
setIsWarningPopUp(true);
setWaitDelete(false);
setOnSearchReady(true);
});
}

React.useEffect(() => { React.useEffect(() => {
if (Object.keys(exportCriteria).length > 0) { if (Object.keys(exportCriteria).length > 0) {
doExport(); doExport();
@@ -138,6 +177,7 @@ const Index = () => {
setWaitImport(true); setWaitImport(true);
if (!attachments || attachments.length <= 0) { if (!attachments || attachments.length <= 0) {
setWarningText("Please upload file."); setWarningText("Please upload file.");
setIsWarningPopUp(true);
setWaitImport(false); setWaitImport(false);
return; return;
} }
@@ -149,6 +189,14 @@ const Index = () => {
setWaitImport(false); setWaitImport(false);
setAttachments([]); setAttachments([]);
loadCombo(); loadCombo();
},
onError: (error) => {
const msg = error?.response?.data?.error
|| "Import failed. Please try again.";
setWarningText(msg);
setIsWarningPopUp(true);
setWaitImport(false);
setAttachments([]);
} }
}); });
}; };
@@ -185,10 +233,9 @@ const Index = () => {
{isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) && {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) &&
<Grid item xs={12} md={12} lg={6} width="100%"> <Grid item xs={12} md={12} lg={6} width="100%">
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }}> <Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }}>
<ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button <Button
variant="contained" variant="contained"
size="large"
disabled={waitImport} disabled={waitImport}
type="button" type="button"
onClick={() => { onClick={() => {
@@ -205,7 +252,7 @@ const Index = () => {
} }
}} }}
> >
<Typography variant="h5">Upload Files</Typography>
Upload Gazette Issue
</Button> </Button>
<input <input
id="uploadFileBtn" id="uploadFileBtn"
@@ -227,8 +274,11 @@ const Index = () => {
<Grid item xs={12} md={12} lg={12} width="100%"> <Grid item xs={12} md={12} lg={12} width="100%">
<SearchForm <SearchForm
applySearch={applySearch} applySearch={applySearch}
searchCriteria={searchCriteria}
comboData={comboData} comboData={comboData}
onGridReady={onGridReady} onGridReady={onGridReady}
onDeleteYear={deleteYear}
waitDelete={waitDelete}
/> />
</Grid> </Grid>


@@ -260,7 +310,7 @@ const Index = () => {
<Typography variant="h3">Warning</Typography> <Typography variant="h3">Warning</Typography>
</DialogTitle> </DialogTitle>
<DialogContent style={{ display: 'flex' }}> <DialogContent style={{ display: 'flex' }}>
<Typography variant="h4" style={{ padding: '16px' }}>{warningText}</Typography>
<Typography variant="h4" style={{ padding: '16px', whiteSpace: 'pre-line' }}>{warningText}</Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setIsWarningPopUp(false)}> <Button onClick={() => setIsWarningPopUp(false)}>


+ 62
- 36
src/pages/Holiday/SearchForm.js Просмотреть файл

@@ -12,14 +12,17 @@ import * as React from "react";
// import * as DateUtils from "utils/DateUtils"; // import * as DateUtils from "utils/DateUtils";
import {PNSPS_BUTTON_THEME} from "themes/buttonConst"; import {PNSPS_BUTTON_THEME} from "themes/buttonConst";
import {ThemeProvider} from "@emotion/react"; import {ThemeProvider} from "@emotion/react";
import { GeneralConfirmWindow } from "utils/CommonFunction";
import { isGrantedAny } from "auth/utils";
// import * as ComboData from "utils/ComboData"; // import * as ComboData from "utils/ComboData";
// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //




const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => {
const SearchHolidayForm = ({ applySearch, comboData, searchCriteria, onGridReady, onDeleteYear, waitDelete }) => {
const [selectedYear, setSelectedYear] = React.useState(null); const [selectedYear, setSelectedYear] = React.useState(null);
// const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year); // const [defaultYear, setDefaultYear] = React.useState(searchCriteria.year);
const [comboList, setComboList] = React.useState([]); const [comboList, setComboList] = React.useState([]);
const [isConfirmOpen, setIsConfirmOpen] = React.useState(false);
// const [onReady, setOnReady] = React.useState(false); // const [onReady, setOnReady] = React.useState(false);


const { const {
@@ -36,17 +39,28 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => {
}; };


React.useEffect(() => { React.useEffect(() => {
if (comboData && comboData.length > 0) {
// console.log(comboData)
// const labelValue = comboData.find(obj => obj.label === searchCriteria.year);
// console.log(labelValue)
if (!selectedYear) {
setSelectedYear(comboData[0]);
}
setComboList(comboData)
// setSelectedYear(searchCriteria.dateFrom)
const nextCombo = Array.isArray(comboData) ? [...comboData] : [];
setComboList(nextCombo);

if (nextCombo.length === 0) {
setSelectedYear(null);
return;
}

const criteriaYear = searchCriteria?.year;
const matchedByCriteria = criteriaYear != null
? nextCombo.find((obj) => String(obj.label) === String(criteriaYear))
: null;
if (matchedByCriteria) {
setSelectedYear(matchedByCriteria);
return;
} }
}, [comboData]);

const matchedBySelected = selectedYear
? nextCombo.find((obj) => String(obj.label) === String(selectedYear.label))
: null;
setSelectedYear(matchedBySelected || nextCombo[0]);
}, [comboData, searchCriteria]);




return ( return (
@@ -65,8 +79,8 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => {
</Grid> </Grid>
{/*row 2*/} {/*row 2*/}


<Grid container display="flex" alignItems={"center"}>
<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}>
<Grid container display="flex" alignItems={"center"} sx={{ mb: 1 }}>
<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3 }}>
<Autocomplete <Autocomplete
disablePortal disablePortal
id="year-combo" id="year-combo"
@@ -79,6 +93,9 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => {
? String(option.label) ? String(option.label)
: "" : ""
} }
isOptionEqualToValue={(option, value) =>
String(option?.label) === String(value?.label)
}
onChange={(event, newValue) => { onChange={(event, newValue) => {
setSelectedYear(newValue); setSelectedYear(newValue);
}} }}
@@ -91,30 +108,8 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => {
/> />
</Grid> </Grid>


<Grid item xs={9} s={6} md={4} lg={4} sx={{ ml: 3, mr: 3, mb: 3 }}>
{/* <TextField
fullWidth
InputLabelProps={{
shrink: true
}}
{...register("dateTo")}
InputProps={{ inputProps: { min: minDate } }}
onChange={(newValue) => {
setMaxDate(DateUtils.dateValue(newValue));
}}
id="dateTo"
type="date"
label="To"
defaultValue={searchCriteria.dateTo}
/> */}
</Grid>

{/* <Grid item xs={9} s={6} md={4} lg={3}>
</Grid> */}
</Grid>
<Grid container justifyContent="flex-end" direction="row" alignItems="center" spacing={3}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}> <ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Grid item sx={{ ml: 3, mb: 3, }} >
<Grid item sx={{ mr: 3 }}>
<Button <Button
variant="contained" variant="contained"
type="submit" type="submit"
@@ -124,9 +119,40 @@ const SearchHolidayForm = ({ applySearch, comboData, onGridReady}) => {
</Button> </Button>
</Grid> </Grid>
</ThemeProvider> </ThemeProvider>

{isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ?
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Grid item sx={{ mr: 3 }}>
<Button
color="delete"
variant="contained"
disabled={!selectedYear || waitDelete || onGridReady}
onClick={() => setIsConfirmOpen(true)}
>
Delete
</Button>
</Grid>
</ThemeProvider>
: null
}
</Grid> </Grid>
</Grid> </Grid>
</form> </form>

<GeneralConfirmWindow
isWindowOpen={isConfirmOpen}
title="Confirm Delete"
content={selectedYear
? `Confirm to delete all holidays of year ${selectedYear.label}?`
: "Confirm to delete all holidays of the selected year?"}
onNormalClose={() => setIsConfirmOpen(false)}
onConfirmClose={() => {
setIsConfirmOpen(false);
if (selectedYear != null && onDeleteYear) {
onDeleteYear(selectedYear.label);
}
}}
/>
</MainCard> </MainCard>
); );
}; };


+ 68
- 15
src/pages/Holiday/index.js Просмотреть файл

@@ -8,6 +8,7 @@ import {
import * as UrlUtils from "utils/ApiPathConst"; import * as UrlUtils from "utils/ApiPathConst";
import * as React from "react"; import * as React from "react";
import * as HttpUtils from "utils/HttpUtils"; import * as HttpUtils from "utils/HttpUtils";
import axios from "axios";


import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
@@ -26,10 +27,10 @@ const BackgroundHead = {
backgroundPosition: 'right' backgroundPosition: 'right'
}; };


import { PNSPS_LONG_BUTTON_THEME } from "themes/buttonConst";
import { PNSPS_BUTTON_THEME } from "themes/buttonConst";
import { ThemeProvider } from "@emotion/react"; import { ThemeProvider } from "@emotion/react";
import { dateStr_Year } from "utils/DateUtils"; import { dateStr_Year } from "utils/DateUtils";
import { notifySaveSuccess } from 'utils/CommonFunction';
import { notifyDeleteSuccess, notifySaveSuccess } from 'utils/CommonFunction';
import { isGrantedAny } from "auth/utils"; import { isGrantedAny } from "auth/utils";
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';


@@ -49,6 +50,7 @@ const Index = () => {
const [attachments, setAttachments] = React.useState([]); const [attachments, setAttachments] = React.useState([]);
const [waitImport, setWaitImport] = React.useState(false); const [waitImport, setWaitImport] = React.useState(false);
const [waitDownload, setWaitDownload] = React.useState(false); const [waitDownload, setWaitDownload] = React.useState(false);
const [waitDelete, setWaitDelete] = React.useState(false);
const [isWarningPopUp, setIsWarningPopUp] = React.useState(false); const [isWarningPopUp, setIsWarningPopUp] = React.useState(false);
const [warningText, setWarningText] = React.useState(""); const [warningText, setWarningText] = React.useState("");
const fileInputRef = React.useRef(null); const fileInputRef = React.useRef(null);
@@ -58,13 +60,17 @@ const Index = () => {
loadForm(); loadForm();
}, [searchCriteria]); }, [searchCriteria]);


function loadForm() {
function loadForm(refreshCombo = false, criteria = null) {
const params = criteria || searchCriteria;
HttpUtils.get({ HttpUtils.get({
url: UrlUtils.GET_HOLIDAY, url: UrlUtils.GET_HOLIDAY,
params: searchCriteria,
params,
onSuccess: (responseData) => { onSuccess: (responseData) => {
setRecord(responseData); setRecord(responseData);
if (comboData.length === 0) {
if (criteria) {
setSearchCriteria(criteria);
}
if (refreshCombo || comboData.length === 0) {
loadCombo(); loadCombo();
} else { } else {
setOnSearchReady(true); setOnSearchReady(true);
@@ -77,8 +83,7 @@ const Index = () => {
HttpUtils.get({ HttpUtils.get({
url: UrlUtils.GET_HOLIDAY_COMBO, url: UrlUtils.GET_HOLIDAY_COMBO,
onSuccess: (responseData) => { onSuccess: (responseData) => {
let combo = responseData;
setComboData(combo);
setComboData(Array.isArray(responseData) ? [...responseData] : []);
setOnReady(true); setOnReady(true);
setOnSearchReady(true); setOnSearchReady(true);
} }
@@ -94,6 +99,43 @@ const Index = () => {
setGridOnReady(input); setGridOnReady(input);
} }


function deleteYear(year) {
if (year == null || year === "") {
setWarningText("Please select a year.");
setIsWarningPopUp(true);
return;
}
setWaitDelete(true);
setOnSearchReady(false);
axios.delete(`${UrlUtils.DELETE_HOLIDAY_YEAR}/${year}`)
.then(() => {
notifyDeleteSuccess();
HttpUtils.get({
url: UrlUtils.GET_HOLIDAY_COMBO,
onSuccess: (responseData) => {
const combo = Array.isArray(responseData) ? [...responseData] : [];
setComboData(combo);
setWaitDelete(false);
const nextYear = combo.length > 0 ? combo[0].label : year;
applySearch({ year: nextYear });
},
onError: () => {
setComboData([]);
setWaitDelete(false);
applySearch({ year });
}
});
})
.catch((error) => {
const msg = error?.response?.data?.error
|| "Delete failed. Please try again.";
setWarningText(msg);
setIsWarningPopUp(true);
setWaitDelete(false);
setOnSearchReady(true);
});
}

React.useEffect(() => { React.useEffect(() => {
if (attachments.length > 0) { if (attachments.length > 0) {
importHoliday(); importHoliday();
@@ -132,7 +174,9 @@ const Index = () => {
setOnSearchReady(false); setOnSearchReady(false);
if (!attachments || attachments.length <= 0) { if (!attachments || attachments.length <= 0) {
setWarningText("Please upload file."); setWarningText("Please upload file.");
setIsWarningPopUp(true);
setWaitImport(false); setWaitImport(false);
setOnSearchReady(true);
return; return;
} }
HttpUtils.postWithFiles({ HttpUtils.postWithFiles({
@@ -142,7 +186,16 @@ const Index = () => {
notifySaveSuccess(); notifySaveSuccess();
setWaitImport(false); setWaitImport(false);
setAttachments([]); setAttachments([]);
loadForm();
loadForm(true);
},
onError: (error) => {
const msg = error?.response?.data?.error
|| "Import failed. Please try again.";
setWarningText(msg);
setIsWarningPopUp(true);
setWaitImport(false);
setAttachments([]);
setOnSearchReady(true);
} }
}); });
}; };
@@ -168,22 +221,20 @@ const Index = () => {
<Grid item xs={12} md={12} lg={6} width="100%"> <Grid item xs={12} md={12} lg={6} width="100%">
<Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }} > <Stack direction="row" justifyContent="flex-start" alignItems="center" spacing={2} sx={{ ml: 2, mt: 1 }} >


<ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button <Button
variant="contained" variant="contained"
size="large"
disabled={waitDownload} disabled={waitDownload}
onClick={doExport} onClick={doExport}
aria-label={intl.formatMessage({ id: 'ariaExportHolidayTemplate' })} aria-label={intl.formatMessage({ id: 'ariaExportHolidayTemplate' })}
> >
<Typography variant="h5">Export</Typography>
Download Template
</Button> </Button>
</ThemeProvider> </ThemeProvider>
{isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ? {isGrantedAny(["MAINTAIN_GAZETTE_ISSUE"]) ?
<ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button <Button
variant="contained" variant="contained"
size="large"
disabled={waitImport} disabled={waitImport}
type="button" type="button"
onClick={() => { onClick={() => {
@@ -200,7 +251,7 @@ const Index = () => {
} }
}} }}
> >
<Typography variant="h5">Upload Files</Typography>
Upload Holiday
</Button> </Button>
<input <input
id="uploadFileBtn" id="uploadFileBtn"
@@ -226,6 +277,8 @@ const Index = () => {
searchCriteria={searchCriteria} searchCriteria={searchCriteria}
comboData={comboData} comboData={comboData}
onGridReady={onGridReady} onGridReady={onGridReady}
onDeleteYear={deleteYear}
waitDelete={waitDelete}
/> />
</Grid> </Grid>


@@ -255,7 +308,7 @@ const Index = () => {
> >
<DialogTitle><Typography variant="h3">Warning</Typography></DialogTitle> <DialogTitle><Typography variant="h3">Warning</Typography></DialogTitle>
<DialogContent style={{ display: 'flex' }}> <DialogContent style={{ display: 'flex' }}>
<Typography variant="h4" style={{ padding: '16px' }}>{warningText}</Typography>
<Typography variant="h4" style={{ padding: '16px', whiteSpace: 'pre-line' }}>{warningText}</Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setIsWarningPopUp(false)}><Typography variant="h5">OK</Typography></Button> <Button onClick={() => setIsWarningPopUp(false)}><Typography variant="h5">OK</Typography></Button>


+ 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 { FormattedMessage } from "react-intl";
import usePageTitle from 'components/usePageTitle'; import usePageTitle from 'components/usePageTitle';
import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst'; import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst';
import SafeHtml from 'components/SafeHtml';


const BackgroundHead = { const BackgroundHead = {
backgroundImage: `url(${titleBackgroundImg})`, backgroundImage: `url(${titleBackgroundImg})`,
@@ -93,7 +94,7 @@ const Index = () => {
</Typography> </Typography>
<Typography sx={{p:1}} align="justify">{DateUtils.datetimeStr(record?.sentDate)}</Typography> <Typography sx={{p:1}} align="justify">{DateUtils.datetimeStr(record?.sentDate)}</Typography>
<Typography component="div" variant="body1" sx={{ p: 4, textAlign: "left", bgcolor: "#f8f8f8" }} align="justify"> <Typography component="div" variant="body1" sx={{ p: 4, textAlign: "left", bgcolor: "#f8f8f8" }} align="justify">
<div dangerouslySetInnerHTML={{ __html: record?.content }} />
<SafeHtml html={record?.content} />
</Typography> </Typography>


<Typography component="h3" variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> <Typography component="h3" variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}>


+ 2
- 2
src/pages/Organization/DetailPage/OrganizationCard.js Просмотреть файл

@@ -227,7 +227,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {


const markAsNonCreditor = () => { const markAsNonCreditor = () => {
setNonCreditorConfirmPopUp(false); setNonCreditorConfirmPopUp(false);
HttpUtils.get({
HttpUtils.post({
url: UrlUtils.GET_ORG_MARK_AS_NON_CREDITOR + "/" + id, url: UrlUtils.GET_ORG_MARK_AS_NON_CREDITOR + "/" + id,
onSuccess: () => { onSuccess: () => {
loadDataFun(); loadDataFun();
@@ -237,7 +237,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {


const sendDn_Overdue = () => { const sendDn_Overdue = () => {
setNonCreditorConfirmPopUp(false); setNonCreditorConfirmPopUp(false);
HttpUtils.get({
HttpUtils.post({
url: UrlUtils.GET_SEND_OVERDUE_CREDITOR_LIST + "/" + id, url: UrlUtils.GET_SEND_OVERDUE_CREDITOR_LIST + "/" + id,
onSuccess: (responseData) => { onSuccess: (responseData) => {
setOverduePublicNotice(responseData.overduePublicNotice); setOverduePublicNotice(responseData.overduePublicNotice);


+ 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'))); const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
import * as FormatUtils from "utils/FormatUtils"; import * as FormatUtils from "utils/FormatUtils";


import SafeHtml from 'components/SafeHtml';
import VisaIcon from "assets/images/icons/visacard.svg"; import VisaIcon from "assets/images/icons/visacard.svg";
import MasterIcon from "assets/images/icons/mastercard.svg"; import MasterIcon from "assets/images/icons/mastercard.svg";
import JcbIcon from "assets/images/icons/jcb.svg"; import JcbIcon from "assets/images/icons/jcb.svg";
@@ -58,6 +59,8 @@ const MultiPaymentWindow = (props) => {
const [paymentHoldedErrText, setPaymentHoldedErrText] = React.useState(""); const [paymentHoldedErrText, setPaymentHoldedErrText] = React.useState("");
const [paymentHoldedErr, setPaymentHoldedErr] = React.useState(false); const [paymentHoldedErr, setPaymentHoldedErr] = React.useState(false);
const [isPaying, setIsPaying] = React.useState(false);
const payingRef = React.useRef(false);
const mobileBrowser = "Mobile"; const mobileBrowser = "Mobile";


@@ -70,10 +73,7 @@ const MultiPaymentWindow = (props) => {
}, [props.transactionData]); }, [props.transactionData]);


useEffect(() => { useEffect(() => {
// console.log(props.availableMethods)
if(props.availableMethods.length > 0){
setLoadAvailableMethodData(props.availableMethods)
}
setLoadAvailableMethodData(props.availableMethods || [])
}, [props.availableMethods]); }, [props.availableMethods]);


useEffect(() => { useEffect(() => {
@@ -89,9 +89,7 @@ const MultiPaymentWindow = (props) => {
}, [transactionData]); }, [transactionData]);


useEffect(() => { useEffect(() => {
if(loadAvailableMethodData.length > 0){
setAvailableMethodData(loadAvailableMethodData)
}
setAvailableMethodData(loadAvailableMethodData || [])
}, [loadAvailableMethodData]); }, [loadAvailableMethodData]);


useEffect(() => { useEffect(() => {
@@ -144,7 +142,13 @@ const MultiPaymentWindow = (props) => {


}, [paymentMethod]); }, [paymentMethod]);
const releasePayLock = () => {
payingRef.current = false;
setIsPaying(false);
};

const selectedPaymentMethodHandle = (method) => () =>{ const selectedPaymentMethodHandle = (method) => () =>{
if (payingRef.current) return;
if (method != paymentMethod){ if (method != paymentMethod){
resetForm() resetForm()
let totalAmount = props.totalAmount; let totalAmount = props.totalAmount;
@@ -198,6 +202,10 @@ const MultiPaymentWindow = (props) => {
}; };


const handlePaymentCheck = () => { const handlePaymentCheck = () => {
if (payingRef.current) return;
payingRef.current = true;
setIsPaying(true);

let appIdList = props.appIds let appIdList = props.appIds
// console.log(props.appIds) // console.log(props.appIds)
// console.log(appIdList) // console.log(appIdList)
@@ -207,6 +215,8 @@ const MultiPaymentWindow = (props) => {
appIds: appIdList appIds: appIdList
}, },
onSuccess: (responseData) => { onSuccess: (responseData) => {
if (!payingRef.current) return;

const latestData = {}; const latestData = {};


responseData.forEach(item => { responseData.forEach(item => {
@@ -235,13 +245,21 @@ const MultiPaymentWindow = (props) => {
const resultString = HoldingApplication.map(item => item.appNo).join(' , '); const resultString = HoldingApplication.map(item => item.appNo).join(' , ');
setPaymentHoldedErrText(resultString); setPaymentHoldedErrText(resultString);
// setPaymentHoldedErrText(intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: record.appNo })); // setPaymentHoldedErrText(intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: record.appNo }));
releasePayLock();
setPaymentHoldedErr(true); setPaymentHoldedErr(true);
} }
},
onFail: () => {
releasePayLock();
},
onError: () => {
releasePayLock();
} }
}); });
}; };


const closeHandle = () => () =>{ const closeHandle = () => () =>{
releasePayLock();
resetForm() resetForm()
props.setOpen(false) props.setOpen(false)
}; };
@@ -261,6 +279,12 @@ const MultiPaymentWindow = (props) => {
} }
}, [availableMethodData]); }, [availableMethodData]);


useEffect(() => {
if (!props.confirmPayment) {
releasePayLock();
}
}, [props.confirmPayment]);

const formik = useFormik({ const formik = useFormik({
initialValues: ({ initialValues: ({
username: '', username: '',
@@ -269,10 +293,16 @@ const MultiPaymentWindow = (props) => {
}), }),
}); });


const showFps = props.fpsStatus?.active === "Y";
const showCreditCard = props.creditCardStatus?.active === "Y";
const showUnionPay = props.unionPayStatus?.active === "Y";
const showPps = props.ppsStatus?.active === "Y" && props.browserType !== mobileBrowser;
const hasVisiblePaymentMethod = showFps || showCreditCard || showUnionPay || showPps;

return ( return (
<Dialog <Dialog
open={props.open} open={props.open}
onClose={() => props.setOpen(false)}
onClose={closeHandle()}
fullWidth={true} fullWidth={true}
maxWidth={'xl'} maxWidth={'xl'}
fullScreen={props.isFullScreen} fullScreen={props.isFullScreen}
@@ -307,7 +337,7 @@ const MultiPaymentWindow = (props) => {
</Typography> */} </Typography> */}
{!props.onReady ? {!props.onReady ?
<LoadingComponent /> <LoadingComponent />
:availableMethodData.length>0?
:hasVisiblePaymentMethod?
<Grid container spacing={2} direction="column" justifyContent="space-between" alignItems="flex-start"> <Grid container spacing={2} direction="column" justifyContent="space-between" alignItems="flex-start">
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="center"> <Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="center">
@@ -317,40 +347,48 @@ const MultiPaymentWindow = (props) => {
</Typography> </Typography>
</Grid> </Grid>
<Grid item sx={{display: { sm: 'block', md: 'none' }}}></Grid> <Grid item sx={{display: { sm: 'block', md: 'none' }}}></Grid>
{showFps ?
<Grid item> <Grid item>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("FPS")} disabled={props.fpsStatus.active === "N"}>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("FPS")} disabled={isPaying}>
<img className={fpsClass} src={FpsIcon} width="80" height="80" alt="FPS"></img> <img className={fpsClass} src={FpsIcon} width="80" height="80" alt="FPS"></img>
</Button> </Button>
</Grid> </Grid>
: null}
{showCreditCard ?
<Grid item> <Grid item>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("Visa")} disabled={props.creditCardStatus.active === "N"}>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("Visa")} disabled={isPaying}>
<img className={visaClass} src={VisaIcon} width="80" height="80" alt="Visa"></img> <img className={visaClass} src={VisaIcon} width="80" height="80" alt="Visa"></img>
</Button> </Button>
</Grid> </Grid>
: null}
{showCreditCard ?
<Grid item> <Grid item>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("MasterCard")} disabled={props.creditCardStatus.active === "N"}>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("MasterCard")} disabled={isPaying}>
<img className={mastercardClass} src={MasterIcon} width="80" height="80" alt="MasterCard"></img> <img className={mastercardClass} src={MasterIcon} width="80" height="80" alt="MasterCard"></img>
</Button> </Button>
</Grid> </Grid>
: null}
{showUnionPay ?
<Grid item> <Grid item>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("UnionPay")} disabled={props.unionPayStatus.active === "N"}>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("UnionPay")} disabled={isPaying}>
<img className={unionPayClass} src={UnionPayIcon} width="80" height="80" alt="UnionPay"></img> <img className={unionPayClass} src={UnionPayIcon} width="80" height="80" alt="UnionPay"></img>
</Button> </Button>
</Grid> </Grid>
: null}
{showCreditCard ?
<Grid item> <Grid item>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("JCB")} disabled={props.unionPayStatus.active === "N"}>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("JCB")} disabled={isPaying}>
<img className={jCBClass} src={JcbIcon} width="80" height="80" alt="JCB"></img> <img className={jCBClass} src={JcbIcon} width="80" height="80" alt="JCB"></img>
</Button> </Button>
</Grid> </Grid>
: null}
{showPps ?
<Grid item> <Grid item>
{props.browserType==mobileBrowser?
null
:
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("PPS")} disabled={props.ppsStatus.active === "N"}>
<Button variant="contained" color="white" onClick={selectedPaymentMethodHandle("PPS")} disabled={isPaying}>
<img className={pPSClass} src={PpsIcon} width="80" height="80" alt="PPS"></img> <img className={pPSClass} src={PpsIcon} width="80" height="80" alt="PPS"></img>
</Button> </Button>
}
</Grid> </Grid>
: null}
</Grid> </Grid>
</Grid> </Grid>
{paymentMethod !=""? {paymentMethod !=""?
@@ -447,10 +485,10 @@ const MultiPaymentWindow = (props) => {
</DialogActions> </DialogActions>
<DialogActions> <DialogActions>
{ {
props.onPayment?
props.onPayment || isPaying?
<LoadingComponent disableText={true} alignItems="flex-start"/> <LoadingComponent disableText={true} alignItems="flex-start"/>
: :
<Button variant="contained" onClick={confirmPaymentHandle()} disabled={paymentMethod === "" || isLimit || isPPSLimit} sx={PAY_CONTAINED_BUTTON_SX}>
<Button variant="contained" onClick={confirmPaymentHandle()} disabled={paymentMethod === "" || isLimit || isPPSLimit || isPaying} sx={PAY_CONTAINED_BUTTON_SX}>
<FormattedMessage id="pay"/> <FormattedMessage id="pay"/>
</Button> </Button>
} }
@@ -472,7 +510,7 @@ const MultiPaymentWindow = (props) => {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}> <DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between"> <Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack> </Stack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>


+ 1
- 1
src/pages/Payment/Search_GLD/DataGrid.js Просмотреть файл

@@ -194,7 +194,7 @@ export default function SearchPaymentTable({ searchCriteria, applyGridOnReady, a
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<FiDataGrid <FiDataGrid
sx={_sx} sx={_sx}
rowHeight={80}
getRowHeight={() => 'auto'}
columns={columns} columns={columns}
customPageSize={10} customPageSize={10}
onRowDoubleClick={handleEditClick} onRowDoubleClick={handleEditClick}


+ 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 {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14));
const getDefaultDateTo = () => DateUtils.dateValue(new Date());

const isSubmitDateEmpty = (value) =>
value == null || value === "" || value === "dd / mm / yyyy";

const isBlank = (value) => value == null || String(value).trim() === "";

const toDayjsOrNull = (value) => {
if (isSubmitDateEmpty(value)) return null;
const d = dayjs(value);
return d.isValid() ? d : null;
};

// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => {


@@ -25,12 +40,11 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo);
const [status, setStatus] = React.useState(ComboData.paymentStatus[0]); const [status, setStatus] = React.useState(ComboData.paymentStatus[0]);
const [payMethod, setPayMethod] = React.useState(ComboData.payMethod[0]); const [payMethod, setPayMethod] = React.useState(ComboData.payMethod[0]);

const { reset, register, handleSubmit } = useForm()
const marginBottom = 2.5; const marginBottom = 2.5;


const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy");
const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy");
const prevHasOtherRef = React.useRef(null);


React.useEffect(() => { React.useEffect(() => {
if(searchCriteria.status!=undefined){ if(searchCriteria.status!=undefined){
@@ -72,20 +86,77 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
setToDateValue(maxDate); setToDateValue(maxDate);
}, [maxDate]); }, [maxDate]);


const { reset, register, handleSubmit, watch } = useForm({
defaultValues: {
code: searchCriteria.code || "",
transNo: searchCriteria.transNo || ""
}
});
const code = watch("code");
const transNo = watch("transNo");

// add near the top inside the component (after useState for payMethod) // add near the top inside the component (after useState for payMethod)
const toPayMethodArray = (opt) => { const toPayMethodArray = (opt) => {
if (!opt || opt.type === 'all') return []; if (!opt || opt.type === 'all') return [];
return Array.isArray(opt.type) ? opt.type : [opt.type]; return Array.isArray(opt.type) ? opt.type : [opt.type];
}; };


const clearSubmitDates = () => {
setMinDate(null);
setMaxDate(null);
};

const restoreDefaultSubmitDates = () => {
setMinDate(getDefaultDateFrom());
setMaxDate(getDefaultDateTo());
};

const hasOtherCriteria = (textFields = {}) => {
if (!isBlank(textFields.code)) return true;
if (!isBlank(textFields.transNo)) return true;
if (status?.type && status.type !== "all" && status.type !== "") return true;
if (payMethod?.type && payMethod.type !== "all") return true;
return false;
};

React.useEffect(() => {
const hasOther = hasOtherCriteria({ code, transNo });
if (prevHasOtherRef.current === null) {
prevHasOtherRef.current = hasOther;
return;
}
if (hasOther && !prevHasOtherRef.current) {
clearSubmitDates();
} else if (!hasOther && prevHasOtherRef.current) {
// Only refill defaults when From or To is empty; keep user-entered dates otherwise
if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) {
restoreDefaultSubmitDates();
}
}
prevHasOtherRef.current = hasOther;
}, [code, transNo, status, payMethod]);


const onSubmit = (data) => { const onSubmit = (data) => {
let sentDateFrom = ""; let sentDateFrom = "";
let sentDateTo = ""; let sentDateTo = "";


if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") {
sentDateFrom = DateUtils.dateValue(fromDateValue)
sentDateTo = DateUtils.dateValue(toDateValue)
const hasOther = hasOtherCriteria({
code: data.code,
transNo: data.transNo
});
const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue)
|| minDate == null || maxDate == null;

if (!hasOther && datesEmpty) {
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
sentDateFrom = dateFrom;
sentDateTo = dateTo;
} else if (!datesEmpty) {
sentDateFrom = DateUtils.dateValue(fromDateValue);
sentDateTo = DateUtils.dateValue(toDateValue);
} }


const temp = { const temp = {
@@ -98,18 +169,36 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
start:0, start:0,
limit:10 limit:10
}; };
if (searchCriteria?.sort && searchCriteria?.direction) {
temp.sort = searchCriteria.sort;
temp.direction = searchCriteria.direction;
}
applySearch(temp); applySearch(temp);
}; };


function resetForm() { function resetForm() {
setStatus(ComboData.paymentStatus[0]); setStatus(ComboData.paymentStatus[0]);
setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14)))
setMaxDate(DateUtils.dateValue(new Date()))
setPayMethod(ComboData.payMethod[0]);
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
reset({ reset({
code:"", code:"",
transNo:"" transNo:""
}); });
localStorage.setItem('searchCriteria',"")
prevHasOtherRef.current = false;
localStorage.setItem('searchCriteria',"");
applySearch({
code: "",
transNo: "",
dateFrom,
dateTo,
status: "",
payMethod: [],
start: 0,
limit: 10
});
} }




@@ -150,22 +239,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateFrom" id="dateFrom"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label="Payment Date (From)" label="Payment Date (From)"
value={minDate === null ? null : dayjs(minDate)}
maxDate={maxDate === null ? null : dayjs(maxDate)}
value={toDayjsOrNull(minDate)}
maxDate={toDayjsOrNull(maxDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMinDate(newValue);
}
setMinDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >
@@ -176,22 +264,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateTo" id="dateTo"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label="Payment Date (To)" label="Payment Date (To)"
value={maxDate === null ? null : dayjs(maxDate)}
minDate={minDate === null ? null : dayjs(minDate)}
value={toDayjsOrNull(maxDate)}
minDate={toDayjsOrNull(minDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMaxDate(newValue);
}
setMaxDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >


+ 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%' }}> <div style={{ minHeight: 400, width: '100%' }}>
<FiDataGrid <FiDataGrid
sx={_sx} sx={_sx}
rowHeight={80}
getRowHeight={() => 'auto'}
columns={columns} columns={columns}
customPageSize={10} customPageSize={10}
onRowDoubleClick={handleEditDoubleClick} onRowDoubleClick={handleEditDoubleClick}


+ 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 {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14));
const getDefaultDateTo = () => DateUtils.dateValue(new Date());

const isSubmitDateEmpty = (value) =>
value == null || value === "" || value === "dd / mm / yyyy";

const isBlank = (value) => value == null || String(value).trim() === "";

const toDayjsOrNull = (value) => {
if (isSubmitDateEmpty(value)) return null;
const d = dayjs(value);
return d.isValid() ? d : null;
};

// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => {
const intl = useIntl(); const intl = useIntl();
@@ -29,9 +44,9 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>


const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy");
const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy");
const prevHasOtherRef = React.useRef(null);


React.useEffect(() => { React.useEffect(() => {
// console.log(minDate)
setFromDateValue(minDate); setFromDateValue(minDate);
}, [minDate]); }, [minDate]);
@@ -55,15 +70,72 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
} }
} }


const { reset, register, handleSubmit } = useForm()
const { reset, register, handleSubmit, watch } = useForm({
defaultValues: {
code: searchCriteria.code || "",
transNo: searchCriteria.transNo || ""
}
});
const code = watch("code");
const transNo = watch("transNo");

const clearSubmitDates = () => {
setMinDate(null);
setMaxDate(null);
};

const restoreDefaultSubmitDates = () => {
setMinDate(getDefaultDateFrom());
setMaxDate(getDefaultDateTo());
};

const hasOtherCriteria = (textFields = {}) => {
if (!isBlank(textFields.code)) return true;
if (!isBlank(textFields.transNo)) return true;
if (status?.type && status.type !== "all" && status.type !== "") return true;
return false;
};

React.useEffect(() => {
const hasOther = hasOtherCriteria({ code, transNo });
if (prevHasOtherRef.current === null) {
prevHasOtherRef.current = hasOther;
return;
}
if (hasOther && !prevHasOtherRef.current) {
clearSubmitDates();
} else if (!hasOther && prevHasOtherRef.current) {
// Only refill defaults when From or To is empty; keep user-entered dates otherwise
if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) {
restoreDefaultSubmitDates();
}
}
prevHasOtherRef.current = hasOther;
}, [code, transNo, status]);


const onSubmit = (data) => { const onSubmit = (data) => {
let sentDateFrom = ""; let sentDateFrom = "";
let sentDateTo = ""; let sentDateTo = "";
if( fromDateValue!="dd / mm / yyyy"&&toDateValue!="dd / mm / yyyy"){
sentDateFrom = DateUtils.dateValue(fromDateValue)
sentDateTo = DateUtils.dateValue(toDateValue)

const hasOther = hasOtherCriteria({
code: data.code,
transNo: data.transNo
});
const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue)
|| minDate == null || maxDate == null;

if (!hasOther && datesEmpty) {
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
sentDateFrom = dateFrom;
sentDateTo = dateTo;
} else if (!datesEmpty) {
sentDateFrom = DateUtils.dateValue(fromDateValue);
sentDateTo = DateUtils.dateValue(toDateValue);
} }

const temp = { const temp = {
code: data.code, code: data.code,
transNo: data.transNo, transNo: data.transNo,
@@ -73,18 +145,34 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
start:0, start:0,
limit:10 limit:10
}; };
if (searchCriteria?.sort && searchCriteria?.direction) {
temp.sort = searchCriteria.sort;
temp.direction = searchCriteria.direction;
}
applySearch(temp); applySearch(temp);
}; };


function resetForm() { function resetForm() {
setStatus(ComboData.paymentStatus[0]); setStatus(ComboData.paymentStatus[0]);
setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14)))
setMaxDate(DateUtils.dateValue(new Date()))
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
reset({ reset({
code:"", code:"",
transNo:"" transNo:""
}); });
localStorage.setItem('searchCriteria',"")
prevHasOtherRef.current = false;
localStorage.setItem('searchCriteria',"");
applySearch({
code: "",
transNo: "",
dateFrom,
dateTo,
status: "",
start: 0,
limit: 10
});
} }




@@ -128,23 +216,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateFrom" id="dateFrom"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label={intl.formatMessage({id: 'payDateFrom'})} label={intl.formatMessage({id: 'payDateFrom'})}
// defaultValue={searchCriteria.dateFrom}
value={minDate === null ? null : dayjs(minDate)}
maxDate={maxDate === null ? null : dayjs(maxDate)}
value={toDayjsOrNull(minDate)}
maxDate={toDayjsOrNull(maxDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMinDate(newValue);
}
setMinDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >
@@ -156,23 +242,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateTo" id="dateTo"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label={intl.formatMessage({id: 'payDateTo'})} label={intl.formatMessage({id: 'payDateTo'})}
// defaultValue={searchCriteria.dateTo}
value={maxDate === null ? null : dayjs(maxDate)}
minDate={minDate === null ? null : dayjs(minDate)}
value={toDayjsOrNull(maxDate)}
minDate={toDayjsOrNull(minDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMaxDate(newValue);
}
setMaxDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >


+ 90
- 23
src/pages/Payment/index.js Просмотреть файл

@@ -106,6 +106,12 @@ const Index = () => {
setTotalAmount(totalAmount); setTotalAmount(totalAmount);
setSelectedPaymentMethod("") setSelectedPaymentMethod("")
setConfirmPayment(false) setConfirmPayment(false)
setOnReady(false)
setAvailableMethods([])
setFPSStatus({})
setCreditCardStatus({})
setUnionPayStatus({})
setPPSStatus({})
if (totalAmount > 0) { if (totalAmount > 0) {
getAvailablePayment() getAvailablePayment()
getTransactionId() getTransactionId()
@@ -190,48 +196,97 @@ const Index = () => {
} }
}, [afterConfirmPayment]); }, [afterConfirmPayment]);


const getAvailablePayment = () =>{
const filterPreferPaymentMethodsByLimit = (methods, amount, limits) => {
if (!methods) {
return [];
}
if (!limits) {
return methods;
}
return methods.filter((method) => {
const key = (method || "").toLowerCase();
let maxLimit;
if (key === "fps") {
maxLimit = limits.fpsLimitRecord?.maxLimit;
} else if (key === "pps" || key === "ppsb") {
maxLimit = limits.ppsbLimitRecord?.maxLimit;
} else if (key === "unionpay") {
maxLimit = limits.unionPlayLimitRecord?.maxLimit;
} else if (key === "visa" || key === "mastercard" || key === "jcb" || key === "creditcard") {
maxLimit = limits.creditCardLimitRecord?.maxLimit;
} else {
return true;
}
return maxLimit == null || amount <= maxLimit;
});
};

const filterAvailableMethodsByLimit = (methods, amount, limits) => {
if (!methods) {
return [];
}
if (!limits) {
return methods;
}
return methods.filter((method) => {
if (method.subtype === "FPS") {
return limits.fpsLimitRecord?.maxLimit == null || amount <= limits.fpsLimitRecord.maxLimit;
}
if (method.subtype === "PPS") {
return limits.ppsbLimitRecord?.maxLimit == null || amount <= limits.ppsbLimitRecord.maxLimit;
}
if (method.subtype === "CreditCard") {
const isUnionPay = method.supportedcard?.some((card) => card === "UnionPay");
if (isUnionPay) {
return limits.unionPlayLimitRecord?.maxLimit == null || amount <= limits.unionPlayLimitRecord.maxLimit;
}
return limits.creditCardLimitRecord?.maxLimit == null || amount <= limits.creditCardLimitRecord.maxLimit;
}
return true;
});
};

const requestPaymentAvailability = (methods, limits) => {
HttpUtils.post({ HttpUtils.post({
url: UrlUtils.PAYMENT_AVAILABLE_PAYMENT, url: UrlUtils.PAYMENT_AVAILABLE_PAYMENT,
params: { params: {
"locale": locale === 'en' ?local.en:locale === 'zh-HK' ?local.zh:local.cn, "locale": locale === 'en' ?local.en:locale === 'zh-HK' ?local.zh:local.cn,
"amount": totalAmount, "amount": totalAmount,
// "eserviceids": [
// "<eserviceid>", "<eserviceid>"
// ],
"preferpaymentmethods": preferpaymentmethods
"preferpaymentmethods": methods
}, },
onSuccess: (responseData) => { onSuccess: (responseData) => {
let availableMethods = responseData.availablepaymentmethods;
let availableMethods = filterAvailableMethodsByLimit(
responseData.availablepaymentmethods || [], totalAmount, limits
);
setAvailableMethods(availableMethods); setAvailableMethods(availableMethods);
HttpUtils.get({
url: UrlUtils.PAYMENT_LIMIT_SETTING_LIST,
params: {},
onSuccess: (responseData) => {
// console.log(responseData)
setPaymentLimit(responseData)
},
onError: () =>{
// setOnReady(true)
}
});
if (availableMethods.length === 0) {
setOnReady(true)
}
}, },
onError: () =>{ onError: () =>{
setOnReady(true) setOnReady(true)
} }
}); });
};

const getAvailablePayment = () =>{
HttpUtils.get({ HttpUtils.get({
url: UrlUtils.PAYMENT_LIMIT_SETTING_LIST, url: UrlUtils.PAYMENT_LIMIT_SETTING_LIST,
params: {}, params: {},
onSuccess: (responseData) => { onSuccess: (responseData) => {
// console.log(responseData)
setPaymentLimit(responseData) setPaymentLimit(responseData)

const methodsWithinLimit = filterPreferPaymentMethodsByLimit(
preferpaymentmethods, totalAmount, responseData
);
if (methodsWithinLimit.length === 0) {
setAvailableMethods([])
setOnReady(true)
return;
}
requestPaymentAvailability(methodsWithinLimit, responseData);
}, },
onError: () =>{ onError: () =>{
// setOnReady(true)
requestPaymentAvailability(preferpaymentmethods);
} }
}); });


@@ -383,7 +438,7 @@ const Index = () => {
setFPSStatus(method) setFPSStatus(method)
} }
} else if (method.subtype === "CreditCard") { } else if (method.subtype === "CreditCard") {
method.supportedcard.forEach((supportedcard) => {
(method.supportedcard || []).forEach((supportedcard) => {
if (supportedcard === "JCB" || supportedcard === "MasterCard" || supportedcard === "Visa") { if (supportedcard === "JCB" || supportedcard === "MasterCard" || supportedcard === "Visa") {
setCreditCardStatus(method) setCreditCardStatus(method)
} else { } else {
@@ -435,6 +490,7 @@ const Index = () => {
// const confirmPaymentHandle = () => () => { // const confirmPaymentHandle = () => () => {
useEffect(() => { useEffect(() => {
if (confirmPayment){ if (confirmPayment){
setOnPayment(true);
HttpUtils.post({ HttpUtils.post({
url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE,
params: { params: {
@@ -445,12 +501,22 @@ const Index = () => {
setAfterConfirmPayment(true); setAfterConfirmPayment(true);
return; return;
} }
setOnPayment(false);
setConfirmPayment(false);
let str = ""; let str = "";
responData.msg.forEach((item) => { responData.msg.forEach((item) => {
str += "App: " + item.appNo + ", 到期日: " + DateUtils.datetimeStr_Cht(item.expiryDate) + "\n"; str += "App: " + item.appNo + ", 到期日: " + DateUtils.datetimeStr_Cht(item.expiryDate) + "\n";
}); });
setExpiryDateErrText(str.split('\n').map(str => <>{str}<br/></>)); setExpiryDateErrText(str.split('\n').map(str => <>{str}<br/></>));
setExpiryDateErr(true); setExpiryDateErr(true);
},
onFail: () => {
setOnPayment(false);
setConfirmPayment(false);
},
onError: () => {
setOnPayment(false);
setConfirmPayment(false);
} }
}); });
} }
@@ -551,6 +617,7 @@ const Index = () => {
setSelectedPaymentMethod={setSelectedPaymentMethod} setSelectedPaymentMethod={setSelectedPaymentMethod}
selectedPaymentMethod={selectedPaymentMethod} selectedPaymentMethod={selectedPaymentMethod}
setConfirmPayment={setConfirmPayment} setConfirmPayment={setConfirmPayment}
confirmPayment={confirmPayment}
getMethodImgClass = {getMethodImgClass} getMethodImgClass = {getMethodImgClass}
onReady = {onReady} onReady = {onReady}
locale = {locale} locale = {locale}


+ 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> </Button>
<input <input
id="uploadFileBtn" 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 { PNSPS_BUTTON_THEME, PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst";
import { ThemeProvider } from "@emotion/react"; import { ThemeProvider } from "@emotion/react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import SafeHtml from 'components/SafeHtml';
// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //


const Index = ({ record }) => { const Index = ({ record }) => {
@@ -107,7 +108,7 @@ const Index = ({ record }) => {
</Typography> </Typography>


<Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> <Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.payMsg1' }, { appNo: record.appNo }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.payMsg1' }, { appNo: record.appNo })} />
<br /> <br />
<FormattedMessage id="MSG.payMsg2_1" /> <FormattedMessage id="MSG.payMsg2_1" />
<span style={{ color: "red" }}> <span style={{ color: "red" }}>
@@ -116,14 +117,12 @@ const Index = ({ record }) => {
<FormattedMessage id="MSG.payMsg2_2" /> <FormattedMessage id="MSG.payMsg2_2" />
<br /> <br />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'MSG.payMsg3' },
<SafeHtml html={intl.formatMessage({ id: 'MSG.payMsg3' },
{ {
issueYear: record?.issueYear, issueYear: record?.issueYear,
issueVolume: record?.issueVolume, issueVolume: record?.issueVolume,
issueNo: record?.issueNo, issueNo: record?.issueNo,
})
}} />
})} />
</Typography> </Typography>


<Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> <Typography variant="h4" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
@@ -213,7 +212,7 @@ const Index = ({ record }) => {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}> <DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between"> <Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack> </Stack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>


+ 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 titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import SafeHtml from 'components/SafeHtml';
const BackgroundHead = { const BackgroundHead = {
backgroundImage: `url(${titleBackgroundImg})`, backgroundImage: `url(${titleBackgroundImg})`,
width: '100%', width: '100%',
@@ -76,19 +77,16 @@ const isAfter = checkIsOnlyOnlinePayment();
)} )}


<Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_demandNote' id: 'proofPaymentBody_demandNote'
}, },
{ {
appNo: record?.appNo, appNo: record?.appNo,
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_demandNote2' id: 'proofPaymentBody_demandNote2'
}, },
@@ -96,27 +94,22 @@ const isAfter = checkIsOnlyOnlinePayment();
closingDateOff: DateUtils.dateFormat(record?.closingDateOff, intl.formatMessage({id: "dateStrFormat"})), closingDateOff: DateUtils.dateFormat(record?.closingDateOff, intl.formatMessage({id: "dateStrFormat"})),
email: record?.mail, email: record?.mail,
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_demandNote3' id: 'proofPaymentBody_demandNote3'
}, },
{ {
paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})),
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_demandNote4' id: 'proofPaymentBody_demandNote4'
} }
)
}} />
)} />




</Typography> </Typography>


+ 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 titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import SafeHtml from 'components/SafeHtml';
const BackgroundHead = { const BackgroundHead = {
backgroundImage: `url(${titleBackgroundImg})`, backgroundImage: `url(${titleBackgroundImg})`,
width: '100%', width: '100%',
@@ -78,59 +79,47 @@ const isAfter = checkIsOnlyOnlinePayment();




<Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}> <Typography variant="h5" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "left" }}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_office' id: 'proofPaymentBody_office'
}, },
{ {
appNo: record?.appNo, appNo: record?.appNo,
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_office2' id: 'proofPaymentBody_office2'
}, },
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_office3' id: 'proofPaymentBody_office3'
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_office4' id: 'proofPaymentBody_office4'
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_office5' id: 'proofPaymentBody_office5'
}, },
{ {
paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})), paymentDeadline: DateUtils.dateFormat(record?.closingDate, intl.formatMessage({id: "dateStrFormat"})),
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_office6' id: 'proofPaymentBody_office6'
} }
)
}} />
)} />




</Typography> </Typography>


+ 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'))); const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));


import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
import SafeHtml from 'components/SafeHtml';
const BackgroundHead = { const BackgroundHead = {
backgroundImage: `url(${titleBackgroundImg})`, backgroundImage: `url(${titleBackgroundImg})`,
width: '100%', width: '100%',
@@ -195,32 +196,28 @@ const Index = () => {
)} )}


<Typography variant="h5" sx={bodySx}> <Typography variant="h5" sx={bodySx}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_online' id: 'proofPaymentBody_online'
}, },
{ {
appNo: record?.appNo, appNo: record?.appNo,
} }
)
}} />
)} />
<br /> <br />
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_online2' id: 'proofPaymentBody_online2'
}, },
{ {
paymentDeadline: DateUtils.formatDateForLocale(record?.expiryDate, intl, locale), paymentDeadline: DateUtils.formatDateForLocale(record?.expiryDate, intl, locale),
} }
)
}} />
)} />
<br /> <br />
{checkPaymentSuspension()? {checkPaymentSuspension()?
<div> <div>
<Typography style={{ textAlign: "flex-start", color: "red" }}> <Typography style={{ textAlign: "flex-start", color: "red" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography> </Typography>
<br /> <br />
</div>:null </div>:null
@@ -272,13 +269,11 @@ const Index = () => {
} }


<Typography variant="h4" sx={bodySx}> <Typography variant="h4" sx={bodySx}>
<div dangerouslySetInnerHTML={{
__html: intl.formatMessage(
<SafeHtml html={intl.formatMessage(
{ {
id: 'proofPaymentBody_online3' id: 'proofPaymentBody_online3'
} }
)
}} />
)} />
<br /> <br />
</Typography> </Typography>


@@ -338,7 +333,7 @@ const Index = () => {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}> <DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between"> <Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack> </Stack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>


+ 1
- 1
src/pages/Proof/Reply_GLD/ApplicationDetails.js Просмотреть файл

@@ -104,7 +104,7 @@ const ApplicationDetailCard = ({
if (cancellingRef.current) return; if (cancellingRef.current) return;
cancellingRef.current = true; cancellingRef.current = true;
setCancelLoading(true); setCancelLoading(true);
HttpUtils.get({
HttpUtils.post({
url: UrlUtils.CANCEL_PROOF + "/" + params.id, url: UrlUtils.CANCEL_PROOF + "/" + params.id,
onSuccess: function (responseData) { onSuccess: function (responseData) {
cancellingRef.current = false; cancellingRef.current = false;


+ 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 * as DateUtils from "utils/DateUtils"
import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
import { notifyActionSuccess } from 'utils/CommonFunction'; import { notifyActionSuccess } from 'utils/CommonFunction';
import SafeHtml from 'components/SafeHtml';
import { PNSPS_BUTTON_THEME } from "themes/buttonConst"; import { PNSPS_BUTTON_THEME } from "themes/buttonConst";
import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";
import { ThemeProvider } from "@emotion/react"; import { ThemeProvider } from "@emotion/react";
@@ -382,7 +383,7 @@ const FormPanel = ({ formData }) => {
<li><FormattedMessage id="post" /></li> <li><FormattedMessage id="post" /></li>
</ul> </ul>
<Typography variant="h6" component="span"> <Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography> </Typography>
</> </>
); );
@@ -468,7 +469,7 @@ const FormPanel = ({ formData }) => {
<FormattedMessage id="payOnline" /> <FormattedMessage id="payOnline" />
{checkPaymentSuspension()? {checkPaymentSuspension()?
<Typography style={{ padding: '16px', color: "red" }}> <Typography style={{ padding: '16px', color: "red" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography>:null </Typography>:null
} }
<br /><a href="#payOnlineDetails" color='#fff' onClick={() => { <br /><a href="#payOnlineDetails" color='#fff' onClick={() => {
@@ -536,7 +537,7 @@ const FormPanel = ({ formData }) => {
<li><FormattedMessage id="post" /></li> <li><FormattedMessage id="post" /></li>
</ul> </ul>
<Typography variant="h6" component="span"> <Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography> </Typography>
</> </>
); );
@@ -599,13 +600,13 @@ const FormPanel = ({ formData }) => {
} }
{/* <Grid item xs={12}> {/* <Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" > <Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography> </Typography>
</Grid> */} </Grid> */}
{!isOnlyOnlinePayment? {!isOnlyOnlinePayment?
<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" > <Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofImportant" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofImportant" })} style={{ padding: 12 }} />
</Typography> </Typography>
</Grid>:null </Grid>:null
} }


+ 12
- 28
src/pages/Proof/Search_GLD/DataGrid.js Просмотреть файл

@@ -21,9 +21,10 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead


const columns = [ const columns = [
{ {
field: 'actions',
field: 'refNo',
headerName: 'Proof No.', headerName: 'Proof No.',
width: 170, width: 170,
sortable: true,
cellClassName: 'actions', cellClassName: 'actions',
renderCell: (params) => { renderCell: (params) => {
return clickableLink('/proof/reply/' + params.row.id, params.row.refNo); return clickableLink('/proof/reply/' + params.row.id, params.row.refNo);
@@ -34,6 +35,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
field: 'appId', field: 'appId',
headerName: 'Application No./ Gazette Code/ Gazette Issue No.', headerName: 'Application No./ Gazette Code/ Gazette Issue No.',
width: 400, width: 400,
sortable: true,
renderCell: (params) => { renderCell: (params) => {
let appNo = params.row.appNo; let appNo = params.row.appNo;
let code = params.row.groupNo; let code = params.row.groupNo;
@@ -51,7 +53,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
headerName: 'Status', headerName: 'Status',
flex: 1, flex: 1,
minWidth: 150, minWidth: 150,
sortable: false,
sortable: true,
filterable: false, filterable: false,
valueGetter: () => '', valueGetter: () => '',
renderCell: (params) => { renderCell: (params) => {
@@ -64,23 +66,14 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
headerName: 'Proof Issue Date', headerName: 'Proof Issue Date',
flex: 1, flex: 1,
minWidth: 200, minWidth: 200,
// sorting/filtering uses this value
sortable: true,
valueGetter: (params) => DateUtils.toDate(params?.value), valueGetter: (params) => DateUtils.toDate(params?.value),

// display uses this (params.value is the *Date* returned above)
valueFormatter: (params) => { valueFormatter: (params) => {
const d = params.value; // Date or Invalid Date
const d = params.value;
return d instanceof Date && !isNaN(d.getTime()) return d instanceof Date && !isNaN(d.getTime())
? DateUtils.dateStr(d) ? DateUtils.dateStr(d)
: ""; : "";
}, },

// make sorting 100% deterministic
sortComparator: (v1, v2) => {
const t1 = v1 instanceof Date && !isNaN(v1.getTime()) ? v1.getTime() : -Infinity;
const t2 = v2 instanceof Date && !isNaN(v2.getTime()) ? v2.getTime() : -Infinity;
return t1 - t2;
}
}, },
{ {
id: 'replyDate', id: 'replyDate',
@@ -88,23 +81,14 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
headerName: 'Confirmed/ Return Date', headerName: 'Confirmed/ Return Date',
flex: 1, flex: 1,
minWidth: 200, minWidth: 200,
// sorting/filtering uses this value
sortable: true,
valueGetter: (params) => DateUtils.toDate(params?.value), valueGetter: (params) => DateUtils.toDate(params?.value),

// display uses this (params.value is the *Date* returned above)
valueFormatter: (params) => { valueFormatter: (params) => {
const d = params.value; // Date or Invalid Date
const d = params.value;
return d instanceof Date && !isNaN(d.getTime()) return d instanceof Date && !isNaN(d.getTime())
? DateUtils.dateStr(d) ? DateUtils.dateStr(d)
: ""; : "";
}, },

// make sorting 100% deterministic
sortComparator: (v1, v2) => {
const t1 = v1 instanceof Date && !isNaN(v1.getTime()) ? v1.getTime() : -Infinity;
const t2 = v2 instanceof Date && !isNaN(v2.getTime()) ? v2.getTime() : -Infinity;
return t1 - t2;
}
}, },
{ {
id: 'contactPerson', id: 'contactPerson',
@@ -112,6 +96,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
headerName: 'Client', headerName: 'Client',
flex: 1, flex: 1,
minWidth: 200, minWidth: 200,
sortable: true,
renderCell: (params) => { renderCell: (params) => {
let company = params.row.enCompanyName != null?params.row.enCompanyName: params.row.chCompanyName; let company = params.row.enCompanyName != null?params.row.enCompanyName: params.row.chCompanyName;
company = company != null ? company : ""; company = company != null ? company : "";
@@ -129,6 +114,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
headerName: 'Gazette Group', headerName: 'Gazette Group',
flex: 1, flex: 1,
minWidth: 200, minWidth: 200,
sortable: true,
valueGetter: (params) => { valueGetter: (params) => {
return (params?.value) ? (params?.value) : ""; return (params?.value) ? (params?.value) : "";
} }
@@ -139,6 +125,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
headerName: 'Amount ($)', headerName: 'Amount ($)',
flex: 1, flex: 1,
minWidth: 200, minWidth: 200,
sortable: true,
valueGetter: (params) => { valueGetter: (params) => {
return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : "";
} }
@@ -158,10 +145,7 @@ export default function SearchPublicNoticeTable({searchCriteria, applyGridOnRead
onRowDoubleClick={handleRowDoubleClick} onRowDoubleClick={handleRowDoubleClick}
applyGridOnReady={applyGridOnReady} applyGridOnReady={applyGridOnReady}
applySearch = {applySearch} applySearch = {applySearch}
// doLoad={{
// url: LIST_PROOF,
// params: _searchCriteria,
// }}
serverSorting
doLoad={React.useMemo(() => ({ doLoad={React.useMemo(() => ({
url: LIST_PROOF, url: LIST_PROOF,
params: _searchCriteria, params: _searchCriteria,


+ 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 {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14));
const getDefaultDateTo = () => DateUtils.dateValue(new Date());

const isSubmitDateEmpty = (value) =>
value == null || value === "" || value === "dd / mm / yyyy";

const isBlank = (value) => value == null || String(value).trim() === "";

const toDayjsOrNull = (value) => {
if (isSubmitDateEmpty(value)) return null;
const d = dayjs(value);
return d.isValid() ? d : null;
};

// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady
}) => { }) => {
@@ -32,7 +47,7 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
const [issueCombo, setIssueCombo] = React.useState([]); const [issueCombo, setIssueCombo] = React.useState([]);
const [groupSelected, setGroupSelected] = React.useState( const [groupSelected, setGroupSelected] = React.useState(
searchCriteria.gazettGroup != undefined searchCriteria.gazettGroup != undefined
? ComboData.groupTitle.find(item => item.code === searchCriteria.gazettGroup) ?? null
? ComboData.groupTitle.find(item => item.title === searchCriteria.gazettGroup) ?? null
: null : null
); );


@@ -40,19 +55,8 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo);
const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy");
const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy");
const prevHasOtherRef = React.useRef(null);


// React.useEffect(() => {
// if(searchCriteria.status!=undefined){
// if(searchCriteria.status === ""){
// ComboData.proofStatus_GLD[0]
// }else{
// setSelectedStatus(ComboData.proofStatus_GLD.find(item => item.type === searchCriteria.status))
// }
// }else{
// setSelectedStatus(ComboData.proofStatus_GLD[0])
// }
// }, [searchCriteria]);
React.useEffect(() => { React.useEffect(() => {
setFromDateValue(minDate); setFromDateValue(minDate);
}, [minDate]); }, [minDate]);
@@ -65,7 +69,55 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
const { locale } = intl; const { locale } = intl;


const marginBottom = 2.5; const marginBottom = 2.5;
const { reset, register, handleSubmit } = useForm()
const { reset, register, handleSubmit, watch } = useForm({
defaultValues: {
refNo: searchCriteria.refNo || "",
code: searchCriteria.code || "",
contact: searchCriteria.contact || ""
}
});
const refNo = watch("refNo");
const code = watch("code");
const contact = watch("contact");

const clearSubmitDates = () => {
setMinDate(null);
setMaxDate(null);
};

const restoreDefaultSubmitDates = () => {
setMinDate(getDefaultDateFrom());
setMaxDate(getDefaultDateTo());
};

const hasOtherCriteria = (textFields = {}) => {
if (!isBlank(textFields.refNo)) return true;
if (!isBlank(textFields.code)) return true;
if (!isBlank(textFields.contact)) return true;
if (groupSelected?.title) return true;
if (orgSelected?.key && orgSelected.key > 0) return true;
if (issueSelected?.id) return true;
if (status?.type && status.type !== "all" && status.type !== "") return true;
return false;
};

React.useEffect(() => {
const hasOther = hasOtherCriteria({ refNo, code, contact });
if (prevHasOtherRef.current === null) {
prevHasOtherRef.current = hasOther;
return;
}
if (hasOther && !prevHasOtherRef.current) {
clearSubmitDates();
} else if (!hasOther && prevHasOtherRef.current) {
// Only refill defaults when From or To is empty; keep user-entered dates otherwise
if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) {
restoreDefaultSubmitDates();
}
}
prevHasOtherRef.current = hasOther;
}, [refNo, code, contact, groupSelected, orgSelected, issueSelected, status]);

const onSubmit = (data) => { const onSubmit = (data) => {
let typeArray = []; let typeArray = [];
let sentDateFrom = ""; let sentDateFrom = "";
@@ -75,24 +127,49 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
typeArray.push(type[i].label); typeArray.push(type[i].label);
} }


if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") {
sentDateFrom = DateUtils.dateValue(fromDateValue)
sentDateTo = DateUtils.dateValue(toDateValue)
const hasOther = hasOtherCriteria({
refNo: data.refNo,
code: data.code,
contact: data.contact
});
const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue)
|| minDate == null || maxDate == null;

if (!hasOther && datesEmpty) {
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
sentDateFrom = dateFrom;
sentDateTo = dateTo;
} else if (!datesEmpty) {
sentDateFrom = DateUtils.dateValue(fromDateValue);
sentDateTo = DateUtils.dateValue(toDateValue);
} }
const temp = { const temp = {
refNo: data.refNo, refNo: data.refNo,
code: data.code, code: data.code,
issueId: issueSelected?.id, issueId: issueSelected?.id,
gazettGroup: groupSelected?.code,
gazettGroup: groupSelected?.title,
dateFrom: sentDateFrom, dateFrom: sentDateFrom,
dateTo: sentDateTo, dateTo: sentDateTo,
contact: data.contact, contact: data.contact,
orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "", orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "",
statusKey:status?.key, statusKey:status?.key,

start: 0,
limit: 10,
}; };


if (issueSelected?.id) {
delete temp.start;
delete temp.limit;
}

if (searchCriteria?.sort && searchCriteria?.direction) {
temp.sort = searchCriteria.sort;
temp.direction = searchCriteria.direction;
}


if(status?.type && status?.type != 'all'){ if(status?.type && status?.type != 'all'){
if (status?.type == "Confirmed"){ if (status?.type == "Confirmed"){
@@ -130,18 +207,34 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o


function resetForm() { function resetForm() {
setType([]); setType([]);
setStatus(ComboData.proofStatus[0]);
setStatus(ComboData.proofStatus_GLD[0]);
setOrgSelected(null); setOrgSelected(null);
setIssueSelected(null); setIssueSelected(null);
setGroupSelected(null); setGroupSelected(null);
setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14)))
setMaxDate(DateUtils.dateValue(new Date()))
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
reset({ reset({
refNo:"", refNo:"",
code:"", code:"",
contact:"" contact:""
}); });
localStorage.setItem('searchCriteria',"")
prevHasOtherRef.current = false;
localStorage.setItem('searchCriteria',"");
applySearch({
refNo: "",
code: "",
issueId: "",
gazettGroup: "",
dateFrom,
dateTo,
contact: "",
orgId: "",
statusKey: 0,
start: 0,
limit: 10
});
} }


function getIssueLabel(data) { function getIssueLabel(data) {
@@ -279,22 +372,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateFrom" id="dateFrom"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label="Proof Issue Date (From)" label="Proof Issue Date (From)"
value={minDate === null ? null : dayjs(minDate)}
maxDate={maxDate === null ? null : dayjs(maxDate)}
value={toDayjsOrNull(minDate)}
maxDate={toDayjsOrNull(maxDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMinDate(newValue);
}
setMinDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >
@@ -306,22 +398,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateTo" id="dateTo"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label="Proof Issue Date (To)" label="Proof Issue Date (To)"
value={maxDate === null ? null : dayjs(maxDate)}
minDate={minDate === null ? null : dayjs(minDate)}
value={toDayjsOrNull(maxDate)}
minDate={toDayjsOrNull(minDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMaxDate(newValue);
}
setMaxDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >


+ 11
- 2
src/pages/Proof/Search_Public/DataGrid.js Просмотреть файл

@@ -106,10 +106,11 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea


const columns = [ const columns = [
{ {
field: 'actions',
field: 'refNo',
headerName: intl.formatMessage({ id: 'proofId' }), headerName: intl.formatMessage({ id: 'proofId' }),
width: isMdOrLg ? 'auto' : 200, width: isMdOrLg ? 'auto' : 200,
flex: isMdOrLg ? 1.5 : undefined, flex: isMdOrLg ? 1.5 : undefined,
sortable: true,
cellClassName: 'actions', cellClassName: 'actions',
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
renderCell: (params) => { renderCell: (params) => {
@@ -122,6 +123,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount3' }) : intl.formatMessage({ id: 'gazetteCount2' }), headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount3' }) : intl.formatMessage({ id: 'gazetteCount2' }),
width: isMdOrLg ? 'auto' : 330, width: isMdOrLg ? 'auto' : 330,
flex: isMdOrLg ? 2 : undefined, flex: isMdOrLg ? 2 : undefined,
sortable: true,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
renderCell: (params) => { renderCell: (params) => {
// let appNo = params.row.appNo; // let appNo = params.row.appNo;
@@ -138,6 +140,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
headerName: intl.formatMessage({ id: 'proofDate' }), headerName: intl.formatMessage({ id: 'proofDate' }),
width: isMdOrLg ? 'auto' : 200, width: isMdOrLg ? 'auto' : 200,
flex: isMdOrLg ? 1.5 : undefined, flex: isMdOrLg ? 1.5 : undefined,
sortable: true,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
valueGetter: (params) => { valueGetter: (params) => {
return DateUtils.datetimeStr(params?.value); return DateUtils.datetimeStr(params?.value);
@@ -149,6 +152,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
headerName: intl.formatMessage({ id: 'replyBefore' }), headerName: intl.formatMessage({ id: 'replyBefore' }),
width: isMdOrLg ? 'auto' : 200, width: isMdOrLg ? 'auto' : 200,
flex: isMdOrLg ? 1.5 : undefined, flex: isMdOrLg ? 1.5 : undefined,
sortable: true,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
valueGetter: (params) => { valueGetter: (params) => {
const proofPaymentDeadline = DateUtils.convertToDate(params?.value); const proofPaymentDeadline = DateUtils.convertToDate(params?.value);
@@ -164,6 +168,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
headerName: intl.formatMessage({ id: 'replyDate' }), headerName: intl.formatMessage({ id: 'replyDate' }),
width: isMdOrLg ? 'auto' : 200, width: isMdOrLg ? 'auto' : 200,
flex: isMdOrLg ? 1.5 : undefined, flex: isMdOrLg ? 1.5 : undefined,
sortable: true,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
valueGetter: (params) => { valueGetter: (params) => {
return params?.value ? DateUtils.datetimeStr(params?.value) : ""; return params?.value ? DateUtils.datetimeStr(params?.value) : "";
@@ -174,6 +179,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
headerName: intl.formatMessage({ id: 'status' }), headerName: intl.formatMessage({ id: 'status' }),
width: isMdOrLg ? 'auto' : 160, width: isMdOrLg ? 'auto' : 160,
flex: isMdOrLg ? 1 : undefined, flex: isMdOrLg ? 1 : undefined,
sortable: true,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
renderCell: (params) => { renderCell: (params) => {
return locale === 'en' ? ProofStatus.getStatus_Eng(params) : locale === 'zh-HK' ? ProofStatus.getStatus_Cht(params) : ProofStatus.getStatus_Cn(params); return locale === 'en' ? ProofStatus.getStatus_Eng(params) : locale === 'zh-HK' ? ProofStatus.getStatus_Cht(params) : ProofStatus.getStatus_Cn(params);
@@ -185,6 +191,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
headerName: intl.formatMessage({ id: 'fee' }), headerName: intl.formatMessage({ id: 'fee' }),
width: isMdOrLg ? 'auto' : 160, width: isMdOrLg ? 'auto' : 160,
flex: isMdOrLg ? 1 : undefined, flex: isMdOrLg ? 1 : undefined,
sortable: true,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
valueGetter: (params) => { valueGetter: (params) => {
return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : ""; return (params?.value) ? "$ " + FormatUtils.currencyFormat(params?.value) : "";
@@ -207,6 +214,8 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
onRowDoubleClick={handleRowDoubleClick} onRowDoubleClick={handleRowDoubleClick}
applyGridOnReady={applyGridOnReady} applyGridOnReady={applyGridOnReady}
applySearch={applySearch} applySearch={applySearch}
serverSorting
disablePagingOnGazetteIssue={false}
doLoad={React.useMemo(() => ({ doLoad={React.useMemo(() => ({
url: LIST_PROOF, url: LIST_PROOF,
params: _searchCriteria, params: _searchCriteria,
@@ -214,4 +223,4 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
/> />
</div> </div>
); );
}
}

+ 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 {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14));
const getDefaultDateTo = () => DateUtils.dateValue(new Date());

const isSubmitDateEmpty = (value) =>
value == null || value === "" || value === "dd / mm / yyyy";

const isBlank = (value) => value == null || String(value).trim() === "";

const toDayjsOrNull = (value) => {
if (isSubmitDateEmpty(value)) return null;
const d = dayjs(value);
return d.isValid() ? d : null;
};

// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady
}) => { }) => {
@@ -31,12 +46,13 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
const [status, setStatus] = React.useState(searchCriteria.statusKey!=undefined?ComboData.proofStatusFull[searchCriteria.statusKey]:ComboData.proofStatusFull[0]); const [status, setStatus] = React.useState(searchCriteria.statusKey!=undefined?ComboData.proofStatusFull[searchCriteria.statusKey]:ComboData.proofStatusFull[0]);
const [issueSelected, setIssueSelected] = React.useState(null); const [issueSelected, setIssueSelected] = React.useState(null);
const [issueCombo, setIssueCombo] = React.useState([]); const [issueCombo, setIssueCombo] = React.useState([]);
const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.code === searchCriteria.gazettGroup):{});
const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.title === searchCriteria.gazettGroup):{});


const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom); const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom);
const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo);
const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy");
const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy");
const prevHasOtherRef = React.useRef(null);


React.useEffect(() => { React.useEffect(() => {
setFromDateValue(minDate); setFromDateValue(minDate);
@@ -62,7 +78,50 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
} }
} }


const { reset, register, handleSubmit } = useForm()
const { reset, register, handleSubmit, watch } = useForm({
defaultValues: {
refNo: searchCriteria.refNo || "",
code: searchCriteria.code || ""
}
});
const refNo = watch("refNo");
const code = watch("code");

const clearSubmitDates = () => {
setMinDate(null);
setMaxDate(null);
};

const restoreDefaultSubmitDates = () => {
setMinDate(getDefaultDateFrom());
setMaxDate(getDefaultDateTo());
};

const hasOtherCriteria = (textFields = {}) => {
if (!isBlank(textFields.refNo)) return true;
if (!isBlank(textFields.code)) return true;
if (issueSelected?.id) return true;
if (status?.type && status.type !== "all" && status.type !== "") return true;
return false;
};

React.useEffect(() => {
const hasOther = hasOtherCriteria({ refNo, code });
if (prevHasOtherRef.current === null) {
prevHasOtherRef.current = hasOther;
return;
}
if (hasOther && !prevHasOtherRef.current) {
clearSubmitDates();
} else if (!hasOther && prevHasOtherRef.current) {
// Only refill defaults when From or To is empty; keep user-entered dates otherwise
if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) {
restoreDefaultSubmitDates();
}
}
prevHasOtherRef.current = hasOther;
}, [refNo, code, issueSelected, status]);

const onSubmit = (data) => { const onSubmit = (data) => {
let typeArray = []; let typeArray = [];
let sentDateFrom = ""; let sentDateFrom = "";
@@ -72,20 +131,42 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
typeArray.push(type[i].label); typeArray.push(type[i].label);
} }


if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") {
sentDateFrom = DateUtils.dateValue(fromDateValue)
sentDateTo = DateUtils.dateValue(toDateValue)
const hasOther = hasOtherCriteria({
refNo: data.refNo,
code: data.code
});
const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue)
|| minDate == null || maxDate == null;

if (!hasOther && datesEmpty) {
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
sentDateFrom = dateFrom;
sentDateTo = dateTo;
} else if (!datesEmpty) {
sentDateFrom = DateUtils.dateValue(fromDateValue);
sentDateTo = DateUtils.dateValue(toDateValue);
} }
const temp = { const temp = {
refNo: data.refNo, refNo: data.refNo,
code: data.code, code: data.code,
issueId: issueSelected?.id, issueId: issueSelected?.id,
gazettGroup: groupSelected?.type,
gazettGroup: groupSelected?.title,
dateFrom: sentDateFrom, dateFrom: sentDateFrom,
dateTo: sentDateTo, dateTo: sentDateTo,
statusKey:status?.key, statusKey:status?.key,
start: 0,
limit: 10,
}; };

if (searchCriteria?.sort && searchCriteria?.direction) {
temp.sort = searchCriteria.sort;
temp.direction = searchCriteria.direction;
}

if(status?.type && status?.type != 'all'){ if(status?.type && status?.type != 'all'){
if (status?.type == "Confirmed"){ if (status?.type == "Confirmed"){
temp["replyed"] = "T"; temp["replyed"] = "T";
@@ -120,13 +201,27 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
setStatus(ComboData.proofStatusFull[0]); setStatus(ComboData.proofStatusFull[0]);
setIssueSelected(null); setIssueSelected(null);
setGroupSelected({}); setGroupSelected({});
setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14)))
setMaxDate(DateUtils.dateValue(new Date()))
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
reset({ reset({
refNo:"", refNo:"",
code:"", code:"",
}); });
localStorage.setItem('searchCriteria',"")
prevHasOtherRef.current = false;
localStorage.setItem('searchCriteria',"");
applySearch({
refNo: "",
code: "",
issueId: "",
gazettGroup: "",
dateFrom,
dateTo,
statusKey: 0,
start: 0,
limit: 10
});
} }


function getIssueLabel(data) { function getIssueLabel(data) {
@@ -261,22 +356,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateFrom" id="dateFrom"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label={intl.formatMessage({id: 'proofDateFrom'})} label={intl.formatMessage({id: 'proofDateFrom'})}
value={minDate === null ? null : dayjs(minDate)}
maxDate={maxDate === null ? null : dayjs(maxDate)}
value={toDayjsOrNull(minDate)}
maxDate={toDayjsOrNull(maxDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMinDate(newValue);
}
setMinDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >
@@ -288,22 +382,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateTo" id="dateTo"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label={intl.formatMessage({id: 'proofDateTo'})} label={intl.formatMessage({id: 'proofDateTo'})}
value={maxDate === null ? null : dayjs(maxDate)}
minDate={minDate === null ? null : dayjs(minDate)}
value={toDayjsOrNull(maxDate)}
minDate={toDayjsOrNull(minDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMaxDate(newValue);
}
setMaxDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >


+ 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 { useState, useEffect, useRef } from 'react';
import { checkPaymentSuspension } from "utils/Utils"; import { checkPaymentSuspension } from "utils/Utils";
import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";
import SafeHtml from 'components/SafeHtml';


// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //


@@ -409,13 +410,12 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
ml:2 ml:2
}} }}
> >
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
id: "suspensionMessageText",
defaultMessage: ""
})
}}
<SafeHtml
component="span"
html={intl.formatMessage({
id: "suspensionMessageText",
defaultMessage: ""
})}
/> />
</Typography> </Typography>
)} )}
@@ -459,7 +459,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
<li><FormattedMessage id="post" /></li> <li><FormattedMessage id="post" /></li>
</ul> </ul>
<Typography variant="h6" component="span"> <Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography> </Typography>
</> </>
); );
@@ -603,14 +603,14 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>


<Grid item xs={12} mr={1} mb={2}> <Grid item xs={12} mr={1} mb={2}>
<Typography variant="pnspsFormParagraphBold"> <Typography variant="pnspsFormParagraphBold">
<span style={{ textAlign: 'justify', }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr0" }) }} />
<SafeHtml component="span" style={{ textAlign: 'justify', }} html={intl.formatMessage({ id: "applyTickUnderStr0" })} />
</Typography> </Typography>
<Typography display="inline" variant="subtitle1" component="span" sx={{ color: 'primary.primary' }} > <Typography display="inline" variant="subtitle1" component="span" sx={{ color: 'primary.primary' }} >
<ol style={{ textAlign: 'justify', }}> <ol style={{ textAlign: 'justify', }}>
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr1" }) }} />
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr2" }) }} />
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickUnderStr3" }) }} />
<li dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "tradeMarkFootnote" }) }} />
<SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr1" })} />
<SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr2" })} />
<SafeHtml component="li" html={intl.formatMessage({ id: "applyTickUnderStr3" })} />
<SafeHtml component="li" html={intl.formatMessage({ id: "tradeMarkFootnote" })} />
</ol> </ol>
</Typography> </Typography>
</Grid> </Grid>
@@ -630,7 +630,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>
}} }}
/> />
<Typography variant="h6" component="span" height="100%" > <Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12, textAlign: 'justify' }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyTickStr" }) }} />
<SafeHtml html={intl.formatMessage({ id: "applyTickStr" })} style={{ padding: 12, textAlign: 'justify' }} />
</Typography> </Typography>
</Stack> </Stack>
</Grid> </Grid>
@@ -653,7 +653,7 @@ const PublicNoticeApplyForm = ({ loadedData, _selections, gazetteIssueList }) =>


<Grid item xs={12}> <Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" > <Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "applyPublicNoticeText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "applyPublicNoticeText" })} style={{ padding: 12 }} />
</Typography> </Typography>
</Grid> </Grid>




+ 6
- 6
src/pages/PublicNotice/Details_GLD/index.js Просмотреть файл

@@ -274,7 +274,7 @@ const PublicNoticeDetail_GLD = () => {
setStatusWindowAccepted(false); setStatusWindowAccepted(false);
return; return;
} }
HttpUtils.get({
HttpUtils.post({
url: `${SET_PUBLIC_NOTICE_STATUS_PUBLISH}/${params.id}`, url: `${SET_PUBLIC_NOTICE_STATUS_PUBLISH}/${params.id}`,
onSuccess: function () { onSuccess: function () {
setOpen(false); setOpen(false);
@@ -299,7 +299,7 @@ const PublicNoticeDetail_GLD = () => {


const onPaidClick = () => { const onPaidClick = () => {
if (params.id > 0) { if (params.id > 0) {
axios.get(`${SET_PUBLIC_NOTICE_STATUS_PAID}/${params.id}`)
axios.post(`${SET_PUBLIC_NOTICE_STATUS_PAID}/${params.id}`)
.then((response) => { .then((response) => {
if (response.status === 204) { if (response.status === 204) {
setOpen(false); setOpen(false);
@@ -325,7 +325,7 @@ const PublicNoticeDetail_GLD = () => {


const onComplatedClick = () => { const onComplatedClick = () => {
if (params.id > 0) { if (params.id > 0) {
axios.get(`${SET_PUBLIC_NOTICE_STATUS_COMPLATED}/${params.id}`)
axios.post(`${SET_PUBLIC_NOTICE_STATUS_COMPLATED}/${params.id}`)
.then((response) => { .then((response) => {
if (response.status === 204) { if (response.status === 204) {
setOpen(false); setOpen(false);
@@ -351,7 +351,7 @@ const PublicNoticeDetail_GLD = () => {


const onWithdrawnClick = () => { const onWithdrawnClick = () => {
if (params.id > 0) { if (params.id > 0) {
axios.get(`${SET_PUBLIC_NOTICE_STATUS_WITHDRAW}/${params.id}`)
axios.post(`${SET_PUBLIC_NOTICE_STATUS_WITHDRAW}/${params.id}`)
.then((response) => { .then((response) => {
if (response.status === 204) { if (response.status === 204) {
setOpen(false); setOpen(false);
@@ -421,7 +421,7 @@ const PublicNoticeDetail_GLD = () => {
setStatusWindowAccepted(false); setStatusWindowAccepted(false);
return; return;
} }
HttpUtils.get({
HttpUtils.post({
url: `${SET_PUBLIC_NOTICE_STATUS_REVOKE}/${params.id}`, url: `${SET_PUBLIC_NOTICE_STATUS_REVOKE}/${params.id}`,
onSuccess: function () { onSuccess: function () {
setOpen(false); setOpen(false);
@@ -447,7 +447,7 @@ const PublicNoticeDetail_GLD = () => {
useEffect(() => { useEffect(() => {
const status = applicationDetailData.data != undefined ? applicationDetailData.data.status : "" const status = applicationDetailData.data != undefined ? applicationDetailData.data.status : ""
if (status === "submitted" && params.id > 0 && getUploadStatus) { if (status === "submitted" && params.id > 0 && getUploadStatus) {
axios.get(`${SET_PUBLIC_NOTICE_STATUS_REVIEWED}/${params.id}`)
axios.post(`${SET_PUBLIC_NOTICE_STATUS_REVIEWED}/${params.id}`)
.then((response) => { .then((response) => {
if (response.status === 204) { if (response.status === 204) {
setUploadStatus(false); setUploadStatus(false);


+ 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 DownloadIcon from '@mui/icons-material/Download';
import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst";
import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";
import SafeHtml from 'components/SafeHtml';
import { ThemeProvider } from "@emotion/react"; import { ThemeProvider } from "@emotion/react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
@@ -114,6 +115,13 @@ const ApplicationDetailCard = (
setStatus("cancel") setStatus("cancel")
}; };


const isPastPaymentDeadline = (expiryDate) => {
if (!expiryDate) return false;
const deadline = DateUtils.convertToDate(expiryDate);
if (!deadline || Number.isNaN(deadline.getTime())) return false;
return Date.now() >= deadline.getTime();
};

const checkExprityDate = () => { const checkExprityDate = () => {
HttpUtils.post({ HttpUtils.post({
url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE, url: UrlUtils.POST_CHECK_APP_EXPRITY_DATE,
@@ -202,7 +210,8 @@ const ApplicationDetailCard = (
> >
<ThemeProvider theme={PNSPS_BUTTON_THEME}> <ThemeProvider theme={PNSPS_BUTTON_THEME}>
{ {
currentApplicationDetailData.status == "confirmed" ?
currentApplicationDetailData.status == "confirmed"
&& !isPastPaymentDeadline(currentApplicationDetailData.expiryDate) ?
<Button <Button
variant="contained" variant="contained"
color="create" color="create"
@@ -834,7 +843,7 @@ const ApplicationDetailCard = (
<Typography variant="h4" component="span" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> <Typography variant="h4" component="span" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}> <DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between"> <Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack> </Stack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>


+ 1
- 1
src/pages/PublicNotice/Details_Public/index.js Просмотреть файл

@@ -145,7 +145,7 @@ const DashboardDefault = () => {
if (params.id > 0) { if (params.id > 0) {
cancellingRef.current = true; cancellingRef.current = true;
setCancelLoading(true); setCancelLoading(true);
axios.get(`${SET_PUBLIC_NOTICE_STATUS_CANCELLED}/${params.id}`)
axios.post(`${SET_PUBLIC_NOTICE_STATUS_CANCELLED}/${params.id}`)
.then((response) => { .then((response) => {
if (response.status === 204) { if (response.status === 204) {
notifyActionSuccess("取消成功!") notifyActionSuccess("取消成功!")


+ 2
- 1
src/pages/PublicNotice/ListPanel/PendingPaymentTab.js Просмотреть файл

@@ -28,6 +28,7 @@ import {
// import { dateStr } from "utils/DateUtils"; // import { dateStr } from "utils/DateUtils";
import { ThemeProvider, useTheme } from "@emotion/react"; import { ThemeProvider, useTheme } from "@emotion/react";
import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst"; import { PNSPS_BUTTON_THEME } from "../../../themes/buttonConst";
import SafeHtml from 'components/SafeHtml';
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { PRIMARY_TEXT_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst"; import { PRIMARY_TEXT_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";
// ==============================|| EVENT TABLE ||============================== // // ==============================|| EVENT TABLE ||============================== //
@@ -580,7 +581,7 @@ export default function SubmittedTab({ setCount, url }) {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography> <Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}> <DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between"> <Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack> </Stack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>


+ 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 {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";


const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14));
const getDefaultDateTo = () => DateUtils.dateValue(new Date());

const isSubmitDateEmpty = (value) =>
value == null || value === "" || value === "dd / mm / yyyy";

const isBlank = (value) => value == null || String(value).trim() === "";

const toDayjsOrNull = (value) => {
if (isSubmitDateEmpty(value)) return null;
const d = dayjs(value);
return d.isValid() ? d : null;
};

// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => { const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) => {
const intl = useIntl(); const intl = useIntl();
@@ -33,9 +47,19 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo);
const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy");
const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy");
const prevHasOtherRef = React.useRef(null);
// const [selectedLabelsString, setSelectedLabelsString] = React.useState(''); // const [selectedLabelsString, setSelectedLabelsString] = React.useState('');


const { reset, register, handleSubmit } = useForm()
const { reset, register, handleSubmit, watch } = useForm({
defaultValues: {
appNo: searchCriteria.appNo || "",
careOf: searchCriteria.careOf || "",
contact: searchCriteria.contact || ""
}
});
const appNo = watch("appNo");
const careOf = watch("careOf");
const contact = watch("contact");
const marginBottom = 2.5; const marginBottom = 2.5;


React.useEffect(() => { React.useEffect(() => {
@@ -70,6 +94,41 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
setToDateValue(maxDate); setToDateValue(maxDate);
}, [maxDate]); }, [maxDate]);


const clearSubmitDates = () => {
setMinDate(null);
setMaxDate(null);
};

const restoreDefaultSubmitDates = () => {
setMinDate(getDefaultDateFrom());
setMaxDate(getDefaultDateTo());
};

const hasOtherCriteria = (textFields = {}) => {
if (!isBlank(textFields.appNo)) return true;
if (!isBlank(textFields.contact)) return true;
if (!isBlank(textFields.careOf)) return true;
if (status?.type && status.type !== "all" && status.type !== "") return true;
return false;
};

React.useEffect(() => {
const hasOther = hasOtherCriteria({ appNo, contact, careOf });
if (prevHasOtherRef.current === null) {
prevHasOtherRef.current = hasOther;
return;
}
if (hasOther && !prevHasOtherRef.current) {
clearSubmitDates();
} else if (!hasOther && prevHasOtherRef.current) {
// Only refill defaults when From or To is empty; keep user-entered dates otherwise
if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) {
restoreDefaultSubmitDates();
}
}
prevHasOtherRef.current = hasOther;
}, [appNo, contact, careOf, status]);

const onSubmit = (data) => { const onSubmit = (data) => {
data.status = status.type; data.status = status.type;
let typeArray = []; let typeArray = [];
@@ -80,9 +139,24 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
typeArray.push(type[i].label); typeArray.push(type[i].label);
} }


if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") {
sentDateFrom = DateUtils.dateValue(fromDateValue)
sentDateTo = DateUtils.dateValue(toDateValue)
const hasOther = hasOtherCriteria({
appNo: data.appNo,
contact: data.contact,
careOf: data.careOf
});
const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue)
|| minDate == null || maxDate == null;

if (!hasOther && datesEmpty) {
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
sentDateFrom = dateFrom;
sentDateTo = dateTo;
} else if (!datesEmpty) {
sentDateFrom = DateUtils.dateValue(fromDateValue);
sentDateTo = DateUtils.dateValue(toDateValue);
} }


const temp = { const temp = {
@@ -95,18 +169,38 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
start:0, start:0,
limit:10 limit:10
}; };
if (searchCriteria?.sort && searchCriteria?.direction) {
temp.sort = searchCriteria.sort;
temp.direction = searchCriteria.direction;
}
applySearch(temp); applySearch(temp);
}; };


function resetForm() { function resetForm() {
setType([]); setType([]);
setStatus(localStorage.getItem('userData').creditor?ComboData.publicNoticeStatic_Creditor[0]:ComboData.publicNoticeStatic[0]); setStatus(localStorage.getItem('userData').creditor?ComboData.publicNoticeStatic_Creditor[0]:ComboData.publicNoticeStatic[0]);
setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14)))
setMaxDate(DateUtils.dateValue(new Date()))
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
reset({ reset({
appNo:""
appNo: "",
careOf: "",
contact: ""
});
prevHasOtherRef.current = false;
localStorage.setItem('searchCriteria', "");
// Reset form criteria and sorting first, then reload with backend default sort
applySearch({
appNo: "",
dateFrom,
dateTo,
contact: "",
careOf: "",
status: "",
start: 0,
limit: 10
}); });
localStorage.setItem('searchCriteria',"")
} }


return ( return (
@@ -142,23 +236,22 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateFrom" id="dateFrom"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
aria-label={intl.formatMessage({id: 'submitDateFrom'})} aria-label={intl.formatMessage({id: 'submitDateFrom'})}
label={intl.formatMessage({id: 'submitDateFrom'})} label={intl.formatMessage({id: 'submitDateFrom'})}
value={minDate === null ? null : dayjs(minDate)}
maxDate={maxDate === null ? null : dayjs(maxDate)}
value={toDayjsOrNull(minDate)}
maxDate={toDayjsOrNull(maxDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMinDate(newValue);
}
setMinDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >
@@ -170,22 +263,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, onGridReady }) =>
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateTo" id="dateTo"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label={intl.formatMessage({id: 'submitDateTo'})} label={intl.formatMessage({id: 'submitDateTo'})}
value={maxDate === null ? null : dayjs(maxDate)}
minDate={minDate === null ? null : dayjs(minDate)}
value={toDayjsOrNull(maxDate)}
minDate={toDayjsOrNull(minDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMaxDate(newValue);
}
setMaxDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >


+ 6
- 26
src/pages/PublicNotice/ListPanel/SearchPublicNoticeTable.js Просмотреть файл

@@ -45,6 +45,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'appNo', id: 'appNo',
field: 'appNo', field: 'appNo',
headerName: intl.formatMessage({ id: 'applicationId' }), headerName: intl.formatMessage({ id: 'applicationId' }),
sortable: true,
width: isMdOrLg ? 'auto' : 160, width: isMdOrLg ? 'auto' : 160,
flex: isMdOrLg ? 1 : undefined, flex: isMdOrLg ? 1 : undefined,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
@@ -56,6 +57,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'created', id: 'created',
field: 'created', field: 'created',
headerName: intl.formatMessage({ id: 'submitDate' }), headerName: intl.formatMessage({ id: 'submitDate' }),
sortable: true,
width: isMdOrLg ? 'auto' : 160, width: isMdOrLg ? 'auto' : 160,
flex: isMdOrLg ? 1 : undefined, flex: isMdOrLg ? 1 : undefined,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
@@ -63,36 +65,11 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
return DateUtils.datetimeStr(params?.value); return DateUtils.datetimeStr(params?.value);
} }
}, },
// {
// id: 'contactPerson',
// field: 'contactPerson',
// headerName: '聯絡人',
// flex: 2,
// renderCell: (params) => {
// let phone = JSON.parse(params.row.contactTelNo);
// let faxNo = JSON.parse(params.row.contactFaxNo);

// let contact = "";
// if (phone) {
// contact = "電話: " + phone?.countryCode + " " + phone?.phoneNumber
// }

// if (faxNo && faxNo?.faxNumber) {
// if (contact != "")
// contact = contact + ", "
// contact = contact + "傳真:" + faxNo?.countryCode + " " + faxNo?.faxNumber
// }

// return (<>
// {params?.value}<br />
// {contact}
// </>);
// }
// },
{ {
id: 'remarks', id: 'remarks',
field: 'remarks', field: 'remarks',
headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount2_1' }) : intl.formatMessage({ id: 'myRemarks' }), headerName: isORGLoggedIn() ? intl.formatMessage({ id: 'gazetteCount2_1' }) : intl.formatMessage({ id: 'myRemarks' }),
sortable: true,
width: isMdOrLg ? 'auto' : 400, width: isMdOrLg ? 'auto' : 400,
flex: isMdOrLg ? 3 : undefined, flex: isMdOrLg ? 3 : undefined,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
@@ -124,6 +101,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'status', id: 'status',
field: 'status', field: 'status',
headerName: intl.formatMessage({ id: 'status' }), headerName: intl.formatMessage({ id: 'status' }),
sortable: true,
width: 200, width: 200,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
renderCell: (params) => { renderCell: (params) => {
@@ -134,6 +112,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
field: 'actions', field: 'actions',
type: 'actions', type: 'actions',
headerName: '', headerName: '',
sortable: false,
width: 150, width: 150,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
cellClassName: 'actions', cellClassName: 'actions',
@@ -157,6 +136,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
<FiDataGrid <FiDataGrid
columns={columns} columns={columns}
customPageSize={10} customPageSize={10}
serverSorting
getRowHeight={() => 'auto'} getRowHeight={() => 'auto'}
onRowDoubleClick={handleRowDoubleClick} onRowDoubleClick={handleRowDoubleClick}
applyGridOnReady = {applyGridOnReady} applyGridOnReady = {applyGridOnReady}


+ 16
- 9
src/pages/PublicNotice/Search_GLD/DataGrid.js Просмотреть файл

@@ -67,9 +67,9 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea


const columns = [ const columns = [
{ {
field: 'actions',
field: 'appNo',
headerName: 'Application No.', headerName: 'Application No.',
sortable: false,
sortable: true,
width: 150, width: 150,
cellClassName: 'actions', cellClassName: 'actions',
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
@@ -81,7 +81,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'mode', id: 'mode',
field: 'mode', field: 'mode',
headerName: 'Mode', headerName: 'Mode',
sortable: false,
sortable: true,
width: 100, width: 100,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
renderCell: (params) => { renderCell: (params) => {
@@ -92,7 +92,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'status', id: 'status',
field: 'status', field: 'status',
headerName: 'Status', headerName: 'Status',
sortable: false,
sortable: true,
width: 240, width: 240,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
renderCell: (params) => { renderCell: (params) => {
@@ -103,7 +103,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'withProof', id: 'withProof',
field: 'withProof', field: 'withProof',
headerName: 'With Proof', headerName: 'With Proof',
sortable: false,
sortable: true,
width: 120, width: 120,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
renderCell: (params) => { renderCell: (params) => {
@@ -114,7 +114,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'created', id: 'created',
field: 'created', field: 'created',
headerName: 'Submit Date', headerName: 'Submit Date',
sortable: false,
sortable: true,
flex: 1, flex: 1,
minWidth: 200, minWidth: 200,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
@@ -126,7 +126,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'contactPerson', id: 'contactPerson',
field: 'contactPerson', field: 'contactPerson',
headerName: 'Client / Payment Means', headerName: 'Client / Payment Means',
sortable: false,
sortable: true,
minWidth: 250, minWidth: 250,
flex: 2, flex: 2,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
@@ -137,8 +137,14 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
company = company + ": " + params.row.custName company = company + ": " + params.row.custName
} }
let paymentMethod = params.row.paymentMethod!=null?intl.formatMessage({ id: utils.getPaymentMethod(params.row.paymentMethod)}):"" let paymentMethod = params.row.paymentMethod!=null?intl.formatMessage({ id: utils.getPaymentMethod(params.row.paymentMethod)}):""
const lines = [params?.value, company, paymentMethod].filter(line => line != null && line !== "");
return (<> return (<>
{params?.value}<br />{company} <br/>{paymentMethod}
{lines.map((line, index) => (
<React.Fragment key={index}>
{index > 0 && <br />}
{line}
</React.Fragment>
))}
</>); </>);
} }
}, },
@@ -146,7 +152,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
id: 'issueNoAndCode', id: 'issueNoAndCode',
field: 'issueNoAndCode', field: 'issueNoAndCode',
headerName: 'Gazette Issue No. / Gazette Code', headerName: 'Gazette Issue No. / Gazette Code',
sortable: false,
sortable: true,
flex: 1.5, flex: 1.5,
minWidth: 350, minWidth: 350,
renderHeader: renderHeaderWithAria, renderHeader: renderHeaderWithAria,
@@ -220,6 +226,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
<FiDataGrid <FiDataGrid
checkboxSelection checkboxSelection
disableRowSelectionOnClick disableRowSelectionOnClick
serverSorting
onRowSelectionModelChange={(newSelection) => { onRowSelectionModelChange={(newSelection) => {
setSelectedRowItems(newSelection); setSelectedRowItems(newSelection);
}} }}


+ 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 {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider"; import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"; import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

const getDefaultDateFrom = () => DateUtils.dateValue(new Date().setDate(new Date().getDate() - 14));
const getDefaultDateTo = () => DateUtils.dateValue(new Date());

const isSubmitDateEmpty = (value) =>
value == null || value === "" || value === "dd / mm / yyyy";

const isBlank = (value) => value == null || String(value).trim() === "";

const toDayjsOrNull = (value) => {
if (isSubmitDateEmpty(value)) return null;
const d = dayjs(value);
return d.isValid() ? d : null;
};

// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, onGridReady
}) => { }) => {
@@ -31,12 +46,13 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
const [issueCombo, setIssueCombo] = React.useState([]); const [issueCombo, setIssueCombo] = React.useState([]);
const [selectedStatus, setSelectedStatus] = React.useState({key: 0, label: 'All', type: 'all'}); const [selectedStatus, setSelectedStatus] = React.useState({key: 0, label: 'All', type: 'all'});
const [selectedMode, setSelectedMode] = React.useState({key: 0, label: 'All', type: 'all'}); const [selectedMode, setSelectedMode] = React.useState({key: 0, label: 'All', type: 'all'});
const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.code === searchCriteria.gazettGroup):{});
const [groupSelected, setGroupSelected] = React.useState(searchCriteria.gazettGroup!=undefined?ComboData.groupTitle.find(item => item.title === searchCriteria.gazettGroup):{});


const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom); const [minDate, setMinDate] = React.useState(searchCriteria.dateFrom);
const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo); const [maxDate, setMaxDate] = React.useState(searchCriteria.dateTo);
const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy"); const [fromDateValue, setFromDateValue] = React.useState("dd / mm / yyyy");
const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy"); const [toDateValue, setToDateValue] = React.useState("dd / mm / yyyy");
const prevHasOtherRef = React.useRef(null);


React.useEffect(() => { React.useEffect(() => {
if(searchCriteria.status!=undefined){ if(searchCriteria.status!=undefined){
@@ -64,7 +80,56 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
const { locale } = intl; const { locale } = intl;


const marginBottom = 2.5; const marginBottom = 2.5;
const { reset, register, handleSubmit } = useForm()
const { reset, register, handleSubmit, watch } = useForm({
defaultValues: {
appNo: searchCriteria.appNo || "",
contact: searchCriteria.contact || "",
groupNo: searchCriteria.groupNo || ""
}
});
const appNo = watch("appNo");
const contact = watch("contact");
const groupNo = watch("groupNo");

const clearSubmitDates = () => {
setMinDate(null);
setMaxDate(null);
};

const restoreDefaultSubmitDates = () => {
setMinDate(getDefaultDateFrom());
setMaxDate(getDefaultDateTo());
};

const hasOtherCriteria = (textFields = {}) => {
if (!isBlank(textFields.appNo)) return true;
if (!isBlank(textFields.contact)) return true;
if (!isBlank(textFields.groupNo)) return true;
if (groupSelected?.title) return true;
if (orgSelected?.key && orgSelected.key > 0) return true;
if (issueSelected?.id) return true;
if (selectedStatus?.type && selectedStatus.type !== "all" && selectedStatus.type !== "") return true;
if (selectedMode?.type && selectedMode.type !== "all" && selectedMode.type !== "") return true;
return false;
};

React.useEffect(() => {
const hasOther = hasOtherCriteria({ appNo, contact, groupNo });
if (prevHasOtherRef.current === null) {
prevHasOtherRef.current = hasOther;
return;
}
if (hasOther && !prevHasOtherRef.current) {
clearSubmitDates();
} else if (!hasOther && prevHasOtherRef.current) {
// Only refill defaults when From or To is empty; keep user-entered dates otherwise
if (isSubmitDateEmpty(minDate) || isSubmitDateEmpty(maxDate)) {
restoreDefaultSubmitDates();
}
}
prevHasOtherRef.current = hasOther;
}, [appNo, contact, groupNo, groupSelected, orgSelected, issueSelected, selectedStatus, selectedMode]);

const onSubmit = (data) => { const onSubmit = (data) => {
// localStorage.setItem('searchCriteria',"") // localStorage.setItem('searchCriteria',"")
data.status = selectedStatus?.type data.status = selectedStatus?.type
@@ -76,10 +141,25 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
for (let i = 0; i < type.length; i++) { for (let i = 0; i < type.length; i++) {
typeArray.push(type[i].label); typeArray.push(type[i].label);
} }
if (fromDateValue != "dd / mm / yyyy" && toDateValue != "dd / mm / yyyy") {
sentDateFrom = DateUtils.dateValue(fromDateValue)
sentDateTo = DateUtils.dateValue(toDateValue)

const hasOther = hasOtherCriteria({
appNo: data.appNo,
contact: data.contact,
groupNo: data.groupNo
});
const datesEmpty = isSubmitDateEmpty(fromDateValue) || isSubmitDateEmpty(toDateValue)
|| minDate == null || maxDate == null;

if (!hasOther && datesEmpty) {
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
sentDateFrom = dateFrom;
sentDateTo = dateTo;
} else if (!datesEmpty) {
sentDateFrom = DateUtils.dateValue(fromDateValue);
sentDateTo = DateUtils.dateValue(toDateValue);
} }


const temp = { const temp = {
@@ -91,11 +171,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "", orgId: (orgSelected?.key && orgSelected?.key > 0) ? orgSelected?.key : "",
issueId: issueSelected?.id, issueId: issueSelected?.id,
groupNo: data.groupNo, groupNo: data.groupNo,
gazettGroup: groupSelected?.code,
gazettGroup: groupSelected?.title,
mode: (data.mode === '' || data.mode?.includes("all")) ? "" : data.mode, mode: (data.mode === '' || data.mode?.includes("all")) ? "" : data.mode,
start:0,
limit:10
}; };
// Gazette Issue No. search loads all matching rows (no paging)
if (issueSelected?.id) {
delete temp.start;
delete temp.limit;
} else {
temp.start = 0;
temp.limit = 10;
}
if (searchCriteria?.sort && searchCriteria?.direction) {
temp.sort = searchCriteria.sort;
temp.direction = searchCriteria.direction;
}
applySearch(temp); applySearch(temp);
// setSearchReady(true) // setSearchReady(true)
}; };
@@ -117,14 +207,32 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
setGroupSelected({}); setGroupSelected({});
setSelectedStatus({key: 0, label: 'All', type: 'all'}); setSelectedStatus({key: 0, label: 'All', type: 'all'});
setSelectedMode({key: 0, label: 'All', type: 'all'}); setSelectedMode({key: 0, label: 'All', type: 'all'});
setMinDate(DateUtils.dateValue(new Date().setDate(new Date().getDate()-14)))
setMaxDate(DateUtils.dateValue(new Date()))
const dateFrom = getDefaultDateFrom();
const dateTo = getDefaultDateTo();
setMinDate(dateFrom);
setMaxDate(dateTo);
reset({ reset({
appNo:"", appNo:"",
contact:"", contact:"",
groupNo:"" groupNo:""
}); });
localStorage.setItem('searchCriteria',"")
prevHasOtherRef.current = false;
localStorage.setItem('searchCriteria',"");
// Reset form criteria and sorting first, then reload with backend default sort
applySearch({
appNo: "",
dateFrom,
dateTo,
contact: "",
status: "",
orgId: "",
issueId: "",
groupNo: "",
gazettGroup: "",
mode: "",
start: 0,
limit: 10
});
} }


const getIssueLabel=(data)=> { const getIssueLabel=(data)=> {
@@ -188,22 +296,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateFrom" id="dateFrom"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label={"Submit Date (From)"} label={"Submit Date (From)"}
value={minDate === null ? null : dayjs(minDate)}
maxDate={maxDate === null ? null : dayjs(maxDate)}
value={toDayjsOrNull(minDate)}
maxDate={toDayjsOrNull(maxDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMinDate(newValue);
}
setMinDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >
@@ -215,22 +322,21 @@ const SearchPublicNoticeForm = ({ applySearch, searchCriteria, issueComboData, o
<DemoItem components={['DatePicker']}> <DemoItem components={['DatePicker']}>
<DatePicker <DatePicker
id="dateTo" id="dateTo"
// onError={(newError) => setReceiptFromError(newError)}
onError={() => {}}
slotProps={{ slotProps={{
field: { readOnly: true, },
// textField: {
// helperText: receiptFromErrorMessage,
// },
field: { readOnly: true, clearable: true },
textField: {
InputLabelProps: { shrink: true },
error: false,
helperText: null
},
}} }}
format="DD/MM/YYYY" format="DD/MM/YYYY"
label={"Submit Date (To)"} label={"Submit Date (To)"}
value={maxDate === null ? null : dayjs(maxDate)}
minDate={minDate === null ? null : dayjs(minDate)}
value={toDayjsOrNull(maxDate)}
minDate={toDayjsOrNull(minDate)}
onChange={(newValue) => { onChange={(newValue) => {
// console.log(newValue)
if(newValue!=null){
setMaxDate(newValue);
}
setMaxDate(newValue && newValue.isValid?.() ? newValue : null);
}} }}
/> />
</DemoItem > </DemoItem >


+ 2
- 1
src/pages/Setting/SystemSetting/Table.js Просмотреть файл

@@ -7,6 +7,7 @@ import MainCard from "components/MainCard";
import * as React from "react"; import * as React from "react";
import { FiDataGrid } from "components/FiDataGrid"; import { FiDataGrid } from "components/FiDataGrid";
import { GET_SYS_PARAMS } from "utils/ApiPathConst"; import { GET_SYS_PARAMS } from "utils/ApiPathConst";
import SafeHtml from 'components/SafeHtml';




// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //
@@ -45,7 +46,7 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => {
flex: 1, flex: 1,
minWidth: 400, minWidth: 400,
renderCell:(params)=>{ renderCell:(params)=>{
return <div dangerouslySetInnerHTML={{__html: params.value}} />
return <SafeHtml html={params.value} />
} }
}, },
]; ];


+ 3
- 3
src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js Просмотреть файл

@@ -264,7 +264,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => {
}; };


const onVerifiedClick = () => { const onVerifiedClick = () => {
HttpUtils.get({
HttpUtils.post({
url: UrlUtils.GET_IND_USER_VERIFY + "/" + formData.id, url: UrlUtils.GET_IND_USER_VERIFY + "/" + formData.id,
onSuccess: function () { onSuccess: function () {
notifyVerifySuccess() notifyVerifySuccess()
@@ -274,7 +274,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => {
}; };


const doLock = () => { const doLock = () => {
HttpUtils.get({
HttpUtils.post({
url: UrlUtils.GET_USER_LOCK + "/" + formData.id, url: UrlUtils.GET_USER_LOCK + "/" + formData.id,
onSuccess: function () { onSuccess: function () {
notifyLockSuccess() notifyLockSuccess()
@@ -284,7 +284,7 @@ const UserInformationCard_Individual = ({ formData, loadDataFun }) => {
}; };


const doUnlock = () => { const doUnlock = () => {
HttpUtils.get({
HttpUtils.post({
url: UrlUtils.GET_USER_UNLOCK + "/" + formData.id, url: UrlUtils.GET_USER_UNLOCK + "/" + formData.id,
onSuccess: function () { onSuccess: function () {
notifyActiveSuccess() notifyActiveSuccess()


+ 3
- 3
src/pages/User/DetailsPage_Organization/UserInformationCard_Organization.js Просмотреть файл

@@ -130,7 +130,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) =>


const onVerifiedClick = () => { const onVerifiedClick = () => {
if (formik?.values?.orgId) { if (formik?.values?.orgId) {
HttpUtils.get({
HttpUtils.post({
url: GET_IND_USER_VERIFY + "/" + userData.id, url: GET_IND_USER_VERIFY + "/" + userData.id,
onSuccess: function () { onSuccess: function () {
notifyVerifySuccess() notifyVerifySuccess()
@@ -148,7 +148,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) =>
setConfirmText("Confirm to Lock this Account?"); setConfirmText("Confirm to Lock this Account?");
setConfirmAction({ setConfirmAction({
function: function () { function: function () {
HttpUtils.get({
HttpUtils.post({
url: GET_USER_LOCK + "/" + userData.id, url: GET_USER_LOCK + "/" + userData.id,
onSuccess: function () { onSuccess: function () {
notifyLockSuccess() notifyLockSuccess()
@@ -165,7 +165,7 @@ const UserInformationCard_Organization = ({ userData, loadDataFun, orgData }) =>


setConfirmAction({ setConfirmAction({
function: function () { function: function () {
HttpUtils.get({
HttpUtils.post({
url: GET_USER_UNLOCK + "/" + userData.id, url: GET_USER_UNLOCK + "/" + userData.id,
onSuccess: function () { onSuccess: function () {
notifyActiveSuccess() notifyActiveSuccess()


+ 2
- 2
src/pages/User/ManagePage_OrgPublic/index.js Просмотреть файл

@@ -77,7 +77,7 @@ const ManageOrgUserPage = () => {




function onActiveClick(params) { function onActiveClick(params) {
HttpUtils.get({
HttpUtils.post({
url: GET_USER_UNLOCK + "/" + params.row.id, url: GET_USER_UNLOCK + "/" + params.row.id,
onSuccess: () => { onSuccess: () => {
setReloadTime(new Date()); setReloadTime(new Date());
@@ -117,7 +117,7 @@ const ManageOrgUserPage = () => {


const setPrimaryUser = () => { const setPrimaryUser = () => {
setIsWarningPopUp(false) setIsWarningPopUp(false)
HttpUtils.get(
HttpUtils.post(
{ {
url: (!selectUser.row.primaryUser ? GET_SET_PRIMARY_USER : GET_SET_UN_PRIMARY_USER) + "/" + selectUser.row.id, url: (!selectUser.row.primaryUser ? GET_SET_PRIMARY_USER : GET_SET_UN_PRIMARY_USER) + "/" + selectUser.row.id,
onSuccess: function () { onSuccess: function () {


+ 2
- 2
src/pages/User/SearchPage/UserTable.js Просмотреть файл

@@ -37,7 +37,7 @@ export default function UserTable({searchCriteria, applyGridOnReady,applySearch}
} }


const doLock = (id) => { const doLock = (id) => {
HttpUtils.get({
HttpUtils.post({
url: GET_USER_LOCK+"/"+id, url: GET_USER_LOCK+"/"+id,
onSuccess: function(){ onSuccess: function(){
//setChangeLocked(true) //setChangeLocked(true)
@@ -48,7 +48,7 @@ export default function UserTable({searchCriteria, applyGridOnReady,applySearch}
}; };


const doUnlock = (id) => { const doUnlock = (id) => {
HttpUtils.get({
HttpUtils.post({
url: GET_USER_UNLOCK+"/"+id, url: GET_USER_UNLOCK+"/"+id,
onSuccess: function(){ onSuccess: function(){
//setChangeLocked(true) //setChangeLocked(true)


+ 1
- 1
src/pages/_Test/index.js Просмотреть файл

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


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


+ 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 lgceImg from 'assets/images/2025_lgce.jpg';
import 'assets/style/loginStyles.css'; import 'assets/style/loginStyles.css';
import { SysContext } from 'components/SysSettingProvider'; import { SysContext } from 'components/SysSettingProvider';
import SafeHtml from 'components/SafeHtml';
const AuthCard = Loadable(lazy(() => import('./AuthCardCustom'))); const AuthCard = Loadable(lazy(() => import('./AuthCardCustom')));


const BackgroundHead = { const BackgroundHead = {
@@ -175,11 +176,9 @@ const AuthWrapper = ({ children }) => {
'& p': { marginBottom: '12px', lineHeight: 1.7 } '& p': { marginBottom: '12px', lineHeight: 1.7 }
}} }}
> >
<Typography
component="div"
sx={{ fontSize: 18, lineHeight: 1.8 }}
dangerouslySetInnerHTML={{ __html: popupHtml }}
/>
<Typography component="div" sx={{ fontSize: 18, lineHeight: 1.8 }}>
<SafeHtml html={popupHtml} />
</Typography>
</DialogContent> </DialogContent>
</Dialog> </Dialog>


@@ -207,24 +206,18 @@ const AuthWrapper = ({ children }) => {
> >
<Grid item xs={12} sx={{ px: { xs: 2 }, pl: { md: 4 }, pt: { xs: 4, sm: 2, md: 1.5 }, pb: { xs: 4, sm: 0 } }}> <Grid item xs={12} sx={{ px: { xs: 2 }, pl: { md: 4 }, pt: { xs: 4, sm: 2, md: 1.5 }, pb: { xs: 4, sm: 0 } }}>
{checkPaymentSuspension() ? ( {checkPaymentSuspension() ? (
<Typography
component="div"
sx={{ color: 'error.main', textAlign: 'start', p: 1.5 }}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({ id: 'suspensionMessageText', defaultMessage: '' })
}}
/>
<Typography component="div" sx={{ color: 'error.main', textAlign: 'start', p: 1.5 }}>
<SafeHtml html={intl.formatMessage({ id: 'suspensionMessageText', defaultMessage: '' })} />
</Typography>
) : ( ) : (
<Typography
component="div"
sx={{ textAlign: 'start', p: 1.5 }}
dangerouslySetInnerHTML={{
__html: intl.formatMessage({
<Typography component="div" sx={{ textAlign: 'start', p: 1.5 }}>
<SafeHtml
html={intl.formatMessage({
id: isOnlyOnline ? 'landingMessage' : 'homePageHeaderMessage', id: isOnlyOnline ? 'landingMessage' : 'homePageHeaderMessage',
defaultMessage: '' defaultMessage: ''
})
}}
/>
})}
/>
</Typography>
)} )}
</Grid> </Grid>


@@ -246,10 +239,14 @@ const AuthWrapper = ({ children }) => {
<Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}> <Typography style={{ textAlign: 'center', fontSize: '1.8rem' }}>
<FormattedMessage id="PNSPS_fullname" /> <FormattedMessage id="PNSPS_fullname" />
</Typography> </Typography>
{checkSysEnv() !== '' ? (
{checkSysEnv() === 'uat' ? (
<Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}> <Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}>
User Acceptance Test Environment User Acceptance Test Environment
</Typography> </Typography>
) : checkSysEnv() === 'dev' ? (
<Typography style={{ color: 'red', textAlign: 'center', fontSize: '1.8rem' }}>
Development Environment
</Typography>
) : ( ) : (
'' ''
)} )}


+ 2
- 1
src/pages/authentication/ForgotPassword/AuthCallback/index.js Просмотреть файл

@@ -105,7 +105,8 @@ const Index = () => {
url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD, url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD,
params:{ params:{
username: username, username: username,
newPassword: values.password
newPassword: values.password,
emailVerifyHash: decodeURIComponent(params.verifyCode)
}, },
onSuccess: () => { onSuccess: () => {
useJwt useJwt


+ 2
- 1
src/pages/authentication/ForgotUsername/AuthCallback/index.js Просмотреть файл

@@ -99,7 +99,8 @@ const Index = () => {
url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD, url: UrlUtils.POST_FORGOT_PASSWORD_NEW_PASSWORD,
params:{ params:{
username: username, username: username,
newPassword: values.password
newPassword: values.password,
emailVerifyHash: decodeURIComponent(params.verifyCode)
}, },
onSuccess: () => { onSuccess: () => {
useJwt useJwt


+ 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 * as React from 'react';
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import usePageTitle from "components/usePageTitle"; import usePageTitle from "components/usePageTitle";
import SafeHtml from 'components/SafeHtml';


// ================================|| LOGIN ||================================ // // ================================|| LOGIN ||================================ //


@@ -127,7 +128,7 @@ const RegisterCustom = () => {
/> />
<Box mt={4} ml={2} mr={2} bgcolor="grey.100" p={1.5} > <Box mt={4} ml={2} mr={2} bgcolor="grey.100" p={1.5} >
<Typography textAlign='justify' variant="body1" display="block" gutterBottom> <Typography textAlign='justify' variant="body1" display="block" gutterBottom>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerIAmSmart' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.registerIAmSmart' })} />
</Typography> </Typography>
<Link href={intl.formatMessage({ id: "iamsmartLink" })}> <Link href={intl.formatMessage({ id: "iamsmartLink" })}>
<FormattedMessage id="learnMore" /> <FormattedMessage id="learnMore" />
@@ -160,7 +161,7 @@ const RegisterCustom = () => {
</Button> </Button>


<Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom> <Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerPersonal' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.registerPersonal' })} />
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} md={6} sx={{ borderLeft: 1, borderColor: 'grey.500' }}> <Grid item xs={12} md={6} sx={{ borderLeft: 1, borderColor: 'grey.500' }}>
@@ -189,7 +190,7 @@ const RegisterCustom = () => {
</Typography> </Typography>
</Button> </Button>
<Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom> <Typography ml={4} mr={4} mt={4} variant="body1" display="block" sx={{ fontWeight: 'bold' }} gutterBottom>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.registerOrg' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.registerOrg' })} />
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>


+ 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 * as ComboData from "utils/ComboData";


import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
import SafeHtml from 'components/SafeHtml';
import { lazy } from 'react'; import { lazy } from 'react';
const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable')));
const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent')));
@@ -1618,7 +1619,7 @@ const BusCustomFormWizard = (props) => {
<Grid container> <Grid container>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
<Typography component="span" variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e", display: "block" }}> <Typography component="span" variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e", display: "block" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
<SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} />
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>


+ 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 * as ComboData from "utils/ComboData";


import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
import SafeHtml from 'components/SafeHtml';
import { lazy } from 'react'; import { lazy } from 'react';
const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable'))); const UploadFileTable = Loadable(lazy(() => import('./UploadFileTable')));
const PreviewUploadFileTable = Loadable(lazy(() => import('./PreviewUploadFileTable'))); const PreviewUploadFileTable = Loadable(lazy(() => import('./PreviewUploadFileTable')));
@@ -1946,7 +1947,7 @@ const CustomFormWizard = (props) => {
<Grid container> <Grid container>
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
<Typography height="100%" component="span" sx={{ textAlign: "left", display: "block", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}> <Typography height="100%" component="span" sx={{ textAlign: "left", display: "block", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
<SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} />
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>


+ 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 * as ComboData from "utils/ComboData";


import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
import SafeHtml from 'components/SafeHtml';
import { lazy } from 'react'; import { lazy } from 'react';
const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent'))); const LoadingComponent = Loadable(lazy(() => import('../../extra-pages/LoadingComponent')));


@@ -1055,7 +1056,7 @@ const CustomFormWizard = (props) => {
<Grid item xs={12} md={12}> <Grid item xs={12} md={12}>
<Typography component="span" sx={{ textAlign: "left", display: "block", borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}> <Typography component="span" sx={{ textAlign: "left", display: "block", borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "termsAndCon" }) }} />
<SafeHtml html={intl.formatMessage({ id: "termsAndCon" })} style={{ padding: 12 }} />
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>


+ 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 { useNavigate } from "react-router-dom";
import usePageTitle from "components/usePageTitle"; import usePageTitle from "components/usePageTitle";
import SafeHtml from 'components/SafeHtml';


// ==============================|| DASHBOARD - DEFAULT ||============================== // // ==============================|| DASHBOARD - DEFAULT ||============================== //


@@ -297,7 +298,7 @@ const DashboardDefault = () => {
</DialogTitle> </DialogTitle>
<DialogContent style={{ color: '#B00020', display: 'flex', }}> <DialogContent style={{ color: '#B00020', display: 'flex', }}>
<Typography variant="h5" style={{ padding: '16px' }}> <Typography variant="h5" style={{ padding: '16px' }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography> </Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>


+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_cn.js Просмотреть файл

@@ -1,3 +1,4 @@
import SafeHtml from 'components/SafeHtml';


const AboutUs = () => { const AboutUs = () => {


@@ -155,7 +156,7 @@ const AboutUs = () => {
` `
; ;


return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;


} }



+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_en.js Просмотреть файл

@@ -1,3 +1,4 @@
import SafeHtml from 'components/SafeHtml';


const AboutUs = () => { const AboutUs = () => {


@@ -162,7 +163,7 @@ const AboutUs = () => {
</div>` </div>`
; ;


return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;


} }



+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_zh.js Просмотреть файл

@@ -1,3 +1,4 @@
import SafeHtml from 'components/SafeHtml';


const AboutUs = () => { const AboutUs = () => {


@@ -165,7 +166,7 @@ const AboutUs = () => {
` `
; ;


return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;


} }



+ 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 usePageTitle from "components/usePageTitle";


import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png';
import SafeHtml from 'components/SafeHtml';


const BackgroundHead = { const BackgroundHead = {
backgroundImage: `url(${titleBackgroundImg})`, backgroundImage: `url(${titleBackgroundImg})`,
@@ -38,7 +39,7 @@ const ImportantNotice = () => {
</div> </div>
</Grid> </Grid>
<Grid item xs={10} md={8} lg={6}> <Grid item xs={10} md={8} lg={6}>
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} />
<SafeHtml html={htmlContent} style={{ padding: 12 }} />
</Grid> </Grid>
</Grid> </Grid>
); );


+ 2
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js Просмотреть файл

@@ -1,3 +1,4 @@
import SafeHtml from 'components/SafeHtml';


const Page = () => { const Page = () => {


@@ -83,7 +84,7 @@ const Page = () => {
</div> </div>
`; `;


return (<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />);
return <SafeHtml html={content} style={{ padding: 12 }} />;


} }



+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js Просмотреть файл

@@ -1,3 +1,5 @@
import SafeHtml from 'components/SafeHtml';

const Page = () => { const Page = () => {
const content = ` const content = `
<style> <style>
@@ -125,7 +127,7 @@ const Page = () => {
</div> </div>
`; `;


return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />;
return <SafeHtml html={content} style={{ padding: 12 }} />;
}; };


export default Page; export default Page;

+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js Просмотреть файл

@@ -1,3 +1,5 @@
import SafeHtml from 'components/SafeHtml';

const Page = () => { const Page = () => {
const content = ` const content = `
<style> <style>
@@ -98,7 +100,7 @@ const Page = () => {
</div> </div>
`; `;


return <div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: content }} />;
return <SafeHtml html={content} style={{ padding: 12 }} />;
}; };


export default Page; export default Page;

+ 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 usePageTitle from "components/usePageTitle";


import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'; import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png';
import SafeHtml from 'components/SafeHtml';


const BackgroundHead = { const BackgroundHead = {
backgroundImage: `url(${titleBackgroundImg})`, backgroundImage: `url(${titleBackgroundImg})`,
@@ -38,7 +39,7 @@ const PrivacyPolicy = () => {
</div> </div>
</Grid> </Grid>
<Grid item xs={10} md={8} lg={6}> <Grid item xs={10} md={8} lg={6}>
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: htmlContent }} />
<SafeHtml html={htmlContent} style={{ padding: 12 }} />
</Grid> </Grid>
</Grid> </Grid>
); );


+ 2
- 1
src/pages/iAmSmart/PleaseLoginCallback/index.js Просмотреть файл

@@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom";
import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
import AccountCircleIcon from '@mui/icons-material/AccountCircle'; import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import SafeHtml from 'components/SafeHtml';


import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
const BackgroundHead = { const BackgroundHead = {
@@ -66,7 +67,7 @@ const Index = () => {
<AccountCircleIcon color="secondary" sx={{ width: "200px", height: "200px" }} /> <AccountCircleIcon color="secondary" sx={{ width: "200px", height: "200px" }} />
<Grid item xs={12} md={12} > <Grid item xs={12} md={12} >
<Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}> <Typography variant="h3" sx={{ ml: 8, mt: 4, mr: 8, textAlign: "center" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'pleaseLloginMessage2' }) }} />
<SafeHtml html={intl.formatMessage({ id: 'pleaseLloginMessage2' })} />
</Typography> </Typography>
</Grid> </Grid>
</center> </center>


+ 4
- 2
src/pages/iAmSmart/SuccessCallback/index.js Просмотреть файл

@@ -12,6 +12,7 @@ import { useNavigate } from "react-router-dom";
import Loadable from 'components/Loadable'; import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent'))); const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import SafeHtml from 'components/SafeHtml';


import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png' import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
const BackgroundHead = { const BackgroundHead = {
@@ -69,8 +70,9 @@ const Index = () => {
component="div" component="div"
variant="h3" variant="h3"
sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }} sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }}
dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'loginSuccessMessage2' }) }}
/>
>
<SafeHtml html={intl.formatMessage({ id: 'loginSuccessMessage2' })} />
</Typography>
) : ( ) : (
<Typography component="div" variant="h3" sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }}> <Typography component="div" variant="h3" sx={{ mt: 4, maxWidth: 900, textAlign: 'center' }}>
<FormattedMessage id="iAmSmartLoginSuccessHeading" /> <FormattedMessage id="iAmSmartLoginSuccessHeading" />


+ 15
- 7
src/pages/pnspsUserGroupDetailPage/GroupAuthCard.js Просмотреть файл

@@ -15,8 +15,8 @@ const GroupAuthTable = Loadable(lazy(() => import('./GroupAuthTable')));
const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRecord, editMode}) => { const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRecord, editMode}) => {
const [currentAuthData, setCurrentAuthData] = React.useState({}); const [currentAuthData, setCurrentAuthData] = React.useState({});
const [onReady, setOnReady] = useState(false); const [onReady, setOnReady] = useState(false);
const [selectedRow, setSelectedRow] = useState([]);
const [referenceRow, setReferenceRow] = useState([]);
const [selectedRow, setSelectedRow] = useState(userGroupData?.authIds || []);
const [referenceRow, setReferenceRow] = useState(userGroupData?.authIds || []);
const [_editMode, setEditMode] = useState(editMode); const [_editMode, setEditMode] = useState(editMode);


useEffect(()=>{ useEffect(()=>{
@@ -39,14 +39,22 @@ const GroupAuthCard = ({isCollectData, updateUserAuthList,userGroupData,isNewRec
} }
}, [currentAuthData]); }, [currentAuthData]);


const toIdList = (ids) => {
if (!ids) {
return [];
}
const raw = Array.isArray(ids) ? ids : (ids.ids ? Array.from(ids.ids) : []);
return raw.map((id) => Number(id)).filter((id) => !Number.isNaN(id));
};

useEffect(() => { useEffect(() => {
//upload latest data to parent
let deletedList = referenceRow.filter(x => !selectedRow.includes(x));
const currentIds = toIdList(selectedRow);
const referenceIds = toIdList(referenceRow);
updateUserAuthList({ updateUserAuthList({
"currentList": selectedRow,
"deletedList": deletedList
currentList: currentIds,
deletedList: referenceIds.filter((id) => !currentIds.includes(id))
}); });
}, [isCollectData]);
}, [isCollectData, selectedRow, referenceRow]);




return ( return (


+ 4
- 2
src/pages/pnspsUserGroupDetailPage/GroupAuthTable.js Просмотреть файл

@@ -98,8 +98,10 @@ export default function GroupAuthTable({setSelectedRow, userAuth,isNewRecord, ed
rowSelectionModel={currentSelectedRow} rowSelectionModel={currentSelectedRow}
onRowSelectionModelChange={(ids) => { onRowSelectionModelChange={(ids) => {
if(_editMode){ if(_editMode){
setSelectedRow(ids);
setCurrentSelectedRow(ids);
const raw = Array.isArray(ids) ? ids : (ids?.ids ? Array.from(ids.ids) : []);
const nextIds = raw.map((id) => Number(id)).filter((id) => !Number.isNaN(id));
setSelectedRow(nextIds);
setCurrentSelectedRow(nextIds);
} }
}} }}
autoHeight autoHeight


+ 3
- 4
src/pages/pnspsUserGroupDetailPage/UserAddCard.js Просмотреть файл

@@ -90,12 +90,11 @@ const UserAddCard = ({ isCollectData, updateGroupMember, userGroupData, isNewRec
}, [currentUserData]); }, [currentUserData]);


useEffect(() => { useEffect(() => {
//upload latest data to parent
updateGroupMember({ updateGroupMember({
"currentList": groupUserData,
"deletedList": deletedList
currentList: groupUserData,
deletedList: deletedList
}); });
}, [isCollectData]);
}, [isCollectData, groupUserData, deletedList]);




return ( return (


+ 46
- 20
src/pages/pnspsUserGroupDetailPage/index.js Просмотреть файл

@@ -47,12 +47,12 @@ const UserMaintainPage = () => {
const [isCollectData, setIsCollectData] = useState(false); const [isCollectData, setIsCollectData] = useState(false);
const [editedGroupData, setEditedGroupData] = useState({}); const [editedGroupData, setEditedGroupData] = useState({});
const [userGroupData, setUserGroupData] = useState([]); const [userGroupData, setUserGroupData] = useState([]);
const [userAuthData, setUserAuthData] = useState([]);
const saveInProgressRef = useRef(false); const saveInProgressRef = useRef(false);
const [groupMember, setGroupMember] = useState([]);
const userAuthDataRef = useRef([]);
const deletedAuthListRef = useRef([]);
const groupMemberRef = useRef([]);
const deletedUserListRef = useRef([]);
const [isNewRecord, setIsNewRecord] = useState(false); const [isNewRecord, setIsNewRecord] = useState(false);
const [deletedUserList, setDeletedUserList] = useState([]);
const [deletedAuthList, setDeletedAuthList] = useState([]);


const [isWindowOpen, setIsWindowOpen] = useState(false); const [isWindowOpen, setIsWindowOpen] = useState(false);
const handleClose = () => { const handleClose = () => {
@@ -84,13 +84,17 @@ const UserMaintainPage = () => {
} }


function updateGroupMember(groupMember) { function updateGroupMember(groupMember) {
setGroupMember(groupMember.currentList);
setDeletedUserList(groupMember.deletedList);
const currentList = groupMember.currentList || [];
const deletedList = groupMember.deletedList || [];
groupMemberRef.current = currentList;
deletedUserListRef.current = deletedList;
} }


function updateUserAuthList(userAuthData) {
setUserAuthData(userAuthData.currentList);
setDeletedAuthList(userAuthData.deletedList);
function updateUserAuthList(authData) {
const currentList = authData.currentList || [];
const deletedList = authData.deletedList || [];
userAuthDataRef.current = currentList;
deletedAuthListRef.current = deletedList;
} }


const submitData = async () => { const submitData = async () => {
@@ -106,19 +110,36 @@ const UserMaintainPage = () => {
return; return;
} }
const latestGroupFormData = getLatestGroupFormData(); const latestGroupFormData = getLatestGroupFormData();
const finalDeletedUserList = getDeletedRecordWithRefList(deletedUserList, getIdList(groupMember));
const latestGroupMember = groupMemberRef.current;
const latestAuthIds = userAuthDataRef.current;
const latestDeletedAuthIds = deletedAuthListRef.current;
const finalDeletedUserList = getDeletedRecordWithRefList(
deletedUserListRef.current,
getIdList(latestGroupMember)
);
const response = await axios.post(POST_AND_UPDATE_USER_GROUP, { const response = await axios.post(POST_AND_UPDATE_USER_GROUP, {
id: parseInt(params.id) !== -1 ? parseInt(params.id) : null, id: parseInt(params.id) !== -1 ? parseInt(params.id) : null,
name: latestGroupFormData.userGroupName, name: latestGroupFormData.userGroupName,
description: latestGroupFormData.description, description: latestGroupFormData.description,
addUserIds: getIdList(groupMember),
addUserIds: getIdList(latestGroupMember),
removeUserIds: finalDeletedUserList, removeUserIds: finalDeletedUserList,
addAuthIds: userAuthData,
removeAuthIds: deletedAuthList,
addAuthIds: latestAuthIds,
removeAuthIds: latestDeletedAuthIds,
}); });
if (response.status === 200) { if (response.status === 200) {
navigate('/usergroupSearchview');
notifySaveSuccess(); notifySaveSuccess();
const savedId = response.data?.id;
const currentId = parseInt(params.id);
if ((currentId === -1 || Number.isNaN(currentId)) && savedId) {
navigate(`/userGroup/${savedId}`, { replace: true });
await loadGroupData(savedId);
} else {
await loadGroupData(currentId);
}
setIsNewRecord(false);
setEditMode(false);
deletedUserListRef.current = [];
deletedAuthListRef.current = [];
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -177,14 +198,19 @@ const UserMaintainPage = () => {
return true; return true;
}; };


const loadGroupData = async (groupId) => {
const response = await axios.get(`${GET_GROUP_LIST_PATH}/${groupId}`);
if (response.status === 200) {
setUserGroupData(response.data);
const loadedAuthIds = response.data?.authIds || [];
userAuthDataRef.current = loadedAuthIds;
}
return response;
};

useEffect(() => { useEffect(() => {
if (params.id > 0) { if (params.id > 0) {
axios.get(`${GET_GROUP_LIST_PATH}/${params.id}`)
.then((response) => {
if (response.status === 200) {
setUserGroupData(response.data);
}
})
loadGroupData(params.id)
.catch(error => { .catch(error => {
console.log(error); console.log(error);
return false; return false;


+ 1
- 1
src/pages/pnspsUserGroupSearchPage/UserGroupTable.js Просмотреть файл

@@ -68,7 +68,7 @@ export default function UserGroupTable({searchCriteria, applyGridOnReady,applySe
<FiDataGrid <FiDataGrid
columns={columns} columns={columns}
customPageSize={10} customPageSize={10}
pageSizeOptions={[10, 15, 20]}
hideRowsPerPage
onRowDoubleClick={handleRowDoubleClick} onRowDoubleClick={handleRowDoubleClick}
applyGridOnReady={applyGridOnReady} applyGridOnReady={applyGridOnReady}
applySearch={applySearch} applySearch={applySearch}


+ 13
- 10
src/utils/ApiPathConst.js Просмотреть файл

@@ -5,6 +5,7 @@ import {


// GET request // GET request
export const REFRESH_TOKEN = "/refresh-token" export const REFRESH_TOKEN = "/refresh-token"
export const LOGOUT = "/logout"
export const CHANGE_PASSWORD_PATH = "/user/change-password" export const CHANGE_PASSWORD_PATH = "/user/change-password"


export const GET_SYS_PARAMS = apiPath+'/settings'; export const GET_SYS_PARAMS = apiPath+'/settings';
@@ -28,11 +29,11 @@ export const GLD_USER_PROFILE_PATH = apiPath+'/user/gldProfile';
export const GET_AUTH_LIST = '/user/auth/combo'; export const GET_AUTH_LIST = '/user/auth/combo';
export const GET_USER_COMBO_LIST = '/user/combo'; export const GET_USER_COMBO_LIST = '/user/combo';
export const GET_USER_GLD_COMBO_LIST = '/user/combo/gld'; export const GET_USER_GLD_COMBO_LIST = '/user/combo/gld';
export const GET_USER_LOCK = apiPath+'/user/lock';
export const GET_USER_UNLOCK = apiPath+'/user/unlock';
export const GET_USER_LOCK = apiPath+'/user/lock'; //POST
export const GET_USER_UNLOCK = apiPath+'/user/unlock'; //POST


export const GET_IND_USER_PATH = apiPath+'/user/ind'; export const GET_IND_USER_PATH = apiPath+'/user/ind';
export const GET_IND_USER_VERIFY = apiPath+'/user/verify';
export const GET_IND_USER_VERIFY = apiPath+'/user/verify'; //POST
export const POST_IND_USER = apiPath+'/user/ind'; export const POST_IND_USER = apiPath+'/user/ind';


export const GET_ORG_USER_PATH = apiPath+'/user/org'; export const GET_ORG_USER_PATH = apiPath+'/user/org';
@@ -50,10 +51,10 @@ export const POST_ORG_SAVE_PATH = apiPath+'/org/save';
export const GET_ORG_COMBO = apiPath+'/org/combo'; export const GET_ORG_COMBO = apiPath+'/org/combo';
export const GET_ORG_CHECK_CREDITOR = apiPath+'/org/check-creditor'; export const GET_ORG_CHECK_CREDITOR = apiPath+'/org/check-creditor';
export const GET_ORG_MARK_AS_CREDITOR = apiPath+'/org/mark-as-creditor'; export const GET_ORG_MARK_AS_CREDITOR = apiPath+'/org/mark-as-creditor';
export const GET_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/mark-as-non-creditor';
export const GET_SEND_TERMINATION_OF_CREDIT = apiPath+'/org/sendDn_terminationOfCredit';
export const GET_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/mark-as-non-creditor'; //POST
export const GET_SEND_TERMINATION_OF_CREDIT = apiPath+'/org/sendDn_terminationOfCredit'; //POST
export const GET_ORG_EXPORT = apiPath+'/org/export'; export const GET_ORG_EXPORT = apiPath+'/org/export';
export const GET_SEND_OVERDUE_CREDITOR_LIST = apiPath+'/org/sendDn_OverdueCreditorList';
export const GET_SEND_OVERDUE_CREDITOR_LIST = apiPath+'/org/sendDn_OverdueCreditorList'; //POST


//public //public
export const GET_PUB_ORG_PATH = apiPath+'/org/pub'; export const GET_PUB_ORG_PATH = apiPath+'/org/pub';
@@ -78,7 +79,7 @@ export const CHECK_OVERDUE = apiPath+'/application/check-overdue';
export const FILE_UP_POST = apiPath+'/file/ul'; export const FILE_UP_POST = apiPath+'/file/ul';
export const FILE_DOWN_GET = apiPath+"/file/dl"; export const FILE_DOWN_GET = apiPath+"/file/dl";
export const POST_FILE_LIST = apiPath+'/file/list'; export const POST_FILE_LIST = apiPath+'/file/list';
export const GET_FILE_DELETE = apiPath+'/file/delete';
export const GET_FILE_DELETE = apiPath+'/file/delete'; //DELETE /{fileId}/{skey}/{filename}
//export const FILE_DOWN_GET = ({id,skey,filename})=>{ return apiPath+'/file/dl/'+id+'/'+skey+'/'+filename}; //export const FILE_DOWN_GET = ({id,skey,filename})=>{ return apiPath+'/file/dl/'+id+'/'+skey+'/'+filename};


export const DR_EXPORT = apiPath+'/settings/dr/export'; export const DR_EXPORT = apiPath+'/settings/dr/export';
@@ -119,8 +120,8 @@ export const PATCH_CHANGE_PASSWORD = apiPath+'/user/change-password';


//Public //Public
export const GET_PUBLIC_ORG_USER_LIST = apiPath+'/user/listOrg'; export const GET_PUBLIC_ORG_USER_LIST = apiPath+'/user/listOrg';
export const GET_SET_PRIMARY_USER = apiPath+'/user/primary';
export const GET_SET_UN_PRIMARY_USER = apiPath+'/user/un-primary';
export const GET_SET_PRIMARY_USER = apiPath+'/user/primary'; //POST
export const GET_SET_UN_PRIMARY_USER = apiPath+'/user/un-primary'; //POST


export const GET_PUBLIC_NOTICE_LIST = apiPath+'/application/list'; export const GET_PUBLIC_NOTICE_LIST = apiPath+'/application/list';
export const GET_PUBLIC_NOTICE_LIST_ListByStatus = apiPath+'/application/status-list'; export const GET_PUBLIC_NOTICE_LIST_ListByStatus = apiPath+'/application/status-list';
@@ -164,6 +165,7 @@ export const GET_ISSUE_LIST = apiPath+'/gazette-issue/export';//GET
export const POST_ISSUE_FILE = apiPath+'/gazette-issue/import';//POST export const POST_ISSUE_FILE = apiPath+'/gazette-issue/import';//POST
export const GET_ISSUE = apiPath+'/gazette-issue/list'; //GET export const GET_ISSUE = apiPath+'/gazette-issue/list'; //GET
export const GET_ISSUE_YEAR_COMBO = apiPath+'/gazette-issue/combo-year'; //GET export const GET_ISSUE_YEAR_COMBO = apiPath+'/gazette-issue/combo-year'; //GET
export const DELETE_ISSUE_YEAR = apiPath+'/gazette-issue/year'; //DELETE /{year}


export const CHECK_CREATE_PROOF = apiPath+'/proof/check-create';//GET export const CHECK_CREATE_PROOF = apiPath+'/proof/check-create';//GET
export const LIST_PROOF = apiPath+'/proof/list';//GET export const LIST_PROOF = apiPath+'/proof/list';//GET
@@ -173,7 +175,7 @@ export const GET_PROOF = apiPath+'/proof/details';//GET
export const REPLY_PROOF = apiPath+'/proof/reply';//GET export const REPLY_PROOF = apiPath+'/proof/reply';//GET
export const PROOF_CHECK_PRICE = apiPath+'/proof/check-price';//GET export const PROOF_CHECK_PRICE = apiPath+'/proof/check-price';//GET
export const GET_PROOF_PAY = apiPath+'/proof/pay-details';//GET export const GET_PROOF_PAY = apiPath+'/proof/pay-details';//GET
export const CANCEL_PROOF = apiPath+'/proof/cancel';//GET
export const CANCEL_PROOF = apiPath+'/proof/cancel';//POST


//payment //payment
export const PAYMENT_CREATE = apiPath+'/payment/create';//POST export const PAYMENT_CREATE = apiPath+'/payment/create';//POST
@@ -254,6 +256,7 @@ export const GET_HOLIDAY = apiPath+'/holiday/list'; //GET
export const POST_HOLIDAY = apiPath+'/holiday/import'; //POST export const POST_HOLIDAY = apiPath+'/holiday/import'; //POST
export const GET_HOLIDAY_COMBO = apiPath+'/holiday/combo'; //GET export const GET_HOLIDAY_COMBO = apiPath+'/holiday/combo'; //GET
export const GET_HOLIDAY_TEMPLATE = apiPath+'/holiday/export'; //GET export const GET_HOLIDAY_TEMPLATE = apiPath+'/holiday/export'; //GET
export const DELETE_HOLIDAY_YEAR = apiPath+'/holiday/year'; //DELETE /{year}




export const GET_JVM_INFO = apiPath+'/jvm-info'; //GET export const GET_JVM_INFO = apiPath+'/jvm-info'; //GET

+ 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 }) => { export const post = ({ url, params, onSuccess, onFail, onError, headers }) => {
headers = headers ? headers : { headers = headers ? headers : {
"Content-Type": "application/json" "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: dependencies:
"@types/jest" "*" "@types/jest" "*"


"@types/trusted-types@^2.0.2":
"@types/trusted-types@^2.0.2", "@types/trusted-types@^2.0.7":
version "2.0.7" version "2.0.7"
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
@@ -3436,13 +3436,14 @@ axe-core@^4.10.0:
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz"
integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==


axios@^1.12.2:
version "1.15.0"
resolved "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz"
integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==
axios@^1.20.0:
version "1.20.0"
resolved "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz"
integrity sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==
dependencies: dependencies:
follow-redirects "^1.15.11"
form-data "^4.0.5"
follow-redirects "^1.16.0"
form-data "^4.0.6"
https-proxy-agent "^5.0.1"
proxy-from-env "^2.1.0" proxy-from-env "^2.1.0"


axobject-query@^4.1.0: axobject-query@^4.1.0:
@@ -4680,6 +4681,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
dependencies: dependencies:
domelementtype "^2.2.0" domelementtype "^2.2.0"


dompurify@^3.4.15:
version "3.4.15"
resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz"
integrity sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==
optionalDependencies:
"@types/trusted-types" "^2.0.7"

domutils@^1.7.0: domutils@^1.7.0:
version "1.7.0" version "1.7.0"
resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz" resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz"
@@ -5573,10 +5581,10 @@ flatted@^3.2.9:
resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==


follow-redirects@^1.0.0, follow-redirects@^1.15.11:
version "1.15.11"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz"
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
follow-redirects@^1.0.0, follow-redirects@^1.16.0:
version "1.16.0"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz"
integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==


for-each@^0.3.3, for-each@^0.3.5: for-each@^0.3.3, for-each@^0.3.5:
version "0.3.5" version "0.3.5"
@@ -5623,16 +5631,16 @@ form-data@^3.0.0:
hasown "^2.0.2" hasown "^2.0.2"
mime-types "^2.1.35" mime-types "^2.1.35"


form-data@^4.0.5:
version "4.0.5"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz"
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
form-data@^4.0.6:
version "4.0.6"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz"
integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==
dependencies: dependencies:
asynckit "^0.4.0" asynckit "^0.4.0"
combined-stream "^1.0.8" combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0" es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
hasown "^2.0.4"
mime-types "^2.1.35"


format@^0.2.0: format@^0.2.0:
version "0.2.2" version "0.2.2"
@@ -5718,6 +5726,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==


fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==

function-bind@^1.1.2: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -5964,10 +5977,10 @@ has-tostringtag@^1.0.2:
dependencies: dependencies:
has-symbols "^1.0.3" has-symbols "^1.0.3"


hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
hasown@^2.0.2, hasown@^2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz"
integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==
dependencies: dependencies:
function-bind "^1.1.2" function-bind "^1.1.2"


@@ -6147,7 +6160,7 @@ http-proxy@^1.18.1:
follow-redirects "^1.0.0" follow-redirects "^1.0.0"
requires-port "^1.0.0" requires-port "^1.0.0"


https-proxy-agent@^5.0.0:
https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
@@ -7732,7 +7745,7 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8:
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==


mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.35, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35" version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==


Загрузка…
Отмена
Сохранить