コミットを比較

...

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:

- [permissions-policy-apache.conf.md](./permissions-policy-apache.conf.md) — Permissions-Policy reporting via the same `/api/csp-report` URL
- [csp-report-review-2026-08-14.md](./csp-report-review-2026-08-14.md) — latest PROD + UAT report review and add/remove summary
- [csp-report-review-2026-08-03.md](./csp-report-review-2026-08-03.md) — earlier PROD report analysis that drove `frame-src` / `media-src` updates



+ 152
- 0
docs/permissions-policy-apache.conf.md ファイルの表示

@@ -0,0 +1,152 @@
# Permissions-Policy reporting (Apache)

The live `Permissions-Policy` header denies features with an empty allowlist
(`feature=()` — grant to nobody). That header does **not** send reports by
itself.

Unlike CSP, Permissions-Policy has **no `report-uri`**. Reporting uses the
Reporting API: a named endpoint in `Reporting-Endpoints`, plus a per-feature
`report-to` parameter.

Reuse the existing CSP collector:

- UAT: `https://pnspsuat.gld.gov.hk/api/csp-report`
- PROD: `https://pnsps.gld.gov.hk/api/csp-report`

Backend: `CspReportController` (`POST /csp-report`) already logs the raw JSON
and `Content-Type` and returns `204`. No backend change is required.

See also: [csp-apache.conf.md](./csp-apache.conf.md)

## Why not `report-uri`

This CSP-style trailer does **not** produce Permissions-Policy reports:

```apache
# Wrong — browsers ignore report-uri on Permissions-Policy
Header always set Permissions-Policy "camera=(), geolocation=(); report-uri https://pnspsuat.gld.gov.hk/api/csp-report"
```

`report-to` is a **parameter of each feature**, not a global trailing directive.

## Does every feature need `report-to`?

Yes, **if you want a report for that feature**.

- `feature=()` — still blocked, but silent (no report)
- `feature=();report-to=csp-endpoint` — blocked **and** reported

There is no `all=()` reporter and no header-level `report-to` that applies to
every feature. Add `;report-to=csp-endpoint` only on features you want in the
log. Leave the rest as `()` if you do not need those reports.

```apache
# camera is reported; geolocation is still blocked, but not reported
Header always set Permissions-Policy "camera=();report-to=csp-endpoint, geolocation=()"
```

## Enforcing + reporting

Keep the current deny-all policy. Add `Reporting-Endpoints`, then append
`;report-to=csp-endpoint` on each feature you want to monitor.

Copy the feature list from the live header; only the `report-to` parameter is
new. Example (UAT):

```apache
Header always set Reporting-Endpoints "csp-endpoint=\"https://pnspsuat.gld.gov.hk/api/csp-report\""

Header always set Permissions-Policy "\
accelerometer=();report-to=csp-endpoint, \
autoplay=();report-to=csp-endpoint, \
camera=();report-to=csp-endpoint, \
display-capture=();report-to=csp-endpoint, \
encrypted-media=();report-to=csp-endpoint, \
fullscreen=();report-to=csp-endpoint, \
geolocation=();report-to=csp-endpoint, \
gyroscope=();report-to=csp-endpoint, \
magnetometer=();report-to=csp-endpoint, \
microphone=();report-to=csp-endpoint, \
midi=();report-to=csp-endpoint, \
payment=();report-to=csp-endpoint, \
picture-in-picture=();report-to=csp-endpoint, \
publickey-credentials-get=();report-to=csp-endpoint, \
screen-wake-lock=();report-to=csp-endpoint, \
usb=();report-to=csp-endpoint, \
web-share=();report-to=csp-endpoint, \
xr-spatial-tracking=();report-to=csp-endpoint"
```

PROD: same headers, with

`https://pnsps.gld.gov.hk/api/csp-report`

Because deny-all is already enforced, put `report-to` on **`Permissions-Policy`**
(not only Report-Only). Those reports have `disposition: "enforce"`.

