Feature/user management

This commit is contained in:
AmirHossein Mahmoodi
2025-05-10 05:31:24 +00:00
parent bc59952f7b
commit f07d27b273
101 changed files with 7136 additions and 14 deletions

View File

@@ -17,9 +17,14 @@
"@mui/icons-material": "^7.0.2", "@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2", "@mui/material": "^7.0.2",
"@mui/material-nextjs": "^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", "axios": "^1.9.0",
"date-fns-jalali": "4.1.0-0",
"jalali-moment": "^3.3.11", "jalali-moment": "^3.3.11",
"lz-string": "^1.5.0", "lz-string": "^1.5.0",
"material-react-table": "^3.2.1",
"moment": "^2.30.1",
"next": "15.3.1", "next": "15.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

215
pnpm-lock.yaml generated
View File

@@ -29,15 +29,30 @@ importers:
'@mui/material-nextjs': '@mui/material-nextjs':
specifier: ^7.0.2 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) 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: axios:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.9.0 version: 1.9.0
date-fns-jalali:
specifier: 4.1.0-0
version: 4.1.0-0
jalali-moment: jalali-moment:
specifier: ^3.3.11 specifier: ^3.3.11
version: 3.3.11 version: 3.3.11
lz-string: lz-string:
specifier: ^1.5.0 specifier: ^1.5.0
version: 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: next:
specifier: 15.3.1 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) 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': '@types/react':
optional: true 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': '@napi-rs/wasm-runtime@0.2.9':
resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==}
@@ -671,6 +745,30 @@ packages:
'@swc/helpers@0.5.15': '@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} 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': '@tybys/wasm-util@0.9.0':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
@@ -1091,6 +1189,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'} 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: debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies: peerDependencies:
@@ -1533,6 +1634,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} 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: hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
@@ -1786,6 +1891,18 @@ packages:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true 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: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2097,6 +2214,12 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'} 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: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2927,6 +3050,56 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.1.2 '@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': '@napi-rs/wasm-runtime@0.2.9':
dependencies: dependencies:
'@emnapi/core': 1.4.3 '@emnapi/core': 1.4.3
@@ -3055,6 +3228,26 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 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': '@tybys/wasm-util@0.9.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -3508,6 +3701,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
is-data-view: 1.0.2 is-data-view: 1.0.2
date-fns-jalali@4.1.0-0: {}
debug@3.2.7: debug@3.2.7:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -4110,6 +4305,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
highlight-words@2.0.0: {}
hoist-non-react-statics@3.3.2: hoist-non-react-statics@3.3.2:
dependencies: dependencies:
react-is: 16.13.1 react-is: 16.13.1
@@ -4375,6 +4572,20 @@ snapshots:
lz-string@1.5.0: {} 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: {} math-intrinsics@1.1.0: {}
media-typer@1.1.0: {} media-typer@1.1.0: {}
@@ -4679,6 +4890,10 @@ snapshots:
gopd: 1.2.0 gopd: 1.2.0
set-function-name: 2.0.2 set-function-name: 2.0.2
remove-accents@0.5.0: {}
reselect@5.1.1: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0: {}

View File

@@ -0,0 +1,11 @@
import UsersPage from "@/components/dashboard/Users";
export const metadata = {
title: "کاربران",
};
const Page = () => {
return <UsersPage></UsersPage>;
};
export default Page;

View File

@@ -9,7 +9,7 @@ import { Stack } from "@mui/material";
const Layout = ({ children }) => { const Layout = ({ children }) => {
const { data: permissions } = usePermissions(); const { data: permissions } = usePermissions();
const isCallWidgetEnabled = permissions?.some( const isCallWidgetEnabled = permissions?.some(
(permission) => permission === "manage_calls" (permission) => permission === "manage_calls",
); );
return ( return (

View File

@@ -13,7 +13,7 @@ const Template = ({ children }) => {
styles={{ styles={{
"*": { "*": {
scrollbarWidth: "thin", scrollbarWidth: "thin",
scrollbarColor: `#155175 transparent`, scrollbarColor: `${theme.palette.primary.dark} transparent`,
}, },
"*&::-webkit-scrollbar": { "*&::-webkit-scrollbar": {
width: "4px", width: "4px",
@@ -23,7 +23,7 @@ const Template = ({ children }) => {
borderRadius: "4px", borderRadius: "4px",
}, },
"*&::-webkit-scrollbar-thumb": { "*&::-webkit-scrollbar-thumb": {
background: "#155175", background: `${theme.palette.primary.dark}`,
borderRadius: "4px", borderRadius: "4px",
}, },
}} }}

View File

@@ -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 (
<CustomSelectByDependency
{...props}
value={
loadingProvinces ? "loading" : props.filterParameters.value
}
columnSelectOption={getColumnSelectOptions}
/>
);
},
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 }) => <TelephoneId user_id={row.original.id} />,
},
{
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 (
<CustomSelectByDependency
{...props}
value={loadingRoles ? "loading" : props.filterParameters.value}
columnSelectOption={getColumnSelectOptions}
/>
);
},
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 }) => <OnlineCell user_id={row.original.id} />,
},
{
accessorKey: "gender",
header: "جنسیت",
id: "gender",
enableColumnFilter: false,
grow: false,
size: 100,
Cell: ({ renderedCellValue }) => (
<>{renderedCellValue == "male" ? "آقا" : "خانم"}</>
),
},
],
[],
);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
sorting={[{ id: "id", desc: true }]}
table_url={GET_USER_LIST}
page_name={"usersPage"}
table_name={"usersList"}
TableToolbar={Toolbar}
enableRowActions
positionActionsColumn={"last"}
RowActions={RowActions}
/>
</Box>
</>
);
};
export default DataTable;

View File

@@ -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 (
<>
<DialogContent>
<Stack alignItems="center" spacing={2}>
<Typography mt={2}>آیا از حذف کاربر اطمینان دارید؟</Typography>
</Stack>
</DialogContent>
<DialogActions
sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }}
>
<Button
onClick={() => setOpenDeleteDialog(false)}
variant="outlined"
color="secondary"
>
خیر !
</Button>
<Button
onClick={handleClick}
variant="contained"
color="error"
disabled={submitting}
>
{submitting ? "درحال حذف..." : "بله اطمینان دارم"}
</Button>
</DialogActions>
</>
);
};
export default DeleteContent;

View File

@@ -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 (
<>
<Tooltip title="حذف">
<IconButton color="error" onClick={() => setOpenDeleteDialog(true)}>
<DeleteIcon />
</IconButton>
</Tooltip>
<Dialog
open={openDeleteDialog}
onClose={() => 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"}
>
<DialogTitle>حذف</DialogTitle>
<DeleteContent
rowId={rowId}
mutate={mutate}
setOpenDeleteDialog={setOpenDeleteDialog}
/>
</Dialog>
</>
);
};
export default Delete;

View File

@@ -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 (
<>
<DialogContent dividers>
<Divider sx={{ mb: 3 }}>
<Chip color="primary" variant="outlined" label="کاربر جدید" />
</Divider>
<StyledForm
id="UpdateUserForm"
onSubmit={handleSubmit(onSubmit)}
style={{ width: "100%", height: "100%" }}
>
<Stack spacing={2}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="نام کاربری"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"username"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<TextField
{...field}
label="نام و نام خانوادگی"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"full_name"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<TextField
{...field}
label="سمت"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"position"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="شماره همراه"
variant="outlined"
fullWidth
type="tel"
onChange={(e) => {
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"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error}>
<InputLabel id={`label-phone-number`} shrink>
جنسیت
</InputLabel>
<Select
labelId={`label-phone-number`}
id={"phone-number"}
name={`gender`}
value={field.value}
input={<OutlinedInput label={"جنسیت"} />}
onChange={(e) => field.onChange(e.target.value)}
displayEmpty
>
<MenuItem value={"male"}>مرد</MenuItem>
<MenuItem value={"female"}>زن</MenuItem>
</Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message}
</FormHelperText>
</FormControl>
);
}}
name={"gender"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="شماره ملی"
variant="outlined"
fullWidth
type="tel"
onChange={(e) => {
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"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error}>
<InputLabel id={`label-province`} shrink>
استان
</InputLabel>
<Select
labelId={`label-province`}
id={"province"}
name={`province_id`}
value={loadingProvinces ? "loading" : field.value}
input={<OutlinedInput label={"استان"} />}
onChange={(e) => field.onChange(e.target.value)}
displayEmpty
>
{provinceSelectOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message}
</FormHelperText>
</FormControl>
);
}}
name={"province_id"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error}>
<InputLabel id={`label-role`} shrink>
نقش
</InputLabel>
<Select
labelId={`label-role`}
id={"role"}
name={`role_id`}
value={loadingRoles ? "loading" : field.value}
input={<OutlinedInput label={"نقش"} />}
onChange={(e) => field.onChange(e.target.value)}
displayEmpty
>
{roleSelectOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message}
</FormHelperText>
</FormControl>
);
}}
name={"role_id"}
/>
</Grid>
</Grid>
</Stack>
</StyledForm>
</DialogContent>
<DialogActions>
<Button
disabled={isSubmitting}
variant="outlined"
color="secondary"
size="large"
onClick={() => setOpen(false)}
>
بستن
</Button>
<Button
type="submit"
disabled={isSubmitting}
form="UpdateUserForm"
size="large"
autoFocus
variant="contained"
>
{isSubmitting ? "در حال ویرایش کاربر..." : "ویرایش کاربر"}
</Button>
</DialogActions>
</>
);
};
export default UpdateUserForm;

