コミットを比較

...

37 コミット

作成者 SHA1 メッセージ 日付
  Jason Chuang d647678c93 CR026 Item 3 Registration File Format 1日前
  Jason Chuang 0294c7c8ad CR026 Item 2 upload BR 1日前
  Jason Chuang 42ff4d0b65 CR026 Item 1 support JPG PNG 1日前
  Jason Chuang 394ce437f9 npm compile refine 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ヶ月前
92個のファイルの変更3066行の追加942行の削除
分割表示
  1. +30
    -17
      package-lock.json
  2. +2
    -1
      package.json
  3. バイナリ
      public/favicon.ico
  4. +7
    -2
      src/auth/index.js
  5. +130
    -0
      src/components/BrStatusDialog.js
  6. +106
    -23
      src/components/FiDataGrid.js
  7. +2
    -7
      src/components/FileList.js
  8. +17
    -0
      src/components/SafeHtml.js
  9. +20
    -4
      src/pages/GFMIS/DataGrid.js
  10. +26
    -2
      src/pages/GFMIS/TransactionDataGrid.js
  11. +1
    -13
      src/pages/GazetteIssue/DataGrid.js
  12. +3
    -26
      src/pages/GazetteIssue/ExportForm.js
  13. +64
    -38
      src/pages/GazetteIssue/SearchForm.js
  14. +56
    -6
      src/pages/GazetteIssue/index.js
  15. +62
    -36
      src/pages/Holiday/SearchForm.js
  16. +68
    -15
      src/pages/Holiday/index.js
  17. +2
    -1
      src/pages/Message/Details/index.js
  18. +142
    -11
      src/pages/Organization/DetailPage/OrganizationCard.js
  19. +21
    -4
      src/pages/Organization/DetailPage/OrganizationPubCard.js
  20. +6
    -0
      src/pages/Organization/DetailPage/index.js
  21. +30
    -0
      src/pages/Organization/SearchPage/OrganizationTable.js
  22. +388
    -0
      src/pages/Organization/SubmitBrPage/index.js
  23. +60
    -22
      src/pages/Payment/MultiPaymentWindow.js
  24. +1
    -1
      src/pages/Payment/Search_GLD/DataGrid.js
  25. +117
    -30
      src/pages/Payment/Search_GLD/SearchForm.js
  26. +1
    -1
      src/pages/Payment/Search_Public/DataGrid.js
  27. +116
    -32
      src/pages/Payment/Search_Public/SearchForm.js
  28. +90
    -23
      src/pages/Payment/index.js
  29. +1
    -1
      src/pages/Proof/Create_FromApp/ProofForm.js
  30. +5
    -6
      src/pages/Proof/Payment/Pay.js
  31. +9
    -16
      src/pages/Proof/Payment/Pay_DN.js
  32. +13
    -24
      src/pages/Proof/Payment/Pay_Office.js
  33. +9
    -14
      src/pages/Proof/Payment/Pay_Online.js
  34. +1
    -1
      src/pages/Proof/Reply_GLD/ApplicationDetails.js
  35. +27
    -8
      src/pages/Proof/Reply_Public/ProofForm.js
  36. +12
    -28
      src/pages/Proof/Search_GLD/DataGrid.js
  37. +137
    -46
      src/pages/Proof/Search_GLD/SearchForm.js
  38. +11
    -2
      src/pages/Proof/Search_Public/DataGrid.js
  39. +125
    -32
      src/pages/Proof/Search_Public/SearchForm.js
  40. +15
    -15
      src/pages/PublicNotice/ApplyForm/PublicNoticeApplyForm.js
  41. +15
    -3
      src/pages/PublicNotice/ApplyForm/index.js
  42. +6
    -6
      src/pages/PublicNotice/Details_GLD/index.js
  43. +11
    -2
      src/pages/PublicNotice/Details_Public/ApplicationDetailCard.js
  44. +1
    -1
      src/pages/PublicNotice/Details_Public/index.js
  45. +7
    -4
      src/pages/PublicNotice/ListPanel/PendingPaymentTab.js
  46. +122
    -30
      src/pages/PublicNotice/ListPanel/SearchPublicNoticeForm.js
  47. +6
    -26
      src/pages/PublicNotice/ListPanel/SearchPublicNoticeTable.js
  48. +28
    -0
      src/pages/PublicNotice/ListPanel/index.js
  49. +16
    -9
      src/pages/PublicNotice/Search_GLD/DataGrid.js
  50. +140
    -34
      src/pages/PublicNotice/Search_GLD/SearchForm.js
  51. +66
    -5
      src/pages/Setting/SystemSetting/Table.js
  52. +3
    -3
      src/pages/User/DetailsPage_Individual/UserInformationCard_Individual.js
  53. +3
    -3
      src/pages/User/DetailsPage_Organization/UserInformationCard_Organization.js
  54. +2
    -2
      src/pages/User/ManagePage_OrgPublic/index.js
  55. +2
    -2
      src/pages/User/SearchPage/UserTable.js
  56. +1
    -1
      src/pages/_Test/index.js
  57. +18
    -21
      src/pages/authentication/AuthWrapper.js
  58. +2
    -1
      src/pages/authentication/ForgotPassword/AuthCallback/index.js
  59. +2
    -1
      src/pages/authentication/ForgotUsername/AuthCallback/index.js
  60. +4
    -3
      src/pages/authentication/RegisterCustom.js
  61. +6
    -2
      src/pages/authentication/auth-forms/BusCustomFormWizard.js
  62. +6
    -2
      src/pages/authentication/auth-forms/CustomFormWizard.js
  63. +2
    -1
      src/pages/authentication/auth-forms/IAmSmartFormWizard.js
  64. +39
    -2
      src/pages/dashboard/Public/index.js
  65. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_cn.js
  66. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_en.js
  67. +2
    -1
      src/pages/extra-pages/AboutUs/AboutUs_zh.js
  68. +2
    -1
      src/pages/extra-pages/ImportantNoticePage/index.js
  69. +2
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_cn.js
  70. +3
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_en.js
  71. +3
    -1
      src/pages/extra-pages/PrivacyPolicyPage/PrivacyPolicy_zh.js
  72. +2
    -1
      src/pages/extra-pages/PrivacyPolicyPage/index.js
  73. +2
    -1
      src/pages/iAmSmart/PleaseLoginCallback/index.js
  74. +4
    -2
      src/pages/iAmSmart/SuccessCallback/index.js
  75. +15
    -7
      src/pages/pnspsUserGroupDetailPage/GroupAuthCard.js
  76. +4
    -2
      src/pages/pnspsUserGroupDetailPage/GroupAuthTable.js
  77. +3
    -4
      src/pages/pnspsUserGroupDetailPage/UserAddCard.js
  78. +46
    -20
      src/pages/pnspsUserGroupDetailPage/index.js
  79. +1
    -1
      src/pages/pnspsUserGroupSearchPage/UserGroupTable.js
  80. +5
    -0
      src/routes/PublicUserRoutes.js
  81. +15
    -0
      src/themes/colorConst.js
  82. +28
    -5
      src/translations/en.json
  83. +27
    -4
      src/translations/zh-CN.json
  84. +27
    -4
      src/translations/zh-HK.json
  85. +16
    -10
      src/utils/ApiPathConst.js
  86. +5
    -4
      src/utils/FieldUtils.js
  87. +151
    -129
      src/utils/HttpUtils.js
  88. +56
    -0
      src/utils/orgBrUtils.js
  89. +49
    -46
      src/utils/registerValidation.js
  90. +64
    -0
      src/utils/sanitizeHtml.js
  91. +53
    -0
      src/utils/sanitizeHtml.test.js
  92. +36
    -23
      yarn.lock

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


+ 130
- 0
src/components/BrStatusDialog.js ファイルの表示

@@ -0,0 +1,130 @@
import {
Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography
} from '@mui/material';
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
import SafeHtml from "components/SafeHtml";
import * as DateUtils from "utils/DateUtils";
import { GREY_CONTAINED_BUTTON_SX, HEADER_BACKGROUND_COLOR, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";

const TITLE_IDS = {
expiring: "brPopupTitleExpiring",
expired: "brPopupTitleExpired",
pending: "brPopupTitlePendingVerify",
submitNotExpired: "brPopupTitlePendingVerify",
submitExpired: "brPopupTitlePendingVerify",
};

const MESSAGE_IDS = {
expiring: "brPopupExpiring",
expired: "brPopupExpired",
pending: "brPopupPendingVerify",
submitNotExpired: "brSubmitSuccessNotExpired",
submitExpired: "brSubmitSuccessExpired",
};

const TITLE_PREFIX = /^(?:<strong>)?\s*(Reminder|Action Required(?:\s*:[^<]*)?|Notice|【\s*溫馨提示\s*】|【\s*温馨提示\s*】|系統提示(?:[::][^<]*)?|系统提示(?:[::][^<]*)?)\s*(?:<\/strong>)?\s*[::]?\s*/i;

function stripTags(html) {
return (html || "")
.replace(/<[^>]+>/g, " ")
.replace(/&nbsp;/gi, " ")
.replace(/\s+/g, " ")
.trim();
}

function splitTitleFromHtml(html, fallbackTitle) {
const raw = (html || "").trim();
if (!raw) {
return { title: fallbackTitle, bodyHtml: "" };
}

const inner = raw
.replace(/^<p[^>]*>/i, "")
.replace(/<\/p>\s*$/i, "")
.trim();

const brMatch = inner.match(/<br\s*\/?>/i);
if (brMatch && brMatch.index >= 0) {
const title = stripTags(inner.slice(0, brMatch.index)).replace(/[::]\s*$/, "").trim();
const body = inner.slice(brMatch.index + brMatch[0].length).trim();
return {
title: title || fallbackTitle,
bodyHtml: body ? `<p>${body}</p>` : ""
};
}

const prefix = inner.match(TITLE_PREFIX);
if (prefix) {
const title = stripTags(prefix[1]).replace(/[::]\s*$/, "").trim();
const rest = inner.slice(prefix[0].length).trim();
return {
title: title || fallbackTitle,
bodyHtml: rest ? `<p>${rest}</p>` : raw
};
}

return { title: fallbackTitle, bodyHtml: raw.startsWith("<") ? raw : `<p>${raw}</p>` };
}

export default function BrStatusDialog({ open, variant, expiryDate, onClose }) {
const intl = useIntl();
const navigate = useNavigate();
const messageId = MESSAGE_IDS[variant] || MESSAGE_IDS.expiring;
const titleId = TITLE_IDS[variant] || TITLE_IDS.expiring;
const formattedDate = expiryDate ? DateUtils.dateStr(expiryDate) : "";
const html = (intl.formatMessage({ id: messageId, defaultMessage: "" }) || "")
.replaceAll("[BR_EXPIRY_DATE]", formattedDate);
const fallbackTitle = intl.formatMessage({ id: titleId, defaultMessage: "" });
const { title, bodyHtml } = splitTitleFromHtml(html, fallbackTitle);
const showGo = variant === "expiring" || variant === "expired";

const goToSubmit = () => {
if (onClose) {
onClose();
}
navigate("/org/submit-br");
};

return (
<Dialog
open={Boolean(open && variant)}
onClose={onClose}
maxWidth={false}
PaperProps={{
sx: {
width: { xs: "90vw", md: "40vw" },
maxWidth: { xs: "90vw", md: "40vw" },
maxHeight: { xs: "90vh", md: "70vh" },
overflow: "hidden"
}
}}
>
<DialogTitle
sx={{
bgcolor: HEADER_BACKGROUND_COLOR,
color: "#FFFFFF",
py: 1,
px: 3
}}
>
<Typography component="h2" variant="h6" sx={{ color: "#FFFFFF", fontWeight: 600 }}>
{title}
</Typography>
</DialogTitle>
<DialogContent sx={{ px: 3, pt: 2, pb: 2, "&.MuiDialogContent-root": { paddingTop: 2 }, "& p:first-of-type": { mt: 0 } }}>
<SafeHtml html={bodyHtml} />
</DialogContent>
<DialogActions>
{showGo ?
<Button variant="contained" onClick={goToSubmit} sx={PRIMARY_CONTAINED_BUTTON_SX}>
<Typography variant="h5"><FormattedMessage id="goToSubmitBr" /></Typography>
</Button>
: null}
<Button variant="contained" onClick={onClose} sx={GREY_CONTAINED_BUTTON_SX}>
<Typography variant="h5"><FormattedMessage id="close" /></Typography>
</Button>
</DialogActions>
</Dialog>
);
}

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


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

@@ -22,8 +22,10 @@ import Loadable from 'components/Loadable';
import { notifySaveSuccess } from 'utils/CommonFunction';
import { useIntl } from "react-intl";
import { PNSPS_BUTTON_THEME } from "themes/buttonConst";
import { GREY_CONTAINED_BUTTON_SX, PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";
import { ThemeProvider } from "@emotion/react";
import { isGrantedAny } from "auth/utils";
import FileList from "components/FileList";

import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import dayjs from "dayjs";
@@ -36,6 +38,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false);
const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false);
const [afterSendPopUp, setAfterSendPopUp] = React.useState(false);
const [confirmBrPopUp, setConfirmBrPopUp] = React.useState(false);

