From f07d27b2737224444c1098e3e0f4007a08efb6db Mon Sep 17 00:00:00 2001 From: AmirHossein Mahmoodi Date: Sat, 10 May 2025 05:31:24 +0000 Subject: [PATCH] Feature/user management --- package.json | 5 + pnpm-lock.yaml | 215 +++++++++ .../(dashboardLayout)/dashboard/users/page.js | 11 + .../(withAuth)/(dashboardLayout)/layout.js | 2 +- src/app/template.js | 4 +- src/components/dashboard/Users/DataTable.jsx | 205 +++++++++ .../Users/RowActions/Delete/DeleteContent.jsx | 56 +++ .../Users/RowActions/Delete/index.jsx | 37 ++ .../Users/RowActions/Edit/Form/index.jsx | 369 ++++++++++++++++ .../dashboard/Users/RowActions/Edit/index.jsx | 32 ++ .../Users/RowActions/Online/index.jsx | 17 + .../Users/RowActions/TelephoneId/index.jsx | 11 + .../dashboard/Users/RowActions/index.jsx | 12 + .../Users/Toolbar/Create/Form/index.jsx | 416 ++++++++++++++++++ .../dashboard/Users/Toolbar/Create/index.jsx | 49 +++ .../dashboard/Users/Toolbar/index.jsx | 10 + src/components/dashboard/Users/index.jsx | 14 + .../CallTabPanel/CallHistory/index.jsx | 2 +- src/core/components/CustomDatePicker.jsx | 60 +++ src/core/components/DataTable/Main.js | 199 +++++++++ .../components/DataTable/body/TableBody.js | 201 +++++++++ .../DataTable/body/TableBodyCell.js | 308 +++++++++++++ .../DataTable/body/TableBodyCellValue.js | 112 +++++ .../components/DataTable/body/TableBodyRow.js | 258 +++++++++++ .../DataTable/body/TableDetailPanel.js | 91 ++++ .../DataTable/buttons/CopyButton.js | 74 ++++ .../DataTable/buttons/GrabHandleButton.js | 52 +++ .../DataTable/buttons/ToggleFiltersButton.js | 4 + .../DataTable/filter/FilterBody.jsx | 133 ++++++ .../DataTable/filter/FilterBodyController.jsx | 25 ++ .../FilterBodyControllerWithDependency.jsx | 32 ++ .../DataTable/filter/FilterBodyField.jsx | 105 +++++ .../DataTable/filter/FilterButton.jsx | 39 ++ .../DataTable/filter/FilterCustom.jsx | 38 ++ .../DataTable/filter/FilterHeader.jsx | 35 ++ .../DataTable/filter/FilterOptionList.jsx | 58 +++ .../CustomDate/CustomDatePicker.jsx | 31 ++ .../CustomDate/CustomDatePickerRange.jsx | 60 +++ .../fieldsType/CustomDate/MuiDatePicker.jsx | 86 ++++ .../filter/fieldsType/CustomSelect.jsx | 38 ++ .../fieldsType/CustomSelectByDependency.jsx | 49 +++ .../fieldsType/CustomSelectMultiple.jsx | 62 +++ .../filter/fieldsType/CustomTextField.jsx | 42 ++ .../fieldsType/CustomTextFieldRange.jsx | 85 ++++ .../components/DataTable/filter/index.jsx | 41 ++ .../components/DataTable/head/TableHead.js | 56 +++ .../DataTable/head/TableHeadCell.js | 290 ++++++++++++ .../components/DataTable/head/TableHeadRow.js | 69 +++ .../components/DataTable/hide/HideBody.jsx | 43 ++ .../DataTable/hide/HideBodyField.jsx | 80 ++++ .../components/DataTable/hide/HideButton.jsx | 37 ++ .../components/DataTable/hide/HideHeader.jsx | 35 ++ .../DataTable/hide/HideOrShowAll.jsx | 70 +++ src/core/components/DataTable/hide/index.jsx | 24 + src/core/components/DataTable/index.js | 18 + .../DataTable/localization/fa/datatable.js | 92 ++++ .../DataTable/menus/ActionMenuItem.js | 49 +++ .../DataTable/menus/CellActionMenu.js | 96 ++++ .../DataTable/reset/ResetStorage.jsx | 36 ++ src/core/components/DataTable/table/Paper.js | 102 +++++ src/core/components/DataTable/table/Table.js | 76 ++++ .../DataTable/table/TableContainer.js | 84 ++++ .../DataTable/table/TableLoadingOverlay.js | 44 ++ .../DataTable/toolbar/BottomToolbar.js | 85 ++++ .../DataTable/toolbar/LinearProgressBar.js | 40 ++ .../DataTable/toolbar/TablePagination.js | 228 ++++++++++ .../DataTable/toolbar/TopToolbar.js | 156 +++++++ .../DataTable/update/UpdateTable.jsx | 19 + src/core/components/DataTableWithAuth.jsx | 8 + .../FilterDrawer/FilterController.jsx | 25 ++ .../FilterControllerWithDependency.jsx | 27 ++ .../components/FilterDrawer/FilterField.jsx | 105 +++++ .../FilterDrawer/FilterOptionList.jsx | 58 +++ .../CustomDate/CustomDatePicker.jsx | 31 ++ .../CustomDate/CustomDatePickerRange.jsx | 60 +++ .../fieldsType/CustomDate/MuiDatePicker.jsx | 86 ++++ .../FilterDrawer/fieldsType/CustomSelect.jsx | 38 ++ .../fieldsType/CustomSelectByDependency.jsx | 44 ++ .../fieldsType/CustomSelectMultiple.jsx | 57 +++ .../fieldsType/CustomTextField.jsx | 41 ++ .../fieldsType/CustomTextFieldRange.jsx | 85 ++++ src/core/components/FilterDrawer/index.jsx | 160 +++++++ .../NotificationDesign/AskForKeepData.jsx | 94 ++++ src/core/components/PageTitle.jsx | 18 + .../Profile/ChangePassword/Form.jsx | 12 +- src/core/components/ScrollBox.jsx | 20 + .../utils/DataTableFilterDataStructure.js | 10 + src/core/utils/flattenArrayOfObjects.js | 9 + src/core/utils/flattenObjectOfObjects.js | 12 + src/core/utils/isArrayEmpty.js | 6 + src/core/utils/nationalCodeValidation.js | 30 ++ src/core/utils/pageMenu.js | 17 + src/core/utils/routes.js | 8 +- src/core/utils/theme.js | 1 - src/core/utils/utils.js | 149 +++++++ src/lib/contexts/DataTable.js | 211 +++++++++ src/lib/contexts/socket.js | 19 +- src/lib/hooks/useDataTable.js | 27 ++ src/lib/hooks/useProvince.js | 29 ++ src/lib/hooks/useRoles.js | 29 ++ src/lib/hooks/useTableSetting.js | 10 + 101 files changed, 7136 insertions(+), 14 deletions(-) create mode 100644 src/app/(withAuth)/(dashboardLayout)/dashboard/users/page.js create mode 100644 src/components/dashboard/Users/DataTable.jsx create mode 100644 src/components/dashboard/Users/RowActions/Delete/DeleteContent.jsx create mode 100644 src/components/dashboard/Users/RowActions/Delete/index.jsx create mode 100644 src/components/dashboard/Users/RowActions/Edit/Form/index.jsx create mode 100644 src/components/dashboard/Users/RowActions/Edit/index.jsx create mode 100644 src/components/dashboard/Users/RowActions/Online/index.jsx create mode 100644 src/components/dashboard/Users/RowActions/TelephoneId/index.jsx create mode 100644 src/components/dashboard/Users/RowActions/index.jsx create mode 100644 src/components/dashboard/Users/Toolbar/Create/Form/index.jsx create mode 100644 src/components/dashboard/Users/Toolbar/Create/index.jsx create mode 100644 src/components/dashboard/Users/Toolbar/index.jsx create mode 100644 src/components/dashboard/Users/index.jsx create mode 100644 src/core/components/CustomDatePicker.jsx create mode 100644 src/core/components/DataTable/Main.js create mode 100644 src/core/components/DataTable/body/TableBody.js create mode 100644 src/core/components/DataTable/body/TableBodyCell.js create mode 100644 src/core/components/DataTable/body/TableBodyCellValue.js create mode 100644 src/core/components/DataTable/body/TableBodyRow.js create mode 100644 src/core/components/DataTable/body/TableDetailPanel.js create mode 100644 src/core/components/DataTable/buttons/CopyButton.js create mode 100644 src/core/components/DataTable/buttons/GrabHandleButton.js create mode 100644 src/core/components/DataTable/buttons/ToggleFiltersButton.js create mode 100644 src/core/components/DataTable/filter/FilterBody.jsx create mode 100644 src/core/components/DataTable/filter/FilterBodyController.jsx create mode 100644 src/core/components/DataTable/filter/FilterBodyControllerWithDependency.jsx create mode 100644 src/core/components/DataTable/filter/FilterBodyField.jsx create mode 100644 src/core/components/DataTable/filter/FilterButton.jsx create mode 100644 src/core/components/DataTable/filter/FilterCustom.jsx create mode 100644 src/core/components/DataTable/filter/FilterHeader.jsx create mode 100644 src/core/components/DataTable/filter/FilterOptionList.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePicker.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePickerRange.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomSelect.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomSelectByDependency.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomSelectMultiple.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomTextField.jsx create mode 100644 src/core/components/DataTable/filter/fieldsType/CustomTextFieldRange.jsx create mode 100644 src/core/components/DataTable/filter/index.jsx create mode 100644 src/core/components/DataTable/head/TableHead.js create mode 100644 src/core/components/DataTable/head/TableHeadCell.js create mode 100644 src/core/components/DataTable/head/TableHeadRow.js create mode 100644 src/core/components/DataTable/hide/HideBody.jsx create mode 100644 src/core/components/DataTable/hide/HideBodyField.jsx create mode 100644 src/core/components/DataTable/hide/HideButton.jsx create mode 100644 src/core/components/DataTable/hide/HideHeader.jsx create mode 100644 src/core/components/DataTable/hide/HideOrShowAll.jsx create mode 100644 src/core/components/DataTable/hide/index.jsx create mode 100644 src/core/components/DataTable/index.js create mode 100644 src/core/components/DataTable/localization/fa/datatable.js create mode 100644 src/core/components/DataTable/menus/ActionMenuItem.js create mode 100644 src/core/components/DataTable/menus/CellActionMenu.js create mode 100644 src/core/components/DataTable/reset/ResetStorage.jsx create mode 100644 src/core/components/DataTable/table/Paper.js create mode 100644 src/core/components/DataTable/table/Table.js create mode 100644 src/core/components/DataTable/table/TableContainer.js create mode 100644 src/core/components/DataTable/table/TableLoadingOverlay.js create mode 100644 src/core/components/DataTable/toolbar/BottomToolbar.js create mode 100644 src/core/components/DataTable/toolbar/LinearProgressBar.js create mode 100644 src/core/components/DataTable/toolbar/TablePagination.js create mode 100644 src/core/components/DataTable/toolbar/TopToolbar.js create mode 100644 src/core/components/DataTable/update/UpdateTable.jsx create mode 100644 src/core/components/DataTableWithAuth.jsx create mode 100644 src/core/components/FilterDrawer/FilterController.jsx create mode 100644 src/core/components/FilterDrawer/FilterControllerWithDependency.jsx create mode 100644 src/core/components/FilterDrawer/FilterField.jsx create mode 100644 src/core/components/FilterDrawer/FilterOptionList.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePicker.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePickerRange.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomDate/MuiDatePicker.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomSelect.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomSelectByDependency.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomSelectMultiple.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomTextField.jsx create mode 100644 src/core/components/FilterDrawer/fieldsType/CustomTextFieldRange.jsx create mode 100644 src/core/components/FilterDrawer/index.jsx create mode 100644 src/core/components/NotificationDesign/AskForKeepData.jsx create mode 100644 src/core/components/PageTitle.jsx create mode 100644 src/core/components/ScrollBox.jsx create mode 100644 src/core/utils/DataTableFilterDataStructure.js create mode 100644 src/core/utils/flattenArrayOfObjects.js create mode 100644 src/core/utils/flattenObjectOfObjects.js create mode 100644 src/core/utils/isArrayEmpty.js create mode 100644 src/core/utils/nationalCodeValidation.js create mode 100644 src/core/utils/utils.js create mode 100644 src/lib/contexts/DataTable.js create mode 100644 src/lib/hooks/useDataTable.js create mode 100644 src/lib/hooks/useProvince.js create mode 100644 src/lib/hooks/useRoles.js create mode 100644 src/lib/hooks/useTableSetting.js diff --git a/package.json b/package.json index 5551615..df8779e 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,14 @@ "@mui/icons-material": "^7.0.2", "@mui/material": "^7.0.2", "@mui/material-nextjs": "^7.0.2", + "@mui/x-date-pickers": "^8.2.0", + "@mui/x-tree-view": "^8.2.0", "axios": "^1.9.0", + "date-fns-jalali": "4.1.0-0", "jalali-moment": "^3.3.11", "lz-string": "^1.5.0", + "material-react-table": "^3.2.1", + "moment": "^2.30.1", "next": "15.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5900a63..6966f5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,15 +29,30 @@ importers: '@mui/material-nextjs': specifier: ^7.0.2 version: 7.0.2(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0))(react@19.1.0) + '@mui/x-date-pickers': + specifier: ^8.2.0 + version: 8.2.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@mui/material@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(date-fns-jalali@4.1.0-0)(moment@2.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/x-tree-view': + specifier: ^8.2.0 + version: 8.2.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@mui/material@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) axios: specifier: ^1.9.0 version: 1.9.0 + date-fns-jalali: + specifier: 4.1.0-0 + version: 4.1.0-0 jalali-moment: specifier: ^3.3.11 version: 3.3.11 lz-string: specifier: ^1.5.0 version: 1.5.0 + material-react-table: + specifier: ^3.2.1 + version: 3.2.1(ec2ae31a6c05688f49c8e8bdcad7fbc8) + moment: + specifier: ^2.30.1 + version: 2.30.1 next: specifier: 15.3.1 version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0) @@ -495,6 +510,65 @@ packages: '@types/react': optional: true + '@mui/x-date-pickers@8.2.0': + resolution: {integrity: sha512-nkFzKkaDr4a9uto4pSOsNBMh+M2YQWiW9iYyBDOi5wOa983e2xMeb7zFjHGJ0xaycg1kfxNcbvJd7tm4Ub+RhA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0 + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + date-fns: ^2.25.0 || ^3.2.0 || ^4.0.0 + date-fns-jalali: ^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 || ^3.0.0 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + + '@mui/x-internals@8.2.0': + resolution: {integrity: sha512-qV4Qr+m4sAPBSuqu8/Ofi5m+nMMvIybGno6cp757bHSmwxkqrn5SKaGyFnH5kB58fOhYA9hG1UivFp7mO1dE4A==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@mui/x-tree-view@8.2.0': + resolution: {integrity: sha512-58ZRFXsUfJaqZ/1kiXaAUILfxQOZHZErZbApK3dj0O7jGHgj4mLdjetwlTmwHjZiEO//ryckaDxl4K83sntANQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0 + '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@napi-rs/wasm-runtime@0.2.9': resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} @@ -671,6 +745,30 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tanstack/match-sorter-utils@8.19.4': + resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} + engines: {node: '>=12'} + + '@tanstack/react-table@8.20.6': + resolution: {integrity: sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-virtual@3.11.2': + resolution: {integrity: sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/table-core@8.20.5': + resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.11.2': + resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} + '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -1091,6 +1189,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1533,6 +1634,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + highlight-words@2.0.0: + resolution: {integrity: sha512-If5n+IhSBRXTScE7wl16VPmd+44Vy7kof24EdqhjsZsDuHikpv1OCagVcJFpB4fS4UPUniedlWqrjIO8vWOsIQ==} + engines: {node: '>= 20', npm: '>= 9'} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -1786,6 +1891,18 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + material-react-table@3.2.1: + resolution: {integrity: sha512-sQtTf7bETpkPN2Hm5BVtz89wrfXCVQguz6XlwMChSnfKFO5QCKAJJC5aSIKnUc3S0AvTz/k/ILi00FnnY1Gixw==} + engines: {node: '>=16'} + peerDependencies: + '@emotion/react': '>=11.13' + '@emotion/styled': '>=11.13' + '@mui/icons-material': '>=6' + '@mui/material': '>=6' + '@mui/x-date-pickers': '>=7.15' + react: '>=18.0' + react-dom: '>=18.0' + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2097,6 +2214,12 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2927,6 +3050,56 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + '@mui/x-date-pickers@8.2.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@mui/material@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(date-fns-jalali@4.1.0-0)(moment@2.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/material': 7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/system': 7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0) + '@mui/utils': 7.0.2(@types/react@19.1.2)(react@19.1.0) + '@mui/x-internals': 8.2.0(@types/react@19.1.2)(react@19.1.0) + '@types/react-transition-group': 4.4.12(@types/react@19.1.2) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.2)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0) + date-fns-jalali: 4.1.0-0 + moment: 2.30.1 + transitivePeerDependencies: + - '@types/react' + + '@mui/x-internals@8.2.0(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/utils': 7.0.2(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + transitivePeerDependencies: + - '@types/react' + + '@mui/x-tree-view@8.2.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@mui/material@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.1 + '@mui/material': 7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/system': 7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0) + '@mui/utils': 7.0.2(@types/react@19.1.2)(react@19.1.0) + '@mui/x-internals': 8.2.0(@types/react@19.1.2)(react@19.1.0) + '@types/react-transition-group': 4.4.12(@types/react@19.1.2) + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + reselect: 5.1.1 + use-sync-external-store: 1.5.0(react@19.1.0) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.1.2)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0) + transitivePeerDependencies: + - '@types/react' + '@napi-rs/wasm-runtime@0.2.9': dependencies: '@emnapi/core': 1.4.3 @@ -3055,6 +3228,26 @@ snapshots: dependencies: tslib: 2.8.1 + '@tanstack/match-sorter-utils@8.19.4': + dependencies: + remove-accents: 0.5.0 + + '@tanstack/react-table@8.20.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/table-core': 8.20.5 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@tanstack/react-virtual@3.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/virtual-core': 3.11.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@tanstack/table-core@8.20.5': {} + + '@tanstack/virtual-core@3.11.2': {} + '@tybys/wasm-util@0.9.0': dependencies: tslib: 2.8.1 @@ -3508,6 +3701,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns-jalali@4.1.0-0: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -4110,6 +4305,8 @@ snapshots: dependencies: function-bind: 1.1.2 + highlight-words@2.0.0: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -4375,6 +4572,20 @@ snapshots: lz-string@1.5.0: {} + material-react-table@3.2.1(ec2ae31a6c05688f49c8e8bdcad7fbc8): + dependencies: + '@emotion/react': 11.14.0(@types/react@19.1.2)(react@19.1.0) + '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0) + '@mui/icons-material': 7.0.2(@mui/material@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.2)(react@19.1.0) + '@mui/material': 7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/x-date-pickers': 8.2.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@mui/material@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.0.2(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(react@19.1.0))(@types/react@19.1.2)(date-fns-jalali@4.1.0-0)(moment@2.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/match-sorter-utils': 8.19.4 + '@tanstack/react-table': 8.20.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-virtual': 3.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + highlight-words: 2.0.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -4679,6 +4890,10 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remove-accents@0.5.0: {} + + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} diff --git a/src/app/(withAuth)/(dashboardLayout)/dashboard/users/page.js b/src/app/(withAuth)/(dashboardLayout)/dashboard/users/page.js new file mode 100644 index 0000000..6dcffdc --- /dev/null +++ b/src/app/(withAuth)/(dashboardLayout)/dashboard/users/page.js @@ -0,0 +1,11 @@ +import UsersPage from "@/components/dashboard/Users"; + +export const metadata = { + title: "کاربران", +}; + +const Page = () => { + return ; +}; + +export default Page; diff --git a/src/app/(withAuth)/(dashboardLayout)/layout.js b/src/app/(withAuth)/(dashboardLayout)/layout.js index 24dc923..54c6ee0 100644 --- a/src/app/(withAuth)/(dashboardLayout)/layout.js +++ b/src/app/(withAuth)/(dashboardLayout)/layout.js @@ -9,7 +9,7 @@ import { Stack } from "@mui/material"; const Layout = ({ children }) => { const { data: permissions } = usePermissions(); const isCallWidgetEnabled = permissions?.some( - (permission) => permission === "manage_calls" + (permission) => permission === "manage_calls", ); return ( diff --git a/src/app/template.js b/src/app/template.js index 505ebf8..cd9e11e 100644 --- a/src/app/template.js +++ b/src/app/template.js @@ -13,7 +13,7 @@ const Template = ({ children }) => { styles={{ "*": { scrollbarWidth: "thin", - scrollbarColor: `#155175 transparent`, + scrollbarColor: `${theme.palette.primary.dark} transparent`, }, "*&::-webkit-scrollbar": { width: "4px", @@ -23,7 +23,7 @@ const Template = ({ children }) => { borderRadius: "4px", }, "*&::-webkit-scrollbar-thumb": { - background: "#155175", + background: `${theme.palette.primary.dark}`, borderRadius: "4px", }, }} diff --git a/src/components/dashboard/Users/DataTable.jsx b/src/components/dashboard/Users/DataTable.jsx new file mode 100644 index 0000000..65e4837 --- /dev/null +++ b/src/components/dashboard/Users/DataTable.jsx @@ -0,0 +1,205 @@ +"use client"; +import DataTableWithAuth from "@/core/components/DataTableWithAuth"; +import { GET_USER_LIST } from "@/core/utils/routes"; +import { Box, Stack } from "@mui/material"; +import { useMemo } from "react"; +import RowActions from "./RowActions"; +import Toolbar from "./Toolbar"; +import useProvinces from "@/lib/hooks/useProvince"; +import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency"; +import { Power, PowerOff } from "@mui/icons-material"; +import useRoles from "@/lib/hooks/useRoles"; +import OnlineCell from "./RowActions/Online"; +import TelephoneId from "./RowActions/TelephoneId"; + +const DataTable = () => { + const columns = useMemo( + () => [ + { + accessorKey: "id", + header: "کد یکتا", + id: "id", + enableColumnFilter: true, + datatype: "text", + filterMode: "equals", + sortDescFirst: false, + columnFilterModeOptions: ["equals", "contains"], + grow: false, + size: 100, + }, + { + header: "استان", + id: "province_id", + enableColumnFilter: true, + datatype: "numeric", + filterMode: "equals", + grow: false, + size: 130, + ColumnSelectComponent: (props) => { + const { provinces, errorProvinces, loadingProvinces } = + useProvinces(); + const getColumnSelectOptions = useMemo(() => { + if (loadingProvinces) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorProvinces) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "کل کشور" }, + ...provinces.map((province) => ({ + value: province.id, + label: province.name, + })), + ]; + }, [provinces, errorProvinces, loadingProvinces]); + return ( + + ); + }, + Cell: ({ renderedCellValue, row }) => <>{row.original.province_fa}, + }, + { + header: "شماره تماس داخلی", + id: "telephone_id", + enableColumnFilter: true, + datatype: "text", + filterMode: "equals", + sortDescFirst: false, + columnFilterModeOptions: ["equals", "contains"], + grow: false, + size: 100, + Cell: ({ row }) => , + }, + { + accessorKey: "username", + header: "نام کاربری", + id: "username", + enableColumnFilter: true, + datatype: "text", + filterMode: "equals", + sortDescFirst: false, + columnFilterModeOptions: ["equals", "contains"], + grow: false, + size: 100, + }, + { + accessorKey: "position", + header: "سمت", + id: "position", + enableColumnFilter: false, + grow: false, + size: 100, + }, + { + accessorKey: "phone_number", + header: "شماره همراه", + id: "phone_number", + enableColumnFilter: false, + grow: false, + size: 100, + }, + { + header: "نقش", + id: "role__id", + enableColumnFilter: false, + grow: false, + size: 100, + enableColumnFilter: true, + datatype: "numeric", + filterMode: "equals", + grow: false, + size: 130, + ColumnSelectComponent: (props) => { + const { roles, errorRoles, loadingRoles } = useRoles(); + const getColumnSelectOptions = useMemo(() => { + if (loadingRoles) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorRoles) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "همه نقش ها" }, + ...roles.map((role) => ({ + value: role.id, + label: role.name_fa, + })), + ]; + }, [roles, errorRoles, loadingRoles]); + return ( + + ); + }, + Cell: ({ row }) => <>{row.original.roles[0].name_fa}, + }, + { + accessorKey: "national_id", + header: "کد ملی", + id: "national_id", + enableColumnFilter: true, + datatype: "text", + filterMode: "equals", + sortDescFirst: false, + columnFilterModeOptions: ["equals", "contains"], + grow: false, + size: 100, + }, + { + accessorKey: "is_online", + header: "وضعیت برخط", + id: "is_online", + enableColumnFilter: true, + datatype: "text", + filterMode: "equals", + sortDescFirst: false, + columnFilterModeOptions: ["equals", "contains"], + grow: false, + size: 100, + Cell: ({ row }) => , + }, + { + accessorKey: "gender", + header: "جنسیت", + id: "gender", + enableColumnFilter: false, + grow: false, + size: 100, + Cell: ({ renderedCellValue }) => ( + <>{renderedCellValue == "male" ? "آقا" : "خانم"} + ), + }, + ], + [], + ); + + return ( + <> + + + + + ); +}; +export default DataTable; diff --git a/src/components/dashboard/Users/RowActions/Delete/DeleteContent.jsx b/src/components/dashboard/Users/RowActions/Delete/DeleteContent.jsx new file mode 100644 index 0000000..36a8710 --- /dev/null +++ b/src/components/dashboard/Users/RowActions/Delete/DeleteContent.jsx @@ -0,0 +1,56 @@ +import { DELETE_USER } from "@/core/utils/routes"; +import useRequest from "@/lib/hooks/useRequest"; +import { + Button, + DialogActions, + DialogContent, + Stack, + Typography, +} from "@mui/material"; +import { useState } from "react"; + +const DeleteContent = ({ rowId, mutate, setOpenDeleteDialog }) => { + const [submitting, setSubmitting] = useState(false); + const requestServer = useRequest({ notificationSuccess: true }); + const handleClick = async () => { + setSubmitting(true); + try { + await requestServer(`${DELETE_USER}/${rowId}`, "delete"); + mutate(); + setOpenDeleteDialog(false); + } catch (error) { + } finally { + setSubmitting(false); + } + }; + return ( + <> + + + آیا از حذف کاربر اطمینان دارید؟ + + + + + + + + ); +}; + +export default DeleteContent; diff --git a/src/components/dashboard/Users/RowActions/Delete/index.jsx b/src/components/dashboard/Users/RowActions/Delete/index.jsx new file mode 100644 index 0000000..5f55897 --- /dev/null +++ b/src/components/dashboard/Users/RowActions/Delete/index.jsx @@ -0,0 +1,37 @@ +import DeleteIcon from "@mui/icons-material/Delete"; +import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material"; +import { useState } from "react"; +import DeleteContent from "./DeleteContent"; +const Delete = ({ rowId, mutate }) => { + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + + return ( + <> + + setOpenDeleteDialog(true)}> + + + + setOpenDeleteDialog(false)} + PaperProps={{ + sx: { + boxShadow: + "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px", + }, + }} + dir="rtl" + maxWidth={"md"} + > + حذف + + + + ); +}; +export default Delete; diff --git a/src/components/dashboard/Users/RowActions/Edit/Form/index.jsx b/src/components/dashboard/Users/RowActions/Edit/Form/index.jsx new file mode 100644 index 0000000..3094eb3 --- /dev/null +++ b/src/components/dashboard/Users/RowActions/Edit/Form/index.jsx @@ -0,0 +1,369 @@ +import LtrTextField from "@/core/components/LtrTextField"; +import StyledForm from "@/core/components/StyledForm"; +import validateNationalCode from "@/core/utils/nationalCodeValidation"; +import { UPDATE_USER } from "@/core/utils/routes"; +import useProvinces from "@/lib/hooks/useProvince"; +import useRequest from "@/lib/hooks/useRequest"; +import useRoles from "@/lib/hooks/useRoles"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { + Button, + Chip, + DialogActions, + DialogContent, + Divider, + FormControl, + FormHelperText, + Grid, + InputLabel, + MenuItem, + OutlinedInput, + Select, + Stack, + TextField, +} from "@mui/material"; +import { useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { object, string } from "yup"; + +const validationSchema = object().shape({ + full_name: string().required("نام و نام خانوادگی خود را وارد کنید"), + username: string().required("نام کاربری خود را وارد کنید"), + phone_number: string() + .matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.") + .required("شماره همراه خود را وارد کنید"), + gender: string().required("جنسیت خود را وارد کنید"), + national_id: string() + .test( + "max", + "کد ملی باید شامل 10 رقم باشد", + (value) => value.toString().length === 10, + ) + .test("validation", "کد ملی صحیح نمی باشد", (value) => + validateNationalCode(value), + ) + .required("کد ملی خود را وارد کنید"), + position: string().required("سمت خود را وارد کنید"), + province_id: string().required("استان خود را وارد کنید"), + role_id: string().required("نقش خود را وارد کنید"), +}); + +const UpdateUserForm = ({ values, setOpen, mutate }) => { + const requestServer = useRequest({ notificationSuccess: true }); + const { provinces, errorProvinces, loadingProvinces } = useProvinces(); + const { roles, errorRoles, loadingRoles } = useRoles(); + + const defaultValues = { + full_name: values.full_name, + username: values.username, + position: values.position, + gender: values.gender, + national_id: values.national_id, + phone_number: values.phone_number, + province_id: values.province_id, + role_id: values.roles[0].id, + }; + + const provinceSelectOptions = useMemo(() => { + if (loadingProvinces) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorProvinces) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "انتخاب استان" }, + ...provinces.map((province) => ({ + value: province.id, + label: province.name, + })), + ]; + }, [provinces, errorProvinces, loadingProvinces]); + + const roleSelectOptions = useMemo(() => { + if (loadingRoles) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorRoles) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "انتخاب نقش" }, + ...roles.map((role) => ({ + value: role.id, + label: role.name_fa, + })), + ]; + }, [roles, errorRoles, loadingRoles]); + + const { + control, + handleSubmit, + formState: { isSubmitting, errors }, + } = useForm({ defaultValues, resolver: yupResolver(validationSchema) }); + + const onSubmit = async (data) => { + const formData = new FormData(); + Object.keys(data).forEach((key) => { + formData.append(key, data[key]); + }); + try { + await requestServer(`${UPDATE_USER}/${values.id}`, "post", { + data: formData, + }); + setOpen(false); + mutate(); + } catch (error) {} + }; + + return ( + <> + + + + + + + + + { + return ( + + ); + }} + name={"username"} + /> + + + { + return ( + + ); + }} + name={"full_name"} + /> + + + { + return ( + + ); + }} + name={"position"} + /> + + + { + return ( + { + const inputValue = event.target.value; + if (isNaN(Number(inputValue))) { + return; + } + if (inputValue.length > 11) { + return; + } + field.onChange(inputValue); + }} + error={!!error} + helperText={!!error && error.message} + /> + ); + }} + name={"phone_number"} + /> + + + { + return ( + + + جنسیت + + + + {!!error && error.message} + + + ); + }} + name={"gender"} + /> + + + { + return ( + { + const inputValue = event.target.value; + if (isNaN(Number(inputValue))) { + return; + } + if (inputValue.length > 10) { + return; + } + field.onChange(inputValue); + }} + error={!!error} + helperText={!!error && error.message} + /> + ); + }} + name={"national_id"} + /> + + + { + return ( + + + استان + + + + {!!error && error.message} + + + ); + }} + name={"province_id"} + /> + + + { + return ( + + + نقش + + + + {!!error && error.message} + + + ); + }} + name={"role_id"} + /> + + + + + + + + + + + ); +}; + +export default UpdateUserForm; diff --git a/src/components/dashboard/Users/RowActions/Edit/index.jsx b/src/components/dashboard/Users/RowActions/Edit/index.jsx new file mode 100644 index 0000000..22c7638 --- /dev/null +++ b/src/components/dashboard/Users/RowActions/Edit/index.jsx @@ -0,0 +1,32 @@ +"use client"; +import { Edit } from "@mui/icons-material"; +import { Dialog, IconButton, Tooltip } from "@mui/material"; +import { useState } from "react"; +import UpdateUserForm from "./Form"; + +const UpdateUser = ({ values, mutate }) => { + const [open, setOpen] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + return ( + <> + + + + + + + + + + ); +}; +export default UpdateUser; diff --git a/src/components/dashboard/Users/RowActions/Online/index.jsx b/src/components/dashboard/Users/RowActions/Online/index.jsx new file mode 100644 index 0000000..14a730e --- /dev/null +++ b/src/components/dashboard/Users/RowActions/Online/index.jsx @@ -0,0 +1,17 @@ +import { useSocket } from "@/lib/contexts/socket"; +import { Power, PowerOff } from "@mui/icons-material"; +import { Stack } from "@mui/material"; + +const OnlineCell = ({ user_id }) => { + const { clientsOnline } = useSocket(); + return ( + + {clientsOnline.some((co) => co.user_id == user_id) ? ( + + ) : ( + + )} + + ); +}; +export default OnlineCell; diff --git a/src/components/dashboard/Users/RowActions/TelephoneId/index.jsx b/src/components/dashboard/Users/RowActions/TelephoneId/index.jsx new file mode 100644 index 0000000..63aea1b --- /dev/null +++ b/src/components/dashboard/Users/RowActions/TelephoneId/index.jsx @@ -0,0 +1,11 @@ +import { useSocket } from "@/lib/contexts/socket"; +import { Stack } from "@mui/material"; + +const TelephoneId = ({ user_id }) => { + const { clientsOnline } = useSocket(); + const telephone_id = clientsOnline.find( + (co) => co.user_id == user_id, + )?.telephone_id; + return {telephone_id}; +}; +export default TelephoneId; diff --git a/src/components/dashboard/Users/RowActions/index.jsx b/src/components/dashboard/Users/RowActions/index.jsx new file mode 100644 index 0000000..2b15ee4 --- /dev/null +++ b/src/components/dashboard/Users/RowActions/index.jsx @@ -0,0 +1,12 @@ +import { Box } from "@mui/material"; +import Delete from "./Delete"; +import UpdateUser from "./Edit"; +const RowActions = ({ row, mutate }) => { + return ( + + + + + ); +}; +export default RowActions; diff --git a/src/components/dashboard/Users/Toolbar/Create/Form/index.jsx b/src/components/dashboard/Users/Toolbar/Create/Form/index.jsx new file mode 100644 index 0000000..c84864a --- /dev/null +++ b/src/components/dashboard/Users/Toolbar/Create/Form/index.jsx @@ -0,0 +1,416 @@ +import LtrTextField from "@/core/components/LtrTextField"; +import StyledForm from "@/core/components/StyledForm"; +import validateNationalCode from "@/core/utils/nationalCodeValidation"; +import useProvinces from "@/lib/hooks/useProvince"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { + Button, + Chip, + DialogActions, + DialogContent, + Divider, + FormControl, + FormHelperText, + Grid, + IconButton, + InputAdornment, + InputLabel, + MenuItem, + OutlinedInput, + Select, + Stack, + TextField, +} from "@mui/material"; +import { Controller, useForm } from "react-hook-form"; +import { object, string } from "yup"; +import { useMemo, useState } from "react"; +import useRoles from "@/lib/hooks/useRoles"; +import useRequest from "@/lib/hooks/useRequest"; +import { CREATE_USER } from "@/core/utils/routes"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; + +const defaultValues = { + full_name: "", + username: "", + position: "", + gender: "male", + national_id: "", + phone_number: "", + province_id: "", + password: "", + role_id: "", +}; + +const validationSchema = object().shape({ + full_name: string().required("نام و نام خانوادگی خود را وارد کنید"), + username: string().required("نام کاربری خود را وارد کنید"), + phone_number: string() + .matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.") + .required("شماره همراه خود را وارد کنید"), + gender: string().required("جنسیت خود را وارد کنید"), + national_id: string() + .test( + "max", + "کد ملی باید شامل 10 رقم باشد", + (value) => value.toString().length === 10, + ) + .test("validation", "کد ملی صحیح نمی باشد", (value) => + validateNationalCode(value), + ) + .required("کد ملی خود را وارد کنید"), + password: string() + .required("رمز عبور خود را وارد کنید") + .matches( + /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/, + "رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد", + ), + position: string().required("سمت خود را وارد کنید"), + province_id: string().required("استان خود را وارد کنید"), + role_id: string().required("نقش خود را وارد کنید"), +}); + +const CreateUserForm = ({ setOpen, mutate }) => { + const requestServer = useRequest({ notificationSuccess: true }); + const [showPassword, setShowPassword] = useState(false); + const { provinces, errorProvinces, loadingProvinces } = useProvinces(); + const { roles, errorRoles, loadingRoles } = useRoles(); + + const handleClickShowPassword = () => setShowPassword((show) => !show); + + const provinceSelectOptions = useMemo(() => { + if (loadingProvinces) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorProvinces) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "انتخاب استان" }, + ...provinces.map((province) => ({ + value: province.id, + label: province.name, + })), + ]; + }, [provinces, errorProvinces, loadingProvinces]); + + const roleSelectOptions = useMemo(() => { + if (loadingRoles) { + return [{ value: "loading", label: "در حال بارگذاری..." }]; + } + if (errorRoles) { + return [{ value: "error", label: "خطا در بارگذاری" }]; + } + return [ + { value: "", label: "انتخاب نقش" }, + ...roles.map((role) => ({ + value: role.id, + label: role.name_fa, + })), + ]; + }, [roles, errorRoles, loadingRoles]); + + const { + control, + handleSubmit, + formState: { isSubmitting, errors }, + } = useForm({ defaultValues, resolver: yupResolver(validationSchema) }); + + const onSubmit = async (data) => { + const formData = new FormData(); + Object.keys(data).forEach((key) => { + formData.append(key, data[key]); + }); + try { + await requestServer(CREATE_USER, "post", { + data: formData, + }); + setOpen(false); + mutate(); + } catch (error) {} + }; + + return ( + <> + + + + + + + + + { + return ( + + ); + }} + name={"username"} + /> + + + { + return ( + + + {showPassword ? ( + + ) : ( + + )} + + + ), + }, + }} + fullWidth + error={!!error} + helperText={!!error && error.message} + /> + ); + }} + name={"password"} + /> + + + { + return ( + + ); + }} + name={"full_name"} + /> + + + { + return ( + + ); + }} + name={"position"} + /> + + + { + return ( + { + const inputValue = event.target.value; + if (isNaN(Number(inputValue))) { + return; + } + if (inputValue.length > 11) { + return; + } + field.onChange(inputValue); + }} + error={!!error} + helperText={!!error && error.message} + /> + ); + }} + name={"phone_number"} + /> + + + { + return ( + + + جنسیت + + + + {!!error && error.message} + + + ); + }} + name={"gender"} + /> + + + { + return ( + { + const inputValue = event.target.value; + if (isNaN(Number(inputValue))) { + return; + } + if (inputValue.length > 10) { + return; + } + field.onChange(inputValue); + }} + error={!!error} + helperText={!!error && error.message} + /> + ); + }} + name={"national_id"} + /> + + + { + return ( + + + استان + + + + {!!error && error.message} + + + ); + }} + name={"province_id"} + /> + + + { + return ( + + + نقش + + + + {!!error && error.message} + + + ); + }} + name={"role_id"} + /> + + + + + + + + + + + ); +}; + +export default CreateUserForm; diff --git a/src/components/dashboard/Users/Toolbar/Create/index.jsx b/src/components/dashboard/Users/Toolbar/Create/index.jsx new file mode 100644 index 0000000..2af6c35 --- /dev/null +++ b/src/components/dashboard/Users/Toolbar/Create/index.jsx @@ -0,0 +1,49 @@ +"use client"; +import { AddCircle } from "@mui/icons-material"; +import { + Button, + Dialog, + IconButton, + useMediaQuery, + useTheme, +} from "@mui/material"; +import CreateUserForm from "./Form"; +import { useState } from "react"; + +const CreateUser = ({ mutate }) => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [open, setOpen] = useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + return ( + <> + {isMobile ? ( + + + + ) : ( + + )} + + + + + ); +}; +export default CreateUser; diff --git a/src/components/dashboard/Users/Toolbar/index.jsx b/src/components/dashboard/Users/Toolbar/index.jsx new file mode 100644 index 0000000..58b990f --- /dev/null +++ b/src/components/dashboard/Users/Toolbar/index.jsx @@ -0,0 +1,10 @@ +import CreateUser from "./Create"; + +const Toolbar = ({ mutate }) => { + return ( + <> + + + ); +}; +export default Toolbar; diff --git a/src/components/dashboard/Users/index.jsx b/src/components/dashboard/Users/index.jsx new file mode 100644 index 0000000..5c4e237 --- /dev/null +++ b/src/components/dashboard/Users/index.jsx @@ -0,0 +1,14 @@ +import PageTitle from "@/core/components/PageTitle"; +import { Stack } from "@mui/material"; +import DataTable from "./DataTable"; + +const UsersPage = () => { + return ( + + + + + ); +}; + +export default UsersPage; diff --git a/src/components/widget/call/CallTabs/CallTabPanel/CallHistory/index.jsx b/src/components/widget/call/CallTabs/CallTabPanel/CallHistory/index.jsx index 79834e1..4d47ce3 100644 --- a/src/components/widget/call/CallTabs/CallTabPanel/CallHistory/index.jsx +++ b/src/components/widget/call/CallTabs/CallTabPanel/CallHistory/index.jsx @@ -33,7 +33,7 @@ const CallHistory = ({ tab }) => { ) : ( - + {historyList && ( { + const handleDateChange = (newValue) => { + setDateValue(newValue); + }; + + return ( + + + { + event.stopPropagation(); + setDateValue(null); + }} + sx={{ + color: "#bfbfbf", + "&:hover": { + backgroundColor: "rgba(189, 189, 189, 0.1)", + color: "#363434", + }, + }} + > + + + + ), + }, + }, + }} + > + + ); +}; +export default CustomDatePicker; diff --git a/src/core/components/DataTable/Main.js b/src/core/components/DataTable/Main.js new file mode 100644 index 0000000..64c8d6a --- /dev/null +++ b/src/core/components/DataTable/Main.js @@ -0,0 +1,199 @@ +import DataTableFilterDataStructure from "@/core/utils/DataTableFilterDataStructure"; +import { flattenArrayOfObjects } from "@/core/utils/flattenArrayOfObjects"; +import { flattenObjectOfObjects } from "@/core/utils/flattenObjectOfObjects"; +import isArrayEmpty from "@/core/utils/isArrayEmpty"; +import useDataTable from "@/lib/hooks/useDataTable"; +import useRequest from "@/lib/hooks/useRequest"; +import { useMaterialReactTable } from "material-react-table"; +import { useCallback, useEffect, useReducer, useState } from "react"; +import useSWR from "swr"; +import { FA_DATATABLE_LOCALIZATION } from "./localization/fa/datatable"; +import DataTable_Paper from "./table/Paper"; + +const rowSelectionReducer = (state, action) => { + switch (action.type) { + case "TOGGLE_ROW": + return { [action.payload]: true }; + case "RESET": + return {}; + default: + return state; + } +}; + +const DataTable_Main = (props) => { + const request = useRequest(); + const [rowSelection, dispatchRowSelection] = useReducer( + rowSelectionReducer, + {}, + ); + const { filterData, sortData, setSortData, hideData } = useDataTable(); + const { + need_filter, + table_url, + user_id, + page_name, + table_name, + columns, + initialStateProps, + TableToolbar, + table_title, + RowActions, + specific_data, + specialFilter, + } = props; + const flatColumns = flattenArrayOfObjects(columns, "columns"); + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); + const [totalRowCount, setTotalRowCount] = useState(0); + const flattenHideData = flattenObjectOfObjects(hideData); + const onSortingChange = (event) => { + setSortData(event); + }; + + const fetchUrl = useCallback(() => { + const params = new URLSearchParams(); + params.set("start", `${pagination.pageIndex * pagination.pageSize}`); + params.set("size", pagination.pageSize); + if (specialFilter) { + const filteredSpecialFilterData = Object.values(specialFilter).filter( + (filter) => !isArrayEmpty(filter.value), + ); + params.set("filters", JSON.stringify(filteredSpecialFilterData || [])); + } else { + const filteredFilterData = DataTableFilterDataStructure( + filterData, + isArrayEmpty, + ); + params.set( + "filters", + JSON.stringify( + filteredFilterData.length === 0 ? [] : filteredFilterData, + ), + ); + } + params.set( + "sorting", + JSON.stringify( + Object.values(sortData).map(({ id, ...rest }) => ({ + ...rest, + id: id.replace(/__/g, "."), + })), + ), + ); + return `${table_url}?${params}`; + }, [table_url, filterData, pagination, sortData, specialFilter]); + + const fetcher = async (url) => { + try { + const response = await request(url); + setTotalRowCount(response.data?.meta?.totalRowCount); + if (specific_data) { + return specific_data(response.data); + } else { + return response.data?.data; + } + } catch (error) { + throw error; + } + }; + + const { data, isValidating, mutate } = useSWR( + props.data ? "" : fetchUrl(), + fetcher, + { + revalidateIfStale: true, + revalidateOnFocus: false, + revalidateOnReconnect: true, + keepPreviousData: true, + }, + ); + + useEffect(() => { + if (!isValidating) return; + dispatchRowSelection({ type: "RESET" }); + }, [isValidating]); + + const table = useMaterialReactTable({ + localization: FA_DATATABLE_LOCALIZATION, + columns, + data: data ?? [], + muiTableBodyRowProps: ({ row }) => ({ + onClick: () => + dispatchRowSelection({ type: "TOGGLE_ROW", payload: row.id }), + selected: !!rowSelection[row.id], + }), + initialState: { + density: "compact", + columnVisibility: flattenHideData, + sorting: sortData, + ...initialStateProps, + }, + state: { + showProgressBars: isValidating || props.loading, + columnVisibility: flattenHideData, + sorting: sortData, + pagination, + rowSelection, + }, + muiTableHeadProps: { + sx: { + borderTop: "1px solid #e1e1e1", + borderBottom: "2px solid #e1e1e1", + }, + }, + muiTableHeadCellProps: { + sx: { + color: "primary.main", + borderLeft: "1px solid #e1e1e1", + "&:first-of-type": { + borderLeft: "unset", + }, + }, + }, + muiTableBodyCellProps: { + sx: { + borderLeft: "1px solid #e1e1e1", + "&:first-of-type": { + borderLeft: "unset", + }, + }, + }, + manualFiltering: true, + manualPagination: true, + manualSorting: true, + onPaginationChange: setPagination, + onSortingChange: onSortingChange, + rowCount: totalRowCount, + renderTopToolbarCustomActions: ({ table }) => ( + <> + {TableToolbar && ( + + )} + + ), + renderRowActions: ({ row }) => , + ...props, + }); + + return ( + + ); +}; +export default DataTable_Main; diff --git a/src/core/components/DataTable/body/TableBody.js b/src/core/components/DataTable/body/TableBody.js new file mode 100644 index 0000000..c427974 --- /dev/null +++ b/src/core/components/DataTable/body/TableBody.js @@ -0,0 +1,201 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { TableBody, Typography } from "@mui/material"; +import { useMRT_RowVirtualizer, useMRT_Rows } from "material-react-table"; +import { memo, useMemo } from "react"; +import DataTable_TableBodyRow, { + Memo_DataTable_TableBodyRow, +} from "./TableBodyRow"; + +const DataTable_TableBody = ({ columnVirtualizer, table, ...rest }) => { + const { + getBottomRows, + getIsSomeRowsPinned, + getRowModel, + getState, + getTopRows, + options: { + enableStickyFooter, + enableStickyHeader, + layoutMode, + localization, + memoMode, + muiTableBodyProps, + renderDetailPanel, + renderEmptyRowsFallback, + rowPinningDisplayMode, + }, + refs: { tableFooterRef, tableHeadRef, tablePaperRef }, + } = table; + const { columnFilters, globalFilter, isFullScreen, rowPinning } = getState(); + + const tableBodyProps = { + ...parseFromValuesOrFunc(muiTableBodyProps, { table }), + ...rest, + }; + + const tableHeadHeight = + ((enableStickyHeader || isFullScreen) && + tableHeadRef.current?.clientHeight) || + 0; + const tableFooterHeight = + (enableStickyFooter && tableFooterRef.current?.clientHeight) || 0; + + const pinnedRowIds = useMemo(() => { + if (!rowPinning.bottom?.length && !rowPinning.top?.length) return []; + return getRowModel() + .rows.filter((row) => row.getIsPinned()) + .map((r) => r.id); + }, [rowPinning, getRowModel().rows]); + + const rows = useMRT_Rows(table); + + const rowVirtualizer = useMRT_RowVirtualizer(table, rows); + + const { virtualRows } = rowVirtualizer ?? {}; + + const commonRowProps = { + columnVirtualizer, + numRows: rows.length, + table, + }; + + return ( + <> + {!rowPinningDisplayMode?.includes("sticky") && + getIsSomeRowsPinned("top") && ( + ({ + display: layoutMode?.startsWith("grid") ? "grid" : undefined, + position: "sticky", + top: tableHeadHeight - 1, + zIndex: 1, + ...parseFromValuesOrFunc(tableBodyProps?.sx, theme), + })} + > + {getTopRows().map((row, staticRowIndex) => { + const props = { + ...commonRowProps, + row, + staticRowIndex, + }; + return memoMode === "rows" ? ( + + ) : ( + + ); + })} + + )} + ({ + display: layoutMode?.startsWith("grid") ? "grid" : undefined, + height: rowVirtualizer + ? `${rowVirtualizer.getTotalSize()}px` + : undefined, + minHeight: !rows.length ? "100px" : undefined, + position: "relative", + ...parseFromValuesOrFunc(tableBodyProps?.sx, theme), + })} + > + {tableBodyProps?.children ?? + (!rows.length ? ( + + + {renderEmptyRowsFallback?.({ table }) ?? ( + + {globalFilter || columnFilters.length + ? localization.noResultsFound + : localization.noRecordsToDisplay} + + )} + + + ) : ( + <> + {(virtualRows ?? rows).map((rowOrVirtualRow, staticRowIndex) => { + let row = rowOrVirtualRow; + if (rowVirtualizer) { + if (renderDetailPanel) { + if (rowOrVirtualRow.index % 2 === 1) { + return null; + } else { + staticRowIndex = rowOrVirtualRow.index / 2; + } + } else { + staticRowIndex = rowOrVirtualRow.index; + } + row = rows[staticRowIndex]; + } + const props = { + ...commonRowProps, + pinnedRowIds, + row, + rowVirtualizer, + staticRowIndex, + virtualRow: rowVirtualizer ? rowOrVirtualRow : undefined, + }; + const key = `${row.id}-${row.index}`; + return memoMode === "rows" ? ( + + ) : ( + + ); + })} + + ))} + + {!rowPinningDisplayMode?.includes("sticky") && + getIsSomeRowsPinned("bottom") && ( + ({ + bottom: tableFooterHeight - 1, + display: layoutMode?.startsWith("grid") ? "grid" : undefined, + position: "sticky", + zIndex: 1, + ...parseFromValuesOrFunc(tableBodyProps?.sx, theme), + })} + > + {getBottomRows().map((row, staticRowIndex) => { + const props = { + ...commonRowProps, + row, + staticRowIndex, + }; + return memoMode === "rows" ? ( + + ) : ( + + ); + })} + + )} + + ); +}; +export default DataTable_TableBody; + +export const Memo_DataTable_TableBody = memo( + DataTable_TableBody, + (prev, next) => prev.table.options.data === next.table.options.data, +); diff --git a/src/core/components/DataTable/body/TableBodyCell.js b/src/core/components/DataTable/body/TableBodyCell.js new file mode 100644 index 0000000..fd36420 --- /dev/null +++ b/src/core/components/DataTable/body/TableBodyCell.js @@ -0,0 +1,308 @@ +import { + getCommonMRTCellStyles, + parseFromValuesOrFunc, +} from "@/core/utils/utils"; +import { useTheme } from "@emotion/react"; +import { Skeleton, TableCell } from "@mui/material"; +import { isCellEditable, openEditingCell } from "material-react-table"; +import { memo, useEffect, useMemo, useState } from "react"; +import DataTable_CopyButton from "../buttons/CopyButton"; +import DataTable_TableBodyCellValue from "./TableBodyCellValue"; + +const DataTable_TableBodyCell = ({ + cell, + numRows, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + ...rest +}) => { + const theme = useTheme(); + const { + getState, + options: { + columnResizeDirection, + columnResizeMode, + createDisplayMode, + editDisplayMode, + enableCellActions, + enableClickToCopy, + enableColumnOrdering, + enableColumnPinning, + enableGrouping, + layoutMode, + mrtTheme: { draggingBorderColor, baseBackgroundColor }, + muiSkeletonProps, + muiTableBodyCellProps, + }, + setHoveredColumn, + } = table; + const { + actionCell, + columnSizingInfo, + creatingRow, + density, + draggingColumn, + draggingRow, + editingCell, + editingRow, + hoveredColumn, + hoveredRow, + isLoading, + showSkeletons, + } = getState(); + const { column, row } = cell; + const { columnDef } = column; + const { columnDefType } = columnDef; + + const args = { cell, column, row, table }; + const tableCellProps = { + ...parseFromValuesOrFunc(muiTableBodyCellProps, args), + ...parseFromValuesOrFunc(columnDef.muiTableBodyCellProps, args), + ...rest, + }; + + const skeletonProps = parseFromValuesOrFunc(muiSkeletonProps, { + cell, + column, + row, + table, + }); + + const [skeletonWidth, setSkeletonWidth] = useState(100); + useEffect(() => { + if ((!isLoading && !showSkeletons) || skeletonWidth !== 100) return; + const size = column.getSize(); + setSkeletonWidth( + columnDefType === "display" + ? size / 2 + : Math.round(Math.random() * (size - size / 3) + size / 3), + ); + }, [isLoading, showSkeletons]); + + const draggingBorders = useMemo(() => { + const isDraggingColumn = draggingColumn?.id === column.id; + const isHoveredColumn = hoveredColumn?.id === column.id; + const isDraggingRow = draggingRow?.id === row.id; + const isHoveredRow = hoveredRow?.id === row.id; + const isFirstColumn = column.getIsFirstColumn(); + const isLastColumn = column.getIsLastColumn(); + const isLastRow = numRows && staticRowIndex === numRows - 1; + const isResizingColumn = columnSizingInfo.isResizingColumn === column.id; + const showResizeBorder = + isResizingColumn && columnResizeMode === "onChange"; + + const borderStyle = showResizeBorder + ? `2px solid ${draggingBorderColor} !important` + : isDraggingColumn || isDraggingRow + ? `1px dashed ${theme.palette.grey[500]} !important` + : isHoveredColumn || isHoveredRow || isResizingColumn + ? `2px dashed ${draggingBorderColor} !important` + : undefined; + + if (showResizeBorder) { + return columnResizeDirection === "ltr" + ? { borderRight: borderStyle } + : { borderLeft: borderStyle }; + } + + return borderStyle + ? { + borderBottom: + isDraggingRow || isHoveredRow || (isLastRow && !isResizingColumn) + ? borderStyle + : undefined, + borderLeft: + isDraggingColumn || + isHoveredColumn || + ((isDraggingRow || isHoveredRow) && isFirstColumn) + ? borderStyle + : undefined, + borderRight: + isDraggingColumn || + isHoveredColumn || + ((isDraggingRow || isHoveredRow) && isLastColumn) + ? borderStyle + : undefined, + borderTop: isDraggingRow || isHoveredRow ? borderStyle : undefined, + } + : undefined; + }, [ + columnSizingInfo.isResizingColumn, + draggingColumn, + draggingRow, + hoveredColumn, + hoveredRow, + staticRowIndex, + ]); + + const isColumnPinned = + enableColumnPinning && + columnDef.columnDefType !== "group" && + column.getIsPinned(); + + const isEditable = isCellEditable({ cell, table }); + + const isEditing = + isEditable && + !["custom", "modal"].includes(editDisplayMode) && + (editDisplayMode === "table" || + editingRow?.id === row.id || + editingCell?.id === cell.id) && + !row.getIsGrouped(); + + const isCreating = + isEditable && createDisplayMode === "row" && creatingRow?.id === row.id; + + const showClickToCopyButton = + (parseFromValuesOrFunc(enableClickToCopy, cell) === true || + parseFromValuesOrFunc(columnDef.enableClickToCopy, cell) === true) && + !["context-menu", false].includes( + // @ts-ignore + parseFromValuesOrFunc(columnDef.enableClickToCopy, cell), + ); + + const isRightClickable = parseFromValuesOrFunc(enableCellActions, cell); + + const cellValueProps = { + cell, + table, + }; + + const handleDoubleClick = (event) => { + tableCellProps?.onDoubleClick?.(event); + openEditingCell({ cell, table }); + }; + + const handleDragEnter = (e) => { + tableCellProps?.onDragEnter?.(e); + if (enableGrouping && hoveredColumn?.id === "drop-zone") { + setHoveredColumn(null); + } + if (enableColumnOrdering && draggingColumn) { + setHoveredColumn( + columnDef.enableColumnOrdering !== false ? column : null, + ); + } + }; + + const handleDragOver = (e) => { + if (columnDef.enableColumnOrdering !== false) { + e.preventDefault(); + } + }; + + const handleContextMenu = (e) => { + tableCellProps?.onContextMenu?.(e); + if (isRightClickable) { + e.preventDefault(); + table.setActionCell(cell); + table.refs.actionCellRef.current = e.currentTarget; + } + }; + + return ( + ({ + "&:hover": { + outline: + actionCell?.id === cell.id || + (editDisplayMode === "cell" && isEditable) || + (editDisplayMode === "table" && (isCreating || isEditing)) + ? `1px solid ${theme.palette.grey[500]}` + : undefined, + textOverflow: "clip", + }, + alignItems: layoutMode?.startsWith("grid") ? "center" : undefined, + cursor: isRightClickable + ? "context-menu" + : isEditable && editDisplayMode === "cell" + ? "pointer" + : "inherit", + outline: + actionCell?.id === cell.id + ? `1px solid ${theme.palette.grey[500]}` + : undefined, + outlineOffset: "-1px", + overflow: "hidden", + p: + density === "compact" + ? columnDefType === "display" + ? "0 0.5rem" + : "0.5rem" + : density === "comfortable" + ? columnDefType === "display" + ? "0.5rem 0.75rem" + : "1rem" + : columnDefType === "display" + ? "1rem 1.25rem" + : "1.5rem", + + textOverflow: columnDefType !== "display" ? "ellipsis" : undefined, + whiteSpace: + row.getIsPinned() || density === "compact" ? "nowrap" : "normal", + ...getCommonMRTCellStyles({ + column, + table, + tableCellProps, + theme, + }), + ...draggingBorders, + backgroundColor: baseBackgroundColor, + })} + > + {tableCellProps.children ?? ( + <> + {cell.getIsPlaceholder() ? ( + (columnDef.PlaceholderCell?.({ cell, column, row, table }) ?? null) + ) : showSkeletons !== false && (isLoading || showSkeletons) ? ( + + ) : columnDefType === "display" && + (["mrt-row-expand", "mrt-row-numbers", "mrt-row-select"].includes( + column.id, + ) || + !row.getIsGrouped()) ? ( + columnDef.Cell?.({ + cell, + column, + renderedCellValue: cell.renderValue(), + row, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + }) + ) : showClickToCopyButton && columnDef.enableClickToCopy !== false ? ( + + + + ) : ( + + )} + {cell.getIsGrouped() && !columnDef.GroupedCell && ( + <> ({row.subRows?.length}) + )} + + )} + + ); +}; +export default DataTable_TableBodyCell; + +export const Memo_DataTable_TableBodyCell = memo( + DataTable_TableBodyCell, + (prev, next) => next.cell === prev.cell, +); diff --git a/src/core/components/DataTable/body/TableBodyCellValue.js b/src/core/components/DataTable/body/TableBodyCellValue.js new file mode 100644 index 0000000..5610935 --- /dev/null +++ b/src/core/components/DataTable/body/TableBodyCellValue.js @@ -0,0 +1,112 @@ +import { Box } from "@mui/material"; + +const allowedTypes = ["string", "number"]; + +const DataTable_TableBodyCellValue = ({ + cell, + rowRef, + staticColumnIndex, + staticRowIndex, + table, +}) => { + const { + getState, + options: { + enableFilterMatchHighlighting, + mrtTheme: { matchHighlightColor }, + }, + } = table; + const { column, row } = cell; + const { columnDef } = column; + const { globalFilter, globalFilterFn } = getState(); + const filterValue = column.getFilterValue(); + + let renderedCellValue = + cell.getIsAggregated() && columnDef.AggregatedCell + ? columnDef.AggregatedCell({ + cell, + column, + row, + table, + }) + : row.getIsGrouped() && !cell.getIsGrouped() + ? null + : cell.getIsGrouped() && columnDef.GroupedCell + ? columnDef.GroupedCell({ + cell, + column, + row, + table, + }) + : undefined; + + const isGroupedValue = renderedCellValue !== undefined; + + if (!isGroupedValue) { + renderedCellValue = cell.renderValue(); + } + + if ( + enableFilterMatchHighlighting && + columnDef.enableFilterMatchHighlighting !== false && + String(renderedCellValue) && + allowedTypes.includes(typeof renderedCellValue) && + ((filterValue && + allowedTypes.includes(typeof filterValue) && + ["autocomplete", "text"].includes(columnDef.filterVariant)) || + (globalFilter && + allowedTypes.includes(typeof globalFilter) && + column.getCanGlobalFilter())) + ) { + const chunks = highlightWords?.({ + matchExactly: + (filterValue ? columnDef._filterFn : globalFilterFn) !== "fuzzy", + query: (filterValue ?? globalFilter ?? "").toString(), + text: renderedCellValue?.toString(), + }); + if (chunks?.length > 1 || chunks?.[0]?.match) { + renderedCellValue = ( + + {chunks?.map(({ key, match, text }) => ( + + )) ?? renderedCellValue} + + ); + } + } + + if (columnDef.Cell && !isGroupedValue) { + renderedCellValue = columnDef.Cell({ + cell, + column, + renderedCellValue, + row, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + }); + } + + return renderedCellValue; +}; +export default DataTable_TableBodyCellValue; diff --git a/src/core/components/DataTable/body/TableBodyRow.js b/src/core/components/DataTable/body/TableBodyRow.js new file mode 100644 index 0000000..8d32a9d --- /dev/null +++ b/src/core/components/DataTable/body/TableBodyRow.js @@ -0,0 +1,258 @@ +import { + commonCellBeforeAfterStyles, + getCommonPinnedCellStyles, + parseFromValuesOrFunc, +} from "@/core/utils/utils"; +import { useTheme } from "@emotion/react"; +import { TableRow, alpha, darken, lighten } from "@mui/material"; +import { getIsRowSelected } from "material-react-table"; +import { memo, useMemo, useRef } from "react"; +import DataTable_TableBodyCell, { + Memo_DataTable_TableBodyCell, +} from "./TableBodyCell"; +import DataTable_TableDetailPanel from "./TableDetailPanel"; + +const DataTable_TableBodyRow = ({ + columnVirtualizer, + numRows, + pinnedRowIds, + row, + rowVirtualizer, + staticRowIndex, + table, + virtualRow, + ...rest +}) => { + const theme = useTheme(); + + const { + getState, + options: { + enableRowOrdering, + enableRowPinning, + enableStickyFooter, + enableStickyHeader, + layoutMode, + memoMode, + mrtTheme: { + baseBackgroundColor, + pinnedRowBackgroundColor, + selectedRowBackgroundColor, + }, + muiTableBodyRowProps, + renderDetailPanel, + rowPinningDisplayMode, + }, + refs: { tableFooterRef, tableHeadRef }, + setHoveredRow, + } = table; + const { + density, + draggingColumn, + draggingRow, + editingCell, + editingRow, + hoveredRow, + isFullScreen, + rowPinning, + } = getState(); + + const visibleCells = row.getVisibleCells(); + + const { virtualColumns, virtualPaddingLeft, virtualPaddingRight } = + columnVirtualizer ?? {}; + + const isRowSelected = getIsRowSelected({ row, table }); + const isRowPinned = enableRowPinning && row.getIsPinned(); + const isDraggingRow = draggingRow?.id === row.id; + const isHoveredRow = hoveredRow?.id === row.id; + + const tableRowProps = { + ...parseFromValuesOrFunc(muiTableBodyRowProps, { + row, + staticRowIndex, + table, + }), + ...rest, + }; + + const [bottomPinnedIndex, topPinnedIndex] = useMemo(() => { + if ( + !enableRowPinning || + !rowPinningDisplayMode?.includes("sticky") || + !pinnedRowIds || + !row.getIsPinned() + ) + return []; + return [ + [...pinnedRowIds].reverse().indexOf(row.id), + pinnedRowIds.indexOf(row.id), + ]; + }, [pinnedRowIds, rowPinning]); + + const tableHeadHeight = + ((enableStickyHeader || isFullScreen) && + tableHeadRef.current?.clientHeight) || + 0; + const tableFooterHeight = + (enableStickyFooter && tableFooterRef.current?.clientHeight) || 0; + + const sx = parseFromValuesOrFunc(tableRowProps?.sx, theme); + + const defaultRowHeight = + density === "compact" ? 37 : density === "comfortable" ? 53 : 69; + + const customRowHeight = + // @ts-ignore + parseInt(tableRowProps?.style?.height ?? sx?.height, 10) || undefined; + + const rowHeight = customRowHeight || defaultRowHeight; + + const handleDragEnter = (_e) => { + if (enableRowOrdering && draggingRow) { + setHoveredRow(row); + } + }; + + const handleDragOver = (e) => { + e.preventDefault(); + }; + + const rowRef = useRef(null); + + const cellHighlightColor = isRowSelected + ? selectedRowBackgroundColor + : isRowPinned + ? pinnedRowBackgroundColor + : undefined; + + const cellHighlightColorHover = + tableRowProps?.hover !== false + ? isRowSelected + ? cellHighlightColor + : theme.palette.mode === "dark" + ? `${lighten(baseBackgroundColor, 0.2)}` + : `${darken(baseBackgroundColor, 0.2)}` + : undefined; + + return ( + <> + { + if (node) { + rowRef.current = node; + rowVirtualizer?.measureElement(node); + } + }} + selected={isRowSelected} + {...tableRowProps} + style={{ + transform: virtualRow + ? `translateY(${virtualRow.start}px)` + : undefined, + ...tableRowProps?.style, + }} + sx={(theme) => ({ + "&:hover td:after": cellHighlightColorHover + ? { + backgroundColor: alpha(cellHighlightColorHover, 0.3), + ...commonCellBeforeAfterStyles, + } + : undefined, + bottom: + !virtualRow && bottomPinnedIndex !== undefined && isRowPinned + ? `${bottomPinnedIndex * rowHeight + (enableStickyFooter ? tableFooterHeight - 1 : 0)}px` + : undefined, + boxSizing: "border-box", + display: layoutMode?.startsWith("grid") ? "flex" : undefined, + opacity: isRowPinned ? 0.97 : isDraggingRow || isHoveredRow ? 0.5 : 1, + position: virtualRow + ? "absolute" + : rowPinningDisplayMode?.includes("sticky") && isRowPinned + ? "sticky" + : "relative", + td: { + ...getCommonPinnedCellStyles({ table, theme }), + }, + "td:after": cellHighlightColor + ? { + backgroundColor: cellHighlightColor, + ...commonCellBeforeAfterStyles, + } + : undefined, + top: virtualRow + ? 0 + : topPinnedIndex !== undefined && isRowPinned + ? `${ + topPinnedIndex * rowHeight + + (enableStickyHeader || isFullScreen ? tableHeadHeight - 1 : 0) + }px` + : undefined, + transition: virtualRow ? "none" : "all 150ms ease-in-out", + width: "100%", + zIndex: + rowPinningDisplayMode?.includes("sticky") && isRowPinned ? 2 : 0, + ...sx, + })} + > + {virtualPaddingLeft ? ( + + ) : null} + {(virtualColumns ?? visibleCells).map( + (cellOrVirtualCell, staticColumnIndex) => { + let cell = cellOrVirtualCell; + if (columnVirtualizer) { + staticColumnIndex = cellOrVirtualCell.index; + cell = visibleCells[staticColumnIndex]; + } + const props = { + cell, + numRows, + rowRef, + staticColumnIndex, + staticRowIndex, + table, + }; + return cell ? ( + memoMode === "cells" && + cell.column.columnDef.columnDefType === "data" && + !draggingColumn && + !draggingRow && + editingCell?.id !== cell.id && + editingRow?.id !== row.id ? ( + + ) : ( + + ) + ) : null; + }, + )} + {virtualPaddingRight ? ( + + ) : null} + + {renderDetailPanel && !row.getIsGrouped() && ( + + )} + + ); +}; +export default DataTable_TableBodyRow; + +export const Memo_DataTable_TableBodyRow = memo( + DataTable_TableBodyRow, + (prev, next) => + prev.row === next.row && prev.staticRowIndex === next.staticRowIndex, +); diff --git a/src/core/components/DataTable/body/TableDetailPanel.js b/src/core/components/DataTable/body/TableDetailPanel.js new file mode 100644 index 0000000..b997947 --- /dev/null +++ b/src/core/components/DataTable/body/TableDetailPanel.js @@ -0,0 +1,91 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Collapse, TableCell, TableRow } from "@mui/material"; + +const DataTable_TableDetailPanel = ({ + parentRowRef, + row, + rowVirtualizer, + staticRowIndex, + table, + virtualRow, + ...rest +}) => { + const { + getState, + getVisibleLeafColumns, + options: { + layoutMode, + mrtTheme: { baseBackgroundColor }, + muiDetailPanelProps, + muiTableBodyRowProps, + renderDetailPanel, + }, + } = table; + const { isLoading } = getState(); + + const tableRowProps = parseFromValuesOrFunc(muiTableBodyRowProps, { + isDetailPanel: true, + row, + staticRowIndex, + table, + }); + + const tableCellProps = { + ...parseFromValuesOrFunc(muiDetailPanelProps, { + row, + table, + }), + ...rest, + }; + + const DetailPanel = !isLoading && renderDetailPanel?.({ row, table }); + + return ( + { + if (node) { + rowVirtualizer?.measureElement?.(node); + } + }} + {...tableRowProps} + sx={(theme) => ({ + display: layoutMode?.startsWith("grid") ? "flex" : undefined, + position: virtualRow ? "absolute" : undefined, + top: virtualRow + ? `${parentRowRef.current?.getBoundingClientRect()?.height}px` + : undefined, + transform: virtualRow + ? `translateY(${virtualRow?.start}px)` + : undefined, + width: "100%", + ...parseFromValuesOrFunc(tableRowProps?.sx, theme), + })} + > + ({ + backgroundColor: virtualRow ? baseBackgroundColor : undefined, + borderBottom: !row.getIsExpanded() ? "none" : undefined, + display: layoutMode?.startsWith("grid") ? "flex" : undefined, + py: !!DetailPanel && row.getIsExpanded() ? "1rem" : 0, + transition: !virtualRow ? "all 150ms ease-in-out" : undefined, + width: `100%`, + ...parseFromValuesOrFunc(tableCellProps?.sx, theme), + })} + > + {virtualRow ? ( + row.getIsExpanded() && DetailPanel + ) : ( + + {DetailPanel} + + )} + + + ); +}; +export default DataTable_TableDetailPanel; diff --git a/src/core/components/DataTable/buttons/CopyButton.js b/src/core/components/DataTable/buttons/CopyButton.js new file mode 100644 index 0000000..3e2ac04 --- /dev/null +++ b/src/core/components/DataTable/buttons/CopyButton.js @@ -0,0 +1,74 @@ +import { + getCommonTooltipProps, + parseFromValuesOrFunc, +} from "@/core/utils/utils"; +import { Button, Tooltip } from "@mui/material"; +import { useState } from "react"; + +const DataTable_CopyButton = ({ cell, table, ...rest }) => { + const { + options: { localization, muiCopyButtonProps }, + } = table; + const { column, row } = cell; + const { columnDef } = column; + + const [copied, setCopied] = useState(false); + + const handleCopy = (event, text) => { + event.stopPropagation(); + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 4000); + }; + + const buttonProps = { + ...parseFromValuesOrFunc(muiCopyButtonProps, { + cell, + column, + row, + table, + }), + ...parseFromValuesOrFunc(columnDef.muiCopyButtonProps, { + cell, + column, + row, + table, + }), + ...rest, + }; + + return ( + + + + + + )} + + ); +} + +export default FilterBody; diff --git a/src/core/components/DataTable/filter/FilterBodyController.jsx b/src/core/components/DataTable/filter/FilterBodyController.jsx new file mode 100644 index 0000000..201f84d --- /dev/null +++ b/src/core/components/DataTable/filter/FilterBodyController.jsx @@ -0,0 +1,25 @@ +import { Controller, useWatch } from "react-hook-form"; +import FilterBodyField from "./FilterBodyField"; + +const FilterBodyController = ({ column, control, reset, errors }) => { + return ( + { + return ( + reset()} + errors={errors} + /> + ); + }} + /> + ); +}; +export default FilterBodyController; diff --git a/src/core/components/DataTable/filter/FilterBodyControllerWithDependency.jsx b/src/core/components/DataTable/filter/FilterBodyControllerWithDependency.jsx new file mode 100644 index 0000000..285da0a --- /dev/null +++ b/src/core/components/DataTable/filter/FilterBodyControllerWithDependency.jsx @@ -0,0 +1,32 @@ +import { Controller, useWatch } from "react-hook-form"; +import FilterBodyField from "./FilterBodyField"; + +const FilterBodyControllerWithDependency = ({ + column, + control, + reset, + errors, +}) => { + const dependencyField = useWatch({ control, name: column.dependencyId }); + + return ( + { + return ( + reset()} + errors={errors} + /> + ); + }} + /> + ); +}; +export default FilterBodyControllerWithDependency; diff --git a/src/core/components/DataTable/filter/FilterBodyField.jsx b/src/core/components/DataTable/filter/FilterBodyField.jsx new file mode 100644 index 0000000..3c9abab --- /dev/null +++ b/src/core/components/DataTable/filter/FilterBodyField.jsx @@ -0,0 +1,105 @@ +import FilterOptionList from "@/core/components/DataTable/filter/FilterOptionList"; +import CustomDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePicker"; +import CustomDatePickerRange from "@/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePickerRange"; +import CustomSelect from "@/core/components/DataTable/filter/fieldsType/CustomSelect"; +import CustomSelectMultiple from "@/core/components/DataTable/filter/fieldsType/CustomSelectMultiple"; +import CustomTextField from "@/core/components/DataTable/filter/fieldsType/CustomTextField"; +import CustomTextFieldRange from "@/core/components/DataTable/filter/fieldsType/CustomTextFieldRange"; +import { useState } from "react"; + +const columnFilterModeOptionFa = { + equals: "برابر", + notEquals: "نابرابر", + contains: "شامل", + lessThan: "کوچکتر", + greaterThan: "بزرگتر", + fuzzy: "فازی", + between: "مابین", +}; + +function FilterBodyField({ + column, + filterParameters, + handleChange, + handleBlur, + dependencyFieldValue, + setFieldValue, + resetForm, + errors, +}) { + const [anchorEl, setAnchorEl] = useState(null); + const defaultFilterTranslation = + columnFilterModeOptionFa[filterParameters.filterMode] || + filterParameters.filterMode; + + const handleOpenFilterBox = (event) => { + setAnchorEl(event.currentTarget); + }; + + const renderField = () => { + const commonProps = { + column, + filterParameters, + defaultFilterTranslation, + handleOpenFilterBox, + dependencyFieldValue, + setFieldValue, + handleChange, + handleBlur, + errors, + }; + + switch (filterParameters.datatype) { + case "numeric": + if (filterParameters.filterMode === "between") { + return ; + } + if (filterParameters.filterMode === "equals") { + return column.ColumnSelectComponent ? ( + + ) : ( + + ); + } + break; + case "date": + if (filterParameters.filterMode === "equals") { + return ; + } + if (filterParameters.filterMode === "between") { + return ; + } + break; + + case "array": + if (filterParameters.filterMode === "equals") { + return ; + } + break; + + default: + return ; + } + }; + + return ( + <> + {renderField()} + {Array.isArray(column.columnFilterModeOptions) && ( + + )} + + ); +} + +export default FilterBodyField; diff --git a/src/core/components/DataTable/filter/FilterButton.jsx b/src/core/components/DataTable/filter/FilterButton.jsx new file mode 100644 index 0000000..e8c1d85 --- /dev/null +++ b/src/core/components/DataTable/filter/FilterButton.jsx @@ -0,0 +1,39 @@ +"use client"; +import FilterAltIcon from "@mui/icons-material/FilterAlt"; +import { IconButton, Tooltip, Badge } from "@mui/material"; +import useDataTable from "@/lib/hooks/useDataTable"; + +function FilterButton({ drawerState, setDrawerState }) { + const { filterData } = useDataTable(); + const isValueEmpty = (value) => { + if (Array.isArray(value)) { + return value.length === 0 || value.every((v) => v === ""); + } + return value === "" || value === null || value === undefined; + }; + const filteredFilterData = Object.values(filterData).filter( + (filter) => !isValueEmpty(filter.value), + ); + return ( + + { + setDrawerState(!drawerState); + }} + aria-label="filter table" + > + + + + + + ); +} +export default FilterButton; diff --git a/src/core/components/DataTable/filter/FilterCustom.jsx b/src/core/components/DataTable/filter/FilterCustom.jsx new file mode 100644 index 0000000..04a2cd1 --- /dev/null +++ b/src/core/components/DataTable/filter/FilterCustom.jsx @@ -0,0 +1,38 @@ +import { useCallback, useState } from "react"; +import FilterButton from "./FilterButton"; +import { Box, Drawer } from "@mui/material"; +import FilterDrawer from "../../FilterDrawer"; + +const FilterCustom = ({ filterData, setFilterData }) => { + const [open, setOpen] = useState(false); + + const closeDrawer = useCallback(() => setOpen(false), []); + + return ( + <> + + setOpen(false)} + sx={{ + overflowY: "hidden", + display: "flex", + flexDirection: "column", + height: "100%", + zIndex: "1300", + }} + > + + {open && ( + + )} + + + + ); +}; +export default FilterCustom; diff --git a/src/core/components/DataTable/filter/FilterHeader.jsx b/src/core/components/DataTable/filter/FilterHeader.jsx new file mode 100644 index 0000000..6a0572d --- /dev/null +++ b/src/core/components/DataTable/filter/FilterHeader.jsx @@ -0,0 +1,35 @@ +"use client"; + +import CancelIcon from "@mui/icons-material/Cancel"; +import FilterAltIcon from "@mui/icons-material/FilterAlt"; +import { Box, IconButton, Typography } from "@mui/material"; + +function FilterHeader({ setDrawerState }) { + return ( + theme.palette.primary.main, + boxShadow: + "rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px", + maxWidth: "450px", + }} + > + + + + فیلتر + + + setDrawerState(false)}> + + + + ); +} + +export default FilterHeader; diff --git a/src/core/components/DataTable/filter/FilterOptionList.jsx b/src/core/components/DataTable/filter/FilterOptionList.jsx new file mode 100644 index 0000000..205b25b --- /dev/null +++ b/src/core/components/DataTable/filter/FilterOptionList.jsx @@ -0,0 +1,58 @@ +"use client"; + +import { ListItem, Menu } from "@mui/material"; +import { useState } from "react"; + +function FilterOptionList({ + filterType, + filterOption, + filterParameters, + anchorEl, + columnFilterModeOptionFa, + setAnchorEl, + handleChange, +}) { + const [selectedFilter, setSelectedFilter] = useState(filterType); + + const handleChangeItem = (event, index) => { + handleChange({ + ...filterParameters, + value: filterOption[index] === "between" ? ["", ""] : "", + filterMode: filterOption[index], + }); + setSelectedFilter(filterOption[index]); + setAnchorEl(null); + }; + + const handleCloseFilterBox = () => { + setAnchorEl(null); + }; + + return ( + + {filterOption.map((option, index) => ( + handleChangeItem(event, index)} + selected={option === selectedFilter} + sx={{ + cursor: "pointer", + width: "100px", + display: "flex", + justifyContent: "center", + }} + > + {columnFilterModeOptionFa[option]} + + ))} + + ); +} + +export default FilterOptionList; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePicker.jsx b/src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePicker.jsx new file mode 100644 index 0000000..85e2b08 --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePicker.jsx @@ -0,0 +1,31 @@ +import React from "react"; +import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker"; +import { Typography } from "@mui/material"; + +function CustomDatePicker({ + column, + filterParameters, + defaultFilterTranslation, + handleChange, +}) { + return ( + { + handleChange({ ...filterParameters, value: formattedDate }); + }} + placeholder={column.header} + helperText={ + theme.palette.primary.main }} + > + نوع فیلتر: {defaultFilterTranslation} (تاریخ) + + } + /> + ); +} + +export default CustomDatePicker; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePickerRange.jsx b/src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePickerRange.jsx new file mode 100644 index 0000000..3b47437 --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomDate/CustomDatePickerRange.jsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import { Box, Typography } from "@mui/material"; +import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker"; + +function CustomDatePickerRange({ + column, + filterParameters, + defaultFilterTranslation, + handleChange, + errors, +}) { + return ( + + { + handleChange({ + ...filterParameters, + value: [formattedDate, filterParameters.value[1]], + }); + }} + maxDate={filterParameters.value[1]} + placeholder={`از تاریخ`} + helperText={ + theme.palette.primary.main }} + > + نوع فیلتر: {defaultFilterTranslation} (تاریخ) + + } + /> + { + handleChange({ + ...filterParameters, + value: [filterParameters.value[0], formattedDate], + }); + }} + minDate={filterParameters.value[0]} + placeholder={`تا تاریخ`} + helperText={ + errors?.[`${column.id}`]?.value + ? errors?.[`${column.id}`]?.value.message + : null + } + error={Boolean(errors?.[`${column.id}`]?.value)} + /> + + ); +} + +export default CustomDatePickerRange; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker.jsx b/src/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker.jsx new file mode 100644 index 0000000..f98e929 --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker.jsx @@ -0,0 +1,86 @@ +import React from "react"; +import { LocalizationProvider, MobileDatePicker } from "@mui/x-date-pickers"; +import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali"; +import { faIR } from "@mui/x-date-pickers/locales"; +import { Box, FormHelperText, IconButton, InputAdornment } from "@mui/material"; +import ClearIcon from "@mui/icons-material/Clear"; +import moment from "jalali-moment"; + +function MuiDatePicker({ + label, + value, + setFieldValue, + name, + minDate, + maxDate, + helperText, + placeholder, + error, +}) { + return ( + + + { + const date = new Date(value); + const formattedDate = moment(date) + .locale("en") + .format("YYYY-MM-DD"); + setFieldValue(name, formattedDate); + }} + minDate={minDate ? new Date(minDate) : null} + maxDate={maxDate ? new Date(maxDate) : null} + slotProps={{ + textField: { + size: "small", + error: error, + placeholder: placeholder, + InputProps: { + endAdornment: ( + + { + event.stopPropagation(); + setFieldValue(name, ""); + }} + sx={{ + color: "#bfbfbf", + "&:hover": { + backgroundColor: "rgba(189, 189, 189, 0.1)", + color: "#363434", + }, + }} + > + + + + ), + }, + InputLabelProps: { + shrink: true, + }, + }, + }} + /> + {/* + {helperText ? helperText : ""} + */} + + + ); +} + +export default MuiDatePicker; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomSelect.jsx b/src/core/components/DataTable/filter/fieldsType/CustomSelect.jsx new file mode 100644 index 0000000..c1e5821 --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomSelect.jsx @@ -0,0 +1,38 @@ +"use client"; + +import { + FormControl, + InputLabel, + MenuItem, + OutlinedInput, + Select, +} from "@mui/material"; +function CustomSelect({ column, filterParameters, handleChange }) { + return ( + + + {column.header} + + + + ); +} + +export default CustomSelect; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomSelectByDependency.jsx b/src/core/components/DataTable/filter/fieldsType/CustomSelectByDependency.jsx new file mode 100644 index 0000000..4e828c3 --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomSelectByDependency.jsx @@ -0,0 +1,49 @@ +"use client"; + +import { + FormControl, + FormHelperText, + InputLabel, + MenuItem, + OutlinedInput, + Select, + Typography, +} from "@mui/material"; +import { useCallback, useEffect } from "react"; + +function CustomSelectByDependency({ + column, + filterParameters, + value, + defaultFilterTranslation, + handleChange, + columnSelectOption, +}) { + return ( + + + {column.header} + + + + ); +} + +export default CustomSelectByDependency; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomSelectMultiple.jsx b/src/core/components/DataTable/filter/fieldsType/CustomSelectMultiple.jsx new file mode 100644 index 0000000..19843f5 --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomSelectMultiple.jsx @@ -0,0 +1,62 @@ +"use client"; + +import { + Box, + Chip, + FormControl, + FormHelperText, + InputLabel, + MenuItem, + OutlinedInput, + Select, + Typography, +} from "@mui/material"; + +function CustomSelectMultiple({ + column, + filterParameters, + defaultFilterTranslation, + handleChange, +}) { + const columnSelectOption = column.columnSelectOption; + + const getLabelForValue = (value) => { + const option = columnSelectOption.find((opt) => opt.value === value); + return option ? option.label : value; + }; + + return ( + + + {column.header} + + + + ); +} + +export default CustomSelectMultiple; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomTextField.jsx b/src/core/components/DataTable/filter/fieldsType/CustomTextField.jsx new file mode 100644 index 0000000..be54a9f --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomTextField.jsx @@ -0,0 +1,42 @@ +import { InputAdornment, TextField, Typography } from "@mui/material"; +import FilterListIcon from "@mui/icons-material/FilterList"; + +function CustomTextField({ + column, + defaultFilterTranslation, + filterParameters, + handleOpenFilterBox, + handleBlur, + handleChange, +}) { + return ( + + handleChange({ ...filterParameters, value: e.target.value }) + } + onBlur={handleBlur} + fullWidth + variant="outlined" + size="small" + sx={{ my: 1 }} + InputProps={{ + endAdornment: ( + + + + ), + }} + InputLabelProps={{ shrink: true }} + /> + ); +} + +export default CustomTextField; diff --git a/src/core/components/DataTable/filter/fieldsType/CustomTextFieldRange.jsx b/src/core/components/DataTable/filter/fieldsType/CustomTextFieldRange.jsx new file mode 100644 index 0000000..fc55634 --- /dev/null +++ b/src/core/components/DataTable/filter/fieldsType/CustomTextFieldRange.jsx @@ -0,0 +1,85 @@ +import { Box, InputAdornment, TextField, Typography } from "@mui/material"; +import FilterListIcon from "@mui/icons-material/FilterList"; + +function CustomTextFieldRange({ + column, + defaultFilterTranslation, + handleOpenFilterBox, + handleChange, + filterParameters, + handleBlur, + errors, +}) { + return ( + + + handleChange({ + ...filterParameters, + value: [e.target.value, filterParameters.value[1]], + }) + } + onBlur={handleBlur} + label={از {column.header}} + value={filterParameters.value[0]} + fullWidth + error={ + touched?.[`${column.id}`]?.value && + Boolean(errors?.[`${column.id}`]?.value) + } + helperText={ + theme.palette.primary.main, + fontWeight: "bold", + }} + > + نوع فیلتر: {defaultFilterTranslation} + + } + variant="outlined" + size="small" + sx={{ my: 1, marginRight: 1 }} + /> + + handleChange({ + ...filterParameters, + value: [filterParameters.value[0], e.target.value], + }) + } + onBlur={handleBlur} + error={Boolean(errors?.[`${column.id}`]?.value)} + helperText={ + errors?.[`${column.id}`]?.value + ? errors?.[`${column.id}`]?.value.message + : null + } + label={تا {column.header}} + value={filterParameters.value[1]} + fullWidth + variant="outlined" + size="small" + sx={{ my: 1 }} + InputProps={{ + endAdornment: ( + + + + ), + }} + /> + + ); +} + +export default CustomTextFieldRange; diff --git a/src/core/components/DataTable/filter/index.jsx b/src/core/components/DataTable/filter/index.jsx new file mode 100644 index 0000000..a03159d --- /dev/null +++ b/src/core/components/DataTable/filter/index.jsx @@ -0,0 +1,41 @@ +"use client"; +import { useState } from "react"; +import FilterButton from "@/core/components/DataTable/filter/FilterButton"; +import FilterBody from "@/core/components/DataTable/filter/FilterBody"; +import { Box, Drawer } from "@mui/material"; + +function FilterColumn({ columns, user_id, page_name, table_name }) { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(false)} + sx={{ + overflowY: "hidden", + display: "flex", + flexDirection: "column", + height: "100%", + zIndex: "1300", + }} + > + + {open && ( + + )} + + + + ); +} + +export default FilterColumn; diff --git a/src/core/components/DataTable/head/TableHead.js b/src/core/components/DataTable/head/TableHead.js new file mode 100644 index 0000000..828cc48 --- /dev/null +++ b/src/core/components/DataTable/head/TableHead.js @@ -0,0 +1,56 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { TableHead } from "@mui/material"; +import DataTable_TableHeadRow from "./TableHeadRow"; + +const DataTable_TableHead = ({ columnVirtualizer, table, ...rest }) => { + const { + getState, + options: { + enableStickyHeader, + layoutMode, + muiTableHeadProps, + positionToolbarAlertBanner, + }, + refs: { tableHeadRef }, + } = table; + const { isFullScreen, showAlertBanner } = getState(); + + const tableHeadProps = { + ...parseFromValuesOrFunc(muiTableHeadProps, { table }), + ...rest, + }; + + const stickyHeader = enableStickyHeader || isFullScreen; + + return ( + { + tableHeadRef.current = ref; + if (tableHeadProps?.ref) { + // @ts-ignore + tableHeadProps.ref.current = ref; + } + }} + sx={(theme) => ({ + display: layoutMode?.startsWith("grid") ? "grid" : undefined, + opacity: 0.97, + position: stickyHeader ? "sticky" : "relative", + top: stickyHeader && layoutMode?.startsWith("grid") ? 0 : undefined, + zIndex: stickyHeader ? 2 : undefined, + ...parseFromValuesOrFunc(tableHeadProps?.sx, theme), + })} + > + {table.getHeaderGroups().map((headerGroup, index) => ( + + ))} + + ); +}; +export default DataTable_TableHead; diff --git a/src/core/components/DataTable/head/TableHeadCell.js b/src/core/components/DataTable/head/TableHeadCell.js new file mode 100644 index 0000000..febf64c --- /dev/null +++ b/src/core/components/DataTable/head/TableHeadCell.js @@ -0,0 +1,290 @@ +import { + getCommonMRTCellStyles, + parseFromValuesOrFunc, +} from "@/core/utils/utils"; +import { useTheme } from "@emotion/react"; +import { Box, TableCell } from "@mui/material"; +import { MRT_TableHeadCellSortLabel } from "material-react-table"; +import { useMemo } from "react"; + +const DataTable_TableHeadCell = ({ + columnVirtualizer, + header, + staticColumnIndex, + backgroundColor, + table, + ...rest +}) => { + const theme = useTheme(); + const { + getState, + options: { + columnResizeDirection, + columnResizeMode, + enableColumnActions, + enableColumnDragging, + enableColumnOrdering, + enableColumnPinning, + enableGrouping, + enableMultiSort, + layoutMode, + mrtTheme: { draggingBorderColor }, + muiTableHeadCellProps, + }, + refs: { tableHeadCellRefs }, + setHoveredColumn, + } = table; + const { + columnSizingInfo, + density, + draggingColumn, + grouping, + hoveredColumn, + showColumnFilters, + } = getState(); + const { column } = header; + const { columnDef } = column; + const { columnDefType } = columnDef; + + const tableCellProps = { + ...parseFromValuesOrFunc(muiTableHeadCellProps, { column, table }), + ...parseFromValuesOrFunc(columnDef.muiTableHeadCellProps, { + column, + table, + }), + ...rest, + }; + + const isColumnPinned = + enableColumnPinning && + columnDef.columnDefType !== "group" && + column.getIsPinned(); + + const showColumnActions = + (enableColumnActions || columnDef.enableColumnActions) && + columnDef.enableColumnActions !== false; + + const showDragHandle = + enableColumnDragging !== false && + columnDef.enableColumnDragging !== false && + (enableColumnDragging || + (enableColumnOrdering && columnDef.enableColumnOrdering !== false) || + (enableGrouping && + columnDef.enableGrouping !== false && + !grouping.includes(column.id))); + + const headerPL = useMemo(() => { + let pl = 0; + if (column.getCanSort()) pl += 1; + if (showColumnActions) pl += 1.75; + if (showDragHandle) pl += 1.5; + return pl; + }, [showColumnActions, showDragHandle]); + + const draggingBorders = useMemo(() => { + const showResizeBorder = + columnSizingInfo.isResizingColumn === column.id && + columnResizeMode === "onChange" && + !header.subHeaders.length; + + const borderStyle = showResizeBorder + ? `2px solid ${draggingBorderColor} !important` + : draggingColumn?.id === column.id + ? `1px dashed ${theme.palette.grey[500]}` + : hoveredColumn?.id === column.id + ? `2px dashed ${draggingBorderColor}` + : undefined; + + if (showResizeBorder) { + return columnResizeDirection === "ltr" + ? { borderRight: borderStyle } + : { borderLeft: borderStyle }; + } + const draggingBorders = borderStyle + ? { + borderLeft: borderStyle, + borderRight: borderStyle, + borderTop: borderStyle, + } + : undefined; + + return draggingBorders; + }, [draggingColumn, hoveredColumn, columnSizingInfo.isResizingColumn]); + + const handleDragEnter = (_e) => { + if (enableGrouping && hoveredColumn?.id === "drop-zone") { + setHoveredColumn(null); + } + if (enableColumnOrdering && draggingColumn && columnDefType !== "group") { + setHoveredColumn( + columnDef.enableColumnOrdering !== false ? column : null, + ); + } + }; + + const handleDragOver = (e) => { + if (columnDef.enableColumnOrdering !== false) { + e.preventDefault(); + } + }; + + const HeaderElement = + parseFromValuesOrFunc(columnDef.Header, { + column, + header, + table, + }) ?? columnDef.header; + + const columnRelativeDepth = header.depth - column.depth; + + if (columnRelativeDepth > 1) { + return null; + } + + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = leafs[leafs.length - 1].depth - header.depth; + } + + return ( + { + if (node) { + tableHeadCellRefs.current[column.id] = node; + if (columnDefType !== "group") { + columnVirtualizer?.measureElement?.(node); + } + } + }} + {...tableCellProps} + sx={(theme) => ({ + "& :hover": { + ".MuiButtonBase-root": { + opacity: 1, + }, + }, + flexDirection: layoutMode?.startsWith("grid") ? "column" : undefined, + fontWeight: "bold", + overflow: "visible", + p: + density === "compact" + ? "0.5rem" + : density === "comfortable" + ? columnDefType === "display" + ? "0.75rem" + : "1rem" + : columnDefType === "display" + ? "1rem 1.25rem" + : "1.5rem", + pb: + columnDefType === "display" + ? 0 + : showColumnFilters || density === "compact" + ? "0.4rem" + : "0.6rem", + pt: + columnDefType === "group" || density === "compact" + ? "0.25rem" + : density === "comfortable" + ? ".75rem" + : "1.25rem", + userSelect: enableMultiSort && column.getCanSort() ? "none" : undefined, + verticalAlign: "middle", + ...getCommonMRTCellStyles({ + column, + header, + table, + tableCellProps, + theme, + }), + ...draggingBorders, + backgroundColor: backgroundColor, + })} + > + {tableCellProps.children ?? ( + + + + {HeaderElement} + + {column.getCanSort() && columnDefType !== "group" && ( + + )} + + + )} + + ); +}; +export default DataTable_TableHeadCell; diff --git a/src/core/components/DataTable/head/TableHeadRow.js b/src/core/components/DataTable/head/TableHeadRow.js new file mode 100644 index 0000000..bee809f --- /dev/null +++ b/src/core/components/DataTable/head/TableHeadRow.js @@ -0,0 +1,69 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { TableRow, useTheme } from "@mui/material"; +import DataTable_TableHeadCell from "./TableHeadCell"; + +const DataTable_TableHeadRow = ({ + columnVirtualizer, + headerGroup, + table, + index, // Add index prop + ...rest +}) => { + const { palette } = useTheme(); + + const { + options: { enableStickyHeader, layoutMode, muiTableHeadRowProps }, + } = table; + const { virtualColumns, virtualPaddingLeft, virtualPaddingRight } = + columnVirtualizer ?? {}; + + const backgroundColor = + index % 2 === 0 ? palette.primary.main : palette.secondary.main; + + const tableRowProps = { + ...parseFromValuesOrFunc(muiTableHeadRowProps, { + headerGroup, + table, + }), + ...rest, + sx: (theme) => ({ + // Access theme from the sx function + ...(parseFromValuesOrFunc(muiTableHeadRowProps?.sx, theme) || {}), + display: layoutMode?.startsWith("grid") ? "flex" : undefined, + position: + enableStickyHeader && layoutMode === "semantic" ? "sticky" : "relative", + top: 0, + }), + }; + + return ( + + {virtualPaddingLeft && ( + + )} + {(virtualColumns ?? headerGroup.headers).map( + (headerOrVirtualHeader, staticColumnIndex) => { + const header = columnVirtualizer + ? headerGroup.headers[headerOrVirtualHeader.index] + : headerOrVirtualHeader; + + return header ? ( + + ) : null; + }, + )} + {virtualPaddingRight && ( + + )} + + ); +}; + +export default DataTable_TableHeadRow; diff --git a/src/core/components/DataTable/hide/HideBody.jsx b/src/core/components/DataTable/hide/HideBody.jsx new file mode 100644 index 0000000..47c96ee --- /dev/null +++ b/src/core/components/DataTable/hide/HideBody.jsx @@ -0,0 +1,43 @@ +"use client"; + +import { Box, Drawer, styled } from "@mui/material"; +import HideBodyField from "@/core/components/DataTable/hide/HideBodyField"; +import HideHeader from "@/core/components/DataTable/hide/HideHeader"; +import useDataTable from "@/lib/hooks/useDataTable"; +import HideOrShowAll from "@/core/components/DataTable/hide/HideOrShowAll"; +import ScrollBox from "../../ScrollBox"; + +function HideBody({ columns, drawerState, setDrawerState }) { + const { hideData, setHideData } = useDataTable(); + + return ( + setDrawerState(false)} + sx={{ + overflowY: "hidden", + display: "flex", + flexDirection: "column", + height: "100%", + zIndex: "1300", + }} + > + + + + + {columns.map((column) => ( + + ))} + + + + ); +} + +export default HideBody; diff --git a/src/core/components/DataTable/hide/HideBodyField.jsx b/src/core/components/DataTable/hide/HideBodyField.jsx new file mode 100644 index 0000000..c008514 --- /dev/null +++ b/src/core/components/DataTable/hide/HideBodyField.jsx @@ -0,0 +1,80 @@ +import { alpha, Box, Checkbox, styled, Typography } from "@mui/material"; +import { SimpleTreeView } from "@mui/x-tree-view"; +import { TreeItem, treeItemClasses } from "@mui/x-tree-view/TreeItem"; + +const CustomTreeItem = styled(TreeItem)(({ theme }) => ({ + [`& .${treeItemClasses.content}`]: { + padding: theme.spacing(0.5, 0.5), + margin: theme.spacing(0.2, 0), + gap: 0, + }, + [`& .${treeItemClasses.iconContainer}`]: { + "& .close": { + opacity: 0.3, + }, + }, + [`& .${treeItemClasses.groupTransition}`]: { + marginLeft: 16, + paddingLeft: 16, + borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`, + }, +})); + +function HideBodyField({ column, hideData, setHideData }) { + const handleCheckboxChange = () => { + setHideData((prevData) => { + const updateHideData = (data, id) => { + if (data.hasOwnProperty(id)) { + return { ...data, [id]: !data[id] }; + } + const updatedData = { ...data }; + for (const key in data) { + if (data[key] && typeof data[key] === "object") { + updatedData[key] = updateHideData(data[key], id); + } + } + return updatedData; + }; + + return updateHideData(prevData, column.id); + }); + }; + + const labelContent = (() => { + if (typeof hideData[column.id] === "boolean") { + return ( + + + {column.header} + + ); + } else { + return ( + + {column.header} + + ); + } + })(); + + return ( + + + {column.columns?.map((subColumn) => ( + + ))} + + + ); +} + +export default HideBodyField; diff --git a/src/core/components/DataTable/hide/HideButton.jsx b/src/core/components/DataTable/hide/HideButton.jsx new file mode 100644 index 0000000..4ada7c7 --- /dev/null +++ b/src/core/components/DataTable/hide/HideButton.jsx @@ -0,0 +1,37 @@ +"use client"; +import ViewColumnIcon from "@mui/icons-material/ViewColumn"; +import { IconButton, Tooltip, Badge } from "@mui/material"; +import useDataTable from "@/lib/hooks/useDataTable"; +import { flattenObjectOfObjects } from "@/core/utils/flattenObjectOfObjects"; + +function HideButton({ drawerState, setDrawerState }) { + const { hideData } = useDataTable(); + const flattenHideData = flattenObjectOfObjects(hideData); + const falseCount = Object.values(flattenHideData).filter( + (value) => value === false, + ).length; + + return ( + + { + setDrawerState(!drawerState); + }} + aria-label="hide table column" + > + + + + + + ); +} + +export default HideButton; diff --git a/src/core/components/DataTable/hide/HideHeader.jsx b/src/core/components/DataTable/hide/HideHeader.jsx new file mode 100644 index 0000000..034271e --- /dev/null +++ b/src/core/components/DataTable/hide/HideHeader.jsx @@ -0,0 +1,35 @@ +"use client"; + +import { Box, IconButton, Typography } from "@mui/material"; +import CancelIcon from "@mui/icons-material/Cancel"; +import ViewColumnIcon from "@mui/icons-material/ViewColumn"; + +function FilterHeader({ setDrawerState }) { + return ( + + + + + نمایش/مخفی کردن ستون ها + + + setDrawerState(false)}> + + + + ); +} + +export default FilterHeader; diff --git a/src/core/components/DataTable/hide/HideOrShowAll.jsx b/src/core/components/DataTable/hide/HideOrShowAll.jsx new file mode 100644 index 0000000..27c8849 --- /dev/null +++ b/src/core/components/DataTable/hide/HideOrShowAll.jsx @@ -0,0 +1,70 @@ +"use client"; + +import { Box, Button } from "@mui/material"; +import VisibilityIcon from "@mui/icons-material/Visibility"; +import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; + +const setAllValues = (obj, value) => { + const result = {}; + for (const key in obj) { + if (typeof obj[key] === "object" && obj[key] !== null) { + result[key] = setAllValues(obj[key], value); + } else { + result[key] = value; + } + } + return result; +}; + +const allValuesAre = (obj, value) => { + for (const key in obj) { + if (typeof obj[key] === "object" && obj[key] !== null) { + if (!allValuesAre(obj[key], value)) { + return false; + } + } else if (obj[key] !== value) { + return false; + } + } + return true; +}; + +function HideOrShowAll({ hideData, setHideData }) { + const handleShowAll = () => { + const newHideData = setAllValues(hideData, true); + setHideData(newHideData); + }; + + const handleHideAll = () => { + const newHideData = setAllValues(hideData, false); + setHideData(newHideData); + }; + + const allHidden = allValuesAre(hideData, false); + const allVisible = allValuesAre(hideData, true); + + return ( + + + + + ); +} + +export default HideOrShowAll; diff --git a/src/core/components/DataTable/hide/index.jsx b/src/core/components/DataTable/hide/index.jsx new file mode 100644 index 0000000..865c0e2 --- /dev/null +++ b/src/core/components/DataTable/hide/index.jsx @@ -0,0 +1,24 @@ +"use client"; + +import { useState } from "react"; +import HideButton from "@/core/components/DataTable/hide/HideButton"; +import HideBody from "@/core/components/DataTable/hide/HideBody"; + +function HideColumn({ columns, user_id, page_name, table_name }) { + const [open, setOpen] = useState(false); + return ( + <> + + + + ); +} + +export default HideColumn; diff --git a/src/core/components/DataTable/index.js b/src/core/components/DataTable/index.js new file mode 100644 index 0000000..6f1c07e --- /dev/null +++ b/src/core/components/DataTable/index.js @@ -0,0 +1,18 @@ +import DataTable_Main from "./Main"; +import DataTableProvider from "@/lib/contexts/DataTable"; + +const DataTable = (props) => { + return ( + + + + ); +}; + +export default DataTable; diff --git a/src/core/components/DataTable/localization/fa/datatable.js b/src/core/components/DataTable/localization/fa/datatable.js new file mode 100644 index 0000000..819a84b --- /dev/null +++ b/src/core/components/DataTable/localization/fa/datatable.js @@ -0,0 +1,92 @@ +export const FA_DATATABLE_LOCALIZATION = { + actions: "عملیات", + and: "و", + cancel: "لغو", + changeFilterMode: "تغییر حالت فیلتر", + changeSearchMode: "تغییر حالت جستجو", + clearFilter: "پاک کردن فیلتر", + clearSearch: "پاک کردن جستجو", + clearSort: "پاک کردن مرتب سازی", + clickToCopy: "کلیک برای کپی", + collapse: "جمع شدن", + collapseAll: "جمع شدن همه", + columnActions: "عملیات ستون", + copiedToClipboard: "کپی شد", + dropToGroupBy: "رها کردن برای گروه بندی بر اساس {column}", + edit: "ویرایش", + expand: "باز شدن", + expandAll: "باز شدن همه", + filterArrIncludes: "شامل", + filterArrIncludesAll: "شامل همه", + filterArrIncludesSome: "شامل", + filterBetween: "میان", + filterBetweenInclusive: "میان با احتساب هر دو", + filterByColumn: "فیلتر بر اساس {column}", + filterContains: "شامل", + filterEmpty: "خالی", + filterEndsWith: "به پایان می‌رسد با", + filterEquals: "برابر", + filterEqualsString: "برابر", + filterFuzzy: "نزدیک", + filterGreaterThan: "بزرگتر از", + filterGreaterThanOrEqualTo: "بزرگتر یا مساوی", + filterInNumberRange: "میان", + filterIncludesString: "شامل", + filterIncludesStringSensitive: "شامل", + filterLessThan: "کوچکتر از", + filterLessThanOrEqualTo: "کوچکتر یا مساوی", + filterMode: "حالت فیلتر: {filterType}", + filterNotEmpty: "غیر خالی", + filterNotEquals: "نا برابر", + filterStartsWith: "شروع می‌شود با", + filterWeakEquals: "برابر", + filteringByColumn: "فیلتر بر اساس {column} - {filterType} {filterValue}", + goToFirstPage: "رفتن به صفحه اول", + goToLastPage: "رفتن به صفحه آخر", + goToNextPage: "رفتن به صفحه بعدی", + goToPreviousPage: "رفتن به صفحه قبلی", + grab: "گرفتن", + groupByColumn: "گروه بندی بر اساس {column}", + groupedBy: "گروه بندی شده بر اساس", + hideAll: "پنهان کردن همه", + hideColumn: "پنهان کردن ستون {column}", + max: "حداکثر", + min: "حداقل", + move: "انتقال", + noRecordsToDisplay: "هیچ رکوردی برای نمایش وجود ندارد", + noResultsFound: "نتیجه‌ای یافت نشد", + of: "از", + or: "یا", + pinToLeft: "چسباندن به سمت چپ", + pinToRight: "چسباندن به سمت راست", + resetColumnSize: "بازنشانی اندازه ستون", + resetOrder: "بازنشانی ترتیب", + rowActions: "عملیات ردیف", + rowNumber: "#", + rowNumbers: "شماره ردیف", + rowsPerPage: "ردیف در هر صفحه", + save: "ذخیره", + search: "جستجو", + selectedCountOfRowCountRowsSelected: + "{selectedCount} از {rowCount} ردیف انتخاب شده", + select: "انتخاب", + showAll: "نمایش همه", + showAllColumns: "نمایش همه ستون‌ها", + showHideColumns: "نمایش/مخفی کردن ستون‌ها", + showHideFilters: "نمایش/مخفی کردن فیلترها", + showHideSearch: "نمایش/مخفی کردن جستجو", + sortByColumnAsc: "مرتب سازی بر اساس {column} صعودی", + sortByColumnDesc: "مرتب سازی بر اساس {column} نزولی", + sortedByColumnAsc: "مرتب شده بر اساس {column} صعودی", + sortedByColumnDesc: "مرتب شده بر اساس {column} نزولی", + thenBy: "، سپس بر اساس ", + toggleDensity: "تغییر تراکم", + toggleFullScreen: "تغییر حالت تمام صفحه", + toggleSelectAll: "انتخاب/عدم انتخاب همه", + toggleSelectRow: "انتخاب/عدم انتخاب ردیف", + toggleVisibility: "تغییر پیدا/پنهان", + ungroupByColumn: "لغو گروه بندی بر اساس {column}", + unpin: "رها کردن", + unpinAll: "رها کردن همه", + unsorted: "بدون مرتب سازی", +}; diff --git a/src/core/components/DataTable/menus/ActionMenuItem.js b/src/core/components/DataTable/menus/ActionMenuItem.js new file mode 100644 index 0000000..a7a03b3 --- /dev/null +++ b/src/core/components/DataTable/menus/ActionMenuItem.js @@ -0,0 +1,49 @@ +import { Box, IconButton, ListItemIcon, MenuItem } from "@mui/material"; + +const DataTable_ActionMenuItem = ({ + icon, + label, + onOpenSubMenu, + table, + ...rest +}) => { + const { + options: { + icons: { ArrowRightIcon }, + }, + } = table; + + return ( + + + {icon} + {label} + + {onOpenSubMenu && ( + + + + )} + + ); +}; +export default DataTable_ActionMenuItem; diff --git a/src/core/components/DataTable/menus/CellActionMenu.js b/src/core/components/DataTable/menus/CellActionMenu.js new file mode 100644 index 0000000..12256ab --- /dev/null +++ b/src/core/components/DataTable/menus/CellActionMenu.js @@ -0,0 +1,96 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Menu } from "@mui/material"; +import DataTable_ActionMenuItem from "./ActionMenuItem"; + +const DataTable_CellActionMenu = ({ table, ...rest }) => { + const { + getState, + options: { + editDisplayMode, + enableClickToCopy, + enableEditing, + icons: { ContentCopy, EditIcon }, + localization, + mrtTheme: { menuBackgroundColor }, + renderCellActionMenuItems, + }, + refs: { actionCellRef }, + } = table; + const { actionCell, density } = getState(); + const cell = actionCell || null; + const { row } = cell; + const { column } = cell; + const { columnDef } = column; + + const handleClose = (event) => { + event?.stopPropagation(); + table.setActionCell(null); + actionCellRef.current = null; + }; + + const internalMenuItems = [ + (parseFromValuesOrFunc(enableClickToCopy, cell) === "context-menu" || + parseFromValuesOrFunc(columnDef.enableClickToCopy, cell) === + "context-menu") && ( + } + key={"mrt-copy"} + label={localization.copy} + onClick={(event) => { + event.stopPropagation(); + navigator.clipboard.writeText(cell.getValue()); + handleClose(); + }} + table={table} + /> + ), + parseFromValuesOrFunc(enableEditing, row) && editDisplayMode === "cell" && ( + } + key={"mrt-edit"} + label={localization.edit} + onClick={() => { + openEditingCell({ cell, table }); + handleClose(); + }} + table={table} + /> + ), + ].filter(Boolean); + + const renderActionProps = { + cell, + closeMenu: handleClose, + column, + internalMenuItems, + row, + table, + }; + + const menuItems = + columnDef.renderCellActionMenuItems?.(renderActionProps) ?? + renderCellActionMenuItems?.(renderActionProps); + + return ( + (!!menuItems?.length || !!internalMenuItems?.length) && ( + event.stopPropagation()} + onClose={handleClose} + open={!!cell} + transformOrigin={{ horizontal: -100, vertical: 8 }} + {...rest} + > + {menuItems ?? internalMenuItems} + + ) + ); +}; +export default DataTable_CellActionMenu; diff --git a/src/core/components/DataTable/reset/ResetStorage.jsx b/src/core/components/DataTable/reset/ResetStorage.jsx new file mode 100644 index 0000000..76edf58 --- /dev/null +++ b/src/core/components/DataTable/reset/ResetStorage.jsx @@ -0,0 +1,36 @@ +"use client"; +import { IconButton, Tooltip, Badge } from "@mui/material"; +import AutorenewIcon from "@mui/icons-material/Autorenew"; +import useTableSetting from "@/lib/hooks/useTableSetting"; +import useDataTable from "@/lib/hooks/useDataTable"; + +function ResetStorage({ user_id, page_name, table_name }) { + const { resetAction } = useTableSetting(); + const { isAnyDirty } = useDataTable(); + const reset = () => { + resetAction(user_id, page_name, table_name); + }; + + return ( + + + {isAnyDirty ? ( + + + + ) : ( + + )} + + + ); +} + +export default ResetStorage; diff --git a/src/core/components/DataTable/table/Paper.js b/src/core/components/DataTable/table/Paper.js new file mode 100644 index 0000000..cdca182 --- /dev/null +++ b/src/core/components/DataTable/table/Paper.js @@ -0,0 +1,102 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Paper } from "@mui/material"; +import DataTable_BottomToolbar from "../toolbar/BottomToolbar"; +import DataTable_TopToolbar from "../toolbar/TopToolbar"; +import DataTable_TableContainer from "./TableContainer"; + +const DataTable_Paper = ({ + table, + table_name, + columns, + user_id, + page_name, + mutate, + need_filter, + table_url, + table_title, + setFilterData, + ...rest +}) => { + const { + getState, + options: { + enableBottomToolbar, + enableTopToolbar, + mrtTheme: { baseBackgroundColor }, + muiTablePaperProps, + renderBottomToolbar, + renderTopToolbar, + }, + refs: { tablePaperRef }, + } = table; + + const { isFullScreen } = getState(); + + const paperProps = { + ...parseFromValuesOrFunc(muiTablePaperProps, { table }), + ...rest, + }; + + return ( + { + tablePaperRef.current = ref; + if (paperProps?.ref) { + //@ts-ignore + paperProps.ref.current = ref; + } + }} + style={{ + ...(isFullScreen + ? { + bottom: 0, + height: "100dvh", + left: 0, + margin: 0, + maxHeight: "100dvh", + maxWidth: "100dvw", + padding: 0, + position: "fixed", + right: 0, + top: 0, + width: "100dvw", + zIndex: 999, + } + : {}), + ...paperProps?.style, + }} + sx={(theme) => ({ + backgroundColor: baseBackgroundColor, + backgroundImage: "unset", + overflow: "hidden", + transition: "all 100ms ease-in-out", + ...parseFromValuesOrFunc(paperProps?.sx, theme), + })} + > + {enableTopToolbar && + (parseFromValuesOrFunc(renderTopToolbar, { table }) ?? ( + + ))} + + {enableBottomToolbar && + (parseFromValuesOrFunc(renderBottomToolbar, { table }) ?? ( + + ))} + + ); +}; +export default DataTable_Paper; diff --git a/src/core/components/DataTable/table/Table.js b/src/core/components/DataTable/table/Table.js new file mode 100644 index 0000000..cfcb591 --- /dev/null +++ b/src/core/components/DataTable/table/Table.js @@ -0,0 +1,76 @@ +import { parseCSSVarId, parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Table } from "@mui/material"; +import { useMRT_ColumnVirtualizer } from "material-react-table"; +import { useMemo } from "react"; +import DataTable_TableBody, { + Memo_DataTable_TableBody, +} from "../body/TableBody"; +import DataTable_TableHead from "../head/TableHead"; + +const DataTable_Table = ({ table, ...rest }) => { + const { + getFlatHeaders, + getState, + options: { + columns, + enableStickyHeader, + enableTableFooter, + enableTableHead, + layoutMode, + memoMode, + muiTableProps, + renderCaption, + }, + } = table; + const { columnSizing, columnSizingInfo, columnVisibility, isFullScreen } = + getState(); + + const tableProps = { + ...parseFromValuesOrFunc(muiTableProps, { table }), + ...rest, + }; + + const Caption = parseFromValuesOrFunc(renderCaption, { table }); + + const columnSizeVars = useMemo(() => { + const headers = getFlatHeaders(); + const colSizes = {}; + for (let i = 0; i < headers.length; i++) { + const header = headers[i]; + const colSize = header.getSize(); + colSizes[`--header-${parseCSSVarId(header.id)}-size`] = colSize; + colSizes[`--col-${parseCSSVarId(header.column.id)}-size`] = colSize; + } + return colSizes; + }, [columns, columnSizing, columnSizingInfo, columnVisibility]); + + const columnVirtualizer = useMRT_ColumnVirtualizer(table); + + const commonTableGroupProps = { + columnVirtualizer, + table, + }; + + return ( + ({ + display: layoutMode?.startsWith("grid") ? "grid" : undefined, + position: "relative", + ...parseFromValuesOrFunc(tableProps?.sx, theme), + })} + > + {!!Caption && } + {enableTableHead && } + {memoMode === "table-body" || columnSizingInfo.isResizingColumn ? ( + + ) : ( + + )} + {/* {enableTableFooter && } */} +
{Caption}
+ ); +}; +export default DataTable_Table; diff --git a/src/core/components/DataTable/table/TableContainer.js b/src/core/components/DataTable/table/TableContainer.js new file mode 100644 index 0000000..67ee485 --- /dev/null +++ b/src/core/components/DataTable/table/TableContainer.js @@ -0,0 +1,84 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { TableContainer } from "@mui/material"; +import { useEffect, useLayoutEffect, useState } from "react"; +import DataTable_CellActionMenu from "../menus/CellActionMenu"; +import DataTable_Table from "./Table"; +import DataTable_TableLoadingOverlay from "./TableLoadingOverlay"; + +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect; + +const DataTable_TableContainer = ({ table, ...rest }) => { + const { + getState, + options: { enableCellActions, enableStickyHeader, muiTableContainerProps }, + refs: { bottomToolbarRef, tableContainerRef, topToolbarRef }, + } = table; + const { actionCell, isFullScreen, isLoading, showLoadingOverlay } = + getState(); + + const loading = + showLoadingOverlay !== false && (isLoading || showLoadingOverlay); + + const [totalToolbarHeight, setTotalToolbarHeight] = useState(0); + + const tableContainerProps = { + ...parseFromValuesOrFunc(muiTableContainerProps, { + table, + }), + ...rest, + }; + + useIsomorphicLayoutEffect(() => { + const topToolbarHeight = + typeof document !== "undefined" + ? (topToolbarRef.current?.offsetHeight ?? 0) + : 0; + + const bottomToolbarHeight = + typeof document !== "undefined" + ? (bottomToolbarRef?.current?.offsetHeight ?? 0) + : 0; + + setTotalToolbarHeight(topToolbarHeight + bottomToolbarHeight); + }); + + return ( + { + if (node) { + tableContainerRef.current = node; + if (tableContainerProps?.ref) { + //@ts-ignore + tableContainerProps.ref.current = node; + } + } + }} + style={{ + maxHeight: isFullScreen + ? `calc(100vh - ${totalToolbarHeight}px)` + : undefined, + ...tableContainerProps?.style, + }} + sx={(theme) => ({ + maxHeight: enableStickyHeader + ? `clamp(350px, calc(100vh - ${totalToolbarHeight}px), 9999px)` + : undefined, + maxWidth: "100%", + overflow: "auto", + position: "relative", + ...parseFromValuesOrFunc(tableContainerProps?.sx, theme), + })} + > + {loading ? : null} + + {enableCellActions && actionCell && ( + + )} + + ); +}; +export default DataTable_TableContainer; diff --git a/src/core/components/DataTable/table/TableLoadingOverlay.js b/src/core/components/DataTable/table/TableLoadingOverlay.js new file mode 100644 index 0000000..946fff7 --- /dev/null +++ b/src/core/components/DataTable/table/TableLoadingOverlay.js @@ -0,0 +1,44 @@ +import { Box, CircularProgress } from "@mui/material"; + +const DataTable_TableLoadingOverlay = ({ table, ...rest }) => { + const { + options: { + localization, + mrtTheme: { baseBackgroundColor }, + muiCircularProgressProps, + }, + } = table; + + const circularProgressProps = { + ...parseFromValuesOrFunc(muiCircularProgressProps, { table }), + ...rest, + }; + + return ( + + {circularProgressProps?.Component ?? ( + + )} + + ); +}; +export default DataTable_TableLoadingOverlay; diff --git a/src/core/components/DataTable/toolbar/BottomToolbar.js b/src/core/components/DataTable/toolbar/BottomToolbar.js new file mode 100644 index 0000000..809a390 --- /dev/null +++ b/src/core/components/DataTable/toolbar/BottomToolbar.js @@ -0,0 +1,85 @@ +import { + getCommonToolbarStyles, + parseFromValuesOrFunc, +} from "@/core/utils/utils"; +import { Box, alpha, useMediaQuery } from "@mui/material"; +import DataTable_LinearProgressBar from "./LinearProgressBar"; +import DataTable_TablePagination from "./TablePagination"; + +const DataTable_BottomToolbar = ({ table, ...rest }) => { + const { + getState, + options: { + enablePagination, + muiBottomToolbarProps, + positionPagination, + renderBottomToolbarCustomActions, + }, + refs: { bottomToolbarRef }, + } = table; + const { isFullScreen } = getState(); + + const isMobile = useMediaQuery("(max-width:720px)"); + const toolbarProps = { + ...parseFromValuesOrFunc(muiBottomToolbarProps, { table }), + ...rest, + }; + + const stackAlertBanner = isMobile || !!renderBottomToolbarCustomActions; + return ( + { + if (node) { + bottomToolbarRef.current = node; + if (toolbarProps?.ref) { + // @ts-ignore + toolbarProps.ref.current = node; + } + } + }} + sx={(theme) => ({ + ...getCommonToolbarStyles({ table, theme }), + bottom: isFullScreen ? "0" : undefined, + boxShadow: `0 1px 2px -1px ${alpha(theme.palette.grey[700], 0.5)} inset`, + left: 0, + position: isFullScreen ? "fixed" : "relative", + right: 0, + ...parseFromValuesOrFunc(toolbarProps?.sx, theme), + })} + > + + + {renderBottomToolbarCustomActions ? ( + renderBottomToolbarCustomActions({ table }) + ) : ( + + )} + + {enablePagination && + ["both", "bottom"].includes(positionPagination ?? "") && ( + + )} + + + + ); +}; +export default DataTable_BottomToolbar; diff --git a/src/core/components/DataTable/toolbar/LinearProgressBar.js b/src/core/components/DataTable/toolbar/LinearProgressBar.js new file mode 100644 index 0000000..9c40fc7 --- /dev/null +++ b/src/core/components/DataTable/toolbar/LinearProgressBar.js @@ -0,0 +1,40 @@ +import { parseFromValuesOrFunc } from "@/core/utils/utils"; +import { Collapse, LinearProgress } from "@mui/material"; + +const DataTable_LinearProgressBar = ({ isTopToolbar, table, ...rest }) => { + const { + getState, + options: { muiLinearProgressProps }, + } = table; + const { isSaving, showProgressBars } = getState(); + + const linearProgressProps = { + ...parseFromValuesOrFunc(muiLinearProgressProps, { + isTopToolbar, + table, + }), + ...rest, + }; + + return ( + + + + ); +}; +export default DataTable_LinearProgressBar; diff --git a/src/core/components/DataTable/toolbar/TablePagination.js b/src/core/components/DataTable/toolbar/TablePagination.js new file mode 100644 index 0000000..7942a3a --- /dev/null +++ b/src/core/components/DataTable/toolbar/TablePagination.js @@ -0,0 +1,228 @@ +import { + flipIconStyles, + getCommonTooltipProps, + parseFromValuesOrFunc, +} from "@/core/utils/utils"; +import { useTheme } from "@emotion/react"; +import { + Box, + IconButton, + InputLabel, + MenuItem, + Pagination, + PaginationItem, + Select, + Tooltip, + Typography, + useMediaQuery, +} from "@mui/material"; + +const defaultRowsPerPage = [5, 10, 15, 20, 25, 30, 50, 100]; + +const DataTable_TablePagination = ({ position = "bottom", table, ...rest }) => { + const theme = useTheme(); + const isMobile = useMediaQuery("(max-width: 720px)"); + + const { + getState, + options: { + enableToolbarInternalActions, + icons: { ChevronLeftIcon, ChevronRightIcon, FirstPageIcon, LastPageIcon }, + localization, + muiPaginationProps, + paginationDisplayMode, + }, + } = table; + + const { + pagination: { pageIndex = 0, pageSize = 10 }, + showGlobalFilter, + } = getState(); + + const paginationProps = { + ...parseFromValuesOrFunc(muiPaginationProps, { + table, + }), + ...rest, + }; + + const totalRowCount = table.getRowCount(); + const numberOfPages = table.getPageCount(); + const showFirstLastPageButtons = numberOfPages > 2; + const firstRowIndex = pageIndex * pageSize; + const lastRowIndex = Math.min(pageIndex * pageSize + pageSize, totalRowCount); + + const { + SelectProps = {}, + disabled = false, + rowsPerPageOptions = defaultRowsPerPage, + showFirstButton = showFirstLastPageButtons, + showLastButton = showFirstLastPageButtons, + showRowsPerPage = true, + ...restPaginationProps + } = paginationProps ?? {}; + + const disableBack = pageIndex <= 0 || disabled; + const disableNext = lastRowIndex >= totalRowCount || disabled; + + if (isMobile && SelectProps?.native !== false) { + SelectProps.native = true; + } + + const tooltipProps = getCommonTooltipProps(); + + return ( + + {showRowsPerPage && ( + + + {localization.rowsPerPage} + + + + )} + {paginationDisplayMode === "pages" ? ( + table.setPageIndex(newPageIndex - 1)} + page={pageIndex + 1} + renderItem={(item) => ( + + )} + showFirstButton={showFirstButton} + showLastButton={showLastButton} + {...restPaginationProps} + /> + ) : paginationDisplayMode === "default" ? ( + <> + {`${ + lastRowIndex === 0 ? 0 : (firstRowIndex + 1).toLocaleString() + }-${lastRowIndex.toLocaleString()} ${ + localization.of + } ${totalRowCount.toLocaleString()}`} + + {showFirstButton && ( + + + table.firstPage()} + size="small" + > + + + + + )} + + + table.previousPage()} + size="small" + > + + + + + + + table.nextPage()} + size="small" + > + + + + + {showLastButton && ( + + + table.lastPage()} + size="small" + > + + + + + )} + + + ) : null} + + ); +}; +export default DataTable_TablePagination; diff --git a/src/core/components/DataTable/toolbar/TopToolbar.js b/src/core/components/DataTable/toolbar/TopToolbar.js new file mode 100644 index 0000000..a1c0aeb --- /dev/null +++ b/src/core/components/DataTable/toolbar/TopToolbar.js @@ -0,0 +1,156 @@ +import { + getCommonToolbarStyles, + parseFromValuesOrFunc, +} from "@/core/utils/utils"; +import { Box, Typography, useMediaQuery } from "@mui/material"; +import DataTable_LinearProgressBar from "./LinearProgressBar"; +import DataTable_TablePagination from "./TablePagination"; +import FilterColumn from "@/core/components/DataTable/filter"; +import UpdateTable from "@/core/components/DataTable/update/UpdateTable"; +import ResetStorage from "@/core/components/DataTable/reset/ResetStorage"; +import HideColumn from "@/core/components/DataTable/hide"; +import FilterCustom from "../filter/FilterCustom"; + +const DataTable_TopToolbar = ({ + mutate, + need_filter, + table, + columns, + table_url, + user_id, + page_name, + table_name, + table_title, + special_data, + special_filter, + setFilterData, +}) => { + const { + getState, + options: { + enablePagination, + enableToolbarInternalActions, + muiTopToolbarProps, + positionPagination, + renderTopToolbarCustomActions, + }, + refs: { topToolbarRef }, + } = table; + const { isFullScreen, showGlobalFilter } = getState(); + + const isMobile = useMediaQuery("(max-width:720px)"); + const isTablet = useMediaQuery("(max-width:1024px)"); + + const toolbarProps = parseFromValuesOrFunc(muiTopToolbarProps, { table }); + + const stackAlertBanner = + isMobile || + !!renderTopToolbarCustomActions || + (showGlobalFilter && isTablet); + + return ( + { + topToolbarRef.current = ref; + if (toolbarProps?.ref) { + // @ts-ignore + toolbarProps.ref.current = ref; + } + }} + sx={(theme) => ({ + ...getCommonToolbarStyles({ table, theme }), + position: isFullScreen ? "sticky" : "relative", + top: isFullScreen ? "0" : undefined, + ...parseFromValuesOrFunc(toolbarProps?.sx, theme), + })} + > + + + {renderTopToolbarCustomActions?.({ table })} + + + {table_title || ""} + + {enableToolbarInternalActions && ( + + {!special_data && ( + <> + + + + )} + + {need_filter && ( + + )} + {special_filter && setFilterData && ( + + )} + + )} + + {enablePagination && + ["both", "top"].includes(positionPagination ?? "") && ( + + )} + + + ); +}; + +export default DataTable_TopToolbar; diff --git a/src/core/components/DataTable/update/UpdateTable.jsx b/src/core/components/DataTable/update/UpdateTable.jsx new file mode 100644 index 0000000..93b54b7 --- /dev/null +++ b/src/core/components/DataTable/update/UpdateTable.jsx @@ -0,0 +1,19 @@ +"use client"; +import { IconButton, Tooltip } from "@mui/material"; +import RestartAltIcon from "@mui/icons-material/RestartAlt"; + +function UpdateTable({ mutate }) { + const update = () => { + mutate(); + }; + + return ( + + + + + + ); +} + +export default UpdateTable; diff --git a/src/core/components/DataTableWithAuth.jsx b/src/core/components/DataTableWithAuth.jsx new file mode 100644 index 0000000..2ef70ab --- /dev/null +++ b/src/core/components/DataTableWithAuth.jsx @@ -0,0 +1,8 @@ +import DataTable from "@/core/components/DataTable"; +import { useAuth } from "@/lib/contexts/auth"; + +const DataTableWithAuth = (props) => { + const { user } = useAuth(); + return ; +}; +export default DataTableWithAuth; diff --git a/src/core/components/FilterDrawer/FilterController.jsx b/src/core/components/FilterDrawer/FilterController.jsx new file mode 100644 index 0000000..b8121d2 --- /dev/null +++ b/src/core/components/FilterDrawer/FilterController.jsx @@ -0,0 +1,25 @@ +import { Controller } from "react-hook-form"; +import FilterField from "./FilterField"; + +const FilterController = ({ item, control, reset, errors }) => { + return ( + { + return ( + reset()} + errors={errors} + /> + ); + }} + /> + ); +}; +export default FilterController; diff --git a/src/core/components/FilterDrawer/FilterControllerWithDependency.jsx b/src/core/components/FilterDrawer/FilterControllerWithDependency.jsx new file mode 100644 index 0000000..699d592 --- /dev/null +++ b/src/core/components/FilterDrawer/FilterControllerWithDependency.jsx @@ -0,0 +1,27 @@ +import { Controller, useWatch } from "react-hook-form"; +import FilterField from "./FilterField"; + +const FilterControllerWithDependency = ({ item, control, reset, errors }) => { + const dependencyField = useWatch({ control, name: item.dependencyId }); + + return ( + { + return ( + reset()} + errors={errors} + /> + ); + }} + /> + ); +}; +export default FilterControllerWithDependency; diff --git a/src/core/components/FilterDrawer/FilterField.jsx b/src/core/components/FilterDrawer/FilterField.jsx new file mode 100644 index 0000000..7210315 --- /dev/null +++ b/src/core/components/FilterDrawer/FilterField.jsx @@ -0,0 +1,105 @@ +import { useState } from "react"; +import CustomTextFieldRange from "./fieldsType/CustomTextFieldRange"; +import CustomSelect from "./fieldsType/CustomSelect"; +import CustomDatePicker from "../CustomDatePicker"; +import CustomDatePickerRange from "./fieldsType/CustomDate/CustomDatePickerRange"; +import CustomSelectMultiple from "./fieldsType/CustomSelectMultiple"; +import CustomTextField from "./fieldsType/CustomTextField"; +import FilterOptionList from "./FilterOptionList"; + +const filterModeOptionFa = { + equals: "برابر", + notEquals: "نابرابر", + contains: "شامل", + lessThan: "کوچکتر", + greaterThan: "بزرگتر", + fuzzy: "فازی", + between: "مابین", +}; + +function FilterField({ + item, + filterParameters, + handleChange, + handleBlur, + dependencyFieldValue, + setFieldValue, + resetForm, + errors, +}) { + const [anchorEl, setAnchorEl] = useState(null); + const defaultFilterTranslation = + filterModeOptionFa[filterParameters.filterMode] || + filterParameters.filterMode; + + const handleOpenFilterBox = (event) => { + setAnchorEl(event.currentTarget); + }; + + const renderField = () => { + const commonProps = { + item, + filterParameters, + defaultFilterTranslation, + handleOpenFilterBox, + dependencyFieldValue, + setFieldValue, + handleChange, + handleBlur, + errors, + }; + + switch (filterParameters.datatype) { + case "numeric": + if (filterParameters.filterMode === "between") { + return ; + } + if (filterParameters.filterMode === "equals") { + return item.SelectComponent ? ( + + ) : ( + + ); + } + break; + case "date": + if (filterParameters.filterMode === "equals") { + return ; + } + if (filterParameters.filterMode === "between") { + return ; + } + break; + + case "array": + if (filterParameters.filterMode === "equals") { + return ; + } + break; + + default: + return ; + } + }; + + return ( + <> + {renderField()} + {Array.isArray(item.filterModeOptions) && ( + + )} + + ); +} + +export default FilterField; diff --git a/src/core/components/FilterDrawer/FilterOptionList.jsx b/src/core/components/FilterDrawer/FilterOptionList.jsx new file mode 100644 index 0000000..19d4d0b --- /dev/null +++ b/src/core/components/FilterDrawer/FilterOptionList.jsx @@ -0,0 +1,58 @@ +"use client"; + +import { ListItem, Menu } from "@mui/material"; +import { useState } from "react"; + +function FilterOptionList({ + filterType, + filterOption, + filterParameters, + anchorEl, + filterModeOptionFa, + setAnchorEl, + handleChange, +}) { + const [selectedFilter, setSelectedFilter] = useState(filterType); + + const handleChangeItem = (event, index) => { + handleChange({ + ...filterParameters, + value: filterOption[index] === "between" ? ["", ""] : "", + filterMode: filterOption[index], + }); + setSelectedFilter(filterOption[index]); + setAnchorEl(null); + }; + + const handleCloseFilterBox = () => { + setAnchorEl(null); + }; + + return ( + + {filterOption.map((option, index) => ( + handleChangeItem(event, index)} + selected={option === selectedFilter} + sx={{ + cursor: "pointer", + width: "100px", + display: "flex", + justifyContent: "center", + }} + > + {filterModeOptionFa[option]} + + ))} + + ); +} + +export default FilterOptionList; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePicker.jsx b/src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePicker.jsx new file mode 100644 index 0000000..85e2b08 --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePicker.jsx @@ -0,0 +1,31 @@ +import React from "react"; +import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker"; +import { Typography } from "@mui/material"; + +function CustomDatePicker({ + column, + filterParameters, + defaultFilterTranslation, + handleChange, +}) { + return ( + { + handleChange({ ...filterParameters, value: formattedDate }); + }} + placeholder={column.header} + helperText={ + theme.palette.primary.main }} + > + نوع فیلتر: {defaultFilterTranslation} (تاریخ) + + } + /> + ); +} + +export default CustomDatePicker; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePickerRange.jsx b/src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePickerRange.jsx new file mode 100644 index 0000000..e10be45 --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomDate/CustomDatePickerRange.jsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import { Box, Typography } from "@mui/material"; +import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker"; + +function CustomDatePickerRange({ + item, + filterParameters, + defaultFilterTranslation, + handleChange, + errors, +}) { + return ( + + { + handleChange({ + ...filterParameters, + value: [formattedDate, filterParameters.value[1]], + }); + }} + maxDate={filterParameters.value[1]} + placeholder={`از تاریخ`} + helperText={ + theme.palette.primary.main }} + > + نوع فیلتر: {defaultFilterTranslation} (تاریخ) + + } + /> + { + handleChange({ + ...filterParameters, + value: [filterParameters.value[0], formattedDate], + }); + }} + minDate={filterParameters.value[0]} + placeholder={`تا تاریخ`} + helperText={ + errors?.[`${item.id}`]?.value + ? errors?.[`${item.id}`]?.value.message + : null + } + error={Boolean(errors?.[`${item.id}`]?.value)} + /> + + ); +} + +export default CustomDatePickerRange; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomDate/MuiDatePicker.jsx b/src/core/components/FilterDrawer/fieldsType/CustomDate/MuiDatePicker.jsx new file mode 100644 index 0000000..f98e929 --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomDate/MuiDatePicker.jsx @@ -0,0 +1,86 @@ +import React from "react"; +import { LocalizationProvider, MobileDatePicker } from "@mui/x-date-pickers"; +import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali"; +import { faIR } from "@mui/x-date-pickers/locales"; +import { Box, FormHelperText, IconButton, InputAdornment } from "@mui/material"; +import ClearIcon from "@mui/icons-material/Clear"; +import moment from "jalali-moment"; + +function MuiDatePicker({ + label, + value, + setFieldValue, + name, + minDate, + maxDate, + helperText, + placeholder, + error, +}) { + return ( + + + { + const date = new Date(value); + const formattedDate = moment(date) + .locale("en") + .format("YYYY-MM-DD"); + setFieldValue(name, formattedDate); + }} + minDate={minDate ? new Date(minDate) : null} + maxDate={maxDate ? new Date(maxDate) : null} + slotProps={{ + textField: { + size: "small", + error: error, + placeholder: placeholder, + InputProps: { + endAdornment: ( + + { + event.stopPropagation(); + setFieldValue(name, ""); + }} + sx={{ + color: "#bfbfbf", + "&:hover": { + backgroundColor: "rgba(189, 189, 189, 0.1)", + color: "#363434", + }, + }} + > + + + + ), + }, + InputLabelProps: { + shrink: true, + }, + }, + }} + /> + {/* + {helperText ? helperText : ""} + */} + + + ); +} + +export default MuiDatePicker; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomSelect.jsx b/src/core/components/FilterDrawer/fieldsType/CustomSelect.jsx new file mode 100644 index 0000000..24e6b37 --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomSelect.jsx @@ -0,0 +1,38 @@ +"use client"; + +import { + FormControl, + InputLabel, + MenuItem, + OutlinedInput, + Select, +} from "@mui/material"; +function CustomSelect({ item, filterParameters, handleChange }) { + return ( + + + {item.header} + + + + ); +} + +export default CustomSelect; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomSelectByDependency.jsx b/src/core/components/FilterDrawer/fieldsType/CustomSelectByDependency.jsx new file mode 100644 index 0000000..6db3d11 --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomSelectByDependency.jsx @@ -0,0 +1,44 @@ +"use client"; + +import { + FormControl, + InputLabel, + MenuItem, + OutlinedInput, + Select, +} from "@mui/material"; +function CustomSelectByDependency({ + item, + filterParameters, + value, + handleChange, + selectOption, +}) { + return ( + + + {item.header} + + + + ); +} + +export default CustomSelectByDependency; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomSelectMultiple.jsx b/src/core/components/FilterDrawer/fieldsType/CustomSelectMultiple.jsx new file mode 100644 index 0000000..4602f8c --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomSelectMultiple.jsx @@ -0,0 +1,57 @@ +"use client"; + +import { + Box, + Chip, + FormControl, + FormHelperText, + InputLabel, + MenuItem, + OutlinedInput, + Select, + Typography, +} from "@mui/material"; + +function CustomSelectMultiple({ item, filterParameters, handleChange }) { + const selectOption = item.selectOption; + + const getLabelForValue = (value) => { + const option = selectOption.find((opt) => opt.value === value); + return option ? option.label : value; + }; + + return ( + + + {item.header} + + + + ); +} + +export default CustomSelectMultiple; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomTextField.jsx b/src/core/components/FilterDrawer/fieldsType/CustomTextField.jsx new file mode 100644 index 0000000..e0e361d --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomTextField.jsx @@ -0,0 +1,41 @@ +import { InputAdornment, TextField, Typography } from "@mui/material"; +import FilterListIcon from "@mui/icons-material/FilterList"; + +function CustomTextField({ + item, + filterParameters, + handleOpenFilterBox, + handleBlur, + handleChange, +}) { + return ( + + handleChange({ ...filterParameters, value: e.target.value }) + } + onBlur={handleBlur} + fullWidth + variant="outlined" + size="small" + sx={{ my: 1 }} + InputProps={{ + endAdornment: ( + + + + ), + }} + InputLabelProps={{ shrink: true }} + /> + ); +} + +export default CustomTextField; diff --git a/src/core/components/FilterDrawer/fieldsType/CustomTextFieldRange.jsx b/src/core/components/FilterDrawer/fieldsType/CustomTextFieldRange.jsx new file mode 100644 index 0000000..84fd028 --- /dev/null +++ b/src/core/components/FilterDrawer/fieldsType/CustomTextFieldRange.jsx @@ -0,0 +1,85 @@ +import { Box, InputAdornment, TextField, Typography } from "@mui/material"; +import FilterListIcon from "@mui/icons-material/FilterList"; + +function CustomTextFieldRange({ + item, + defaultFilterTranslation, + handleOpenFilterBox, + handleChange, + filterParameters, + handleBlur, + errors, +}) { + return ( + + + handleChange({ + ...filterParameters, + value: [e.target.value, filterParameters.value[1]], + }) + } + onBlur={handleBlur} + label={از {item.header}} + value={filterParameters.value[0]} + fullWidth + error={ + touched?.[`${item.id}`]?.value && + Boolean(errors?.[`${item.id}`]?.value) + } + helperText={ + theme.palette.primary.main, + fontWeight: "bold", + }} + > + نوع فیلتر: {defaultFilterTranslation} + + } + variant="outlined" + size="small" + sx={{ my: 1, marginRight: 1 }} + /> + + handleChange({ + ...filterParameters, + value: [filterParameters.value[0], e.target.value], + }) + } + onBlur={handleBlur} + error={Boolean(errors?.[`${item.id}`]?.value)} + helperText={ + errors?.[`${item.id}`]?.value + ? errors?.[`${item.id}`]?.value.message + : null + } + label={تا {item.header}} + value={filterParameters.value[1]} + fullWidth + variant="outlined" + size="small" + sx={{ my: 1 }} + InputProps={{ + endAdornment: ( + + + + ), + }} + /> + + ); +} + +export default CustomTextFieldRange; diff --git a/src/core/components/FilterDrawer/index.jsx b/src/core/components/FilterDrawer/index.jsx new file mode 100644 index 0000000..318f048 --- /dev/null +++ b/src/core/components/FilterDrawer/index.jsx @@ -0,0 +1,160 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import { Cancel, FilterAlt } from "@mui/icons-material"; +import { Box, Button, IconButton, Typography } from "@mui/material"; +import { useForm } from "react-hook-form"; +import * as Yup from "yup"; +import ScrollBox from "../ScrollBox"; +import FilterController from "./FilterController"; +import FilterControllerWithDependency from "./FilterControllerWithDependency"; + +const headerSx = { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + px: 2, + py: 1, + backgroundColor: (theme) => theme.palette.primary.main, + boxShadow: + "rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px", + maxWidth: "450px", +}; + +const headerTitleSx = { display: "flex", alignItems: "center" }; +const headerIconSx = { color: "#fff", mr: 1 }; +const iconButtonSx = { color: "#fff" }; + +const formContainerSx = { px: 2, py: 3 }; +const footerSx = { + display: "flex", + justifyContent: "center", + alignItems: "center", + pb: 2, +}; + +const submitButtonSx = { + px: 8, + boxShadow: + "rgba(0, 0, 0, 0.23) 0px 6px 6px, rgba(0, 0, 0, 0.19) 10px 0px 20px", + backgroundColor: "primary2", + ":hover": { backgroundColor: "primary2" }, +}; + +const validationSchema = Yup.object({ + activity_date_time: Yup.array() + .of(Yup.string().nullable()) + .test({ + test(value, ctx) { + const [start, end] = value || ["", ""]; + if ((start && !end) || (!start && end)) { + return ctx.createError({ message: "این بخش را تکمیل نمایید" }); + } + return true; + }, + }), +}); + +const FilterDrawer = ({ defaultValues, setFilterData, closeDrawer }) => { + const { + control, + errors, + reset, + handleSubmit, + formState: { isDirty }, + } = useForm({ + defaultValues, + resolver: yupResolver( + Yup.object( + Object.keys(defaultValues).reduce((acc, key) => { + const initialValue = defaultValues[key]; + if (initialValue.filterMode === "between") { + acc[key] = Yup.object().shape({ + value: Yup.array() + .of(Yup.string().nullable()) + .test({ + test(value, ctx) { + const [start, end] = value || ["", ""]; + if ( + initialValue.datatype === "numeric" && + parseInt(end, 10) <= parseInt(start, 10) + ) { + return ctx.createError({ + message: `مقدار وارده باید بیشتر از (${start}) باشد`, + }); + } else if ((start && !end) || (!start && end)) { + return ctx.createError({ + message: "این بخش را تکمیل نمایید", + }); + } + return true; + }, + }), + }); + } + return acc; + }, {}), + ), + ), + mode: "all", + }); + + const onSubmit = (data) => { + setFilterData(data); + closeDrawer(); + }; + + return ( + <> + + + + + فیلتر + + + + + + + +
+ + {Object.values(defaultValues).map( + (item) => + item.enable && + (item.dependencyId ? ( + + ) : ( + + )), + )} + + + + + +
+
+ + ); +}; +export default FilterDrawer; diff --git a/src/core/components/NotificationDesign/AskForKeepData.jsx b/src/core/components/NotificationDesign/AskForKeepData.jsx new file mode 100644 index 0000000..3fe7f2e --- /dev/null +++ b/src/core/components/NotificationDesign/AskForKeepData.jsx @@ -0,0 +1,94 @@ +"use client"; + +import SaveIcon from "@mui/icons-material/Save"; +import { Box, Button, Typography } from "@mui/material"; +import DownloadIcon from "@mui/icons-material/Download"; +import { toast } from "react-toastify"; +import useTableSetting from "@/lib/hooks/useTableSetting"; +import { flattenObjectOfObjects } from "@/core/utils/flattenObjectOfObjects"; + +function AskForKeepData({ + filterData, + sortData, + hideData, + user_id, + page_name, + table_name, + columns, +}) { + const { filterAction, sortAction, hideAction } = useTableSetting(); + + const filteredHideData = Object.fromEntries( + Object.entries(hideData).filter(([key, value]) => value === false), + ); + + const flattenHideData = flattenObjectOfObjects(filteredHideData); + + const onSaveFilter = () => { + const filteredItems = Object.keys(filterData) + .map((key) => { + const value = filterData[key].value; + if ( + value !== "" && + !( + Array.isArray(value) && + (value.length === 0 || + (value.length === 2 && value[0] === "" && value[1] === "")) + ) + ) { + return filterData[key]; + } + }) + .filter(Boolean); + filterAction(user_id, page_name, table_name, filteredItems, columns); + sortAction(user_id, page_name, table_name, sortData, columns); + hideAction(user_id, page_name, table_name, flattenHideData, columns); + toast.dismiss({ containerId: "datatable" }); + }; + + const handleDismiss = () => { + toast.dismiss({ containerId: "datatable" }); + }; + + return ( + + + + ذخیره سازی تغییرات + + + + + + آیا مایل به ذخیره تغییر های اعمال شده برای دفعات بعد هستید؟ + + + + + + + + ); +} + +export default AskForKeepData; diff --git a/src/core/components/PageTitle.jsx b/src/core/components/PageTitle.jsx new file mode 100644 index 0000000..675b6b8 --- /dev/null +++ b/src/core/components/PageTitle.jsx @@ -0,0 +1,18 @@ +"use client"; + +import { Chip, Divider, Typography } from "@mui/material"; + +const PageTitle = ({ title }) => { + return ( + + + {title} + + } + /> + + ); +}; +export default PageTitle; diff --git a/src/core/components/Profile/ChangePassword/Form.jsx b/src/core/components/Profile/ChangePassword/Form.jsx index 01b7503..19dec1a 100644 --- a/src/core/components/Profile/ChangePassword/Form.jsx +++ b/src/core/components/Profile/ChangePassword/Form.jsx @@ -35,17 +35,17 @@ const validationSchema = object({ ), }); +const defaultValues = { + current_password: "", + new_password: "", + repeat_new_password: "", +}; + const Form = ({ handleCloseForm, setLoading }) => { const request = useRequest(); const [showNewPassword, setShowNewPassword] = useState(); const [showRepeatNewPassword, setShowRepeatNewPassword] = useState(); - const defaultValues = { - current_password: "", - new_password: "", - repeat_new_password: "", - }; - const { register, handleSubmit, diff --git a/src/core/components/ScrollBox.jsx b/src/core/components/ScrollBox.jsx new file mode 100644 index 0000000..c4a7391 --- /dev/null +++ b/src/core/components/ScrollBox.jsx @@ -0,0 +1,20 @@ +import { Box, styled } from "@mui/material"; + +const ScrollBox = styled(Box)(({ theme }) => ({ + flexGrow: 1, + overflowY: "auto", + maxWidth: "450px", + "::-webkit-scrollbar": { + width: "5px", + }, + "::-webkit-scrollbar-track": { + boxShadow: "inset 0 0 5px #fff", + borderRadius: "5px", + }, + "::-webkit-scrollbar-thumb": { + background: theme.palette.primary.dark, + borderRadius: "0px", + }, +})); + +export default ScrollBox; diff --git a/src/core/utils/DataTableFilterDataStructure.js b/src/core/utils/DataTableFilterDataStructure.js new file mode 100644 index 0000000..e8a5ddb --- /dev/null +++ b/src/core/utils/DataTableFilterDataStructure.js @@ -0,0 +1,10 @@ +export default function DataTableFilterDataStructure(filterData, isValueEmpty) { + return Object.values(filterData) + .filter((filter) => !isValueEmpty(filter.value)) + .map(({ filterMode, id, datatype, value }) => ({ + id: id.replace(/__/g, "."), + fn: filterMode, + datatype, + value, + })); +} diff --git a/src/core/utils/flattenArrayOfObjects.js b/src/core/utils/flattenArrayOfObjects.js new file mode 100644 index 0000000..d503e5b --- /dev/null +++ b/src/core/utils/flattenArrayOfObjects.js @@ -0,0 +1,9 @@ +export const flattenArrayOfObjects = (array, key) => { + return array.reduce((acc, obj) => { + acc.push(obj); + if (Array.isArray(obj[key])) { + acc.push(...flattenArrayOfObjects(obj[key], key)); + } + return acc; + }, []); +}; diff --git a/src/core/utils/flattenObjectOfObjects.js b/src/core/utils/flattenObjectOfObjects.js new file mode 100644 index 0000000..50458dd --- /dev/null +++ b/src/core/utils/flattenObjectOfObjects.js @@ -0,0 +1,12 @@ +export const flattenObjectOfObjects = (obj, res = {}) => { + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + if (typeof obj[key] === "object" && obj[key] !== null) { + flattenObjectOfObjects(obj[key], res); + } else { + res[key] = obj[key]; + } + } + } + return res; +}; diff --git a/src/core/utils/isArrayEmpty.js b/src/core/utils/isArrayEmpty.js new file mode 100644 index 0000000..61f7c8d --- /dev/null +++ b/src/core/utils/isArrayEmpty.js @@ -0,0 +1,6 @@ +export default function isArrayEmpty(value) { + if (Array.isArray(value)) { + return value.length === 0 || value.every((v) => v === ""); + } + return value === "" || value === null || value === undefined; +} diff --git a/src/core/utils/nationalCodeValidation.js b/src/core/utils/nationalCodeValidation.js new file mode 100644 index 0000000..2afec8c --- /dev/null +++ b/src/core/utils/nationalCodeValidation.js @@ -0,0 +1,30 @@ +function validateNationalCode(code) { + const invalidCodes = [ + "1111111111", + "2222222222", + "3333333333", + "4444444444", + "5555555555", + "6666666666", + "7777777777", + "8888888888", + "9999999999", + ]; + if (invalidCodes.includes(code)) return false; + + const L = code.length; + if (L < 8 || parseInt(code, 10) === 0) return false; + + code = code.padStart(10, "0"); + if (parseInt(code.substr(3, 6), 10) === 0) return false; + + const c = parseInt(code.charAt(9), 10); + let s = 0; + for (let i = 0; i < 9; i++) { + s += parseInt(code.charAt(i), 10) * (10 - i); + } + s = s % 11; + return (s < 2 && c === s) || (s >= 2 && c === 11 - s); +} + +export default validateNationalCode; diff --git a/src/core/utils/pageMenu.js b/src/core/utils/pageMenu.js index 9f08204..8a4b62e 100644 --- a/src/core/utils/pageMenu.js +++ b/src/core/utils/pageMenu.js @@ -1,3 +1,4 @@ +import { Settings } from "@mui/icons-material"; import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard"; export const pageMenu = [ @@ -9,4 +10,20 @@ export const pageMenu = [ icon: , permissions: ["all"], }, + { + id: "systemManagment", + label: "مدیریت سامانه", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "systemManagment-users", + label: "کاربران", + type: "page", + route: "/dashboard/users", + permissions: ["manage_users"], + }, + ], + }, ]; diff --git a/src/core/utils/routes.js b/src/core/utils/routes.js index 43a10e5..b803eff 100644 --- a/src/core/utils/routes.js +++ b/src/core/utils/routes.js @@ -6,8 +6,14 @@ export const GET_USER_LOGIN_ROUTE = api + "/auth/login"; export const GET_USER_LOGOUT_ROUTE = api + "/auth/logout"; export const GET_PERMISSIONS_ROUTE = api + "/profile/permissions"; export const GET_SIDEBAR_BADGE_ROUTE = ""; +export const GET_PROVINCE_LISTS = api + "/provinces/list"; +export const GET_ROLE_LISTS = api + "/roles/list"; export const GET_CATEGORY = api + "/categories/list"; - export const GET_CALLER_HISTORY = api + "/calls/list"; export const CALL_ACTION = api + "/calls"; + +export const GET_USER_LIST = api + "/users"; +export const CREATE_USER = api + "/users"; +export const DELETE_USER = api + "/users"; +export const UPDATE_USER = api + "/users"; diff --git a/src/core/utils/theme.js b/src/core/utils/theme.js index 49bc95d..9ec62c4 100644 --- a/src/core/utils/theme.js +++ b/src/core/utils/theme.js @@ -11,7 +11,6 @@ const theme = createTheme({ primary: { main: "#1b4332", contrastText: "#FFFFFF", - }, secondary: { main: "#1B4043", diff --git a/src/core/utils/utils.js b/src/core/utils/utils.js new file mode 100644 index 0000000..fb9a24a --- /dev/null +++ b/src/core/utils/utils.js @@ -0,0 +1,149 @@ +import { alpha, darken } from "@mui/material"; + +export const parseCSSVarId = (id) => id.replace(/[^a-zA-Z0-9]/g, "_"); + +export const parseFromValuesOrFunc = (fn, arg) => + fn instanceof Function ? fn(arg) : fn; + +export const getCommonToolbarStyles = ({ table }) => ({ + alignItems: "flex-start", + backgroundColor: table.options.mrtTheme.baseBackgroundColor, + display: "grid", + flexWrap: "wrap-reverse", + minHeight: "3.5rem", + overflow: "hidden", + position: "relative", + transition: "all 150ms ease-in-out", + zIndex: 1, +}); + +export const getCommonTooltipProps = (placement) => ({ + disableInteractive: true, + enterDelay: 500, + enterNextDelay: 500, + placement, +}); + +export const flipIconStyles = (theme) => + theme.direction === "rtl" + ? { style: { transform: "scaleX(-1)" } } + : undefined; + +export const getCommonPinnedCellStyles = ({ column, table, theme }) => { + const { baseBackgroundColor } = table.options.mrtTheme; + const isPinned = column?.getIsPinned(); + + return { + '&[data-pinned="true"]': { + "&:before": { + backgroundColor: alpha( + darken( + baseBackgroundColor, + theme.palette.mode === "dark" ? 0.05 : 0.01, + ), + 0.97, + ), + boxShadow: column + ? isPinned === "left" && column.getIsLastColumn(isPinned) + ? `-4px 0 4px -4px ${alpha(theme.palette.grey[700], 0.5)} inset` + : isPinned === "right" && column.getIsFirstColumn(isPinned) + ? `4px 0 4px -4px ${alpha(theme.palette.grey[700], 0.5)} inset` + : undefined + : undefined, + ...commonCellBeforeAfterStyles, + }, + }, + }; +}; + +export const getCommonMRTCellStyles = ({ + column, + header, + table, + tableCellProps, + theme, +}) => { + const { + getState, + options: { enableColumnVirtualization, layoutMode }, + } = table; + const { draggingColumn } = getState(); + const { columnDef } = column; + const { columnDefType } = columnDef; + + const isColumnPinned = + columnDef.columnDefType !== "group" && column.getIsPinned(); + + const widthStyles = { + minWidth: `max(calc(var(--${header ? "header" : "col"}-${parseCSSVarId( + header?.id ?? column.id, + )}-size) * 1px), ${columnDef.minSize ?? 30}px)`, + width: `calc(var(--${header ? "header" : "col"}-${parseCSSVarId(header?.id ?? column.id)}-size) * 1px)`, + }; + + if (layoutMode === "grid") { + widthStyles.flex = `${ + [0, false].includes(columnDef.grow) + ? 0 + : `var(--${header ? "header" : "col"}-${parseCSSVarId(header?.id ?? column.id)}-size)` + } 0 auto`; + } else if (layoutMode === "grid-no-grow") { + widthStyles.flex = `${+(columnDef.grow || 0)} 0 auto`; + } + + const pinnedStyles = isColumnPinned + ? { + ...getCommonPinnedCellStyles({ column, table, theme }), + left: + isColumnPinned === "left" + ? `${column.getStart("left")}px` + : undefined, + opacity: 0.97, + position: "sticky", + right: + isColumnPinned === "right" + ? `${column.getAfter("right")}px` + : undefined, + } + : {}; + + return { + backgroundColor: "inherit", + backgroundImage: "inherit", + display: layoutMode?.startsWith("grid") ? "flex" : undefined, + justifyContent: + columnDefType === "group" + ? "center" + : layoutMode?.startsWith("grid") + ? tableCellProps.align + : undefined, + opacity: + table.getState().draggingColumn?.id === column.id || + table.getState().hoveredColumn?.id === column.id + ? 0.5 + : 1, + position: "relative", + transition: enableColumnVirtualization + ? "none" + : `padding 150ms ease-in-out`, + zIndex: + column.getIsResizing() || draggingColumn?.id === column.id + ? 2 + : columnDefType !== "group" && isColumnPinned + ? 1 + : 0, + ...pinnedStyles, + ...widthStyles, + ...parseFromValuesOrFunc(tableCellProps?.sx, theme), + }; +}; + +export const commonCellBeforeAfterStyles = { + content: '""', + height: "100%", + left: 0, + position: "absolute", + top: 0, + width: "100%", + zIndex: -1, +}; diff --git a/src/lib/contexts/DataTable.js b/src/lib/contexts/DataTable.js new file mode 100644 index 0000000..28899d6 --- /dev/null +++ b/src/lib/contexts/DataTable.js @@ -0,0 +1,211 @@ +import { createContext, useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import useTableSetting from "@/lib/hooks/useTableSetting"; +import { flattenArrayOfObjects } from "@/core/utils/flattenArrayOfObjects"; +import AskForKeepData from "@/core/components/NotificationDesign/AskForKeepData"; +import { LinearProgress } from "@mui/material"; + +export const DataTableContext = createContext(); + +const DataTableProvider = ({ + children, + user_id, + page_name, + table_name, + columns, + initialSort = [], +}) => { + const { settingStore } = useTableSetting(); + const [isInitStates, setInitStates] = useState(false); + const flatColumns = flattenArrayOfObjects(columns, "columns"); + const [initFilter, setInitFilter] = useState({}); + const [filterData, setFilterData] = useState({}); + const [initSort, setInitSort] = useState(initialSort || []); + const [sortData, setSortData] = useState(initialSort || []); + const [initHide, setInitHide] = useState({}); + const [hideData, setHideData] = useState({}); + const [isDirty, setIsDirty] = useState(false); + const [isAnyDirty, setIsAnyDirty] = useState(false); + + useEffect(() => { + let hasAnyConflict = false; + const hasConflict = + JSON.stringify(hideData) !== JSON.stringify(initHide) || + JSON.stringify(sortData) !== JSON.stringify(initSort) || + JSON.stringify(filterData) !== JSON.stringify(initFilter); + + const CheckFilterValues = Object.values(filterData).every((innerObject) => { + if ( + innerObject.datatype === "date" && + innerObject.filterMode === "between" + ) { + return ( + Array.isArray(innerObject.value) && + innerObject.value.length === 2 && + innerObject.value.every((v) => v === "") + ); + } else { + return innerObject.value === ""; + } + }); + const flattenObject = (obj) => { + let result = {}; + for (const key in obj) { + if (typeof obj[key] === "object" && obj[key] !== null) { + Object.assign(result, flattenObject(obj[key])); // باز کردن اشیای تودرتو + } else { + result[key] = obj[key]; + } + } + return result; + }; + + // تبدیل آبجکت به آبجکت فلت شده + const flatHideData = flattenObject(hideData); + + const CheckHideValues = Object.values(flatHideData).every( + (value) => value === true, + ); + hasAnyConflict = + !CheckHideValues || + JSON.stringify(sortData) !== JSON.stringify(initialSort) || + !CheckFilterValues; + + setIsDirty(hasConflict); + setIsAnyDirty(hasAnyConflict); + }, [hideData, sortData, filterData, initHide, initSort, initFilter]); + + useEffect(() => { + if (!settingStore) return; + const filterValues = flatColumns.reduce((acc, column) => { + if (!column.enableColumnFilter) return acc; + const storeFilter = settingStore?.[user_id]?.[page_name]?.[table_name]?.[ + "filters" + ]?.find((filter) => filter.id === column.id); + if (column.datatype === "array") { + acc[column.id] = { + id: column.id, + value: storeFilter?.value || [], + filterMode: storeFilter?.filterMode || column.filterMode, + datatype: column.datatype, + }; + } else if (column.filterMode === "between") { + acc[column.id] = { + id: column.id, + value: storeFilter?.value || ["", ""], + filterMode: storeFilter?.filterMode || column.filterMode, + datatype: column.datatype, + }; + } else { + acc[column.id] = { + id: column.id, + value: storeFilter?.value || "", + filterMode: storeFilter?.filterMode || column.filterMode, + datatype: column.datatype, + }; + } + return acc; + }, {}); + + const getHideValues = (columns) => { + return columns.reduce((acc, column) => { + const columnId = column.id; + const storeHide = + settingStore?.[user_id]?.[page_name]?.[table_name]?.["hides"]?.[ + columnId + ]; + if (column.columns && column.columns.length > 0) { + acc[columnId] = getHideValues(column.columns); + } else { + acc[columnId] = storeHide !== undefined ? storeHide : true; + } + + return acc; + }, {}); + }; + + const hideValues = getHideValues(columns); + setHideData(hideValues); + setInitHide(hideValues); + setFilterData(filterValues); + setInitFilter(filterValues); + setSortData( + settingStore?.[user_id]?.[page_name]?.[table_name]?.["sorts"] || + initialSort, + ); + setInitSort( + settingStore?.[user_id]?.[page_name]?.[table_name]?.["sorts"] || + initialSort, + ); + setInitStates(true); + }, [settingStore]); + + useEffect(() => { + if ( + JSON.stringify(initFilter) !== JSON.stringify(filterData) || + JSON.stringify(initSort) !== JSON.stringify(sortData) || + JSON.stringify(initHide) !== JSON.stringify(hideData) + ) { + if (!toast.isActive("keep_data", "filtering")) { + toast( + , + { + containerId: "filtering", + toastId: "keep_data", + className: "filter-toast", + position: "bottom-left", + draggable: true, + autoClose: false, + }, + ); + } else { + toast.update("keep_data", { + containerId: "filtering", + toastId: "keep_data", + render: ( + + ), + }); + } + } else { + toast.dismiss({ containerId: "datatable" }); + } + }, [filterData, sortData, hideData]); + + if (!isInitStates) return ; + + return ( + + {children} + + ); +}; + +export default DataTableProvider; diff --git a/src/lib/contexts/socket.js b/src/lib/contexts/socket.js index dc043bf..a103a3a 100644 --- a/src/lib/contexts/socket.js +++ b/src/lib/contexts/socket.js @@ -24,6 +24,7 @@ export const SocketProvider = ({ children }) => { const { user } = useAuth(); const socketToastId = useRef(null); const [status, setStatus] = useState(""); + const [clientsOnline, setClientsOnline] = useState([]); const socket = useMemo(() => { return io(`${process.env.NEXT_PUBLIC_SERVER_SOCKET_URL}`, { @@ -35,7 +36,7 @@ export const SocketProvider = ({ children }) => { useEffect(() => { if (user?.telephone_id) { - socket.auth.token = user.telephone_id; + socket.auth.token = { telephone_id: user.telephone_id, user_id: user.id }; } }, [user, socket]); @@ -102,7 +103,21 @@ export const SocketProvider = ({ children }) => { }; }, [socket]); - const contextValue = useMemo(() => ({ socket, status }), [socket, status]); + useEffect(() => { + const handleNotification = (data) => { + setClientsOnline(data.clients); + }; + socket.on("onlineClientsUpdated", handleNotification); + + return () => { + socket.off("onlineClientsUpdated", handleNotification); + }; + }, [socket]); + + const contextValue = useMemo( + () => ({ socket, status, clientsOnline }), + [socket, status, clientsOnline], + ); return ( diff --git a/src/lib/hooks/useDataTable.js b/src/lib/hooks/useDataTable.js new file mode 100644 index 0000000..f0cdc03 --- /dev/null +++ b/src/lib/hooks/useDataTable.js @@ -0,0 +1,27 @@ +import { useContext } from "react"; +import { DataTableContext } from "@/lib/contexts/DataTable"; + +const useTableSetting = () => { + const { + filterData, + setFilterData, + sortData, + setSortData, + hideData, + setHideData, + isDirty, + isAnyDirty, + } = useContext(DataTableContext); + return { + filterData, + setFilterData, + sortData, + setSortData, + hideData, + setHideData, + isDirty, + isAnyDirty, + }; +}; + +export default useTableSetting; diff --git a/src/lib/hooks/useProvince.js b/src/lib/hooks/useProvince.js new file mode 100644 index 0000000..d73cfdc --- /dev/null +++ b/src/lib/hooks/useProvince.js @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { GET_PROVINCE_LISTS } from "@/core/utils/routes"; +import useRequest from "@/lib/hooks/useRequest"; + +const useProvinces = () => { + const requestServer = useRequest(); + const [provinces, setProvinces] = useState([]); + const [loadingProvinces, setLoadingProvinces] = useState(true); + const [errorProvinces, setErrorProvinces] = useState(null); + + useEffect(() => { + const fetchProvinces = async () => { + try { + const response = await requestServer(`${GET_PROVINCE_LISTS}`); + setProvinces(response.data.data); + setLoadingProvinces(false); + } catch (e) { + setErrorProvinces(e); + setLoadingProvinces(false); + } + }; + + fetchProvinces(); + }, []); + + return { provinces, loadingProvinces, errorProvinces }; +}; + +export default useProvinces; diff --git a/src/lib/hooks/useRoles.js b/src/lib/hooks/useRoles.js new file mode 100644 index 0000000..822cbef --- /dev/null +++ b/src/lib/hooks/useRoles.js @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { GET_ROLE_LISTS } from "@/core/utils/routes"; +import useRequest from "@/lib/hooks/useRequest"; + +const useRoles = () => { + const requestServer = useRequest(); + const [roles, setRoles] = useState([]); + const [loadingRoles, setLoadingRoles] = useState(true); + const [errorRoles, setErrorRoles] = useState(null); + + useEffect(() => { + const fetchRoles = async () => { + try { + const response = await requestServer(`${GET_ROLE_LISTS}`); + setRoles(response.data.data); + setLoadingRoles(false); + } catch (e) { + setErrorRoles(e); + setLoadingRoles(false); + } + }; + + fetchRoles(); + }, []); + + return { roles, loadingRoles, errorRoles }; +}; + +export default useRoles; diff --git a/src/lib/hooks/useTableSetting.js b/src/lib/hooks/useTableSetting.js new file mode 100644 index 0000000..35860f0 --- /dev/null +++ b/src/lib/hooks/useTableSetting.js @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { TableSettingContext } from "../contexts/tableSetting"; + +const useTableSetting = () => { + const { settingStore, hideAction, sortAction, filterAction, resetAction } = + useContext(TableSettingContext); + return { settingStore, hideAction, sortAction, filterAction, resetAction }; +}; + +export default useTableSetting;