View File

@@ -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 (
<>
<Tooltip title="ویرایش">
<IconButton onClick={handleOpen}>
<Edit />
</IconButton>
</Tooltip>
<Dialog maxWidth="sm" fullWidth={true} open={open}>
<UpdateUserForm
open={open}
setOpen={setOpen}
mutate={mutate}
values={values}
/>
</Dialog>
</>
);
};
export default UpdateUser;

View File

@@ -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 (
<Stack alignItems={"center"}>
{clientsOnline.some((co) => co.user_id == user_id) ? (
<Power color="success" />
) : (
<PowerOff color="error" />
)}
</Stack>
);
};
export default OnlineCell;

View File

@@ -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 <Stack alignItems={"center"}>{telephone_id}</Stack>;
};
export default TelephoneId;

View File

@@ -0,0 +1,12 @@
import { Box } from "@mui/material";
import Delete from "./Delete";
import UpdateUser from "./Edit";
const RowActions = ({ row, mutate }) => {
return (
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<Delete rowId={row.original.id} mutate={mutate} />
<UpdateUser values={row.original} mutate={mutate} />
</Box>
);
};
export default RowActions;

View File

@@ -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 (
<>
<DialogContent dividers>
<Divider sx={{ mb: 3 }}>
<Chip color="primary" variant="outlined" label="کاربر جدید" />
</Divider>
<StyledForm
id="createUserForm"
onSubmit={handleSubmit(onSubmit)}
style={{ width: "100%", height: "100%" }}
>
<Stack spacing={2}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="نام کاربری"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"username"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="رمز عبور"
variant="outlined"
type={showPassword ? "text" : "password"}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<IconButton onClick={handleClickShowPassword}>
{showPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton>
</InputAdornment>
),
},
}}
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"password"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<TextField
{...field}
label="نام و نام خانوادگی"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"full_name"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<TextField
{...field}
label="سمت"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"position"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="شماره همراه"
variant="outlined"
fullWidth
type="tel"
onChange={(e) => {
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"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error}>
<InputLabel id={`label-phone-number`} shrink>
جنسیت
</InputLabel>
<Select
labelId={`label-phone-number`}
id={"phone-number"}
name={`gender`}
value={field.value}
input={<OutlinedInput label={"جنسیت"} />}
onChange={(e) => field.onChange(e.target.value)}
displayEmpty
>
<MenuItem value={"male"}>مرد</MenuItem>
<MenuItem value={"female"}>زن</MenuItem>
</Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message}
</FormHelperText>
</FormControl>
);
}}
name={"gender"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="شماره ملی"
variant="outlined"
fullWidth
type="tel"
onChange={(e) => {
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"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error}>
<InputLabel id={`label-province`} shrink>
استان
</InputLabel>
<Select
labelId={`label-province`}
id={"province"}
name={`province_id`}
value={field.value}
input={<OutlinedInput label={"استان"} />}
onChange={(e) => field.onChange(e.target.value)}
displayEmpty
>
{provinceSelectOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message}
</FormHelperText>
</FormControl>
);
}}
name={"province_id"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error}>
<InputLabel id={`label-role`} shrink>
نقش
</InputLabel>
<Select
labelId={`label-role`}
id={"role"}
name={`role_id`}
value={field.value}
input={<OutlinedInput label={"نقش"} />}
onChange={(e) => field.onChange(e.target.value)}
displayEmpty
>
{roleSelectOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message}
</FormHelperText>
</FormControl>
);
}}
name={"role_id"}
/>
</Grid>
</Grid>
</Stack>
</StyledForm>
</DialogContent>
<DialogActions>
<Button
disabled={isSubmitting}
variant="outlined"
color="secondary"
size="large"
onClick={() => setOpen(false)}
>
بستن
</Button>
<Button
type="submit"
disabled={isSubmitting}
form="createUserForm"
size="large"
autoFocus
variant="contained"
>
{isSubmitting ? "در حال ثبت کاربر جدید..." : "ثبت کاربر جدید"}
</Button>
</DialogActions>
</>
);
};
export default CreateUserForm;

View File

@@ -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 ? (
<IconButton
aria-label="کاربر جدید"
color="primary"
onClick={handleOpen}
>
<AddCircle sx={{ fontSize: "25px" }} />
</IconButton>
) : (
<Button
size={"small"}
variant="contained"
color="primary"
startIcon={<AddCircle />}
onClick={handleOpen}
>
کاربر جدید
</Button>
)}
<Dialog maxWidth="sm" fullWidth={true} open={open}>
<CreateUserForm open={open} setOpen={setOpen} mutate={mutate} />
</Dialog>
</>
);
};
export default CreateUser;

View File

@@ -0,0 +1,10 @@
import CreateUser from "./Create";
const Toolbar = ({ mutate }) => {
return (
<>
<CreateUser mutate={mutate} />
</>
);
};
export default Toolbar;

View File

@@ -0,0 +1,14 @@
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const UsersPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"کاربران"} />
<DataTable />
</Stack>
);
};
export default UsersPage;

View File

@@ -33,7 +33,7 @@ const CallHistory = ({ tab }) => {
<ErrorOrEmpty isErrorOrEmpty="empty" /> <ErrorOrEmpty isErrorOrEmpty="empty" />
) : ( ) : (
<Box> <Box>
<ListItemOfCalls key={'first'} historyItem={firstItemOfHistory} /> <ListItemOfCalls key={"first"} historyItem={firstItemOfHistory} />
{historyList && ( {historyList && (
<Divider> <Divider>
<Chip <Chip

View File

@@ -0,0 +1,60 @@
import React from "react";
import { AdapterDateFnsJalali } from "@mui/x-date-pickers/AdapterDateFnsJalali";
import { faIR } from "@mui/x-date-pickers/locales";
import { LocalizationProvider, MobileDatePicker } from "@mui/x-date-pickers";
import { IconButton, InputAdornment } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear";
const CustomDatePicker = ({
dateValue,
setDateValue,
placeholder = "انتخاب تاریخ",
size = "small",
}) => {
const handleDateChange = (newValue) => {
setDateValue(newValue);
};
return (
<LocalizationProvider
dateAdapter={AdapterDateFnsJalali}
localeText={
faIR.components.MuiLocalizationProvider.defaultProps.localeText
}
>
<MobileDatePicker
value={dateValue || null}
onChange={handleDateChange}
slotProps={{
textField: {
size: size,
placeholder: placeholder,
InputProps: {
endAdornment: (
<InputAdornment position="end">
<IconButton
size={size}
onClick={(event) => {
event.stopPropagation();
setDateValue(null);
}}
sx={{
color: "#bfbfbf",
"&:hover": {
backgroundColor: "rgba(189, 189, 189, 0.1)",
color: "#363434",
},
}}
>
<ClearIcon />
</IconButton>
</InputAdornment>
),
},
},
}}
></MobileDatePicker>
</LocalizationProvider>
);
};
export default CustomDatePicker;

View File

@@ -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 && (
<TableToolbar
table={table}
mutate={mutate}
filterData={specialFilter || filterData}
/>
)}
</>
),
renderRowActions: ({ row }) => <RowActions row={row} mutate={mutate} />,
...props,
});
return (
<DataTable_Paper
table={table}
table_title={table_title}
need_filter={need_filter}
mutate={mutate}
table_url={table_url}
columns={flatColumns}
user_id={user_id}
page_name={page_name}
table_name={table_name}
special_data={props.data}
special_filter={specialFilter}
setFilterData={props.setFilterData}
/>
);
};
export default DataTable_Main;

View File

@@ -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") && (
<TableBody
{...tableBodyProps}
sx={(theme) => ({
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" ? (
<Memo_DataTable_TableBodyRow key={row.id} {...props} />
) : (
<DataTable_TableBodyRow key={row.id} {...props} />
);
})}
</TableBody>
)}
<TableBody
{...tableBodyProps}
sx={(theme) => ({
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 ? (
<tr
style={{
display: layoutMode?.startsWith("grid") ? "grid" : undefined,
}}
>
<td
colSpan={table.getVisibleLeafColumns().length}
style={{
display: layoutMode?.startsWith("grid") ? "grid" : undefined,
}}
>
{renderEmptyRowsFallback?.({ table }) ?? (
<Typography
sx={{
color: "text.secondary",
fontStyle: "italic",
maxWidth: `min(100vw, ${tablePaperRef.current?.clientWidth ?? 360}px)`,
py: "2rem",
textAlign: "center",
width: "100%",
}}
>
{globalFilter || columnFilters.length
? localization.noResultsFound
: localization.noRecordsToDisplay}
</Typography>
)}
</td>
</tr>
) : (
<>
{(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" ? (
<Memo_DataTable_TableBodyRow key={key} {...props} />
) : (
<DataTable_TableBodyRow key={key} {...props} />
);
})}
</>
))}
</TableBody>
{!rowPinningDisplayMode?.includes("sticky") &&
getIsSomeRowsPinned("bottom") && (
<TableBody
{...tableBodyProps}
sx={(theme) => ({
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" ? (
<Memo_DataTable_TableBodyRow key={row.id} {...props} />
) : (
<DataTable_TableBodyRow key={row.id} {...props} />
);
})}
</TableBody>
)}
</>
);
};
export default DataTable_TableBody;
export const Memo_DataTable_TableBody = memo(
DataTable_TableBody,
(prev, next) => prev.table.options.data === next.table.options.data,
);