const [currentUserData, setCurrentUserData] = useState({});
const [overduePublicNotice, setOverduePublicNotice] = useState(0);
@@ -227,7 +230,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {

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

const sendDn_Overdue = () => {
setNonCreditorConfirmPopUp(false);
HttpUtils.get({
HttpUtils.post({
url: UrlUtils.GET_SEND_OVERDUE_CREDITOR_LIST + "/" + id,
onSuccess: (responseData) => {
setOverduePublicNotice(responseData.overduePublicNotice);
@@ -246,6 +249,17 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
});
}

const confirmNewBr = () => {
setConfirmBrPopUp(false);
HttpUtils.post({
url: UrlUtils.POST_ORG_CONFIRM_BR + "/" + id + "/confirm-br",
onSuccess: () => {
notifySaveSuccess();
loadDataFun();
}
});
}

return (
<MainCard elevation={0}
border={false}
@@ -316,6 +330,19 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
</Button>
</ThemeProvider>
</Grid>
{currentUserData.newBrSubmitted ?
<Grid item sx={{ ml: 3, mr: 3 }}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button
variant="contained"
color="success"
onClick={() => setConfirmBrPopUp(true)}
>
{intl.formatMessage({ id: "confirmNewBr" })}
</Button>
</ThemeProvider>
</Grid>
: null}

{
currentUserData.creditor ?
@@ -324,7 +351,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button
variant="contained"
color="error"
color="success"
onClick={() => setNonCreditorConfirmPopUp(true)}
>
Mark as Non-Credit Client
@@ -338,7 +365,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button
variant="contained"
color="orange"
color="success"
onClick={() => setCreditorConfirmPopUp(true)}
>
Mark as Credit Client
@@ -351,7 +378,7 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button
variant="contained"
color="primary"
color="success"
onClick={() => sendDn_Overdue(true)}
>
Generate O&#47;S DN List
@@ -440,10 +467,16 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
<TextField
fullWidth
id="currentExDate"
// error={(fromDate===null)}
// type="date"
name="currentExDate"
value={fromDate != null ? DateUtils.dateStr(fromDate) : DateUtils.dateStr(currentFromDate)}
value={(() => {
const dateStr = fromDate != null ? DateUtils.dateStr(fromDate) : DateUtils.dateStr(currentFromDate);
const statusLabel = currentUserData.brStatus === "Invalid"
? intl.formatMessage({ id: "brStatusInvalid" })
: currentUserData.brStatus === "Valid"
? intl.formatMessage({ id: "brStatusValid" })
: "";
return statusLabel ? `${dateStr} (${statusLabel})` : dateStr;
})()}
disabled={true}
/> :
<LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}>
@@ -480,9 +513,6 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
{
fromDate == null ?
(!editMode && !createMode) ?
// <FormHelperText error id="helper-text-date">
// Please select a date after today.
// </FormHelperText>
null
:
<FormHelperText error id="helper-text-date">
@@ -575,6 +605,81 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
})}
</Grid>

{currentUserData.newBrSubmitted ?
<>
<Grid item xs={12}>
<Typography variant="h4" sx={{ mb: 2, mr: 3, mt: 2, borderBottom: "1px solid black" }}>
{intl.formatMessage({ id: "newBrInformation" })}
</Typography>
</Grid>
<Grid item xs={12} lg={4}>
{FieldUtils.getTextField({
label: FieldUtils.notNullFieldLabel("Name (Eng):"),
valueName: "enCompanyNameTemp",
disabled: true,
form: formik
})}
</Grid>
<Grid item xs={12} lg={4}>
{FieldUtils.getTextField({
label: "Name (Ch):",
valueName: "chCompanyNameTemp",
disabled: true,
form: formik
})}
</Grid>
<Grid item xs={12} lg={4}>
{FieldUtils.getTextField({
label: FieldUtils.notNullFieldLabel("Expiry Date:"),
valueName: "brExpiryDateTemp",
disabled: true,
form: formik
})}
</Grid>
<Grid item xs={12} lg={12}>
{FieldUtils.getAddressField({
label: FieldUtils.notNullFieldLabel("Address:"),
valueName: ["newAddressLine1", "newAddressLine2", "newAddressLine3"],
disabled: true,
form: formik
})}
</Grid>
<Grid item xs={12} lg={12}>
{FieldUtils.getProfileComboField({
label: "",
valueName: "newDistrict",
disabled: true,
dataList: ComboData.district,
getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "",
form: formik
})}
</Grid>
<Grid item xs={12} lg={12}>
{FieldUtils.getProfileComboField({
label: "",
valueName: "newCountry",
disabled: true,
dataList: ComboData.country,
getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "",
form: formik
})}
</Grid>
</>
: null}

{id > 0 ?
<Grid item xs={12} sx={{ mt: 2 }}>
<Typography variant="h4" sx={{ mb: 2, mr: 3, borderBottom: "1px solid black" }}>
{intl.formatMessage({ id: "uploadedFiles" })}
</Typography>
<FileList
key={`${id}-${currentUserData.newBrSubmitted}-${currentUserData.brExpiryDateTemp || ""}`}
refType={"orgBrFile"}
refId={id}
/>
</Grid>
: null}

<Grid item lg={12} ></Grid>

</Grid>
@@ -668,6 +773,32 @@ const OrganizationCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
</DialogActions>
</Dialog>
</div>
<div>
<Dialog
open={confirmBrPopUp}
onClose={() => setConfirmBrPopUp(false)}
PaperProps={{
sx: {
minWidth: '40vw',
maxWidth: { xs: '90vw', md: '70vw', lg: '70vw' },
maxHeight: { xs: '90vh', md: '70vh', lg: '60vh' }
}
}}
>
<DialogTitle><Typography variant="h3">{intl.formatMessage({ id: "confirmNewBr" })}</Typography></DialogTitle>
<DialogContent style={{ display: 'flex' }}>
<Typography variant="h4" style={{ padding: '16px' }}>{intl.formatMessage({ id: "confirmNewBrMessage" })}</Typography>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={() => setConfirmBrPopUp(false)} sx={GREY_CONTAINED_BUTTON_SX}>
<Typography variant="h5">{intl.formatMessage({ id: "close" })}</Typography>
</Button>
<Button variant="contained" onClick={() => confirmNewBr()} sx={PRIMARY_CONTAINED_BUTTON_SX}>
<Typography variant="h5">{intl.formatMessage({ id: "confirmNewBr" })}</Typography>
</Button>
</DialogActions>
</Dialog>
</div>
</MainCard>
);
};


+ 21
- 4
src/pages/Organization/DetailPage/OrganizationPubCard.js ファイルの表示

@@ -23,12 +23,14 @@ import { notifySaveSuccess } from 'utils/CommonFunction';
import { FormattedMessage, useIntl } from "react-intl";
import { PNSPS_BUTTON_THEME } from "themes/buttonConst";
import { ThemeProvider } from "@emotion/react";
import { useNavigate } from "react-router-dom";

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


const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
const intl = useIntl();
const navigate = useNavigate();
const [creditorConfirmPopUp, setCreditorConfirmPopUp] = React.useState(false);
const [nonCreditorConfirmPopUp, setNonCreditorConfirmPopUp] = React.useState(false);

@@ -200,6 +202,19 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
</Button>
</ThemeProvider>
</Grid>
{currentUserData.canSubmitBr ?
<Grid item sx={{ ml: 3, mr: 3 }}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button
variant="contained"
onClick={() => navigate("/org/submit-br")}
color="success"
>
<FormattedMessage id="submitBrCertificate" />
</Button>
</ThemeProvider>
</Grid>
: null}
</>
}
</Grid>
@@ -235,13 +250,15 @@ const OrganizationPubCard = ({ userData, loadDataFun, id, setEditModeFun }) => {
label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'expiryDate' }) + ":"),
valueName: "brExpiryDate",
disabled: true,
form: formik
form: formik,
displayValue: (!editMode && !createMode && currentUserData.brStatus)
? `${formik.values.brExpiryDate || ""} (${currentUserData.brStatus === "Invalid"
? intl.formatMessage({ id: "brStatusInvalid" })
: intl.formatMessage({ id: "brStatusValid" })})`.trim()
: undefined
})}

</Grid>

<Grid item xs={12} lg={4} ></Grid>