## Report-Only (observe without blocking)

Use `Permissions-Policy-Report-Only` only when testing a restriction that is
**not** already denied by the enforcing header. Reports have
`disposition: "report"`.

```apache
Header always set Reporting-Endpoints "csp-endpoint=\"https://pnspsuat.gld.gov.hk/api/csp-report\""
Header always set Permissions-Policy-Report-Only "geolocation=();report-to=csp-endpoint"
```

Report-Only cannot re-enable a feature already denied by `Permissions-Policy`.

## Same URL, different payload

Keep CSP on `report-uri` as it is. The collector URL is shared; the body is not.

| Source | `Content-Type` | Body |
| --- | --- | --- |
| CSP `report-uri` | `application/csp-report` | `{ "csp-report": { … } }` |
| Permissions-Policy | `application/reports+json` | JSON **array**, `type` = `permissions-policy-violation` |

Example Permissions-Policy report:

```json
[{
"type": "permissions-policy-violation",
"url": "https://pnspsuat.gld.gov.hk/",
"body": {
"disposition": "enforce",
"featureId": "geolocation",
"message": "Permissions policy violation: geolocation access has been blocked because of a permissions policy applied to the current document."
}
}]
```

Chrome often serializes the feature as `policyId` instead of `featureId`. Filter
logs on `permissions-policy-violation` vs `csp-report` so the two streams stay
distinct.

Optional: also point CSP at the same named endpoint (CSP `report-uri` remains
for older browsers):

```apache
Header always set Content-Security-Policy-Report-Only "…; report-uri https://pnspsuat.gld.gov.hk/api/csp-report; report-to csp-endpoint"
```

## Follow-up checklist

- [ ] Confirm the live Apache `Permissions-Policy` feature list (deny-all `()`).
- [ ] Add `Reporting-Endpoints` → existing `/api/csp-report` (UAT vs PROD host).
- [ ] Add `;report-to=csp-endpoint` only on features you want in the log.
- [ ] Deploy to UAT first; trigger a blocked API (e.g. `navigator.geolocation`) in Chrome/Edge.
- [ ] Confirm a `permissions-policy-violation` line in the backend log (may be batched, a few seconds later).
- [ ] Repeat on PROD with the PROD report URL.

## Notes

- Reporting is Chromium-only (Chrome / Edge). Safari and Firefox still enforce
`()` and usually send nothing.
- Reports are batched and may arrive a few seconds after the violation, not on
the same page request.
- Same-origin `/api/csp-report` needs no extra CORS setup. The endpoint is
already unauthenticated and CSRF is disabled.
- Do not allowlist a feature in `Permissions-Policy` just to silence a report.
Only grant a feature if the application itself needs it.

+ 30
- 17
package-lock.json ファイルの表示

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


+ 2
- 1
package.json ファイルの表示

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


バイナリ
public/favicon.ico ファイルの表示

変更前 変更後

+ 7
- 2
src/auth/index.js ファイルの表示

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

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

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

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

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


+ 106
- 23
src/components/FiDataGrid.js ファイルの表示

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

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

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

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

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

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

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

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

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

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

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


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

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

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


+ 2
- 7
src/components/FileList.js ファイルの表示

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

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


+ 17
- 0
src/components/SafeHtml.js ファイルの表示

@@ -0,0 +1,17 @@
import PropTypes from 'prop-types';
import { sanitizeHtml } from 'utils/sanitizeHtml';

/**
* Renders HTML after {@link sanitizeHtml}. Use instead of raw
* dangerouslySetInnerHTML for messages, settings, CMS, and i18n HTML.
*/
const SafeHtml = ({ html, component: Component = 'div', ...rest }) => {
return <Component {...rest} dangerouslySetInnerHTML={{ __html: sanitizeHtml(html) }} />;
};

SafeHtml.propTypes = {
html: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
component: PropTypes.elementType
};

export default SafeHtml;