View File

@@ -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 (
<TableCell
data-index={staticColumnIndex}
data-pinned={!!isColumnPinned || undefined}
{...tableCellProps}
onContextMenu={handleContextMenu}
onDoubleClick={handleDoubleClick}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
sx={(theme) => ({
"&: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) ? (
<Skeleton
animation="wave"
height={20}
width={skeletonWidth}
{...skeletonProps}
/>
) : 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 ? (
<DataTable_CopyButton cell={cell} table={table}>
<DataTable_TableBodyCellValue {...cellValueProps} />
</DataTable_CopyButton>
) : (
<DataTable_TableBodyCellValue {...cellValueProps} />
)}
{cell.getIsGrouped() && !columnDef.GroupedCell && (
<> ({row.subRows?.length})</>
)}
</>
)}
</TableCell>
);
};
export default DataTable_TableBodyCell;
export const Memo_DataTable_TableBodyCell = memo(
DataTable_TableBodyCell,
(prev, next) => next.cell === prev.cell,
);

View File

@@ -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 = (
<span aria-label={renderedCellValue} role="note">
{chunks?.map(({ key, match, text }) => (
<Box
aria-hidden="true"
component="span"
key={key}
sx={
match
? {
backgroundColor: matchHighlightColor,
borderRadius: "2px",
color: (theme) =>
theme.palette.mode === "dark"
? theme.palette.common.white
: theme.palette.common.black,
padding: "2px 1px",
}
: undefined
}
>
{text}
</Box>
)) ?? renderedCellValue}
</span>
);
}
}
if (columnDef.Cell && !isGroupedValue) {
renderedCellValue = columnDef.Cell({
cell,
column,
renderedCellValue,
row,
rowRef,
staticColumnIndex,
staticRowIndex,
table,
});
}
return renderedCellValue;
};
export default DataTable_TableBodyCellValue;

View File

@@ -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 (
<>
<TableRow
data-index={renderDetailPanel ? staticRowIndex * 2 : staticRowIndex}
data-pinned={!!isRowPinned || undefined}
data-selected={isRowSelected || undefined}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
ref={(node) => {
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 ? (
<td style={{ display: "flex", width: 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 ? (
<Memo_DataTable_TableBodyCell key={cell.id} {...props} />
) : (
<DataTable_TableBodyCell key={cell.id} {...props} />
)
) : null;
},
)}
{virtualPaddingRight ? (
<td style={{ display: "flex", width: virtualPaddingRight }} />
) : null}
</TableRow>
{renderDetailPanel && !row.getIsGrouped() && (
<DataTable_TableDetailPanel
parentRowRef={rowRef}
row={row}
rowVirtualizer={rowVirtualizer}
staticRowIndex={staticRowIndex}
table={table}
virtualRow={virtualRow}
/>
)}
</>
);
};
export default DataTable_TableBodyRow;
export const Memo_DataTable_TableBodyRow = memo(
DataTable_TableBodyRow,
(prev, next) =>
prev.row === next.row && prev.staticRowIndex === next.staticRowIndex,
);

View File

@@ -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 (
<TableRow
className="Mui-TableBodyCell-DetailPanel"
data-index={renderDetailPanel ? staticRowIndex * 2 + 1 : staticRowIndex}
ref={(node) => {
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),
})}
>
<TableCell
className="Mui-TableBodyCell-DetailPanel"
colSpan={getVisibleLeafColumns().length}
{...tableCellProps}
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
) : (
<Collapse in={row.getIsExpanded()} mountOnEnter unmountOnExit>
{DetailPanel}
</Collapse>
)}
</TableCell>
</TableRow>
);
};
export default DataTable_TableDetailPanel;

View File

@@ -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 (
<Tooltip
{...getCommonTooltipProps("top")}
title={
buttonProps?.title ??
(copied ? localization.copiedToClipboard : localization.clickToCopy)
}
>
<Button
onClick={(e) => handleCopy(e, cell.getValue())}
size="small"
type="button"
variant="text"
{...buttonProps}
sx={(theme) => ({
backgroundColor: "transparent",
border: "none",
color: "inherit",
cursor: "copy",
fontFamily: "inherit",
fontSize: "inherit",
letterSpacing: "inherit",
m: "-0.25rem",
minWidth: "unset",
py: 0,
textAlign: "inherit",
textTransform: "inherit",
...parseFromValuesOrFunc(buttonProps?.sx, theme),
})}
title={undefined}
/>
</Tooltip>
);
};
export default DataTable_CopyButton;

View File

@@ -0,0 +1,52 @@
import {
getCommonTooltipProps,
parseFromValuesOrFunc,
} from "@/core/utils/utils";
import { IconButton, Tooltip } from "@mui/material";
const DataTable_GrabHandleButton = ({ location, table, ...rest }) => {
const {
options: {
icons: { DragHandleIcon },
localization,
},
} = table;
return (
<Tooltip
{...getCommonTooltipProps("top")}
title={rest?.title ?? localization.move}
>
<IconButton
aria-label={rest.title ?? localization.move}
disableRipple
draggable="true"
size="small"
{...rest}
onClick={(e) => {
e.stopPropagation();
rest?.onClick?.(e);
}}
sx={(theme) => ({
"&:active": {
cursor: "grabbing",
},
"&:hover": {
backgroundColor: "transparent",
opacity: 1,
},
cursor: "grab",
m: "0 -0.1rem",
opacity: location === "row" ? 1 : 0.5,
p: "2px",
transition: "all 150ms ease-in-out",
...parseFromValuesOrFunc(rest?.sx, theme),
})}
title={undefined}
>
<DragHandleIcon />
</IconButton>
</Tooltip>
);
};
export default DataTable_GrabHandleButton;

View File

@@ -0,0 +1,4 @@
const DataTable_ToggleFiltersButton = ({ table, ...rest }) => {
return <></>;
};
export default DataTable_ToggleFiltersButton;

View File

@@ -0,0 +1,133 @@
"use client";
import FilterBodyControllerWithDependency from "@/core/components/DataTable/filter/FilterBodyControllerWithDependency";
import FilterHeader from "@/core/components/DataTable/filter/FilterHeader";
import useDataTable from "@/lib/hooks/useDataTable";
import { yupResolver } from "@hookform/resolvers/yup";
import { Box, Button } from "@mui/material";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as Yup from "yup";
import ScrollBox from "../../ScrollBox";
import FilterBodyController from "./FilterBodyController";
function FilterBody({ columns, drawerState, setDrawerState }) {
const { filterData, setFilterData } = useDataTable();
const [validationSchema, setValidationSchema] = useState(Yup.object({}));
console.log(filterData);
useEffect(() => {
const schema = Yup.object(
Object.keys(filterData).reduce((acc, key) => {
const initialValue = filterData[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;
}, {}),
);
setValidationSchema(schema);
}, [filterData]);
const {
handleSubmit,
control,
reset,
formState: { errors, isDirty, isValid },
} = useForm({
defaultValues: filterData,
resolver: yupResolver(validationSchema),
mode: "all",
});
const onSubmit = (data) => {
setDrawerState(false);
setFilterData(data);
};
return (
<>
<FilterHeader setDrawerState={setDrawerState} />
{Object.keys(filterData).length > 0 && (
<ScrollBox>
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={{ px: 2, py: 3 }}>
{columns.map((column) =>
column.enableColumnFilter ? (
column.dependencyId ? (
<FilterBodyControllerWithDependency
key={column.id}
column={column}
control={control}
errors={errors}
reset={reset}
/>
) : (
<FilterBodyController
key={column.id}
column={column}
control={control}
errors={errors}
reset={reset}
/>
)
) : null,
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
pb: 2,
}}
>
<Button
type="submit"
variant="contained"
size="large"
disabled={!isDirty || !isValid}
sx={{
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",
},
}}
>
اعمال فیلتر
</Button>
</Box>
</form>
</ScrollBox>
)}
</>
);
}
export default FilterBody;

View File

@@ -0,0 +1,25 @@
import { Controller, useWatch } from "react-hook-form";
import FilterBodyField from "./FilterBodyField";
const FilterBodyController = ({ column, control, reset, errors }) => {
return (
<Controller
name={`${column.id}`}
control={control}
render={({ field: { value, name, onBlur, onChange } }) => {
return (
<FilterBodyField
column={column}
filterParameters={value}
handleChange={onChange}
handleBlur={onBlur}
dependencyFieldValue={null}
resetForm={() => reset()}
errors={errors}
/>
);
}}
/>
);
};
export default FilterBodyController;