<Grid item xs={12} lg={4} >
{FieldUtils.getTextField({
label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: 'nameEng' }) + ":"),


+ 6
- 0
src/pages/Organization/DetailPage/index.js ファイルの表示

@@ -90,6 +90,12 @@ const OrganizationDetailPage = () => {
response.data["brExpiryDate"] = response.data.brExpiryDate ? DateUtils.dateValue(response.data.brExpiryDate) : "";
response.data["orgShortName"] = response.data.orgShortName ? response.data.orgShortName : "N/A" ;
response.data["newAddressLine1"] = response.data.newBrAddress?.addressLine1;
response.data["newAddressLine2"] = response.data.newBrAddress?.addressLine2;
response.data["newAddressLine3"] = response.data.newBrAddress?.addressLine3;
response.data["newDistrict"] = getObjectByType(ComboData.district, "type", response.data.newBrAddress?.district);
response.data["newCountry"] = getObjectByType(ComboData.country, "type", response.data.newBrAddress?.country);
response.data["brExpiryDateTemp"] = response.data.brExpiryDateTemp ? DateUtils.dateStr(response.data.brExpiryDateTemp) : "";
setFormData(response.data)
setList(response.historyList)
}


+ 30
- 0
src/pages/Organization/SearchPage/OrganizationTable.js ファイルの表示

@@ -9,11 +9,13 @@ import { useNavigate } from "react-router-dom";
import * as DateUtils from "utils/DateUtils";
import { clickableLink} from 'utils/CommonFunction';
import {GET_ORG_PATH} from "utils/ApiPathConst";
import { useIntl } from "react-intl";
// ==============================|| EVENT TABLE ||============================== //

export default function OrganizationTable({ searchCriteria, applyGridOnReady, applySearch}) {
const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria);
const navigate = useNavigate()
const intl = useIntl();

React.useEffect(() => {
set_searchCriteria(searchCriteria);
@@ -91,6 +93,34 @@ export default function OrganizationTable({ searchCriteria, applyGridOnReady, ap
return DateUtils.dateValue(params?.value);
}
},
{
id: 'brStatus',
field: 'brStatus',
headerName: 'BR Status',
flex: 1,
minWidth: 110,
valueGetter: (params) => {
const status = params?.row?.brStatus;
if (status === 'Invalid') {
return intl.formatMessage({ id: 'brStatusInvalid' });
}
if (status === 'Valid') {
return intl.formatMessage({ id: 'brStatusValid' });
}
return status || '';
}
},
{
id: 'newBrSubmitted',
field: 'newBrSubmitted',
headerName: intl.formatMessage({ id: 'newBr' }),
width: 120,
minWidth: 120,
valueGetter: (params) => {
const value = params?.value;
return value === true || value === 1 || value === '1' ? intl.formatMessage({ id: 'newBr' }) : '';
}
},
{
id: 'creditor',
field: 'creditor',


+ 388
- 0
src/pages/Organization/SubmitBrPage/index.js ファイルの表示

@@ -0,0 +1,388 @@
import { Grid, Typography, Stack, Box, Button, FormHelperText, CircularProgress } from '@mui/material';
import * as React from "react";
import { useFormik } from 'formik';
import * as yup from 'yup';
import * as HttpUtils from "utils/HttpUtils";
import * as UrlUtils from "utils/ApiPathConst";
import * as DateUtils from "utils/DateUtils";
import * as FieldUtils from "utils/FieldUtils";
import * as ComboData from "utils/ComboData";
import { getObjectByType } from "utils/CommonFunction";
import { isORGLoggedIn, isPrimaryLoggedIn } from "utils/Utils";
import Loadable from "components/Loadable";
import { lazy } from "react";
import MainCard from "components/MainCard";
import ForwardIcon from "@mui/icons-material/Forward";
import { useNavigate } from "react-router-dom";
import titleBackgroundImg from "assets/images/dashboard/gazette-bar.png";
import { FormattedMessage, useIntl } from "react-intl";
import usePageTitle from "components/usePageTitle";
import { PNSPS_BUTTON_THEME } from "themes/buttonConst";
import { PRIMARY_CONTAINED_BUTTON_SX } from "themes/colorConst";
import { ThemeProvider } from "@emotion/react";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import dayjs from "dayjs";
import { DemoItem } from "@mui/x-date-pickers/internals/demo";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material";
import BrStatusDialog from "components/BrStatusDialog";

const LoadingComponent = Loadable(lazy(() => import("pages/extra-pages/LoadingComponent")));
const UploadFileTable = Loadable(lazy(() => import("pages/Proof/Reply_Public/UploadFileTable")));

const BackgroundHead = {
backgroundImage: `url(${titleBackgroundImg})`,
width: "100%",
height: "100%",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundColor: "#0C489E",
backgroundPosition: "right"
};

const SubmitBrPage = () => {
usePageTitle("submitBrCertificate");
const intl = useIntl();
const navigate = useNavigate();
const [formData, setFormData] = React.useState({});
const [isLoading, setLoading] = React.useState(true);
const [errorMsg, setErrorMsg] = React.useState("");
const [attachments, setAttachments] = React.useState([]);
const attachmentsRef = React.useRef([]);
attachmentsRef.current = attachments;
const [warningText, setWarningText] = React.useState("");
const [isWarningPopUp, setIsWarningPopUp] = React.useState(false);
const [showSubmittedDialog, setShowSubmittedDialog] = React.useState(false);
const fileInputRef = React.useRef(null);
const minDate = React.useMemo(() => new Date().setDate(new Date().getDate() + 1), []);

React.useEffect(() => {
if (!isORGLoggedIn()) {
navigate("/dashboard");
return;
}
HttpUtils.get({
url: UrlUtils.GET_PUB_ORG_PATH,
onSuccess: (response) => {
const data = response.data || {};
if (!data.canSubmitBr) {
navigate(isPrimaryLoggedIn() ? "/org" : "/dashboard");
return;
}
data.country = getObjectByType(ComboData.country, "type", data.addressTemp?.country);
data.district = getObjectByType(ComboData.district, "type", data.addressTemp?.district);
data.addressLine1 = data.addressTemp?.addressLine1;
data.addressLine2 = data.addressTemp?.addressLine2;
data.addressLine3 = data.addressTemp?.addressLine3;
setFormData(data);
setLoading(false);
},
onFail: () => {
navigate("/dashboard");
},
onError: () => {
navigate("/dashboard");
}
});
}, [navigate]);

const displayErrorMsg = (msg) => <Typography variant="errorMessage1">{msg}</Typography>;

const getFileExtension = (fileName) => {
const name = (fileName || "").toLowerCase();
const dot = name.lastIndexOf(".");
if (dot <= 0 || dot === name.length - 1) {
return "";
}
return name.substring(dot + 1);
};

const isAcceptedFile = (fileName) => ["pdf", "jpg", "jpeg", "png"].includes(getFileExtension(fileName));

const initialValues = React.useMemo(() => ({
...formData,
brExpiryDate: null,
certificateFile: null,
}), [formData]);

const formik = useFormik({
enableReinitialize: true,
initialValues,
validationSchema: yup.object().shape({
enCompanyName: yup.string().trim().max(150).required(displayErrorMsg(intl.formatMessage({ id: "userRequireEnglishName" }))),
chCompanyName: yup.string().max(150).nullable(),
addressLine1: yup.string().trim().max(40).required(displayErrorMsg(intl.formatMessage({ id: "validateAddressLine1" }))),
addressLine2: yup.string().max(40, displayErrorMsg(intl.formatMessage({ id: "noMoreThen40Words" }))),
addressLine3: yup.string().max(40, displayErrorMsg(intl.formatMessage({ id: "noMoreThen40Words" }))),
brExpiryDate: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "pleaseFillInBusinessRegCertValidityDate" }))),
certificateFile: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "requireValidFileWithProofReplyFormat" }))),
country: yup.mixed().required(displayErrorMsg(intl.formatMessage({ id: "pleaseFillInCountry" }))),
district: yup.mixed().nullable().test(
"hk-district",
displayErrorMsg(intl.formatMessage({ id: "pleaseFillInDistrict" })),
function (value) {
const country = this.parent.country;
if (country && country.type === "hongKong") {
return value != null;
}
return true;
}
),
}),
onSubmit: (values, { setSubmitting }) => {
setErrorMsg("");
const files = attachmentsRef.current;
if (!values.brExpiryDate || !files || files.length === 0 || values.country == null
|| (values.country.type === "hongKong" && values.district == null)) {
setSubmitting(false);
return;
}
return new Promise((resolve, reject) => {
HttpUtils.postWithFiles({
url: UrlUtils.POST_PUB_ORG_SUBMIT_BR,
params: {
enCompanyName: values.enCompanyName,
chCompanyName: values.chCompanyName,
brExpiryDate: DateUtils.dateValue(values.brExpiryDate),
address: {
country: values.country.type,
district: values.district?.type,
addressLine1: values.addressLine1,
addressLine2: values.addressLine2,
addressLine3: values.addressLine3,
},
},
files: files,
onSuccess: (response) => {
if (response?.msg) {
setErrorMsg(intl.formatMessage({ id: response.msg, defaultMessage: response.msg }));
reject();
return;
}
setShowSubmittedDialog(true);
resolve();
},
onFail: () => reject(),
onError: () => reject(),
});
});
}
});

const setCertificateFiles = (files) => {
const next = files || [];
setAttachments(next);
formik.setFieldValue("certificateFile", next[0] || null, true);
};

const handleSelectCertificate = (event) => {
const file = event.target.files[0];
if (!file) {
return;
}
if (!isAcceptedFile(file.name)) {
setWarningText(intl.formatMessage({ id: "requireValidFileWithProofReplyFormat" }));
setIsWarningPopUp(true);
event.target.value = "";
return;
}
if (file.size >= (10 * 1024 * 1034)) {
setWarningText(intl.formatMessage({ id: "fileSizeWarning" }));
setIsWarningPopUp(true);
event.target.value = "";
return;
}
file.id = 0;
setCertificateFiles([file]);
event.target.value = "";
};

