Compare commits

...

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. BIN
      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",


BIN
public/favicon.ico Прегледај датотеку

Before After

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


Loading…
Откажи
Сачувај