+ 20
- 4
src/pages/GFMIS/DataGrid.js ファイルの表示

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

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


+ 26
- 2
src/pages/GFMIS/TransactionDataGrid.js ファイルの表示

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

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


+ 1
- 13
src/pages/GazetteIssue/DataGrid.js ファイルの表示

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


+ 3
- 26
src/pages/GazetteIssue/ExportForm.js ファイルの表示

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

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

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

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


+ 64
- 38
src/pages/GazetteIssue/SearchForm.js ファイルの表示

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


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

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

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

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

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

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

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


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

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

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

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

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

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


+ 56
- 6
src/pages/GazetteIssue/index.js ファイルの表示

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

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

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

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

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


+ 62
- 36
src/pages/Holiday/SearchForm.js ファイルの表示

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


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

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

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

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

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

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


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

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

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

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

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

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


+ 68
- 15
src/pages/Holiday/index.js ファイルの表示

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

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

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

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

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

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

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

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

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


+ 2
- 1
src/pages/Message/Details/index.js ファイルの表示

@@ -19,6 +19,7 @@ import titleBackgroundImg from 'assets/images/dashboard/gazette-bar.png'
import { FormattedMessage } from "react-intl";
import usePageTitle from 'components/usePageTitle';
import { PRIMARY_CONTAINED_BUTTON_SX } from 'themes/colorConst';
import SafeHtml from 'components/SafeHtml';

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

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


+ 2
- 2
src/pages/Organization/DetailPage/OrganizationCard.js ファイルの表示

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

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

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


+ 60
- 22
src/pages/Payment/MultiPaymentWindow.js ファイルの表示

@@ -25,6 +25,7 @@ import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));
import * as FormatUtils from "utils/FormatUtils";

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

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

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

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

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

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

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

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

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

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

const latestData = {};

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

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

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

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

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

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


+ 1
- 1
src/pages/Payment/Search_GLD/DataGrid.js ファイルの表示

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


+ 117
- 30
src/pages/Payment/Search_GLD/SearchForm.js ファイルの表示