return (
isLoading ?
<Grid container sx={{ minHeight: "87vh", mb: 3 }} direction="column" justifyContent="center" alignItems="center">
<Grid item><LoadingComponent /></Grid>
</Grid>
:
<Grid container direction="column" sx={{ minHeight: "87vh", backgroundColor: "#ffffff" }}>
<Grid item xs={12}>
<div style={BackgroundHead}>
<Stack direction="row" height="70px" justifyContent="flex-start" alignItems="center">
<Typography component="h1" ml={15} color="#FFF" variant="h4" sx={{ display: { xs: "none", md: "block" } }}>
<FormattedMessage id="submitBrCertificate" />
</Typography>
</Stack>
</div>
</Grid>
<Grid item xs={12}>
<Button aria-label={intl.formatMessage({ id: "back" })} title="Back" sx={{ ml: 3.5, mt: 2 }} style={{ border: "2px solid" }} variant="outlined" onClick={() => navigate(-1)}>
<ForwardIcon style={{ height: 30, width: 50, transform: "rotate(180deg)" }} />
</Button>
</Grid>
<Grid item xs={12}>
<Box sx={{ p: 1, borderRadius: "10px" }}>
<MainCard elevation={0} border={false} content={false}>
<form onSubmit={formik.handleSubmit} noValidate style={{ padding: 24 }}>
<Grid container spacing={1}>
<Grid item xs={12}>
<FormHelperText error>
<Typography variant="errorMessage1">{errorMsg}</Typography>
</FormHelperText>
</Grid>
<Grid item xs={12} lg={4}>
{FieldUtils.getTextField({
label: intl.formatMessage({ id: "brNo" }) + ":",
valueName: "brNo",
disabled: true,
form: formik
})}
</Grid>
<Grid item xs={12} lg={4}>
<Grid container alignItems={"center"}>
<Grid item xs={12} md={3} lg={3} sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="pnspsFormParagraphBold">{FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "newBrExpiryDate" }) + ":")}</Typography>
</Grid>
<Grid item xs={12} md={6} lg={6}>
<LocalizationProvider dateAdapter={AdapterDayjs} localeText={DateUtils.getPickerLocaleText(intl.locale)}>
<DemoItem components={["DatePicker"]}>
<DatePicker
id="brExpiryDate"
name="brExpiryDate"
slotProps={{
field: { readOnly: true },
textField: {
error: Boolean(formik.errors.brExpiryDate && (formik.touched.brExpiryDate || formik.submitCount > 0)),
helperText: (formik.touched.brExpiryDate || formik.submitCount > 0) ? formik.errors.brExpiryDate : "",
},
}}
format="DD/MM/YYYY"
value={formik.values.brExpiryDate == null ? null : dayjs(formik.values.brExpiryDate)}
minDate={minDate == null ? null : dayjs(minDate)}
onChange={(newValue) => {
if (DateUtils.dateValue(newValue) > DateUtils.dateValue(new Date())) {
formik.setFieldValue("brExpiryDate", newValue, true);
formik.setFieldTouched("brExpiryDate", true, false);
}
}}
/>
</DemoItem>
</LocalizationProvider>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} lg={4}></Grid>
<Grid item xs={12} lg={4}>
{FieldUtils.getTextField({
label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "nameEng" }) + ":"),
valueName: "enCompanyName",
form: formik
})}
</Grid>
<Grid item xs={12} lg={4}>
{FieldUtils.getTextField({
label: intl.formatMessage({ id: "nameChi" }) + ":",
valueName: "chCompanyName",
form: formik
})}
</Grid>
<Grid item xs={12} lg={4}></Grid>
<Grid item xs={12} lg={12}>
{FieldUtils.getAddressField({
label: FieldUtils.notNullFieldLabel(intl.formatMessage({ id: "formAddress" }) + ":"),
valueName: ["addressLine1", "addressLine2", "addressLine3"],
form: formik
})}
</Grid>
<Grid item xs={12} lg={12}>
{FieldUtils.getProfileComboField({
label: "",
valueName: "district",
dataList: ComboData.district,
getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "",
form: formik
})}
</Grid>
<Grid item xs={12} lg={12}>
{FieldUtils.getProfileComboField({
label: "",
valueName: "country",
disabled: true,
dataList: ComboData.country,
getOptionLabel: (option) => option.type ? intl.formatMessage({ id: option.type }) : "",
form: formik
})}
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle1" sx={{ color: "primary.primary" }}>
<FormattedMessage id="acceptProofReplyFileFormats" />
</Typography>
</Grid>
<Grid item xs={12}>
<input
ref={fileInputRef}
id="uploadFileBtn"
type="file"
accept=".pdf,.jpg,.jpeg,.png,image/png,image/jpeg,application/pdf"
hidden
onChange={handleSelectCertificate}
/>
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button
variant="contained"
type="button"
sx={PRIMARY_CONTAINED_BUTTON_SX}
onClick={() => fileInputRef.current && fileInputRef.current.click()}
>
<FormattedMessage id="selectCertificateFile" />
</Button>
</ThemeProvider>
{(formik.touched.certificateFile || formik.submitCount > 0) && formik.errors.certificateFile ?
<FormHelperText error>{formik.errors.certificateFile}</FormHelperText>
: null}
</Grid>
{attachments.length > 0 ?
<Grid item xs={12} md={8}>
<UploadFileTable recordList={attachments} setRecordList={setCertificateFiles} />
</Grid>
: null}
<Grid item xs={12} sx={{ mt: 2 }}>
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
<Button
variant="contained"
type="submit"
color="success"
disabled={formik.isSubmitting || showSubmittedDialog}
startIcon={formik.isSubmitting ? <CircularProgress color="inherit" size={18} /> : null}
>
<FormattedMessage id="submit" />
</Button>
</ThemeProvider>
</Grid>
</Grid>
</form>
</MainCard>
</Box>
</Grid>
<Dialog open={isWarningPopUp} onClose={() => setIsWarningPopUp(false)}>
<DialogTitle><FormattedMessage id="attention" /></DialogTitle>
<DialogContent><Typography>{warningText}</Typography></DialogContent>
<DialogActions>
<Button onClick={() => setIsWarningPopUp(false)}><FormattedMessage id="close" /></Button>
</DialogActions>
</Dialog>
<BrStatusDialog
open={showSubmittedDialog}
variant={formData.brStatus === "Invalid" || formData.applyBlockState === "expired"
? "submitExpired"
: "submitNotExpired"}
expiryDate={formData.brExpiryDate}
onClose={() => {
setShowSubmittedDialog(false);
navigate(isPrimaryLoggedIn() ? "/org" : "/dashboard");
}}
/>
</Grid>
);
};

export default SubmitBrPage;

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


+ 27
- 8
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";
@@ -184,12 +185,25 @@ const FormPanel = ({ formData }) => {
}
});

const getFileExtension = (fileName) => {
const name = (fileName || "").toLowerCase();
const dot = name.lastIndexOf(".");
if (dot <= 0 || dot === name.length - 1) {
return "";
}
return name.substring(dot + 1);
}

const isAcceptedProofReplyFile = (fileName) => {
return ["pdf", "jpg", "jpeg", "png"].includes(getFileExtension(fileName));
}