View File

@@ -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 (
<Controller
name={`${column.id}`}
control={control}
render={({ field: { value, name, onBlur, onChange } }) => {
return (
<FilterBodyField
column={column}
filterParameters={value}
handleChange={onChange}
handleBlur={onBlur}
dependencyFieldValue={dependencyField}
resetForm={() => reset()}
errors={errors}
/>
);
}}
/>
);
};
export default FilterBodyControllerWithDependency;

View File

@@ -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 <CustomTextFieldRange {...commonProps} />;
}
if (filterParameters.filterMode === "equals") {
return column.ColumnSelectComponent ? (
<column.ColumnSelectComponent {...commonProps} />
) : (
<CustomSelect {...commonProps} />
);
}
break;
case "date":
if (filterParameters.filterMode === "equals") {
return <CustomDatePicker {...commonProps} />;
}
if (filterParameters.filterMode === "between") {
return <CustomDatePickerRange {...commonProps} />;
}
break;
case "array":
if (filterParameters.filterMode === "equals") {
return <CustomSelectMultiple {...commonProps} />;
}
break;
default:
return <CustomTextField {...commonProps} />;
}
};
return (
<>
{renderField()}
{Array.isArray(column.columnFilterModeOptions) && (
<FilterOptionList
column={column}
anchorEl={anchorEl}
setAnchorEl={setAnchorEl}
filterParameters={filterParameters}
filterType={filterParameters.filterMode}
handleChange={handleChange}
resetForm={resetForm}
filterOption={column.columnFilterModeOptions}
columnFilterModeOptionFa={columnFilterModeOptionFa}
/>
)}
</>
);
}
export default FilterBodyField;

View File

@@ -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 (
<Tooltip title="فیلتر">
<IconButton
onClick={() => {
setDrawerState(!drawerState);
}}
aria-label="filter table"
>
<Badge
badgeContent={filteredFilterData.length}
color="primary"
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<FilterAltIcon />
</Badge>
</IconButton>
</Tooltip>
);
}
export default FilterButton;

View File

@@ -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 (
<>
<FilterButton drawerState={open} setDrawerState={setOpen} />
<Drawer
open={open}
onClose={() => setOpen(false)}
sx={{
overflowY: "hidden",
display: "flex",
flexDirection: "column",
height: "100%",
zIndex: "1300",
}}
>
<Box sx={{ width: { xs: 300, sm: 450 } }}>
{open && (
<FilterDrawer
defaultValues={filterData}
setFilterData={setFilterData}
closeDrawer={closeDrawer}
/>
)}
</Box>
</Drawer>
</>
);
};
export default FilterCustom;

View File

@@ -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 (
<Box
sx={{
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",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<FilterAltIcon sx={{ color: "#fff", mr: 1 }} />
<Typography variant="h6" sx={{ color: "#fff" }}>
فیلتر
</Typography>
</Box>
<IconButton sx={{ color: "#fff" }} onClick={() => setDrawerState(false)}>
<CancelIcon sx={{ fontSize: "1em" }} />
</IconButton>
</Box>
);
}
export default FilterHeader;

View File

@@ -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 (
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseFilterBox}
>
{filterOption.map((option, index) => (
<ListItem
key={index}
onClick={(event) => handleChangeItem(event, index)}
selected={option === selectedFilter}
sx={{
cursor: "pointer",
width: "100px",
display: "flex",
justifyContent: "center",
}}
>
{columnFilterModeOptionFa[option]}
</ListItem>
))}
</Menu>
);
}
export default FilterOptionList;

View File

@@ -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 (
<MuiDatePicker
name={`${column.id}.value`}
value={filterParameters.value}
setFieldValue={(name, formattedDate) => {
handleChange({ ...filterParameters, value: formattedDate });
}}
placeholder={column.header}
helperText={
<Typography
variant="button"
sx={{ color: (theme) => theme.palette.primary.main }}
>
نوع فیلتر: {defaultFilterTranslation} (تاریخ)
</Typography>
}
/>
);
}
export default CustomDatePicker;

View File

@@ -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 (
<Box sx={{ display: "flex", gap: 1 }}>
<MuiDatePicker
label={column.header}
name={`${column.id}.value[0]`}
value={filterParameters.value[0]}
setFieldValue={(name, formattedDate) => {
handleChange({
...filterParameters,
value: [formattedDate, filterParameters.value[1]],
});
}}
maxDate={filterParameters.value[1]}
placeholder={`از تاریخ`}
helperText={
<Typography
variant="button"
sx={{ color: (theme) => theme.palette.primary.main }}
>
نوع فیلتر: {defaultFilterTranslation} (تاریخ)
</Typography>
}
/>
<MuiDatePicker
label={column.header}
name={`${column.id}.value[1]`}
value={filterParameters.value[1]}
setFieldValue={(name, formattedDate) => {
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)}
/>
</Box>
);
}
export default CustomDatePickerRange;

View File

@@ -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 (
<Box sx={{ display: "flex", flexDirection: "column", my: 1 }}>
<LocalizationProvider
dateAdapter={AdapterDateFnsJalali}
localeText={
faIR.components.MuiLocalizationProvider.defaultProps.localeText
}
>
<MobileDatePicker
value={value ? new Date(value) : null}
sx={{ width: "100%" }}
id={name}
label={label}
name={name}
closeOnSelect={true}
aria-describedby="component-helper-text"
onChange={(value) => {
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: (
<InputAdornment position="end">
<IconButton
size="small"
onClick={(event) => {
event.stopPropagation();
setFieldValue(name, "");
}}
sx={{
color: "#bfbfbf",
"&:hover": {
backgroundColor: "rgba(189, 189, 189, 0.1)",
color: "#363434",
},
}}
>
<ClearIcon />
</IconButton>
</InputAdornment>
),
},
InputLabelProps: {
shrink: true,
},
},
}}
/>
{/* <FormHelperText id="component-helper-text" variant="caption" sx={{ ml: 2, color: error ? "#d32f2f" : "unset", fontWeight: 'bold' }}>
{helperText ? helperText : ""}
</FormHelperText> */}
</LocalizationProvider>
</Box>
);
}
export default MuiDatePicker;

View File

@@ -0,0 +1,38 @@
"use client";
import {
FormControl,
InputLabel,
MenuItem,
OutlinedInput,
Select,
} from "@mui/material";
function CustomSelect({ column, filterParameters, handleChange }) {
return (
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel id={`label${column.id}`} shrink>
{column.header}
</InputLabel>
<Select
labelId={`label${column.id}`}
id={column.id}
name={`${column.id}.value`}
value={filterParameters.value}
input={<OutlinedInput label={column.header} />}
size="small"
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
displayEmpty
>
{column.columnSelectOption().map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelect;

View File

@@ -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 (
<FormControl fullWidth sx={{ my: 1 }} size="small">
<InputLabel id={`label${column.id}`} shrink>
{column.header}
</InputLabel>
<Select
labelId={`label${column.id}`}
id={column.id}
name={`${column.id}.value`}
value={value}
size="small"
input={<OutlinedInput label={column.header} />}
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
displayEmpty
>
{columnSelectOption.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelectByDependency;

View File

@@ -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 (
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel htmlFor="uncontrolled-native" id={`label${column.id}`}>
{column.header}
</InputLabel>
<Select
labelId={`label${column.id}`}
id={column.id}
value={filterParameters.value}
multiple
size="small"
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={getLabelForValue(value)} />
))}
</Box>
)}
input={<OutlinedInput label={column.header} />}
InputLabelProps={{ shrink: true }}
>
{columnSelectOption.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelectMultiple;

View File

@@ -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 (
<TextField
id={column.id}
name={column.id}
label={column.header}
value={filterParameters.value}
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
onBlur={handleBlur}
fullWidth
variant="outlined"
size="small"
sx={{ my: 1 }}
InputProps={{
endAdornment: (
<InputAdornment
position="end"
sx={{ cursor: "pointer" }}
onClick={handleOpenFilterBox}
>
<FilterListIcon />
</InputAdornment>
),
}}
InputLabelProps={{ shrink: true }}
/>
);
}
export default CustomTextField;

View File

@@ -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 (
<Box sx={{ display: "flex" }}>
<TextField
id={`${column.id}.value[0]`}
name={`${column.id}.value[0]`}
onChange={(e) =>
handleChange({
...filterParameters,
value: [e.target.value, filterParameters.value[1]],
})
}
onBlur={handleBlur}
label={<Typography>از {column.header}</Typography>}
value={filterParameters.value[0]}
fullWidth
error={
touched?.[`${column.id}`]?.value &&
Boolean(errors?.[`${column.id}`]?.value)
}
helperText={
<Typography
variant="caption"
sx={{
color: (theme) => theme.palette.primary.main,
fontWeight: "bold",
}}
>
نوع فیلتر: {defaultFilterTranslation}
</Typography>
}
variant="outlined"
size="small"
sx={{ my: 1, marginRight: 1 }}
/>
<TextField
id={`${column.id}.value[1]`}
name={`${column.id}.value[1]`}
onChange={(e) =>
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={<Typography>تا {column.header}</Typography>}
value={filterParameters.value[1]}
fullWidth
variant="outlined"
size="small"
sx={{ my: 1 }}
InputProps={{
endAdornment: (
<InputAdornment
position="end"
sx={{ cursor: "pointer" }}
onClick={handleOpenFilterBox}
>
<FilterListIcon />
</InputAdornment>
),
}}
/>
</Box>
);
}
export default CustomTextFieldRange;

View File

@@ -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 (
<>
<FilterButton drawerState={open} setDrawerState={setOpen} />
<Drawer
open={open}
onClose={() => setOpen(false)}
sx={{
overflowY: "hidden",
display: "flex",
flexDirection: "column",
height: "100%",
zIndex: "1300",
}}
>
<Box sx={{ width: { xs: 300, sm: 450 } }}>
{open && (
<FilterBody
columns={columns}
drawerState={open}
setDrawerState={setOpen}
user_id={user_id}
page_name={page_name}
table_name={table_name}
/>
)}
</Box>
</Drawer>
</>
);
}
export default FilterColumn;