@@ -18,6 +18,21 @@ import dayjs from "dayjs";
import {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


+ 1
- 1
src/pages/Payment/Search_Public/DataGrid.js ファイルの表示

@@ -119,7 +119,7 @@ export default function SearchPublicNoticeTable({ searchCriteria, applyGridOnRea
<div style={{ minHeight: 400, width: '100%' }}>
<FiDataGrid
sx={_sx}
rowHeight={80}
getRowHeight={() => 'auto'}
columns={columns}
customPageSize={10}
onRowDoubleClick={handleEditDoubleClick}


+ 116
- 32
src/pages/Payment/Search_Public/SearchForm.js ファイルの表示

@@ -20,6 +20,21 @@ import dayjs from "dayjs";
import {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


+ 90
- 23
src/pages/Payment/index.js ファイルの表示

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

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

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

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

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

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

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


+ 1
- 1
src/pages/Proof/Create_FromApp/ProofForm.js ファイルの表示

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


+ 5
- 6
src/pages/Proof/Payment/Pay.js ファイルの表示

@@ -28,6 +28,7 @@ const BackgroundHead = {
import { PNSPS_BUTTON_THEME, PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst";
import { ThemeProvider } from "@emotion/react";
import { FormattedMessage, useIntl } from "react-intl";
import SafeHtml from 'components/SafeHtml';
// ==============================|| DASHBOARD - DEFAULT ||============================== //

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

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

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


+ 9
- 16
src/pages/Proof/Payment/Pay_DN.js ファイルの表示

@@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils";

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

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


</Typography>


+ 13
- 24
src/pages/Proof/Payment/Pay_Office.js ファイルの表示

@@ -14,6 +14,7 @@ import * as DateUtils from "utils/DateUtils";

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


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


</Typography>


+ 9
- 14
src/pages/Proof/Payment/Pay_Online.js ファイルの表示

@@ -22,6 +22,7 @@ import Loadable from 'components/Loadable';
const LoadingComponent = Loadable(React.lazy(() => import('pages/extra-pages/LoadingComponent')));

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

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

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

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


+ 1
- 1
src/pages/Proof/Reply_GLD/ApplicationDetails.js ファイルの表示

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


+ 6
- 5
src/pages/Proof/Reply_Public/ProofForm.js ファイルの表示

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


+ 12
- 28
src/pages/Proof/Search_GLD/DataGrid.js ファイルの表示

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

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

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

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

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

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


+ 137
- 46
src/pages/Proof/Search_GLD/SearchForm.js ファイルの表示

@@ -21,6 +21,21 @@ import dayjs from "dayjs";
import {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

start: 0,
limit: 10,
};

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

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

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

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

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


+ 11
- 2
src/pages/Proof/Search_Public/DataGrid.js ファイルの表示

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

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

+ 125
- 32
src/pages/Proof/Search_Public/SearchForm.js ファイルの表示

@@ -21,6 +21,21 @@ import dayjs from "dayjs";
import {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


+ 15
- 15
src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js ファイルの表示

@@ -34,6 +34,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import { useState, useEffect, useRef } from 'react';
import { checkPaymentSuspension } from "utils/Utils";
import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";
import SafeHtml from 'components/SafeHtml';

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

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

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

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



+ 6
- 6
src/pages/PublicNotice/Details_GLD/index.js ファイルの表示

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

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

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

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


+ 11
- 2
src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js ファイルの表示

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

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

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


+ 1
- 1
src/pages/PublicNotice/Details_Public/index.js ファイルの表示

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


+ 2
- 1
src/pages/PublicNotice/ListPanel/PendingPaymentTab.js ファイルの表示

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


+ 122
- 30
src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js ファイルの表示

@@ -23,6 +23,20 @@ import {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


+ 6
- 26
src/pages/PublicNotice/ListPanel/SearchPublicNoticeTable.js ファイルの表示

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

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

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

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


+ 16
- 9
src/pages/PublicNotice/Search_GLD/DataGrid.js ファイルの表示

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

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


+ 140
- 34
src/pages/PublicNotice/Search_GLD/SearchForm.js ファイルの表示

@@ -21,6 +21,21 @@ import dayjs from "dayjs";
import {DemoItem} from "@mui/x-date-pickers/internals/demo";
import {LocalizationProvider} from "@mui/x-date-pickers/LocalizationProvider";
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


+ 2
- 1
src/pages/Setting/SystemSetting/Table.js ファイルの表示

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


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


+ 3
- 3
src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js ファイルの表示

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

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

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

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


+ 3
- 3
src/pages/User/DetailsPage_Organization/UserInformationCard_Organization.js ファイルの表示

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

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

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


+ 2
- 2
src/pages/User/ManagePage_OrgPublic/index.js ファイルの表示

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


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

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


+ 2
- 2
src/pages/User/SearchPage/UserTable.js ファイルの表示

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

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

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


+ 1
- 1
src/pages/_Test/index.js ファイルの表示

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

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


+ 18
- 21
src/pages/authentication/AuthWrapper.js ファイルの表示

@@ -10,6 +10,7 @@ import backbroundImg from 'assets/images/bg_ml.jpg';
import lgceImg from 'assets/images/2025_lgce.jpg';
import 'assets/style/loginStyles.css';
import { SysContext } from 'components/SysSettingProvider';
import SafeHtml from 'components/SafeHtml';
const AuthCard = Loadable(lazy(() => import('./AuthCardCustom')));

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

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

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


+ 2
- 1
src/pages/authentication/ForgotPassword/AuthCallback/index.js ファイルの表示

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


+ 2
- 1
src/pages/authentication/ForgotUsername/AuthCallback/index.js ファイルの表示

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


+ 4
- 3
src/pages/authentication/RegisterCustom.js ファイルの表示

@@ -17,6 +17,7 @@ import { I_AM_SMART_PATH } from "utils/ApiPathConst";
import * as React from 'react';
import { FormattedMessage, useIntl } from "react-intl";
import usePageTitle from "components/usePageTitle";
import SafeHtml from 'components/SafeHtml';

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

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

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


+ 2
- 1
src/pages/authentication/auth-forms/BusCustomFormWizard.js ファイルの表示

@@ -35,6 +35,7 @@ import { POST_PUBLIC_USER_REGISTER, POST_CAPTCHA, POST_USERNAME, POST_USER_EMAIL
import * as ComboData from "utils/ComboData";

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


+ 2
- 1
src/pages/authentication/auth-forms/CustomFormWizard.js ファイルの表示

@@ -28,6 +28,7 @@ import { POST_USERNAME, POST_USER_EMAIL, POST_CAPTCHA, POST_PUBLIC_USER_REGISTER
import * as ComboData from "utils/ComboData";

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


+ 2
- 1
src/pages/authentication/auth-forms/IAmSmartFormWizard.js ファイルの表示

@@ -26,6 +26,7 @@ import { POST_IAMSMART_USER_REGISTER, POST_CAPTCHA, POST_USER_EMAIL, POST_CAPTCH
import * as ComboData from "utils/ComboData";

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

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


+ 2
- 1
src/pages/dashboard/Public/index.js ファイルの表示

@@ -29,6 +29,7 @@ const LoadingComponent = Loadable(React.lazy(() => import('../../extra-pages/Loa

import { useNavigate } from "react-router-dom";
import usePageTitle from "components/usePageTitle";
import SafeHtml from 'components/SafeHtml';

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

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


+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_cn.js ファイルの表示

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

const AboutUs = () => {

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

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

}


+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_en.js ファイルの表示

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

const AboutUs = () => {

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

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

}


+ 2
- 1
src/pages/extra-pages/AboutUs/AboutUs_zh.js ファイルの表示

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

const AboutUs = () => {

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

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

}


+ 2
- 1
src/pages/extra-pages/ImportantNoticePage/index.js ファイルの表示

@@ -3,6 +3,7 @@ import { useIntl, FormattedMessage } from 'react-intl';
import usePageTitle from "components/usePageTitle";

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

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


+ 2
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js ファイルの表示

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

const Page = () => {

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

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

}


+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js ファイルの表示

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

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

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

export default Page;

+ 3
- 1
src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js ファイルの表示

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

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

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

export default Page;

+ 2
- 1
src/pages/extra-pages/PrivacyPolicyPage/index.js ファイルの表示

@@ -3,6 +3,7 @@ import { useIntl, FormattedMessage } from 'react-intl';
import usePageTitle from "components/usePageTitle";

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

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


+ 2
- 1
src/pages/iAmSmart/PleaseLoginCallback/index.js ファイルの表示

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

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


+ 4
- 2
src/pages/iAmSmart/SuccessCallback/index.js ファイルの表示

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

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


+ 15
- 7
src/pages/pnspsUserGroupDetailPage/GroupAuthCard.js ファイルの表示

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

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

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

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


return (


+ 4
- 2
src/pages/pnspsUserGroupDetailPage/GroupAuthTable.js ファイルの表示

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


+ 3
- 4
src/pages/pnspsUserGroupDetailPage/UserAddCard.js ファイルの表示

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

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


return (


+ 46
- 20
src/pages/pnspsUserGroupDetailPage/index.js ファイルの表示

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

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

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

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

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

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

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


+ 1
- 1
src/pages/pnspsUserGroupSearchPage/UserGroupTable.js ファイルの表示

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


+ 13
- 10
src/utils/ApiPathConst.js ファイルの表示

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

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

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

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

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

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

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

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

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

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

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


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

+ 8
- 0
src/utils/HttpUtils.js ファイルの表示

@@ -34,6 +34,14 @@ export const patch = ({ url, params, onSuccess, onFail, onError }) => {
});
};

export const del = ({ url, params, onSuccess, onFail, onError }) => {
axios.delete(url, { params }).then(
(response) => { onResponse(response, onSuccess, onFail); }
).catch(error => {
return handleError(error, onError);
});
};

export const post = ({ url, params, onSuccess, onFail, onError, headers }) => {
headers = headers ? headers : {
"Content-Type": "application/json"


+ 78
- 0
src/utils/sanitizeHtml.js ファイルの表示

@@ -0,0 +1,78 @@
import DOMPurify from 'dompurify';

const ALLOWED_TAGS = [
'a',
'b',
'blockquote',
'br',
'caption',
'col',
'colgroup',
'div',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'li',
'ol',
'p',
'span',
'strong',
'style',
'sub',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'u',
'ul'
];

const ALLOWED_ATTR = [
'href',
'title',
'target',
'rel',
'src',
'alt',
'width',
'height',
'colspan',
'rowspan',
'scope',
'class',
'style'
];

if (typeof window !== 'undefined') {
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {
node.setAttribute('rel', 'noopener noreferrer');
}
});
}

/**
* Strict allow-list sanitize for API / i18n HTML before innerHTML.
* Scripts, event handlers, iframes, and javascript: URLs are dropped.
*/
export function sanitizeHtml(dirty) {
if (dirty == null) {
return '';
}
return DOMPurify.sanitize(String(dirty), {
ALLOWED_TAGS,
ALLOWED_ATTR,
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i
});
}

+ 53
- 0
src/utils/sanitizeHtml.test.js ファイルの表示

@@ -0,0 +1,53 @@
import { sanitizeHtml } from './sanitizeHtml';

describe('sanitizeHtml', () => {
test('strips script tags and event handlers', () => {
const clean = sanitizeHtml('<p onclick="alert(1)">Hi<script>alert(1)</script></p>');
expect(clean).toContain('Hi');
expect(clean.toLowerCase()).not.toContain('script');
expect(clean.toLowerCase()).not.toContain('onclick');
expect(clean).not.toContain('alert(1)');
});

test('strips javascript URLs and iframes', () => {
const clean = sanitizeHtml(
'<a href="javascript:alert(1)">x</a><iframe src="https://evil.example"></iframe>'
);
expect(clean.toLowerCase()).not.toContain('javascript');
expect(clean.toLowerCase()).not.toContain('iframe');
expect(clean).not.toContain('evil.example');
});

test('keeps safe formatting, https links, and tables', () => {
const clean = sanitizeHtml(
'<p>Hello <strong>world</strong></p><a href="https://pnsps.gld.gov.hk/">PNSPS</a><table><tr><td>cell</td></tr></table>'
);
expect(clean).toContain('<p>');
expect(clean).toMatch(/<(strong|b)>world<\/(strong|b)>/);
expect(clean).toContain('https://pnsps.gld.gov.hk/');
expect(clean).toContain('cell');
});

test('keeps mailto links', () => {
const clean = sanitizeHtml('<a href="mailto:[email protected]">mail</a>');
expect(clean).toContain('mailto:[email protected]');
});

test('adds rel on target=_blank links', () => {
const clean = sanitizeHtml('<a href="https://egazette.gld.gov.hk/en" target="_blank">Gazette</a>');
expect(clean).toContain('target="_blank"');
expect(clean).toContain('noopener');
});

test('null and empty become empty string', () => {
expect(sanitizeHtml(null)).toBe('');
expect(sanitizeHtml(undefined)).toBe('');
expect(sanitizeHtml('')).toBe('');
});

test('img onerror is dropped', () => {
const clean = sanitizeHtml('<img src="https://example.com/a.png" onerror="alert(1)">');
expect(clean.toLowerCase()).not.toContain('onerror');
expect(clean).not.toContain('alert(1)');
});
});

+ 36
- 23
yarn.lock ファイルの表示

@@ -2748,7 +2748,7 @@
dependencies:
"@types/jest" "*"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


読み込み中…
キャンセル
保存