const readFile = (event) => {
let file = event.target.files[0];
if (file) {
if (!file.name.toLowerCase().substr(file.name.length - 4).includes(".pdf")) {
if (!isAcceptedProofReplyFile(file.name)) {
setWarningTitle(intl.formatMessage({ id: "attention" }))
setWarningText(intl.formatMessage({ id: 'requireValidFileWithPdfFormat' }));
setWarningText(intl.formatMessage({ id: 'requireValidFileWithProofReplyFormat' }));
setIsWarningPopUp(true);
document.getElementById("uploadFileBtn").value = "";
return;
@@ -382,7 +396,7 @@ const FormPanel = ({ formData }) => {
<li><FormattedMessage id="post" /></li>
</ul>
<Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography>
</>
);
@@ -468,7 +482,7 @@ const FormPanel = ({ formData }) => {
<FormattedMessage id="payOnline" />
{checkPaymentSuspension()?
<Typography style={{ padding: '16px', color: "red" }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography>:null
}
<br /><a href="#payOnlineDetails" color='#fff' onClick={() => {
@@ -536,7 +550,7 @@ const FormPanel = ({ formData }) => {
<li><FormattedMessage id="post" /></li>
</ul>
<Typography variant="h6" component="span">
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography>
</>
);
@@ -599,13 +613,13 @@ const FormPanel = ({ formData }) => {
}
{/* <Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofNote" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofNote" })} style={{ padding: 12 }} />
</Typography>
</Grid> */}
{!isOnlyOnlinePayment?
<Grid item xs={12}>
<Typography variant="h6" component="span" height="100%" >
<div style={{ padding: 12 }} dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "proofImportant" }) }} />
<SafeHtml html={intl.formatMessage({ id: "proofImportant" })} style={{ padding: 12 }} />
</Typography>
</Grid>:null
}
@@ -629,6 +643,11 @@ const FormPanel = ({ formData }) => {
<FormattedMessage id="requiredUploadFix" />:
</Typography>
</Grid>
<Grid item xs={12} md={12} textAlign="left">
<Typography variant="subtitle1" component="span" sx={{ color: 'primary.primary' }}>
<FormattedMessage id="acceptProofReplyFileFormats" />
</Typography>
</Grid>

<Grid item xs={12} md={12} textAlign="left">
<ThemeProvider theme={PNSPS_BUTTON_THEME}>
@@ -659,7 +678,7 @@ const FormPanel = ({ formData }) => {
id="uploadFileBtn"
name="file"
type="file"
accept=".pdf"
accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png"
style={{ display: 'none' }}
disabled={attachments.length >= (formik.values.groupType === "Private Bill" ? 2 : 1)}
onChange={(event) => {


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



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

@@ -5,6 +5,7 @@ import * as UrlUtils from "utils/ApiPathConst";
import * as FormatUtils from "utils/FormatUtils";
import * as DateUtils from "utils/DateUtils";
import { useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";

import {
Grid,
@@ -24,6 +25,7 @@ import {
// checkIsOnlyOnlinePayment
// isCreditorLoggedIn
} from "utils/Utils";
import { fetchOrgBrData, applyBlockVariant } from "utils/orgBrUtils";
// ==============================|| DASHBOARD - DEFAULT ||============================== //

const ApplyForm = () => {
@@ -35,12 +37,23 @@ const ApplyForm = () => {
const [selections, setSelection] = React.useState([]);
const [isLoading, setLoding] = React.useState(true);
const [orgBrReady, setOrgBrReady] = React.useState(false);

const intl = useIntl();
const navigate = useNavigate();
const { locale } = intl;

React.useEffect(() => {
loadUserData();
fetchOrgBrData({
onSuccess: (org) => {
if (applyBlockVariant(org)) {
navigate("/dashboard", { replace: true });
return;
}
setOrgBrReady(true);
}
});
}, []);

const loadUserData = () => {
@@ -112,11 +125,10 @@ const ApplyForm = () => {


React.useEffect(() => {
if (userData !== null){
if (userData !== null && orgBrReady){
setLoding(false);
// console.log(isOnlyOnlinePayment)
}
}, [userData]);
}, [userData, orgBrReady]);

return (
isLoading ?


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


+ 7
- 4
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 ||============================== //
@@ -397,9 +398,11 @@ export default function SubmittedTab({ setCount, url }) {
<>
<div style={{ minHeight: 400, width: '100%', padding: 4 }}>
{isORGLoggedIn() ?
<Grid container direction="row" justifyContent="flex-start" alignItems="center" >
<Grid item xs={3} md={1}>
<Typography variant="h5"><FormattedMessage id="careOf" />:</Typography>
<Grid container direction="row" justifyContent="flex-start" alignItems="center" columnSpacing={1} sx={{ mb: 1 }}>
<Grid item>
<Typography variant="h5" sx={{ whiteSpace: 'nowrap' }}>
<FormattedMessage id="careOf" />:
</Typography>
</Grid>
<Grid item xs={8} md={2}>
<Autocomplete
@@ -580,7 +583,7 @@ export default function SubmittedTab({ setCount, url }) {
<Typography variant="h4" style={{ paddingLeft: '24px' }}><FormattedMessage id="MSG.actionFail" /></Typography>
<DialogContent style={{ display: 'flex', }}>
<Stack direction="column" justifyContent="space-between">
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText }) }} />
<SafeHtml html={intl.formatMessage({ id: 'MSG.paymentHolded' }, { appNo: paymentHoldedErrText })} />
</Stack>
</DialogContent>
<DialogActions>


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


+ 28
- 0
src/pages/PublicNotice/ListPanel/index.js ファイルの表示

@@ -32,6 +32,8 @@ import { PNSPS_LONG_BUTTON_THEME } from "../../../themes/buttonConst";
import { ThemeProvider } from "@emotion/react";
import { FormattedMessage, useIntl } from "react-intl";
import usePageTitle from 'components/usePageTitle';
import { fetchOrgBrData, applyBlockVariant, applyClickVariant } from "utils/orgBrUtils";
import BrStatusDialog from "components/BrStatusDialog";
// ==============================|| DASHBOARD - DEFAULT ||============================== //

const PublicNotice = () => {
@@ -44,6 +46,8 @@ const PublicNotice = () => {
const [selectedTab, setSelectedTab] = useState("1");
const navigate = useNavigate();
const intl = useIntl();
const [orgBrData, setOrgBrData] = useState(null);
const [brDialog, setBrDialog] = useState({ open: false, variant: null, afterClose: null });

const _sx = {
padding: "4 2 4 2",
@@ -76,6 +80,9 @@ const PublicNotice = () => {

useEffect(() => {
loadData();
fetchOrgBrData({
onSuccess: (org) => setOrgBrData(org)
});
}, []);

const loadData = () => {
@@ -99,6 +106,15 @@ const PublicNotice = () => {
}

const onBtnClick = () => {
const variant = applyClickVariant(orgBrData);
if (variant) {
setBrDialog({
open: true,
variant,
afterClose: applyBlockVariant(orgBrData) ? "stay" : "apply",
});
return;
}
navigate('/publicNotice/apply')
}

@@ -209,6 +225,18 @@ const PublicNotice = () => {
)
}

<BrStatusDialog
open={brDialog.open}
variant={brDialog.variant}
expiryDate={orgBrData?.brExpiryDate}
onClose={() => {
const afterClose = brDialog.afterClose;
setBrDialog({ open: false, variant: null, afterClose: null });
if (afterClose === "apply") {
navigate("/publicNotice/apply");
}
}}
/>
</Grid>
);
};


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


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

@@ -1,21 +1,51 @@
// material-ui
import {
// Box,
Autocomplete,
TextField,
Typography
} from '@mui/material';
import MainCard from "components/MainCard";
import * as React from "react";
import { FiDataGrid } from "components/FiDataGrid";
import { GET_SYS_PARAMS } from "utils/ApiPathConst";
import { GET_SYS_PARAMS, GET_SYS_PARAM_NAMES } from "utils/ApiPathConst";
import SafeHtml from 'components/SafeHtml';
import * as HttpUtils from "utils/HttpUtils";
import { useIntl } from "react-intl";

const LANG_SUFFIX = /\.(en|zh|cn)$/i;

function toSearchKey(name) {
return String(name || "").replace(LANG_SUFFIX, "");
}

// ==============================|| DASHBOARD - DEFAULT ||============================== //
const Table = ({onRowClick, searchCriteria, refreshTrigger}) => {
const intl = useIntl();
const [_searchCriteria, set_searchCriteria] = React.useState(searchCriteria);
const [nameOptions, setNameOptions] = React.useState([]);
const [selectedName, setSelectedName] = React.useState("");

React.useEffect(() => {
set_searchCriteria(searchCriteria);
}, [searchCriteria]);
HttpUtils.get({
url: GET_SYS_PARAM_NAMES,
onSuccess: (responseData) => {
const keys = (Array.isArray(responseData) ? responseData : [])
.map((item) => toSearchKey(item?.label ?? item))
.filter(Boolean);
setNameOptions([...new Set(keys)].sort((a, b) => a.localeCompare(b)));
}
});
}, []);

React.useEffect(() => {
const next = { ...searchCriteria };
if (selectedName) {
next.name = selectedName;
} else {
delete next.name;
}
set_searchCriteria(next);
}, [searchCriteria, selectedName]);

const columns = [
{
@@ -45,7 +75,7 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => {
flex: 1,
minWidth: 400,
renderCell:(params)=>{
return <div dangerouslySetInnerHTML={{__html: params.value}} />
return <SafeHtml html={params.value} />
}
},
];
@@ -60,6 +90,37 @@ const Table = ({onRowClick, searchCriteria, refreshTrigger}) => {
System Params
</Typography>

<Autocomplete
disablePortal
options={nameOptions}
value={selectedName || null}
onChange={(event, newValue) => {
setSelectedName(newValue || "");
}}
getOptionLabel={(option) => (option != null ? String(option) : "")}
isOptionEqualToValue={(option, value) => String(option) === String(value)}
sx={{
mt: 2,
ml: 3,
mr: 3,
maxWidth: 480,
'& .MuiInputBase-root': { alignItems: 'center' },
'& .MuiAutocomplete-endAdornment': { top: '50%', transform: 'translateY(-50%)' },
'& .MuiOutlinedInput-root': { height: 40 }
}}
renderInput={(params) => (
<TextField
{...params}
label={intl.formatMessage({ id: "systemSettingName" })}
InputLabelProps={{ shrink: true }}
/>
)}
clearText={intl.formatMessage({ id: "muiClear" })}
closeText={intl.formatMessage({ id: "muiClose" })}
openText={intl.formatMessage({ id: "muiOpen" })}
noOptionsText={intl.formatMessage({ id: "muiNoOptions" })}
/>

<div style={{ width: { xs: '92vw', sm: '96.5vw', md: "auto" }, }}>
{/* <Box width= '100%' sx={{ backgroundColor: "#fff", ml: 2 }} height='100%'> */}
<FiDataGrid


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


+ 6
- 2
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')));
@@ -1558,6 +1559,9 @@ const BusCustomFormWizard = (props) => {
<Typography display="inline" variant="h6" component="span" sx={{ color: 'primary.primary' }}>
<FormattedMessage id="pleaseUploadDoc"/>
</Typography>
<Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}>
<FormattedMessage id="acceptRegisterUploadFileFormats" />
</Typography>
{/* <Typography display="inline" variant="h6" component="span" sx={{ fontSize: 12,color: 'primary.primary'}}>如: 香港身份證; 護照; 中國內地身份證等</Typography> */}
<Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}>
@@ -1583,7 +1587,7 @@ const BusCustomFormWizard = (props) => {
<FormattedMessage id="uploadFile" />
</Button>
<input
accept="image/png, .jpg, .bmp, .pdf"
accept=".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp"
//className={classes.input}
id="contained-button-file"
multiple
@@ -1618,7 +1622,7 @@ const BusCustomFormWizard = (props) => {
<Grid container>
<Grid item xs={12} md={12}>
<Typography component="span" variant="h6" height="100%" sx={{ textAlign: "left", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e", display: "block" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
<SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} />
</Typography>
</Grid>
</Grid>


+ 6
- 2
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')));
@@ -1887,6 +1888,9 @@ const CustomFormWizard = (props) => {
<Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}>
<FormattedMessage id="pleaseUploadIdDocSubTitle" />
</Typography>
<Typography component="span" variant="subtitle1" sx={{ color: 'primary.primary', display: 'block' }}>
<FormattedMessage id="acceptRegisterUploadFileFormats" />
</Typography>
<Stack mt={1} direction="row" justifyContent="flex-start" alignItems="center" spacing={2}>
<ThemeProvider theme={PNSPS_LONG_BUTTON_THEME}>
<Button
@@ -1911,7 +1915,7 @@ const CustomFormWizard = (props) => {
<FormattedMessage id="uploadIdDoc" />
</Button>
<input
accept="image/png, .jpg, .bmp, .pdf"
accept=".pdf,.jpg,.jpeg,.png,.bmp,application/pdf,image/jpeg,image/png,image/bmp"
//className={classes.input}
id="contained-button-file"
multiple
@@ -1946,7 +1950,7 @@ const CustomFormWizard = (props) => {
<Grid container>
<Grid item xs={12} md={12}>
<Typography height="100%" component="span" sx={{ textAlign: "left", display: "block", /*overflow: "scroll",*/ borderRadius: "inherit", borderStyle: "solid", borderWidth: "1px", borderColor: "#0c489e" }}>
<div style={{padding: 12}} dangerouslySetInnerHTML={{__html: intl.formatMessage({id: "termsAndCon"})}} />
<SafeHtml html={intl.formatMessage({id: "termsAndCon"})} style={{padding: 12}} />
</Typography>
</Grid>
</Grid>


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


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

@@ -20,6 +20,8 @@ import * as HttpUtils from "utils/HttpUtils";
import * as UrlUtils from "utils/ApiPathConst";
import * as DateUtils from "utils/DateUtils";
import { checkPaymentSuspension } from "utils/Utils";
import { fetchOrgBrData, applyBlockVariant, applyClickVariant, loginPopupVariant } from "utils/orgBrUtils";
import BrStatusDialog from "components/BrStatusDialog";

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

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

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

@@ -55,10 +58,21 @@ const DashboardDefault = () => {
const [itemList, setItemList] = React.useState([]);
const [listData, setListData] = React.useState([]);
const [isPopUp, setIsPopUp] = React.useState(false);
const [orgBrData, setOrgBrData] = React.useState(null);
const [brDialog, setBrDialog] = React.useState({ open: false, variant: null });
React.useEffect(() => {
loadMessageData()
loadNoticeData()
fetchOrgBrData({
onSuccess: (org) => {
setOrgBrData(org);
const variant = loginPopupVariant(org);
if (variant) {
setBrDialog({ open: true, variant, afterClose: "stay" });
}
}
});
localStorage.setItem('searchCriteria',"")
}, []);
@@ -198,7 +212,18 @@ const DashboardDefault = () => {
<Grid item xs={12} lg={5} sx={{ pt: 2 }} style={{ height: '100%' }}>
<Button
xs={12}
onClick={() => { navigate("/publicNotice/apply"); }}
onClick={() => {
const variant = applyClickVariant(orgBrData);
if (variant) {
setBrDialog({
open: true,
variant,
afterClose: applyBlockVariant(orgBrData) ? "stay" : "apply",
});
return;
}
navigate("/publicNotice/apply");
}}
aria-label={intl.formatMessage({ id: 'submitApplication' })}
sx={{
width: "100%",
@@ -297,7 +322,7 @@ const DashboardDefault = () => {
</DialogTitle>
<DialogContent style={{ color: '#B00020', display: 'flex', }}>
<Typography variant="h5" style={{ padding: '16px' }}>
<div dangerouslySetInnerHTML={{ __html: intl.formatMessage({ id: "suspensionMessageText" }) }} />
<SafeHtml html={intl.formatMessage({ id: "suspensionMessageText" })} />
</Typography>
</DialogContent>
<DialogActions>
@@ -305,6 +330,18 @@ const DashboardDefault = () => {
</DialogActions>
</Dialog>
</div>
<BrStatusDialog
open={brDialog.open}
variant={brDialog.variant}
expiryDate={orgBrData?.brExpiryDate}
onClose={() => {
const afterClose = brDialog.afterClose;
setBrDialog({ open: false, variant: null, afterClose: null });
if (afterClose === "apply") {
navigate("/publicNotice/apply");
}
}}
/>
</Grid>
);
};


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


+ 5
- 0
src/routes/PublicUserRoutes.js ファイルの表示

@@ -27,6 +27,7 @@ const DemandNote_Public = Loadable(lazy(() => import('pages/DemandNote/Search_Pu
const UserMaintainPage_Individual = Loadable(lazy(() => import('pages/User/DetailsPage_Individual')));
const UserMaintainPage_Organization = Loadable(lazy(() => import('pages/User/DetailsPage_Organization')));
const OrganizationDetailPage = Loadable(lazy(() => import('pages/Organization/DetailPage')));
const SubmitBrPage = Loadable(lazy(() => import('pages/Organization/SubmitBrPage')));
const Msg_Details = Loadable(lazy(() => import('pages/Message/Details')));
const Msg_Search = Loadable(lazy(() => import('pages/Message/Search')));
const AnnouncementSearch = Loadable(lazy(() => import('pages/Announcement/Search_Public')));
@@ -126,6 +127,10 @@ const PublicDashboard = {
path: '/orgUser',
element: <UserMaintainPage_Organization />
},
{
path: '/org/submit-br',
element: <SubmitBrPage />
},
{
path: '/org',
element: <OrganizationDetailPage />


+ 15
- 0
src/themes/colorConst.js ファイルの表示

@@ -69,6 +69,21 @@ export const ERROR_CONTAINED_BUTTON_SX = {
},
};

/** WCAG 2.2 AA contained grey — matches PNSPS containedCancel (#616161 + white, ~5.7:1). */
export const CONTAINED_NEUTRAL_GREY = '#616161';

export const GREY_CONTAINED_BUTTON_SX = {
backgroundColor: CONTAINED_NEUTRAL_GREY,
color: '#FFFFFF',
'&:hover': {
backgroundColor: '#545454',
},
'&:focus-visible': {
outline: '2px solid #616161',
outlineOffset: '2px',
},
};

export const PRIMARY_CONTAINED_BUTTON_SX = {
backgroundColor: CONTAINED_PRIMARY_BLUE,
color: '#FFFFFF',


+ 28
- 5
src/translations/en.json ファイルの表示

@@ -44,6 +44,8 @@
"requireFile": "Please select a file to upload",
"requireValidFile": "Please upload a valid file",
"requireValidFileWithPdfFormat": "Please upload a valid file (file format: .pdf)",
"requireValidFileWithProofReplyFormat": "Please upload a valid file (file format: .pdf, .jpg, .jpeg, .png)",
"acceptProofReplyFileFormats": "Accept file formats: pdf, jpg, jpeg and png.",
"requireValidFileWithFormat": "Please upload a valid file (file format: .doc, .docx, .xls, .xlsx)",
"submissionSuccess": "Application submitted successfully",
"noMoreThen40Words": "Must not exceed 40 characters",
@@ -59,7 +61,7 @@

"MSG.registerIAmSmart": "You may click the \"iAM Smart\" button to fill the personal information automatically or enter the information manually to activate the PNSPS account now.<br/>If you want to use \"iAM Smart\" to fill the personal information, please download the \"iAM Smart\" mobile app and register as an \"iAM Smart\" user first.",
"MSG.registerPersonal": "To complete the online application, you need to upload digital copies of identification documents.<br/>e.g. Hong Kong Identity Card, Passport, Mainland China Identity Card, Professional Practicing Certificate, etc.",
"MSG.registerOrg": "You need to upload the proof documents for the online application.<br/>e.g. Business Registration Certificate, Professional Practicing Certificate, etc.",
"MSG.registerOrg": "You need to upload the Business Registration Certificate for the online application.",
"MSG.paymentMsg": "Your application and payment have been received",
"MSG.expiredApp": "Public Notice application has expired",

@@ -284,12 +286,13 @@
"sameAsBusinessRegistrationCert": "Same as Business Registration Certificate",
"businessRegCert": "Business Registration Certificate",
"businessRegCertNumber": "Hong Kong Business Reg Cert Number",
"businessRegCertAndDoc":"Business Registration Certificate and other documents",
"businessRegCertAndDoc":"Business Registration Certificate",
"businessRegCertExpiryDate": "Business registration certificate expiry date",
"pleaseUploadDoc": "Please upload a digital file of your valid business registration certificate and other documents to verify your identity.",
"uploadFile": "Upload business registration certificate and other documents",
"pleaseUploadDoc": "Please upload a digital file of your valid business registration certificate to verify your identity.",
"uploadFile": "Upload business registration certificate",
"pleaseUploadIdDoc": "Please upload a digital file of your valid identity document to verify your identity.",
"pleaseUploadIdDocSubTitle": "Such as: Hong Kong ID card; passport; Mainland China ID card; professional practice certificate, etc.",
"acceptRegisterUploadFileFormats": "Accept file formats: PDF, JPG, JPEG, PNG, and BMP. Support maximum 5 files.",
"uploadIdDoc": "Upload identity document",
"fileName": "File name",
"forOrgUser": "For Organisation / Company Users",
@@ -319,7 +322,7 @@
"pleaseFillInBusinessRegCertNumber": "Please fill in Business Registration Certificate Number",
"pleaseFillInValidBusinessRegCertNumber": "Please fill in valid Business Registration Certificate Number",
"businessRegCertValidityDate": "Business Reg Cert validity date",
"pleaseFillInBusinessRegCertValidityDate": "Please fill in Business Reg Cert validity date",
"pleaseFillInBusinessRegCertValidityDate": "Please fill in BR certificate validity date",
"formAddress": "Address",
"addressLine1": "First line of address",
"addressLine2": "Second line of address",
@@ -604,6 +607,24 @@
"nameEng": "Name (Eng)",
"nameChi": "Name (Chi)",
"expiryDate": "Expiry Date",
"brStatusValid": "Valid",
"brStatusInvalid": "Invalid",
"submitBrCertificate": "Submit Business Registration Certificate (BR)",
"goToSubmitBr": "Go to submit BR",
"brPopupTitleExpiring": "Reminder",
"brPopupTitleExpired": "Action Required",
"brPopupTitlePendingVerify": "Notice",
"brSubmitSuccessNotExpired": "Thank you for your BR's submission. Please wait for official approval, you may submit new public notice application before the expiry date.",
"brSubmitSuccessExpired": "Thank you for your BR's submission. Please wait for official approval before making any new application submissions.",
"selectCertificateFile": "Select Certificate File",
"newBrExpiryDate": "New BR Expiry Date",
"newBr": "New BR",
"newBrInformation": "New BR Information",
"confirmNewBr": "Confirm New BR",
"confirmNewBrMessage": "Confirm the new BR information and overwrite the existing organisation details?",
"brExpiredMsg": "Your company’s business registration has expired. Please upload a valid BR certificate.",
"brPendingVerifyMsg": "Your company has already submitted the BR certificate for verification.",
"uploadedFiles": "Uploaded Files",
"create": "Create",
"confirmTo": "Confirm to ",

@@ -646,6 +667,8 @@
"connectionError": "Connection error. Please try again.",
"downloadFailed": "Download failed. Please try again.",

"systemSettingName": "Name",

"muiClear": "Clear",
"muiClose": "Close",
"muiOpen": "Open",


+ 27
- 4
src/translations/zh-CN.json ファイルの表示

@@ -44,6 +44,8 @@
"requireFile": "请选择上传档案",
"requireValidFile": "请上传有效档案",
"requireValidFileWithPdfFormat": "请上传有效档案 (档案格式: .pdf)",
"requireValidFileWithProofReplyFormat": "请上传有效档案 (档案格式: .pdf, .jpg, .jpeg, .png)",
"acceptProofReplyFileFormats": "接受档案格式:pdf、jpg、jpeg 及 png。",
"requireValidFileWithFormat": "请上传有效档案 (档案格式: .doc, .docx, .xls, .xlsx)",
"submissionSuccess": "申请提交成功",
"noMoreThen40Words": "不得超过 40 个字符",
@@ -100,7 +102,7 @@
"MSG.registerIAmSmart": "你可点击「智方便」按钮,系统会自动输入个人资料,或自行输入个人资料,以即时启动 公共启事提交及缴费系统 帐户。<br/>如欲使用「智方便」提供个人资料,请先下载「智方便」流动应用程式并登记成为「智方便」用户。",
"MSG.registerPersonal": "需上载身份证明文件数码档案以进行网上申请。 <br/>如:香港身份证; 护照; 中国内地身份证; 专业执业证书等",
"MSG.registerOrg": "需上载以下任何一份证明文件以进行网上申请。 <br/>如:商业登记证;专业执业证书",
"MSG.registerOrg": "需上载商业登记证以进行网上申请。",
"MSG.paymentMsg": "你的申请和付款已收到",
"MSG.expiredApp": "公共启事申请已过期",

@@ -327,12 +329,13 @@
"sameAsBusinessRegistrationCert": "与商业登记证相同",
"businessRegCert": "商业登记证",
"businessRegCertNumber": "香港商业登记证号码",
"businessRegCertAndDoc":"商业登记证及其他文件",
"businessRegCertAndDoc":"商业登记证",
"businessRegCertExpiryDate": "商业登记证有效期届满日期",
"pleaseUploadDoc": "请上传你的 有效商业登记证及其他文件 的数码档案,以验证你的身份。",
"uploadFile": "上传商业登记证及其他文件",
"pleaseUploadDoc": "请上传你的 有效商业登记证 的数码档案,以验证你的身份。",
"uploadFile": "上传商业登记证",
"pleaseUploadIdDoc": "请上传你的 有效身份证明文件 的数码档案,以验证你的身份。",
"pleaseUploadIdDocSubTitle": "如: 香港身份证; 护照; 中国内地身份证; 专业执业证书等",
"acceptRegisterUploadFileFormats": "接受档案格式:PDF、JPG、JPEG、PNG 及 BMP。最多可上传 5 个档案。",
"uploadIdDoc": "上传身份证明文件",
"fileName": "档案名称",
"forOrgUser": "机构/公司用户",
@@ -600,6 +603,24 @@
"nameEng": "名称 (英文)",
"nameChi": "名称 (中文)",
"expiryDate": "屆滿日期",
"brStatusValid": "有效",
"brStatusInvalid": "无效",
"submitBrCertificate": "提交商业登记证 (BR)",
"goToSubmitBr": "去提交证书",
"brPopupTitleExpiring": "温馨提示",
"brPopupTitleExpired": "系统提示",
"brPopupTitlePendingVerify": "系统提示",
"brSubmitSuccessNotExpired": "感谢您提交商业登记证,请等待官方审批通过,您仍可以在旧有商业登记证有效期限届满前提交新的公共启事申请。",
"brSubmitSuccessExpired": "感谢您提交商业登记证,请等待官方审批通过后,再提交新的公共启事申请。",
"selectCertificateFile": "选择证书档案",
"newBrExpiryDate": "新商业登记证届满日期",
"newBr": "新商业登记证",
"newBrInformation": "新商业登记证资料",
"confirmNewBr": "确认新商业登记证",
"confirmNewBrMessage": "确定以新的商业登记证资料覆盖现有机构资料?",
"brExpiredMsg": "贵司的商业登记证已过期。请上传有效的商业登记证。",
"brPendingVerifyMsg": "贵司已上传最新的商业登记证,请等待官方审批。",
"uploadedFiles": "已上传档案",
"create": "创建",
"confirmTo": "确定",

@@ -642,6 +663,8 @@
"connectionError": "连接错误,请稍后再试。",
"downloadFailed": "下载失败,请稍后再试。",

"systemSettingName": "名称",

"muiClear": "清除",
"muiClose": "关闭",
"muiOpen": "打开",


+ 27
- 4
src/translations/zh-HK.json ファイルの表示

@@ -44,6 +44,8 @@
"requireFile": "請選擇上傳檔案",
"requireValidFile": "請上傳有效檔案",
"requireValidFileWithPdfFormat": "請上傳有效檔案 (檔案格式: .pdf)",
"requireValidFileWithProofReplyFormat": "請上傳有效檔案 (檔案格式: .pdf, .jpg, .jpeg, .png)",
"acceptProofReplyFileFormats": "接受檔案格式:pdf、jpg、jpeg 及 png。",
"requireValidFileWithFormat": "請上傳有效檔案 (檔案格式: .doc, .docx, .xls, .xlsx)",
"submissionSuccess": "申請提交成功",
"noMoreThen40Words": "不得超過 40 個字符",
@@ -100,7 +102,7 @@

"MSG.registerIAmSmart": "你可點擊「智方便」按鈕,系統會自動輸入個人資料,或自行輸入個人資料,以即時啟動 公共啟事提交及繳費系統 帳戶。<br/>如欲使用「智方便」提供個人資料,請先下載「智方便」流動應用程式並登記成為「智方便」用戶。",
"MSG.registerPersonal": "需上載身份證明文件數碼檔案以進行網上申請。<br/>如:香港身份證; 護照; 中國內地身份證; 專業執業証書等",
"MSG.registerOrg": "需上載以下任何一份證明文件以進行網上申請。<br/>如:商業登記證;專業執業證書",
"MSG.registerOrg": "需上載商業登記證以進行網上申請。",
"MSG.paymentMsg": "你的申請和付款已收到",
"MSG.expiredApp": "公共啟事申請已過期",

@@ -322,13 +324,14 @@
"sameAsBusinessRegistrationCert": "與商業登記證相同",
"businessRegCert": "商業登記證",
"businessRegCertNumber": "香港商業登記證號碼",
"businessRegCertAndDoc":"商業登記證及其他文件",
"businessRegCertAndDoc":"商業登記證",
"businessRegCertExpiryDate": "商業登記證有效期屆滿日期",
"pleaseUploadDoc": "請上傳你的 有效商業登記證及其他文件 的數碼檔案,以驗證你的身份。",
"pleaseUploadDoc": "請上傳你的 有效商業登記證 的數碼檔案,以驗證你的身份。",
"pleaseUploadIdDoc": "請上傳你的 有效身份證明文件 的數碼檔案,以驗證你的身份。",
"pleaseUploadIdDocSubTitle": "如: 香港身份證; 護照; 中國內地身份證; 專業執業証書等",
"acceptRegisterUploadFileFormats": "接受檔案格式:PDF、JPG、JPEG、PNG 及 BMP。最多可上載 5 個檔案。",
"uploadIdDoc": "上傳身份證明文件",
"uploadFile": "上傳商業登記證及其他文件",
"uploadFile": "上傳商業登記證",
"fileName": "檔案名稱",
"forOrgUser": "機構/公司用戶",
"forIndUser": "個人用戶",
@@ -601,6 +604,24 @@
"nameEng": "名稱 (英文)",
"nameChi": "名稱 (中文)",
"expiryDate": "屆滿日期",
"brStatusValid": "有效",
"brStatusInvalid": "無效",
"submitBrCertificate": "提交商業登記證 (BR)",
"goToSubmitBr": "去提交證書",
"brPopupTitleExpiring": "溫馨提示",
"brPopupTitleExpired": "系統提示",
"brPopupTitlePendingVerify": "系統提示",
"brSubmitSuccessNotExpired": "感謝您提交商業登記證,請等待官方審批通過,您仍可以在舊有商業登記證有效期限屆滿前提交新的公共啟事申請。",
"brSubmitSuccessExpired": "感謝您提交商業登記證,請等待官方審批通過後,再提交新的公共啟事申請。",
"selectCertificateFile": "選擇證書檔案",
"newBrExpiryDate": "新商業登記證屆滿日期",
"newBr": "新商業登記證",
"newBrInformation": "新商業登記證資料",
"confirmNewBr": "確認新商業登記證",
"confirmNewBrMessage": "確定以新的商業登記證資料覆寫現有機構資料?",
"brExpiredMsg": "貴司的商業登記證已過期。請上傳有效的商業登記證。",
"brPendingVerifyMsg": "貴司已上傳最新的商業登記證,請等待官方審批。",
"uploadedFiles": "已上載檔案",
"create": "創建",
"confirmTo": "確定",

@@ -643,6 +664,8 @@
"connectionError": "連線錯誤,請稍後再試。",
"downloadFailed": "下載失敗,請稍後再試。",

"systemSettingName": "名稱",

"muiClear": "清除",
"muiClose": "關閉",
"muiOpen": "開啟",


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

@@ -5,9 +5,11 @@ import {

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

export const GET_SYS_PARAMS = apiPath+'/settings';
export const GET_SYS_PARAM_NAMES = apiPath+'/settings/combo-name';
export const PRIVACY_POLICY_PATH = apiPath+'/privacyPolicy';
export const UPDATE_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/update-payment-suspension';
export const GET_PAYMENT_SUSPENSION_MODE = apiPath+'/settings/get-payment-suspension';
@@ -28,11 +30,11 @@ export const GLD_USER_PROFILE_PATH = apiPath+'/user/gldProfile';
export const GET_AUTH_LIST = '/user/auth/combo';
export const GET_USER_COMBO_LIST = '/user/combo';
export const GET_USER_GLD_COMBO_LIST = '/user/combo/gld';
export const GET_USER_LOCK = apiPath+'/user/lock';
export const GET_USER_UNLOCK = apiPath+'/user/unlock';
export const GET_USER_LOCK = apiPath+'/user/lock'; //POST
export const GET_USER_UNLOCK = apiPath+'/user/unlock'; //POST

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

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

//public
export const GET_PUB_ORG_PATH = apiPath+'/org/pub';
export const POST_PUB_ORG_SAVE_PATH = apiPath+'/org/pub/save';
export const POST_PUB_ORG_SUBMIT_BR = apiPath+'/org/pub/submit-br';
export const POST_ORG_CONFIRM_BR = apiPath+'/org';
export const GET_PUB_ORG_MARK_AS_CREDITOR = apiPath+'/org/pub/mark-as-creditor';
export const GET_PUB_ORG_MARK_AS_NON_CREDITOR = apiPath+'/org/pub/mark-as-non-creditor';

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

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

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

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

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

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


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

+ 5
- 4
src/utils/FieldUtils.js ファイルの表示

@@ -26,7 +26,7 @@ export const getDateField = ({ label, valueName, form, disabled }) => {
</Grid>;
}

export const getTextField = ({ label, valueName, form, disabled, autoFocus }) => {
export const getTextField = ({ label, valueName, form, disabled, autoFocus, displayValue }) => {
return <Grid container alignItems={"center"} sx={{mb:2}}>
<Grid item xs={12} sm={12} md={12} lg={12}>
<Grid container spacing={1}>
@@ -40,7 +40,8 @@ export const getTextField = ({ label, valueName, form, disabled, autoFocus }) =>
valueName: valueName,
form: form,
disabled: disabled,
autoFocus:autoFocus
autoFocus:autoFocus,
displayValue: displayValue
})}
</Grid>
</Grid>
@@ -233,7 +234,7 @@ export const getProfileComboField = ({ label, dataList, valueName, form, disable
</Grid>;
}

export const initField = ({ type, valueName, form, disabled, autoFocus, multiline, handleChange, placeholder, inputProps, InputProps, width, ...props }) => {
export const initField = ({ type, valueName, form, disabled, autoFocus, multiline, handleChange, placeholder, inputProps, InputProps, width, displayValue, ...props }) => {
let err = Boolean(form.errors[valueName]);

return <TextField
@@ -248,7 +249,7 @@ export const initField = ({ type, valueName, form, disabled, autoFocus, multilin
error={err}
helperText={form.errors[valueName] ? form.errors[valueName] : ''}
onChange={handleChange ? handleChange : form.handleChange}
value={form.values[valueName]}
value={displayValue !== undefined ? displayValue : form.values[valueName]}
disabled={disabled}
autoFocus={autoFocus}
sx={{


+ 151
- 129
src/utils/HttpUtils.js ファイルの表示

@@ -1,166 +1,188 @@
import axios from "axios";
import { FILE_UP_POST, FILE_DOWN_GET } from "../utils/ApiPathConst";
import axios from 'axios';
import { FILE_UP_POST, FILE_DOWN_GET } from '../utils/ApiPathConst';
import qs from 'qs';

export const get = ({ url, params, onSuccess, onFail, onError, onFinally }) => {
axios.get(url, {
params,
paramsSerializer: (p) => qs.stringify(p, { arrayFormat: 'repeat' }) // <-- FIX
}).then(
(response) => { onResponse(response, onSuccess, onFail); }
).catch((error) => {
return handleError(error, onError);
}).finally(() => {
if (typeof onFinally === 'function') {
onFinally();
}
axios
.get(url, {
params,
paramsSerializer: (p) => qs.stringify(p, { arrayFormat: 'repeat' }) // <-- FIX
})
.then((response) => {
onResponse(response, onSuccess, onFail);
})
.catch((error) => {
return handleError(error, onError);
})
.finally(() => {
if (typeof onFinally === 'function') {
onFinally();
}
});
};

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

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

export const post = ({ url, params, onSuccess, onFail, onError, headers }) => {
headers = headers ? headers : {
"Content-Type": "application/json"
};
export const del = ({ url, params, onSuccess, onFail, onError }) => {
axios
.delete(url, { params })
.then((response) => {
onResponse(response, onSuccess, onFail);
})
.catch((error) => {
return handleError(error, onError);
});
};

axios.post(url, params,
{
headers: headers
}).then(
(response) => { onResponse(response, onSuccess, onFail); }
).catch(error => {
return handleError(error, onError);
});
export const post = ({ url, params, onSuccess, onFail, onError, headers }) => {
headers = headers
? headers
: {
'Content-Type': 'application/json'
};

axios
.post(url, params, {
headers: headers
})
.then((response) => {
onResponse(response, onSuccess, onFail);
})
.catch((error) => {
return handleError(error, onError);
});
};

export const postWithFiles = ({ url, params, files, onSuccess, onFail, onError }) => {
var formData = new FormData();
for (let i = 0; i < files.length; i++) {
const file = files[i]
formData.append("multipartFileList", file);
var formData = new FormData();
for (let i = 0; i < files.length; i++) {
const file = files[i];
formData.append('multipartFileList', file);
}
if (params)
for (var key in params) {
if (typeof params[key] === 'object') {
formData.append(key, JSON.stringify(params[key]));
} else {
formData.append(key, params[key]);
}
}
if (params)
for (var key in params) {
if (typeof (params[key]) === 'object') {
formData.append(key, JSON.stringify(params[key]));
} else {
formData.append(key, params[key]);
}
}

axios.post(url, formData,
{ headers: { "Content-Type": "multipart/form-data" } })
.then(
(response) => { onResponse(response, onSuccess, onFail); }
).catch(error => {
return handleError(error, onError);
});
axios
.post(url, formData, { headers: { 'Content-Type': 'multipart/form-data' } })
.then((response) => {
onResponse(response, onSuccess, onFail);
})
.catch((error) => {
return handleError(error, onError);
});
};

export const fileDownload = ({ url, fileId, skey, params, method, onResponse, onError }) => {
if (!url) {
url = FILE_DOWN_GET + "/" + fileId + "/" + skey
}
if (method == 'post') {
axios.post(url, params,
{
responseType: 'blob',
headers: {
"Content-Type": "application/json"
}
}
).then(
(response) => {
fileDownloadResponse(response, onResponse)
}
).catch(error => {
return handleError(error, onError);
});

} else {
axios.get(url,
{
responseType: 'blob',
params: params
}
).then(
(response) => {
fileDownloadResponse(response, onResponse)
}
).catch(error => {
return handleError(error, onError);
});
}
if (!url) {
url = FILE_DOWN_GET + '/' + fileId + '/' + skey;
}
if (method == 'post') {
axios
.post(url, params, {
responseType: 'blob',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => {
fileDownloadResponse(response, onResponse);
})
.catch((error) => {
return handleError(error, onError);
});
} else {
axios
.get(url, {
responseType: 'blob',
params: params
})
.then((response) => {
fileDownloadResponse(response, onResponse);
})
.catch((error) => {
return handleError(error, onError);
});
}
};

const fileDownloadResponse = (response, onResponse) => {
const cd = response.headers?.['content-disposition'];
const fn = cd?.split('filename=')[1]?.replaceAll('"','')?.trim() || 'export.xlsx';
const url = URL.createObjectURL(response.data);
const a = document.createElement('a');
a.href = url;
a.download = fn;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
onResponse?.();
const cd = response.headers?.['content-disposition'];
const fn = cd?.split('filename=')[1]?.replaceAll('"', '')?.trim() || 'export.xlsx';
const url = URL.createObjectURL(response.data);
const a = document.createElement('a');
a.href = url;
a.download = fn;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
onResponse?.();
};

export const fileUpload = ({ refType, refId, files, refCode, onSuccess, onFail, onError }) => {
postWithFiles({
url: FILE_UP_POST,
params: {
refType: refType,
refId: refId,
refCode: refCode
},
files: files,
onSuccess: onSuccess,
onFail: onFail,
onError: onError
});
postWithFiles({
url: FILE_UP_POST,
params: {
refType: refType,
refId: refId,
refCode: refCode
},
files: files,
onSuccess: onSuccess,
onFail: onFail,
onError: onError
});
};


const onResponse = (response, onSuccess, onFail) => {
if (response.status >= 300 || response.status < 200) {
console.log("onFail");
if (onFail) {
onFail(response);
} else {
console.log(response);
}
return;
if (response.status >= 300 || response.status < 200) {
console.log('onFail');
if (onFail) {
onFail(response);
} else {
console.log(response);
}
return;
}

if (onSuccess) {
onSuccess(response?.data);
}
}
if (onSuccess) {
onSuccess(response?.data);
}
};

const handleError = (error, onError) => {
if (onError) {
return onError(error);
} else {
// console.log(error);
return false;
}
}
if (onError) {
return onError(error);
} else {
// console.log(error);
return false;
}
};

+ 56
- 0
src/utils/orgBrUtils.js ファイルの表示

@@ -0,0 +1,56 @@
import * as HttpUtils from "utils/HttpUtils";
import { GET_PUB_ORG_PATH } from "utils/ApiPathConst";
import { isORGLoggedIn } from "utils/Utils";

export const fetchOrgBrData = ({ onSuccess }) => {
if (!isORGLoggedIn()) {
onSuccess(null);
return;
}
HttpUtils.get({
url: GET_PUB_ORG_PATH,
onSuccess: (response) => onSuccess(response?.data || null),
onFail: () => onSuccess(null),
onError: () => onSuccess(null),
});
};

export const applyBlockVariant = (org) => {
if (!org) {
return null;
}
if (org.applyBlockState === "expired") {
return "expired";
}
if (org.applyBlockState === "pending") {
return "pending";
}
return null;
};

export const applyClickVariant = (org) => {
if (!org) {
return null;
}
const blocked = applyBlockVariant(org);
if (blocked) {
return blocked;
}
if (org.showExpiringPopup) {
return "expiring";
}
return null;
};

export const loginPopupVariant = (org) => {
if (!org) {
return null;
}
if (org.applyBlockState === "expired") {
return "expired";
}
if (org.showExpiringPopup) {
return "expiring";
}
return null;
};

+ 49
- 46
src/utils/registerValidation.js ファイルの表示

@@ -1,18 +1,18 @@
export const ADDRESS_LINE_MAX_LENGTH = 40;

export function isAddressLineWithinLimit(line, maxLen = ADDRESS_LINE_MAX_LENGTH) {
if (line == null || line === '') return true;
return line.length <= maxLen;
if (line == null || line === '') return true;
return line.length <= maxLen;
}

/** Step-0 gate: line1 required; lines 1–3 each <= maxLen */
export function isRegisterAddressValid(data, maxLen = ADDRESS_LINE_MAX_LENGTH) {
return (
data.address1 !== '' &&
isAddressLineWithinLimit(data.address1, maxLen) &&
isAddressLineWithinLimit(data.address2, maxLen) &&
isAddressLineWithinLimit(data.address3, maxLen)
);
return (
data.address1 !== '' &&
isAddressLineWithinLimit(data.address1, maxLen) &&
isAddressLineWithinLimit(data.address2, maxLen) &&
isAddressLineWithinLimit(data.address3, maxLen)
);
}

/**
@@ -20,48 +20,51 @@ export function isRegisterAddressValid(data, maxLen = ADDRESS_LINE_MAX_LENGTH) {
* Packs at comma boundaries where possible.
*/
export function splitAddressIntoLines(address, maxLen = ADDRESS_LINE_MAX_LENGTH) {
const empty = { address1: '', address2: '', address3: '' };
if (address == null || address === '') return empty;
const empty = { address1: '', address2: '', address3: '' };
if (address == null || address === '') return empty;

const trimmed = address.trim();
if (trimmed.length <= maxLen) {
return { address1: trimmed, address2: '', address3: '' };
}
const trimmed = address.trim();
if (trimmed.length <= maxLen) {
return { address1: trimmed, address2: '', address3: '' };
}

const segments = trimmed.split(',').map((s) => s.trim()).filter(Boolean);
const lines = [];
let current = '';
const segments = trimmed
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const lines = [];
let current = '';

const pushCurrent = () => {
if (current) {
lines.push(current);
current = '';
}
};
const pushCurrent = () => {
if (current) {
lines.push(current);
current = '';
}
};

for (const segment of segments) {
const candidate = current ? `${current}, ${segment}` : segment;
if (candidate.length <= maxLen) {
current = candidate;
} else if (segment.length <= maxLen) {
pushCurrent();
current = segment;
} else {
pushCurrent();
let remaining = segment;
while (remaining.length > maxLen) {
lines.push(remaining.slice(0, maxLen));
remaining = remaining.slice(maxLen);
}
current = remaining;
}
if (lines.length >= 3) break;
for (const segment of segments) {
const candidate = current ? `${current}, ${segment}` : segment;
if (candidate.length <= maxLen) {
current = candidate;
} else if (segment.length <= maxLen) {
pushCurrent();
current = segment;
} else {
pushCurrent();
let remaining = segment;
while (remaining.length > maxLen) {
lines.push(remaining.slice(0, maxLen));
remaining = remaining.slice(maxLen);
}
current = remaining;
}
pushCurrent();
if (lines.length >= 3) break;
}
pushCurrent();

return {
address1: lines[0] ?? '',
address2: lines[1] ?? '',
address3: lines[2] ?? '',
};
return {
address1: lines[0] ?? '',
address2: lines[1] ?? '',
address3: lines[2] ?? ''
};
}

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

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

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

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

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

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

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


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