View File

@@ -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 (
<TableHead
{...tableHeadProps}
ref={(ref) => {
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) => (
<DataTable_TableHeadRow
columnVirtualizer={columnVirtualizer}
headerGroup={headerGroup}
key={headerGroup.id}
table={table}
index={index}
/>
))}
</TableHead>
);
};
export default DataTable_TableHead;

View File

@@ -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 (
<TableCell
align={
columnDefType === "group"
? "center"
: theme.direction === "rtl"
? "right"
: "left"
}
colSpan={header.colSpan}
rowSpan={rowSpan}
data-index={staticColumnIndex}
data-pinned={!!isColumnPinned || undefined}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
ref={(node) => {
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 ?? (
<Box
className="Mui-TableHeadCell-Content"
sx={{
alignItems: "center",
display: "flex",
flexDirection:
tableCellProps?.align === "right" ? "row-reverse" : "row",
justifyContent:
columnDefType === "group" || tableCellProps?.align === "center"
? "center"
: column.getCanResize()
? "space-between"
: "flex-start",
position: "relative",
width: "100%",
}}
>
<Box
className="Mui-TableHeadCell-Content-Labels"
onClick={column.getToggleSortingHandler()}
sx={{
alignItems: "center",
cursor:
column.getCanSort() && columnDefType !== "group"
? "pointer"
: undefined,
display: "flex",
flexDirection:
tableCellProps?.align === "right" ? "row-reverse" : "row",
overflow: columnDefType === "data" ? "hidden" : undefined,
pl:
tableCellProps?.align === "center"
? `${headerPL}rem`
: undefined,
}}
>
<Box
className="Mui-TableHeadCell-Content-Wrapper"
sx={{
"&:hover": {
textOverflow: "clip",
},
minWidth: `${Math.min(columnDef.header?.length ?? 0, 4)}ch`,
overflow: columnDefType === "data" ? "hidden" : undefined,
textOverflow: "ellipsis",
textAlign: "start",
color: "#fff",
fontWeight: "400",
whiteSpace: "nowrap",
}}
>
{HeaderElement}
</Box>
{column.getCanSort() && columnDefType !== "group" && (
<MRT_TableHeadCellSortLabel
sx={{
width: 20,
"& .MuiTableSortLabel-icon": {
color: `#fff !important`,
},
}}
header={header}
table={table}
/>
)}
</Box>
</Box>
)}
</TableCell>
);
};
export default DataTable_TableHeadCell;

View File

@@ -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 (
<TableRow {...tableRowProps}>
{virtualPaddingLeft && (
<th style={{ display: "flex", width: virtualPaddingLeft }} />
)}
{(virtualColumns ?? headerGroup.headers).map(
(headerOrVirtualHeader, staticColumnIndex) => {
const header = columnVirtualizer
? headerGroup.headers[headerOrVirtualHeader.index]
: headerOrVirtualHeader;
return header ? (
<DataTable_TableHeadCell
columnVirtualizer={columnVirtualizer}
header={header}
backgroundColor={backgroundColor}
key={header.id}
staticColumnIndex={staticColumnIndex}
table={table}
/>
) : null;
},
)}
{virtualPaddingRight && (
<th style={{ display: "flex", width: virtualPaddingRight }} />
)}
</TableRow>
);
};
export default DataTable_TableHeadRow;

View File

@@ -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 (
<Drawer
open={drawerState}
onClose={() => setDrawerState(false)}
sx={{
overflowY: "hidden",
display: "flex",
flexDirection: "column",
height: "100%",
zIndex: "1300",
}}
>
<HideHeader setDrawerState={setDrawerState} />
<HideOrShowAll hideData={hideData} setHideData={setHideData} />
<ScrollBox>
<Box sx={{ px: 1, py: 2, display: "flex", flexDirection: "column" }}>
{columns.map((column) => (
<HideBodyField
key={column.id}
column={column}
hideData={hideData}
setHideData={setHideData}
/>
))}
</Box>
</ScrollBox>
</Drawer>
);
}
export default HideBody;

View File

@@ -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 (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Checkbox
checked={hideData[column.id]}
onChange={handleCheckboxChange}
name={column.id}
/>
<Typography variant="subtitle1">{column.header}</Typography>
</Box>
);
} else {
return (
<Box sx={{ display: "flex", alignItems: "center", p: 1 }}>
<Typography variant="subtitle1">{column.header}</Typography>
</Box>
);
}
})();
return (
<SimpleTreeView disableSelection>
<CustomTreeItem itemId={column.id} label={labelContent}>
{column.columns?.map((subColumn) => (
<HideBodyField
key={subColumn.id}
column={subColumn}
hideData={hideData[column.id]}
setHideData={setHideData}
/>
))}
</CustomTreeItem>
</SimpleTreeView>
);
}
export default HideBodyField;

View File

@@ -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 (
<Tooltip title="نمایش/مخفی کردن ستون ها">
<IconButton
onClick={() => {
setDrawerState(!drawerState);
}}
aria-label="hide table column"
>
<Badge
badgeContent={falseCount}
color="primary"
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<ViewColumnIcon />
</Badge>
</IconButton>
</Tooltip>
);
}
export default HideButton;

View File

@@ -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 (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
px: 2,
py: 1,
backgroundColor: "#155175",
boxShadow:
"rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px",
maxWidth: "450px",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<ViewColumnIcon sx={{ color: "#fff", mr: 1 }} />
<Typography variant="h6" sx={{ color: "#fff" }}>
نمایش/مخفی کردن ستون ها
</Typography>
</Box>
<IconButton sx={{ color: "#fff" }} onClick={() => setDrawerState(false)}>
<CancelIcon sx={{ fontSize: "1em" }} />
</IconButton>
</Box>
);
}
export default FilterHeader;

View File

@@ -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 (
<Box sx={{ my: 1, display: "flex" }}>
<Button
sx={{ flex: 1 }}
color="info"
disabled={allVisible}
startIcon={<VisibilityIcon />}
onClick={handleShowAll}
>
نمایش همه
</Button>
<Button
sx={{ flex: 1 }}
color="error"
disabled={allHidden}
startIcon={<VisibilityOffIcon />}
onClick={handleHideAll}
>
مخفی کردن همه
</Button>
</Box>
);
}
export default HideOrShowAll;

View File

@@ -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 (
<>
<HideButton drawerState={open} setDrawerState={setOpen} />
<HideBody
columns={columns}
drawerState={open}
setDrawerState={setOpen}
user_id={user_id}
page_name={page_name}
table_name={table_name}
/>
</>
);
}
export default HideColumn;

View File

@@ -0,0 +1,18 @@
import DataTable_Main from "./Main";
import DataTableProvider from "@/lib/contexts/DataTable";
const DataTable = (props) => {
return (
<DataTableProvider
user_id={props.user_id}
page_name={props.page_name}
table_name={props.table_name}
columns={props.columns}
initialSort={props.sorting}
>
<DataTable_Main {...props} />
</DataTableProvider>
);
};
export default DataTable;

View File

@@ -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: "بدون مرتب سازی",
};

View File

@@ -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 (
<MenuItem
sx={{
alignItems: "center",
justifyContent: "space-between",
minWidth: "120px",
my: 0,
py: "6px",
}}
{...rest}
>
<Box
sx={{
alignItems: "center",
display: "flex",
}}
>
<ListItemIcon>{icon}</ListItemIcon>
{label}
</Box>
{onOpenSubMenu && (
<IconButton
onClick={onOpenSubMenu}
onMouseEnter={onOpenSubMenu}
size="small"
sx={{ p: 0 }}
>
<ArrowRightIcon />
</IconButton>
)}
</MenuItem>
);
};
export default DataTable_ActionMenuItem;

View File

@@ -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") && (
<DataTable_ActionMenuItem
icon={<ContentCopy />}
key={"mrt-copy"}
label={localization.copy}
onClick={(event) => {
event.stopPropagation();
navigator.clipboard.writeText(cell.getValue());
handleClose();
}}
table={table}
/>
),
parseFromValuesOrFunc(enableEditing, row) && editDisplayMode === "cell" && (
<DataTable_ActionMenuItem
icon={<EditIcon />}
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) && (
<Menu
MenuListProps={{
dense: density === "compact",
sx: {
backgroundColor: menuBackgroundColor,
},
}}
anchorEl={actionCellRef.current}
disableScrollLock
onClick={(event) => event.stopPropagation()}
onClose={handleClose}
open={!!cell}
transformOrigin={{ horizontal: -100, vertical: 8 }}
{...rest}
>
{menuItems ?? internalMenuItems}
</Menu>
)
);
};
export default DataTable_CellActionMenu;

View File

@@ -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 (
<Tooltip title="بازنشانی اطلاعات">
<IconButton onClick={reset} aria-label="reset table data">
{isAnyDirty ? (
<Badge
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
color="primary"
variant="dot"
>
<AutorenewIcon />
</Badge>
) : (
<AutorenewIcon />
)}
</IconButton>
</Tooltip>
);
}
export default ResetStorage;

View File

@@ -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 (
<Paper
elevation={0}
{...paperProps}
ref={(ref) => {
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 }) ?? (
<DataTable_TopToolbar
need_filter={need_filter}
table={table}
mutate={mutate}
columns={columns}
table_url={table_url}
user_id={user_id}
page_name={page_name}
table_name={table_name}
table_title={table_title}
setFilterData={setFilterData}
{...paperProps}
/>
))}
<DataTable_TableContainer table={table} />
{enableBottomToolbar &&
(parseFromValuesOrFunc(renderBottomToolbar, { table }) ?? (
<DataTable_BottomToolbar table={table} />
))}
</Paper>
);
};
export default DataTable_Paper;

View File

@@ -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 (
<Table
stickyHeader={enableStickyHeader || isFullScreen}
{...tableProps}
style={{ ...columnSizeVars, ...tableProps?.style }}
sx={(theme) => ({
display: layoutMode?.startsWith("grid") ? "grid" : undefined,
position: "relative",
...parseFromValuesOrFunc(tableProps?.sx, theme),
})}
>
{!!Caption && <caption>{Caption}</caption>}
{enableTableHead && <DataTable_TableHead {...commonTableGroupProps} />}
{memoMode === "table-body" || columnSizingInfo.isResizingColumn ? (
<Memo_DataTable_TableBody {...commonTableGroupProps} />
) : (
<DataTable_TableBody {...commonTableGroupProps} />
)}
{/* {enableTableFooter && <MRT_TableFooter {...commonTableGroupProps} />} */}
</Table>
);
};
export default DataTable_Table;

View File

@@ -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 (
<TableContainer
aria-busy={loading}
aria-describedby={loading ? "mrt-progress" : undefined}
{...tableContainerProps}
ref={(node) => {
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 ? <DataTable_TableLoadingOverlay table={table} /> : null}
<DataTable_Table table={table} />
{enableCellActions && actionCell && (
<DataTable_CellActionMenu table={table} />
)}
</TableContainer>
);
};
export default DataTable_TableContainer;

View File

@@ -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 (
<Box
sx={{
alignItems: "center",
backgroundColor: alpha(baseBackgroundColor, 0.5),
bottom: 0,
display: "flex",
justifyContent: "center",
left: 0,
maxHeight: "100vh",
position: "absolute",
right: 0,
top: 0,
width: "100%",
zIndex: 3,
}}
>
{circularProgressProps?.Component ?? (
<CircularProgress
aria-label={localization.noRecordsToDisplay}
id="mrt-progress"
{...circularProgressProps}
/>
)}
</Box>
);
};
export default DataTable_TableLoadingOverlay;

View File

@@ -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 (
<Box
{...toolbarProps}
ref={(node) => {
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),
})}
>
<DataTable_LinearProgressBar isTopToolbar={false} table={table} />
<Box
sx={{
alignItems: "center",
boxSizing: "border-box",
display: "flex",
justifyContent: "space-between",
p: "0.5rem",
width: "100%",
}}
>
{renderBottomToolbarCustomActions ? (
renderBottomToolbarCustomActions({ table })
) : (
<span />
)}
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
position: stackAlertBanner ? "relative" : "absolute",
right: 0,
top: 0,
}}
>
{enablePagination &&
["both", "bottom"].includes(positionPagination ?? "") && (
<DataTable_TablePagination position="bottom" table={table} />
)}
</Box>
</Box>
</Box>
);
};
export default DataTable_BottomToolbar;

View File

@@ -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 (
<Collapse
in={showProgressBars !== false && (showProgressBars || isSaving)}
mountOnEnter
sx={{
bottom: isTopToolbar ? 0 : undefined,
position: "absolute",
top: !isTopToolbar ? 0 : undefined,
width: "100%",
}}
unmountOnExit
>
<LinearProgress
aria-busy="true"
aria-label="Loading"
sx={{ position: "relative" }}
{...linearProgressProps}
/>
</Collapse>
);
};
export default DataTable_LinearProgressBar;

View File

@@ -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 (
<Box
className="MuiTablePagination-root"
sx={{
alignItems: "center",
display: "flex",
flexWrap: "wrap",
gap: "8px",
justifyContent: { md: "space-between", sm: "center" },
justifySelf: "flex-end",
mt:
position === "top" &&
enableToolbarInternalActions &&
!showGlobalFilter
? "3rem"
: undefined,
position: "relative",
px: "8px",
py: "12px",
zIndex: 2,
}}
>
{showRowsPerPage && (
<Box sx={{ alignItems: "center", display: "flex", gap: "8px" }}>
<InputLabel htmlFor="mrt-rows-per-page" sx={{ mb: 0 }}>
{localization.rowsPerPage}
</InputLabel>
<Select
MenuProps={{ disableScrollLock: true }}
disableUnderline
disabled={disabled}
inputProps={{
"aria-label": localization.rowsPerPage,
id: "mrt-rows-per-page",
}}
label={localization.rowsPerPage}
onChange={(event) => table.setPageSize(+event.target.value)}
sx={{ mb: 0 }}
value={pageSize}
variant="standard"
{...SelectProps}
>
{rowsPerPageOptions.map((option) => {
const value = typeof option !== "number" ? option.value : option;
const label =
typeof option !== "number" ? option.label : `${option}`;
return (
SelectProps?.children ??
(SelectProps?.native ? (
<option key={value} value={value}>
{label}
</option>
) : (
<MenuItem key={value} sx={{ m: 0 }} value={value}>
{label}
</MenuItem>
))
);
})}
</Select>
</Box>
)}
{paginationDisplayMode === "pages" ? (
<Pagination
count={numberOfPages}
disabled={disabled}
onChange={(_e, newPageIndex) => table.setPageIndex(newPageIndex - 1)}
page={pageIndex + 1}
renderItem={(item) => (
<PaginationItem
slots={{
first: FirstPageIcon,
last: LastPageIcon,
next: ChevronRightIcon,
previous: ChevronLeftIcon,
}}
{...item}
/>
)}
showFirstButton={showFirstButton}
showLastButton={showLastButton}
{...restPaginationProps}
/>
) : paginationDisplayMode === "default" ? (
<>
<Typography
align="center"
component="span"
sx={{ m: "0 4px", minWidth: "8ch" }}
variant="body2"
>{`${
lastRowIndex === 0 ? 0 : (firstRowIndex + 1).toLocaleString()
}-${lastRowIndex.toLocaleString()} ${
localization.of
} ${totalRowCount.toLocaleString()}`}</Typography>
<Box gap="xs">
{showFirstButton && (
<Tooltip {...tooltipProps} title={localization.goToFirstPage}>
<span>
<IconButton
aria-label={localization.goToFirstPage}
disabled={disableBack}
onClick={() => table.firstPage()}
size="small"
>
<FirstPageIcon {...flipIconStyles(theme)} />
</IconButton>
</span>
</Tooltip>
)}
<Tooltip {...tooltipProps} title={localization.goToPreviousPage}>
<span>
<IconButton
aria-label={localization.goToPreviousPage}
disabled={disableBack}
onClick={() => table.previousPage()}
size="small"
>
<ChevronLeftIcon {...flipIconStyles(theme)} />
</IconButton>
</span>
</Tooltip>
<Tooltip {...tooltipProps} title={localization.goToNextPage}>
<span>
<IconButton
aria-label={localization.goToNextPage}
disabled={disableNext}
onClick={() => table.nextPage()}
size="small"
>
<ChevronRightIcon {...flipIconStyles(theme)} />
</IconButton>
</span>
</Tooltip>
{showLastButton && (
<Tooltip {...tooltipProps} title={localization.goToLastPage}>
<span>
<IconButton
aria-label={localization.goToLastPage}
disabled={disableNext}
onClick={() => table.lastPage()}
size="small"
>
<LastPageIcon {...flipIconStyles(theme)} />
</IconButton>
</span>
</Tooltip>
)}
</Box>
</>
) : null}
</Box>
);
};
export default DataTable_TablePagination;

View File

@@ -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 (
<Box
{...toolbarProps}
ref={(ref) => {
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),
})}
>
<Box
sx={{
flexWrap: { xs: "wrap", sm: "no-wrap" },
boxSizing: "border-box",
display: "flex",
gap: "0.5rem",
justifyContent: "space-between",
alignItems: "center",
p: "0.5rem",
position: stackAlertBanner ? "relative" : "absolute",
right: 0,
top: 0,
width: "100%",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
order: { xs: 2, sm: 1 },
}}
>
{renderTopToolbarCustomActions?.({ table })}
</Box>
<Typography
sx={{
textAlign: "center",
order: { xs: 1, sm: 2 },
width: { xs: "100%", sm: "unset" },
}}
fontWeight={600}
>
{table_title || ""}
</Typography>
{enableToolbarInternalActions && (
<Box
sx={{
gap: "0.5rem",
flexWrap: "wrap-reverse",
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
order: 3,
}}
>
{!special_data && (
<>
<ResetStorage
user_id={user_id}
page_name={page_name}
table_name={table_name}
/>
<UpdateTable mutate={mutate} />
</>
)}
<HideColumn
columns={table.options.columns}
user_id={user_id}
page_name={page_name}
table_name={table_name}
/>
{need_filter && (
<FilterColumn
columns={columns}
table_url={table_url}
user_id={user_id}
page_name={page_name}
table_name={table_name}
/>
)}
{special_filter && setFilterData && (
<FilterCustom
filterData={special_filter}
setFilterData={setFilterData}
/>
)}
</Box>
)}
</Box>
{enablePagination &&
["both", "top"].includes(positionPagination ?? "") && (
<DataTable_TablePagination position="top" table={table} />
)}
<DataTable_LinearProgressBar isTopToolbar table={table} />
</Box>
);
};
export default DataTable_TopToolbar;

View File

@@ -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 (
<Tooltip title="بروزرسانی اطلاعات">
<IconButton onClick={update} aria-label="refactor table">
<RestartAltIcon />
</IconButton>
</Tooltip>
);
}
export default UpdateTable;

View File

@@ -0,0 +1,8 @@
import DataTable from "@/core/components/DataTable";
import { useAuth } from "@/lib/contexts/auth";
const DataTableWithAuth = (props) => {
const { user } = useAuth();
return <DataTable user_id={user.username} {...props} />;
};
export default DataTableWithAuth;

View File

@@ -0,0 +1,25 @@
import { Controller } from "react-hook-form";
import FilterField from "./FilterField";
const FilterController = ({ item, control, reset, errors }) => {
return (
<Controller
name={`${item.id}`}
control={control}
render={({ field: { value, onBlur, onChange } }) => {
return (
<FilterField
item={item}
filterParameters={value}
handleChange={onChange}
handleBlur={onBlur}
dependencyFieldValue={null}
resetForm={() => reset()}
errors={errors}
/>
);
}}
/>
);
};
export default FilterController;

View File

@@ -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 (
<Controller
name={`${item.id}`}
control={control}
render={({ field: { value, name, onBlur, onChange } }) => {
return (
<FilterField
item={item}
filterParameters={value}
handleChange={onChange}
handleBlur={onBlur}
dependencyFieldValue={dependencyField}
resetForm={() => reset()}
errors={errors}
/>
);
}}
/>
);
};
export default FilterControllerWithDependency;

View File

@@ -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 <CustomTextFieldRange {...commonProps} />;
}
if (filterParameters.filterMode === "equals") {
return item.SelectComponent ? (
<item.SelectComponent {...commonProps} />
) : (
<CustomSelect {...commonProps} />
);
}
break;
case "date":
if (filterParameters.filterMode === "equals") {
return <CustomDatePicker {...commonProps} />;
}
if (filterParameters.filterMode === "between") {
return <CustomDatePickerRange {...commonProps} />;
}
break;
case "array":
if (filterParameters.filterMode === "equals") {
return <CustomSelectMultiple {...commonProps} />;
}
break;
default:
return <CustomTextField {...commonProps} />;
}
};
return (
<>
{renderField()}
{Array.isArray(item.filterModeOptions) && (
<FilterOptionList
item={item}
anchorEl={anchorEl}
setAnchorEl={setAnchorEl}
filterParameters={filterParameters}
filterType={filterParameters.filterMode}
handleChange={handleChange}
resetForm={resetForm}
filterOption={item.filterModeOptions}
filterModeOptionFa={filterModeOptionFa}
/>
)}
</>
);
}
export default FilterField;

View File

@@ -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 (
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseFilterBox}
>
{filterOption.map((option, index) => (
<ListItem
key={index}
onClick={(event) => handleChangeItem(event, index)}
selected={option === selectedFilter}
sx={{
cursor: "pointer",
width: "100px",
display: "flex",
justifyContent: "center",
}}
>
{filterModeOptionFa[option]}
</ListItem>
))}
</Menu>
);
}
export default FilterOptionList;

View File

@@ -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 (
<MuiDatePicker
name={`${column.id}.value`}
value={filterParameters.value}
setFieldValue={(name, formattedDate) => {
handleChange({ ...filterParameters, value: formattedDate });
}}
placeholder={column.header}
helperText={
<Typography
variant="button"
sx={{ color: (theme) => theme.palette.primary.main }}
>
نوع فیلتر: {defaultFilterTranslation} (تاریخ)
</Typography>
}
/>
);
}
export default CustomDatePicker;

View File

@@ -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 (
<Box sx={{ display: "flex", gap: 1 }}>
<MuiDatePicker
label={item.header}
name={`${item.id}.value[0]`}
value={filterParameters.value[0]}
setFieldValue={(name, formattedDate) => {
handleChange({
...filterParameters,
value: [formattedDate, filterParameters.value[1]],
});
}}
maxDate={filterParameters.value[1]}
placeholder={`از تاریخ`}
helperText={
<Typography
variant="button"
sx={{ color: (theme) => theme.palette.primary.main }}
>
نوع فیلتر: {defaultFilterTranslation} (تاریخ)
</Typography>
}
/>
<MuiDatePicker
label={item.header}
name={`${item.id}.value[1]`}
value={filterParameters.value[1]}
setFieldValue={(name, formattedDate) => {
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)}
/>
</Box>
);
}
export default CustomDatePickerRange;

View File

@@ -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 (
<Box sx={{ display: "flex", flexDirection: "column", my: 1 }}>
<LocalizationProvider
dateAdapter={AdapterDateFnsJalali}
localeText={
faIR.components.MuiLocalizationProvider.defaultProps.localeText
}
>
<MobileDatePicker
value={value ? new Date(value) : null}
sx={{ width: "100%" }}
id={name}
label={label}
name={name}
closeOnSelect={true}
aria-describedby="component-helper-text"
onChange={(value) => {
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: (
<InputAdornment position="end">
<IconButton
size="small"
onClick={(event) => {
event.stopPropagation();
setFieldValue(name, "");
}}
sx={{
color: "#bfbfbf",
"&:hover": {
backgroundColor: "rgba(189, 189, 189, 0.1)",
color: "#363434",
},
}}
>
<ClearIcon />
</IconButton>
</InputAdornment>
),
},
InputLabelProps: {
shrink: true,
},
},
}}
/>
{/* <FormHelperText id="component-helper-text" variant="caption" sx={{ ml: 2, color: error ? "#d32f2f" : "unset", fontWeight: 'bold' }}>
{helperText ? helperText : ""}
</FormHelperText> */}
</LocalizationProvider>
</Box>
);
}
export default MuiDatePicker;

View File

@@ -0,0 +1,38 @@
"use client";
import {
FormControl,
InputLabel,
MenuItem,
OutlinedInput,
Select,
} from "@mui/material";
function CustomSelect({ item, filterParameters, handleChange }) {
return (
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel id={`label${item.id}`} shrink>
{item.header}
</InputLabel>
<Select
labelId={`label${item.id}`}
id={item.id}
name={`${item.id}.value`}
value={filterParameters.value}
input={<OutlinedInput label={item.header} />}
size="small"
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
displayEmpty
>
{item.selectOption().map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelect;

View File

@@ -0,0 +1,44 @@
"use client";
import {
FormControl,
InputLabel,
MenuItem,
OutlinedInput,
Select,
} from "@mui/material";
function CustomSelectByDependency({
item,
filterParameters,
value,
handleChange,
selectOption,
}) {
return (
<FormControl fullWidth sx={{ my: 1 }} size="small">
<InputLabel id={`label${item.id}`} shrink>
{item.header}
</InputLabel>
<Select
labelId={`label${item.id}`}
id={item.id}
name={`${item.id}.value`}
value={value}
size="small"
input={<OutlinedInput label={item.header} />}
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
displayEmpty
>
{selectOption.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelectByDependency;

View File

@@ -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 (
<FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel htmlFor="uncontrolled-native" id={`label${item.id}`}>
{item.header}
</InputLabel>
<Select
labelId={`label${item.id}`}
id={item.id}
value={filterParameters.value}
multiple
size="small"
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={getLabelForValue(value)} />
))}
</Box>
)}
input={<OutlinedInput label={item.header} />}
InputLabelProps={{ shrink: true }}
>
{selectOption.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
export default CustomSelectMultiple;

View File

@@ -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 (
<TextField
id={item.id}
name={item.id}
label={item.header}
value={filterParameters.value}
onChange={(e) =>
handleChange({ ...filterParameters, value: e.target.value })
}
onBlur={handleBlur}
fullWidth
variant="outlined"
size="small"
sx={{ my: 1 }}
InputProps={{
endAdornment: (
<InputAdornment
position="end"
sx={{ cursor: "pointer" }}
onClick={handleOpenFilterBox}
>
<FilterListIcon />
</InputAdornment>
),
}}
InputLabelProps={{ shrink: true }}
/>
);
}
export default CustomTextField;

View File

@@ -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 (
<Box sx={{ display: "flex" }}>
<TextField
id={`${item.id}.value[0]`}
name={`${item.id}.value[0]`}
onChange={(e) =>
handleChange({
...filterParameters,
value: [e.target.value, filterParameters.value[1]],
})
}
onBlur={handleBlur}
label={<Typography>از {item.header}</Typography>}
value={filterParameters.value[0]}
fullWidth
error={
touched?.[`${item.id}`]?.value &&
Boolean(errors?.[`${item.id}`]?.value)
}
helperText={
<Typography
variant="caption"
sx={{
color: (theme) => theme.palette.primary.main,
fontWeight: "bold",
}}
>
نوع فیلتر: {defaultFilterTranslation}
</Typography>
}
variant="outlined"
size="small"
sx={{ my: 1, marginRight: 1 }}
/>
<TextField
id={`${item.id}.value[1]`}
name={`${item.id}.value[1]`}
onChange={(e) =>
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={<Typography>تا {item.header}</Typography>}
value={filterParameters.value[1]}
fullWidth
variant="outlined"
size="small"
sx={{ my: 1 }}
InputProps={{
endAdornment: (
<InputAdornment
position="end"
sx={{ cursor: "pointer" }}
onClick={handleOpenFilterBox}
>
<FilterListIcon />
</InputAdornment>
),
}}
/>
</Box>
);
}
export default CustomTextFieldRange;

View File

@@ -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 (
<>
<Box sx={headerSx}>
<Box sx={headerTitleSx}>
<FilterAlt sx={headerIconSx} />
<Typography variant="h6" sx={{ color: "#fff" }}>
فیلتر
</Typography>
</Box>
<IconButton sx={iconButtonSx} onClick={closeDrawer}>
<Cancel sx={{ fontSize: "1em" }} />
</IconButton>
</Box>
<ScrollBox>
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={formContainerSx}>
{Object.values(defaultValues).map(
(item) =>
item.enable &&
(item.dependencyId ? (
<FilterControllerWithDependency
key={item.id}
item={item}
control={control}
errors={errors}
reset={reset}
/>
) : (
<FilterController
key={item.id}
item={item}
control={control}
errors={errors}
reset={reset}
/>
)),
)}
<Box sx={footerSx}>
<Button
type="submit"
variant="contained"
size="large"
disabled={!isDirty}
sx={submitButtonSx}
>
اعمال فیلتر
</Button>
</Box>
</Box>
</form>
</ScrollBox>
</>
);
};
export default FilterDrawer;

View File

@@ -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 (
<Box sx={{ width: "100%" }}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: 1,
}}
>
<Typography
color="primary.main"
variant="subtitle1"
sx={{ fontWeight: "500" }}
>
ذخیره سازی تغییرات
</Typography>
<SaveIcon sx={{ color: "primary.main" }} />
</Box>
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography variant="caption">
آیا مایل به ذخیره تغییر های اعمال شده برای دفعات بعد هستید؟
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 1, mt: 2 }}>
<Button
variant="contained"
size="small"
endIcon={<DownloadIcon />}
onClick={onSaveFilter}
>
ذخیره
</Button>
<Button variant="outlined" size="small" onClick={handleDismiss}>
رد کردن
</Button>
</Box>
</Box>
);
}
export default AskForKeepData;

View File

@@ -0,0 +1,18 @@
"use client";
import { Chip, Divider, Typography } from "@mui/material";
const PageTitle = ({ title }) => {
return (
<Divider>
<Chip
label={
<Typography fontSize={14} fontWeight={500}>
{title}
</Typography>
}
/>
</Divider>
);
};
export default PageTitle;

View File

@@ -35,17 +35,17 @@ const validationSchema = object({
), ),
}); });
const Form = ({ handleCloseForm, setLoading }) => {
const request = useRequest();
const [showNewPassword, setShowNewPassword] = useState();
const [showRepeatNewPassword, setShowRepeatNewPassword] = useState();
const defaultValues = { const defaultValues = {
current_password: "", current_password: "",
new_password: "", new_password: "",
repeat_new_password: "", repeat_new_password: "",
}; };
const Form = ({ handleCloseForm, setLoading }) => {
const request = useRequest();
const [showNewPassword, setShowNewPassword] = useState();
const [showRepeatNewPassword, setShowRepeatNewPassword] = useState();
const { const {
register, register,
handleSubmit, handleSubmit,

View File

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

View File

@@ -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,
}));
}

View File

@@ -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;
}, []);
};

View File

@@ -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;
};

View File

@@ -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;
}

View File

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

View File

@@ -1,3 +1,4 @@
import { Settings } from "@mui/icons-material";
import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard"; import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard";
export const pageMenu = [ export const pageMenu = [
@@ -9,4 +10,20 @@ export const pageMenu = [
icon: <SpaceDashboardIcon sx={{ width: "inherit", height: "inherit" }} />, icon: <SpaceDashboardIcon sx={{ width: "inherit", height: "inherit" }} />,
permissions: ["all"], permissions: ["all"],
}, },
{
id: "systemManagment",
label: "مدیریت سامانه",
type: "menu",
icon: <Settings sx={{ width: "inherit", height: "inherit" }} />,
hasSubitems: true,
Subitems: [
{
id: "systemManagment-users",
label: "کاربران",
type: "page",
route: "/dashboard/users",
permissions: ["manage_users"],
},
],
},
]; ];

View File

@@ -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_USER_LOGOUT_ROUTE = api + "/auth/logout";
export const GET_PERMISSIONS_ROUTE = api + "/profile/permissions"; export const GET_PERMISSIONS_ROUTE = api + "/profile/permissions";
export const GET_SIDEBAR_BADGE_ROUTE = ""; 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_CATEGORY = api + "/categories/list";
export const GET_CALLER_HISTORY = api + "/calls/list"; export const GET_CALLER_HISTORY = api + "/calls/list";
export const CALL_ACTION = api + "/calls"; 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";

View File

@@ -11,7 +11,6 @@ const theme = createTheme({
primary: { primary: {
main: "#1b4332", main: "#1b4332",
contrastText: "#FFFFFF", contrastText: "#FFFFFF",
}, },
secondary: { secondary: {
main: "#1B4043", main: "#1B4043",

149
src/core/utils/utils.js Normal file
View File

@@ -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,
};

View File

@@ -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(
<AskForKeepData
filterData={filterData}
sortData={sortData}
hideData={hideData}
user_id={user_id}
page_name={page_name}
table_name={table_name}
columns={flatColumns}
/>,
{
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: (
<AskForKeepData
filterData={filterData}
sortData={sortData}
hideData={hideData}
user_id={user_id}
page_name={page_name}
table_name={table_name}
columns={flatColumns}
/>
),
});
}
} else {
toast.dismiss({ containerId: "datatable" });
}
}, [filterData, sortData, hideData]);
if (!isInitStates) return <LinearProgress sx={{ borderRadius: 2 }} />;
return (
<DataTableContext.Provider
value={{
filterData,
setFilterData,
sortData,
setSortData,
hideData,
setHideData,
isDirty,
isAnyDirty,
}}
>
{children}
</DataTableContext.Provider>
);
};
export default DataTableProvider;

View File

@@ -24,6 +24,7 @@ export const SocketProvider = ({ children }) => {
const { user } = useAuth(); const { user } = useAuth();
const socketToastId = useRef(null); const socketToastId = useRef(null);
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [clientsOnline, setClientsOnline] = useState([]);
const socket = useMemo(() => { const socket = useMemo(() => {
return io(`${process.env.NEXT_PUBLIC_SERVER_SOCKET_URL}`, { return io(`${process.env.NEXT_PUBLIC_SERVER_SOCKET_URL}`, {
@@ -35,7 +36,7 @@ export const SocketProvider = ({ children }) => {
useEffect(() => { useEffect(() => {
if (user?.telephone_id) { if (user?.telephone_id) {
socket.auth.token = user.telephone_id; socket.auth.token = { telephone_id: user.telephone_id, user_id: user.id };
} }
}, [user, socket]); }, [user, socket]);
@@ -102,7 +103,21 @@ export const SocketProvider = ({ children }) => {
}; };
}, [socket]); }, [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 ( return (
<SocketContext.Provider value={contextValue}> <SocketContext.Provider value={contextValue}>

View File

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

View File

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

29
src/lib/hooks/useRoles.js Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More