Compare commits
113 Commits
328402fd7f
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b96ecf9d9 | |||
| 4bb164be0f | |||
| 2f71b4c96c | |||
| cabf248c0e | |||
| 1990e73368 | |||
| 185961c162 | |||
| b60a38e21d | |||
| f53b5d8fe1 | |||
| f5da44d9cf | |||
| 77475f09cd | |||
| 9413cf03c9 | |||
| b2f9166c22 | |||
| 7fedfdfbfd | |||
| 71e7d4eac2 | |||
| c9713a93ac | |||
| 42528f4d5c | |||
| 52bb36ac3c | |||
| f7871db066 | |||
| 153547d098 | |||
| 09feb73331 | |||
| 9a7858a4ec | |||
| df2f800b54 | |||
| 3537094e7c | |||
| fa55fd0f02 | |||
| d43774a8a1 | |||
| 13781df281 | |||
| 086db34c12 | |||
| b5b8344888 | |||
| 76a68adc1c | |||
| 24171dbaf8 | |||
| 9aadb8e6ad | |||
| 96d750fc88 | |||
| f1173adc6d | |||
| a2d2234a28 | |||
| 81353fc281 | |||
| 402354e122 | |||
| 9004413a33 | |||
| 6b5ac30f32 | |||
| 09adc8bdd0 | |||
| bddf675d66 | |||
| dd9bc0ddca | |||
| 65154a1aab | |||
| 2af7323a6a | |||
| 70e159cefc | |||
| 6609666279 | |||
| a3ce347f02 | |||
| d3fd21177b | |||
| feed194673 | |||
| f2cfbf6a5b | |||
| 94639858f2 | |||
| bd65d28ea3 | |||
| ee792b69bf | |||
| 89d9ad7557 | |||
| 3eae768d5a | |||
| b847dbca69 | |||
| 44fd61c039 | |||
| 2b338d53b1 | |||
| 1fb0091aa2 | |||
| a335f2b66c | |||
| 1d56aae0cb | |||
| ac3e8f5af9 | |||
| 8cd652fece | |||
| 6206b4be17 | |||
| d5d161c7f9 | |||
| 1413744b6f | |||
| 443d97f898 | |||
| a4b263cfd7 | |||
| 59048e9b5f | |||
| f654ec61ef | |||
| 46ac8ca0ca | |||
| a8830ea70d | |||
| 1d92a954d0 | |||
| d6955187cc | |||
| 1963a351d0 | |||
| a356a3fcbd | |||
| 95177b529e | |||
| fd545231d3 | |||
| e1fe030316 | |||
| c11c972c7c | |||
| 7c2d6c465e | |||
| 50d4ff9281 | |||
| 61282e1d1b | |||
| ee8da0b88c | |||
| c4150ab8ac | |||
| 833ac2446f | |||
| 2bc7a37082 | |||
| 9c84048730 | |||
| 651fb16feb | |||
| 1618b5fb57 | |||
| 47691a649f | |||
| b0737ea6ad | |||
| 21a99d9ec1 | |||
| b965dd0a6d | |||
| 43dc693061 | |||
| 9043cd4cb2 | |||
| d0a1d79fad | |||
| f3153c6793 | |||
| f836abd8f4 | |||
| 19d60a9225 | |||
| 73d1ad3492 | |||
| 9c6aa47cf7 | |||
| 92033fb94c | |||
| 707908fe03 | |||
| c62e958e91 | |||
| 908f5e72ed | |||
| a03a147d12 | |||
| 021285bc09 | |||
| c6559b3054 | |||
| 8518662eee | |||
| 90cc91ac86 | |||
| ad523d848f | |||
| 82b2138027 | |||
| 35c1c1c8a9 |
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: danak-admin
|
||||
description: Documents danak-admin React admin panel conventions — page structure, routing, TanStack Query hooks, axios services, sidebar, and Persian i18n. Use when adding pages or features, wiring API calls, updating navigation, or working in this repository.
|
||||
---
|
||||
|
||||
# Danak Admin
|
||||
|
||||
Persian (RTL) admin panel: React 19, Vite, TypeScript, React Router 7, TanStack Query, Formik/Yup, Tailwind, iconsax-react.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Location / tool |
|
||||
|-------|-----------------|
|
||||
| Routes (paths) | `src/config/Pages.ts` |
|
||||
| Route components | `src/router/Main.tsx` |
|
||||
| Auth routes | `src/router/Auth.tsx` |
|
||||
| HTTP client | `src/config/axios.ts` — `import axios from "../../../config/axios"` |
|
||||
| Shared UI | `src/components/` |
|
||||
| Layout | `src/shared/` (Header, SideBar, Footer) |
|
||||
| i18n | `src/langs/fa.json`, namespace `global`, `useTranslation('global')` |
|
||||
| Env | `VITE_BASE_URL`, `VITE_TOKEN_NAME`, `VITE_REFRESH_TOKEN_NAME` |
|
||||
|
||||
## Feature folder layout
|
||||
|
||||
Each domain lives under `src/pages/{feature}/`:
|
||||
|
||||
```
|
||||
{feature}/
|
||||
├── List.tsx # index table view
|
||||
├── Create.tsx # create form
|
||||
├── Update.tsx # edit form (uses :id param)
|
||||
├── hooks/use{Feature}Data.ts
|
||||
├── service/{Feature}Service.ts
|
||||
├── types/{Feature}Types.ts
|
||||
└── components/ # feature-specific UI (Delete, filters, etc.)
|
||||
```
|
||||
|
||||
Mirror an existing feature (e.g. `customer/`, `ads/`) before inventing new patterns.
|
||||
|
||||
## Data layer
|
||||
|
||||
**Service** — plain async functions, one API call each:
|
||||
|
||||
```typescript
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateCustomerType } from "../types/CustomerTypes";
|
||||
|
||||
export const getCustomers = async (page: number) => {
|
||||
const { data } = await axios.get(`/users/customers?page=${page}&paginate=1`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createCustomer = async (params: CreateCustomerType) => {
|
||||
const { data } = await axios.post(`/users/customers`, params);
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
**Hooks** — TanStack Query wrappers in `hooks/use{Feature}Data.ts`:
|
||||
|
||||
- Lists: `useQuery({ queryKey: ["customers", page], queryFn: () => api.getCustomers(page) })`
|
||||
- Detail: add `enabled: !!id`
|
||||
- Mutations: `useMutation` + `queryClient.invalidateQueries({ queryKey: ["customers"] })`
|
||||
- Export hooks as `useGetX`, `useCreateX`, `useUpdateX`, `useDeleteX`
|
||||
|
||||
**Types** — `{Feature}ItemType`, `Create{Feature}Type` in `types/`.
|
||||
|
||||
## Page patterns
|
||||
|
||||
**List** — header + optional filters + `PageLoading` while pending + `<table>` with `Td` + `Pagination`. Link actions via `Pages.{feature}.*`.
|
||||
|
||||
**Create / Update** — Formik + Yup validation, `toast(t('success'), 'success')` on success, `toast(error.response?.data?.error.message[0], 'error')` on error (`ErrorType` from `src/helpers/types.ts`). Import `toast` from `src/components/Toast`. Navigate back to list with `useNavigate()` + `Pages.*`.
|
||||
|
||||
**Delete** — small component using `ModalConfrim` + mutation hook; call `refetch()` or rely on query invalidation.
|
||||
|
||||
## Adding a feature
|
||||
|
||||
Follow the checklist in [add-feature.md](add-feature.md). Minimum steps:
|
||||
|
||||
1. Add paths to `src/config/Pages.ts`
|
||||
2. Create page files under `src/pages/{feature}/`
|
||||
3. Register `<Route>` entries in `src/router/Main.tsx` (static imports, no lazy loading)
|
||||
4. Add sidebar entry in `src/shared/SideBar.tsx` if user-facing
|
||||
5. Add translation keys to `src/langs/fa.json`
|
||||
|
||||
Dynamic routes use string concat: `Pages.customer.update + ":id"`.
|
||||
|
||||
## Sidebar & submenu
|
||||
|
||||
- Main sidebar shows **group** items (`menu`, `financial`, `other`, `content`, …) via `SideBarItem` with `name` matching the group key
|
||||
- Each group opens a side panel: `{Group}SubMenu.tsx` in `src/shared/components/` (e.g. `MenuSubMenu.tsx`)
|
||||
- Map URL → group in `getSubMenuNameFromPath` (`src/config/SideBarSubMenu.ts`)
|
||||
- Flat links inside a group: `SubMenuItem`
|
||||
- Nested feature menus inside a group: `SubMenuAccordion` (arrow open/close) wrapping `SubMenuItem` children
|
||||
- `subMenuName` comes from the group key, not the feature path segment
|
||||
|
||||
## Conventions
|
||||
|
||||
- Components: `const MyPage: FC = () => { ... }; export default MyPage`
|
||||
- Styling: Tailwind utility classes; `clx()` from `src/helpers/utils` for conditional classes
|
||||
- Icons: `iconsax-react` (Outline/Bold variants, `#8C90A3` inactive / `black` active)
|
||||
- Do not add lazy-loaded routes — project uses direct imports in `Main.tsx`
|
||||
- Keep API paths relative to `VITE_BASE_URL`; auth token is attached automatically by axios interceptor
|
||||
- Prefer reusing `Button`, `Input`, `Select`, `Textarea`, `Pagination`, `PageLoading`, `ModalConfrim` from `src/components/`
|
||||
|
||||
## Do not
|
||||
|
||||
- Commit `.env` or secrets
|
||||
- Introduce new state libraries (Zustand is only used in `sharedStore` for layout)
|
||||
- Add English-only UI strings — add keys to `fa.json`
|
||||
- Change axios/auth/token refresh logic unless explicitly requested
|
||||
@@ -0,0 +1,83 @@
|
||||
# Add Feature Checklist
|
||||
|
||||
Copy and track when adding a new admin section (e.g. `coupons`):
|
||||
|
||||
```
|
||||
- [ ] Pages.ts paths
|
||||
- [ ] types/{Feature}Types.ts
|
||||
- [ ] service/{Feature}Service.ts
|
||||
- [ ] hooks/use{Feature}Data.ts
|
||||
- [ ] List.tsx (+ Create/Update if CRUD)
|
||||
- [ ] components/ (Delete, filters, etc.)
|
||||
- [ ] Main.tsx routes + imports
|
||||
- [ ] SideBar.tsx (+ SubMenu if multi-page)
|
||||
- [ ] fa.json translations
|
||||
```
|
||||
|
||||
## 1. Pages.ts
|
||||
|
||||
```typescript
|
||||
coupons: {
|
||||
list: "/coupons/list",
|
||||
create: "/coupons/create",
|
||||
detail: "/coupons/detail/",
|
||||
},
|
||||
```
|
||||
|
||||
Use kebab-case URL segments consistent with existing routes.
|
||||
|
||||
## 2. Service + hooks + types
|
||||
|
||||
Copy structure from `src/pages/customer/` or `src/pages/ads/`. Adjust API paths to match backend.
|
||||
|
||||
## 3. Main.tsx
|
||||
|
||||
Add import at top, then route(s):
|
||||
|
||||
```tsx
|
||||
import CouponsList from "../pages/coupons/List";
|
||||
// ...
|
||||
<Route path={Pages.coupons.list} element={<CouponsList />} />
|
||||
<Route path={Pages.coupons.create} element={<CreateCoupon />} />
|
||||
<Route path={Pages.coupons.detail + ":id"} element={<UpdateCoupon />} />
|
||||
```
|
||||
|
||||
## 4. Sidebar (optional)
|
||||
|
||||
**Single-page feature** — add `SideBarItem`:
|
||||
|
||||
```tsx
|
||||
<SideBarItem
|
||||
icon={<TicketDiscount variant={isActive("coupons") ? "Bold" : "Outline"} color={isActive("coupons") ? "black" : "#8C90A3"} size={iconSizeSideBar} />}
|
||||
title={t("sidebar.coupons")}
|
||||
isActive={isActive("coupons")}
|
||||
link={Pages.coupons.list}
|
||||
activeName="coupons"
|
||||
/>
|
||||
```
|
||||
|
||||
**Multi-page feature** — also:
|
||||
|
||||
1. Add the feature’s URL segment → group mapping in `getSubMenuNameFromPath` / `pathSegmentToSubMenu` (`src/config/SideBarSubMenu.ts`)
|
||||
2. Add links under the matching group SubMenu (e.g. `MenuSubMenu.tsx`) using `SubMenuItem`
|
||||
3. If the feature has nested pages, wrap them in `SubMenuAccordion` (arrow open/close) inside that group SubMenu
|
||||
4. Do not add a new top-level sidebar group unless the feature is a new section (reseller/dmenu-style)
|
||||
|
||||
## 5. Translations
|
||||
|
||||
Add keys under logical groups in `src/langs/fa.json`:
|
||||
|
||||
```json
|
||||
"sidebar": { "coupons": "کوپنها" },
|
||||
"submenu": { "coupon_list": "لیست کوپنها" },
|
||||
"coupon": { "coupon_list": "لیست کوپنها", "add_coupon": "افزودن کوپن" }
|
||||
```
|
||||
|
||||
Use `t('coupon.add_coupon')` in components.
|
||||
|
||||
## 6. Verify
|
||||
|
||||
- `npm run build` passes
|
||||
- List loads, create/update navigate correctly
|
||||
- Sidebar highlights on correct routes
|
||||
- Mutation invalidates list query cache
|
||||
@@ -1,4 +1,5 @@
|
||||
VITE_TOKEN_NAME = 'admin_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||
VITE_BASE_URL = 'https://api.danakcorp.com'
|
||||
# VITE_BASE_URL = 'http://10.73.49.88:3500'
|
||||
|
||||
# VITE_BASE_URL = 'http://192.168.99.131:3500'
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 200,
|
||||
"singleAttributePerLine": true
|
||||
}
|
||||
Generated
+23
-15
@@ -32,7 +32,6 @@
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-router-dom": "^7.1.0",
|
||||
"react-spinners": "^0.15.0",
|
||||
"react-toastify": "^11.0.2",
|
||||
"swiper": "^11.2.1",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
@@ -114,6 +113,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
|
||||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.26.0",
|
||||
@@ -2413,6 +2413,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -2425,6 +2426,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
@@ -3073,6 +3075,7 @@
|
||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7",
|
||||
@@ -3174,6 +3177,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -3250,6 +3254,7 @@
|
||||
"integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.18.1",
|
||||
"@typescript-eslint/types": "8.18.1",
|
||||
@@ -3458,6 +3463,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3813,6 +3819,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
@@ -4543,6 +4550,7 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5457,6 +5465,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
},
|
||||
@@ -6787,6 +6796,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -6953,6 +6963,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -7088,6 +7099,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7103,6 +7115,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.25.0"
|
||||
},
|
||||
@@ -7298,19 +7311,6 @@
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.2.tgz",
|
||||
"integrity": "sha512-GjHuGaiXMvbls3ywqv8XdWONwrcO4DXCJIY1zVLkHU73gEElKvTTXNI5Vom3s/k/M8hnkrfsqgBSX3OwmlonbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -7338,7 +7338,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
@@ -7517,6 +7518,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
|
||||
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.7"
|
||||
},
|
||||
@@ -8420,6 +8422,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -8583,6 +8586,7 @@
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -8790,6 +8794,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -8911,6 +8916,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -9201,6 +9207,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -9262,6 +9269,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-router-dom": "^7.1.0",
|
||||
"react-spinners": "^0.15.0",
|
||||
"react-toastify": "^11.0.2",
|
||||
"swiper": "^11.2.1",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
|
||||
+41
-54
@@ -1,30 +1,30 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import 'swiper/swiper-bundle.css';
|
||||
import 'rc-rate/assets/index.css';
|
||||
import { QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import i18next from "i18next";
|
||||
import "quill/dist/quill.snow.css";
|
||||
import "react-multi-date-picker/styles/layouts/mobile.css"
|
||||
import i18next from 'i18next'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ToastContainer } from 'react-toastify'
|
||||
import FaJson from './langs/fa.json'
|
||||
import { IApiErrorRepsonse } from './types/error.types'
|
||||
import MainRouter from './router/Main'
|
||||
import AuthRouter from './router/Auth'
|
||||
import { Pages } from './config/Pages'
|
||||
import { getRefreshToken, setToken, setRefreshToken, removeToken, removeRefreshToken, getToken } from './config/func';
|
||||
import { refreshToken } from './pages/auth/service/AuthService';
|
||||
import "rc-rate/assets/index.css";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import "react-multi-date-picker/styles/layouts/mobile.css";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import "swiper/swiper-bundle.css";
|
||||
import ToastContainer from "./components/Toast";
|
||||
import { Pages } from "./config/Pages";
|
||||
import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from "./config/func";
|
||||
import FaJson from "./langs/fa.json";
|
||||
import { refreshToken } from "./pages/auth/service/AuthService";
|
||||
import AuthRouter from "./router/Auth";
|
||||
import MainRouter from "./router/Main";
|
||||
import { IApiErrorRepsonse } from "./types/error.types";
|
||||
|
||||
i18next.init({
|
||||
interpolation: { escapeValue: false },
|
||||
lng: 'fa',
|
||||
lng: "fa",
|
||||
resources: {
|
||||
fa: {
|
||||
global: FaJson
|
||||
}
|
||||
}
|
||||
})
|
||||
global: FaJson,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -40,7 +40,7 @@ const queryClient = new QueryClient({
|
||||
// Use a flag to track if refresh is in progress
|
||||
if (window.isRefreshingToken) {
|
||||
// Wait for the refresh to complete
|
||||
await new Promise(resolve => {
|
||||
await new Promise((resolve) => {
|
||||
const checkComplete = setInterval(() => {
|
||||
if (!window.isRefreshingToken) {
|
||||
clearInterval(checkComplete);
|
||||
@@ -56,7 +56,7 @@ const queryClient = new QueryClient({
|
||||
|
||||
try {
|
||||
const refreshTokenValue = await getRefreshToken();
|
||||
const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' });
|
||||
const { data } = await refreshToken({ refreshToken: refreshTokenValue || "" });
|
||||
|
||||
if (data?.accessToken?.token) {
|
||||
// Save the new token
|
||||
@@ -71,67 +71,54 @@ const queryClient = new QueryClient({
|
||||
queryClient.invalidateQueries();
|
||||
return;
|
||||
} else {
|
||||
throw new Error('Invalid token response');
|
||||
throw new Error("Invalid token response");
|
||||
}
|
||||
} finally {
|
||||
window.isRefreshingToken = false;
|
||||
}
|
||||
} catch (refreshError: unknown) {
|
||||
console.error('Token refresh failed:', refreshError);
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
// Clear tokens and redirect to login
|
||||
await removeToken();
|
||||
await removeRefreshToken();
|
||||
window.location.href = '/auth/login';
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false
|
||||
retry: false,
|
||||
// staleTime: 86400000 // 1 day in milliseconds
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const App: FC = () => {
|
||||
|
||||
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
||||
const [isLogin, setIsLogin] = useState<"checking" | "isLogin" | "isNotLogin">("checking");
|
||||
useEffect(() => {
|
||||
const token = getToken()
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
setIsLogin('isLogin')
|
||||
setIsLogin("isLogin");
|
||||
} else {
|
||||
setIsLogin('isNotLogin')
|
||||
if (window.location.href.split('auth').length === 1) {
|
||||
window.location.href = Pages.auth.login
|
||||
setIsLogin("isNotLogin");
|
||||
if (window.location.href.split("auth").length === 1) {
|
||||
window.location.href = Pages.auth.login;
|
||||
}
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
{
|
||||
isLogin === 'checking' ?
|
||||
null
|
||||
:
|
||||
isLogin === 'isLogin' ?
|
||||
<MainRouter />
|
||||
:
|
||||
<AuthRouter />
|
||||
}
|
||||
{isLogin === "checking" ? null : isLogin === "isLogin" ? <MainRouter /> : <AuthRouter />}
|
||||
<ToastContainer />
|
||||
</I18nextProvider>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -1,4 +1,5 @@
|
||||
import { FC, Fragment, ReactNode, useEffect } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
import HeaderModal from './HeaderModal'
|
||||
|
||||
interface Props {
|
||||
@@ -8,6 +9,8 @@ interface Props {
|
||||
isHeader?: boolean,
|
||||
title_header?: string,
|
||||
width?: number
|
||||
/** Raise above popovers (z-80) when confirm opens from nested panels */
|
||||
elevated?: boolean
|
||||
}
|
||||
|
||||
const DefaulModal: FC<Props> = (props: Props) => {
|
||||
@@ -26,9 +29,16 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
||||
{
|
||||
props.open && (
|
||||
<Fragment>
|
||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-y-auto fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
||||
<div className='relative xl:h-full h-[80%] bottom-0 left-0 flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
||||
<div className='border-0 h-auto p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
||||
<div
|
||||
style={{ maxWidth: props.width }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className={clx(
|
||||
'xl:justify-center xl:items-center items-end flex overflow-hidden fixed inset-0 h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto',
|
||||
props.elevated ? 'z-[100]' : 'z-[60]',
|
||||
)}
|
||||
>
|
||||
<div className='relative max-h-[85vh] bottom-0 left-0 flex xl:items-center items-end w-full xl:my-6 xl:p-2'>
|
||||
<div className='border-0 max-h-[85vh] p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
||||
|
||||
|
||||
{
|
||||
@@ -46,7 +56,14 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 modalGlass inset-0 z-50 '></div>
|
||||
<div
|
||||
onClick={props.close}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
className={clx(
|
||||
'fixed size-full top-0 bottom-0 right-0 modalGlass inset-0',
|
||||
props.elevated ? 'z-[90]' : 'z-50',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Button from "./Button";
|
||||
import DefaulModal from "./DefaulModal";
|
||||
import Input from "./Input";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
onConfirm: () => void;
|
||||
isLoading?: boolean;
|
||||
itemName: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const DeleteNameConfirmModal: FC<Props> = ({
|
||||
isOpen,
|
||||
close,
|
||||
onConfirm,
|
||||
isLoading = false,
|
||||
itemName,
|
||||
message,
|
||||
}) => {
|
||||
const { t } = useTranslation("global");
|
||||
const [typedName, setTypedName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setTypedName("");
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const isNameMatched = typedName.trim() === itemName.trim();
|
||||
|
||||
return (
|
||||
<DefaulModal
|
||||
open={isOpen}
|
||||
close={close}
|
||||
title_header={t("confrim.subject")}
|
||||
isHeader
|
||||
>
|
||||
<div className="mt-6">
|
||||
<div className="text-sm text-center">
|
||||
{message ?? t("taskmanager.delete_confirm_message", { name: itemName })}
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<Input
|
||||
value={typedName}
|
||||
onChange={(e) => setTypedName(e.target.value)}
|
||||
placeholder={t("taskmanager.delete_confirm_placeholder")}
|
||||
className="text-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 justify-center mt-10">
|
||||
<Button
|
||||
label={t("confrim.yes")}
|
||||
onClick={onConfirm}
|
||||
isLoading={isLoading}
|
||||
disabled={!isNameMatched}
|
||||
/>
|
||||
<Button
|
||||
label={t("confrim.cancel")}
|
||||
className="bg-transparent text-black border border-primary"
|
||||
onClick={close}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteNameConfirmModal;
|
||||
+84
-92
@@ -1,115 +1,107 @@
|
||||
import { FC, InputHTMLAttributes, useEffect, useState } from 'react'
|
||||
import { clx } from '../helpers/utils';
|
||||
import EyeIcon from '../assets/images/eye.svg'
|
||||
import { SearchNormal } from 'iconsax-react';
|
||||
import Error from './Error';
|
||||
import { SearchNormal } from "iconsax-react";
|
||||
import { FC, InputHTMLAttributes, useEffect, useRef, useState } from "react";
|
||||
import EyeIcon from "../assets/images/eye.svg";
|
||||
import { clx } from "../helpers/utils";
|
||||
import Error from "./Error";
|
||||
|
||||
type Variant = "floating_outlined" | "primary" | "search";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
className?: string;
|
||||
variant?: Variant;
|
||||
error_text?: string;
|
||||
onEnter?: () => void;
|
||||
unit?: string;
|
||||
seprator?: boolean;
|
||||
isNotRequired?: boolean;
|
||||
onChangeSearchFinal?: (value: string) => void;
|
||||
} & InputHTMLAttributes<HTMLInputElement>
|
||||
label?: string;
|
||||
className?: string;
|
||||
variant?: Variant;
|
||||
error_text?: string;
|
||||
onEnter?: () => void;
|
||||
unit?: string;
|
||||
seprator?: boolean;
|
||||
isNotRequired?: boolean;
|
||||
onChangeSearchFinal?: (value: string) => void;
|
||||
labelClassName?: string;
|
||||
} & InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const formatNumber = (value: string | number): string => {
|
||||
if (!value) return '';
|
||||
const inputValue = String(value).replace(/,/g, '');
|
||||
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
if (!value) return "";
|
||||
const inputValue = String(value).replace(/,/g, "");
|
||||
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
};
|
||||
|
||||
const Input: FC<Props> = (props: Props) => {
|
||||
const [formattedValue, setFormattedValue] = useState<string>(props.value ? formatNumber(props.value as string) : "");
|
||||
|
||||
const [formattedValue, setFormattedValue] = useState<string>(
|
||||
props.value ? formatNumber(props.value as string) : ''
|
||||
);
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const onChangeSearchFinalRef = useRef(props.onChangeSearchFinal);
|
||||
onChangeSearchFinalRef.current = props.onChangeSearchFinal;
|
||||
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const inputClass = clx(
|
||||
"w-full h-10 text-black block px-4 text-xs rounded-xl border border-border",
|
||||
props.readOnly && "bg-gray-100 border-0 text-description",
|
||||
props.variant === "search" && "bg-[#EEF0F7] border-0 ps-10",
|
||||
props.className,
|
||||
);
|
||||
|
||||
const inputClass = clx(
|
||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
|
||||
props.className
|
||||
);
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (props.seprator) {
|
||||
const inputValue = event.target.value.replace(/,/g, ""); // حذف کاماها
|
||||
const formatted = formatNumber(inputValue);
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (props.seprator) {
|
||||
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
|
||||
const formatted = formatNumber(inputValue);
|
||||
// بهروزرسانی مقدار قالببندیشده
|
||||
setFormattedValue(formatted);
|
||||
|
||||
// بهروزرسانی مقدار قالببندیشده
|
||||
setFormattedValue(formatted);
|
||||
// ارسال مقدار خام به `onChange` والد
|
||||
props.onChange?.({
|
||||
...event,
|
||||
target: {
|
||||
...event.target,
|
||||
value: inputValue,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
props.onChange?.(event);
|
||||
}
|
||||
};
|
||||
|
||||
// ارسال مقدار خام به `onChange` والد
|
||||
props.onChange?.({
|
||||
...event,
|
||||
target: {
|
||||
...event.target,
|
||||
value: inputValue,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
props.onChange?.(event);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setFormattedValue(formatNumber(props.value as string));
|
||||
} else {
|
||||
setFormattedValue("");
|
||||
}
|
||||
}, [props.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setFormattedValue(formatNumber(props.value as string));
|
||||
} else {
|
||||
setFormattedValue('');
|
||||
}
|
||||
}, [props.value])
|
||||
useEffect(() => {
|
||||
if (props.variant !== "search" || !onChangeSearchFinalRef.current) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (props.variant === 'search' && props.onChangeSearchFinal) {
|
||||
const timeout = setTimeout(() => {
|
||||
props.onChangeSearchFinal?.(search)
|
||||
}, 1000)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [search, props])
|
||||
const timeout = setTimeout(() => {
|
||||
onChangeSearchFinalRef.current?.(search);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [search, props.variant]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label className={clx("text-sm", props.labelClassName)}>{props.label}</label>
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<label className='text-sm'>
|
||||
{props.label}
|
||||
</label>
|
||||
<div className="w-full relative mt-1">
|
||||
<input
|
||||
{...props}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
handleInputChange(e);
|
||||
}}
|
||||
value={props.seprator ? formattedValue : props.value}
|
||||
type={props.type === "password" && showPassword ? "text" : props.type === "password" ? "password" : undefined}
|
||||
className={inputClass}
|
||||
/>
|
||||
|
||||
<div className='w-full relative mt-1'>
|
||||
<input {...props} onChange={(e) => {
|
||||
setSearch(e.target.value)
|
||||
handleInputChange(e)
|
||||
}} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
||||
{props.type === "password" && <img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className="w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3" />}
|
||||
|
||||
{
|
||||
props.type === 'password' &&
|
||||
<img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
|
||||
}
|
||||
{props.variant === "search" && <SearchNormal size={20} color="#8C90A3" className="absolute top-0 w-5 bottom-0 my-auto right-3" />}
|
||||
|
||||
{
|
||||
props.variant === 'search' &&
|
||||
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
||||
}
|
||||
{props.error_text && <Error errorText={props.error_text} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
props.error_text &&
|
||||
<Error
|
||||
errorText={props.error_text}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
export default Input;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FC, useState } from 'react'
|
||||
import DefaulModal from './DefaulModal'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from './Button'
|
||||
import DefaulModal from './DefaulModal'
|
||||
import Textarea from './Textarea'
|
||||
|
||||
type Props = {
|
||||
@@ -18,12 +19,13 @@ const ModalConfrim: FC<Props> = (props: Props) => {
|
||||
const { t } = useTranslation('global')
|
||||
const [description, setDescription] = useState<string>('')
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<DefaulModal
|
||||
open={props.isOpen}
|
||||
close={props.close}
|
||||
title_header={t('confrim.subject')}
|
||||
isHeader
|
||||
elevated
|
||||
>
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm text-center'>
|
||||
@@ -60,7 +62,8 @@ const ModalConfrim: FC<Props> = (props: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
</DefaulModal>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+101
-74
@@ -1,64 +1,90 @@
|
||||
import React from "react";
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
limit?: number;
|
||||
onLimitChange?: (limit: number) => void;
|
||||
limitOptions?: number[];
|
||||
}
|
||||
|
||||
const Pagination: React.FC<PaginationProps> = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}) => {
|
||||
const getPageNumbers = () => {
|
||||
const pageNumbers: (number | string)[] = [];
|
||||
const maxVisiblePages = 5;
|
||||
const Pagination: React.FC<PaginationProps> = ({ currentPage, totalPages, onPageChange, limit = 10, onLimitChange, limitOptions = [10, 20, 50] }) => {
|
||||
const handleLimitChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onLimitChange?.(Number(event.target.value));
|
||||
onPageChange(1);
|
||||
};
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// اگر تعداد صفحات کم است، همه را نشان بده
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
} else {
|
||||
// همیشه صفحه اول را نشان بده
|
||||
pageNumbers.push(1);
|
||||
const getPageNumbers = () => {
|
||||
const pageNumbers: (number | string)[] = [];
|
||||
const maxVisiblePages = 5;
|
||||
|
||||
if (currentPage <= 3) {
|
||||
// در صفحات اولیه (1, 2, 3)
|
||||
for (let i = 2; i <= Math.min(4, totalPages - 1); i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
if (totalPages > 5) {
|
||||
pageNumbers.push("...");
|
||||
}
|
||||
pageNumbers.push(totalPages);
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
// در صفحات آخر
|
||||
pageNumbers.push("...");
|
||||
for (let i = totalPages - 3; i <= totalPages; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
} else {
|
||||
// در صفحات وسط
|
||||
pageNumbers.push("...");
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
pageNumbers.push("...");
|
||||
pageNumbers.push(totalPages);
|
||||
}
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// اگر تعداد صفحات کم است، همه را نشان بده
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
} else {
|
||||
// همیشه صفحه اول را نشان بده
|
||||
pageNumbers.push(1);
|
||||
|
||||
if (currentPage <= 3) {
|
||||
// در صفحات اولیه (1, 2, 3)
|
||||
for (let i = 2; i <= Math.min(4, totalPages - 1); i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
if (totalPages > 5) {
|
||||
pageNumbers.push("...");
|
||||
}
|
||||
pageNumbers.push(totalPages);
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
// در صفحات آخر
|
||||
pageNumbers.push("...");
|
||||
for (let i = totalPages - 3; i <= totalPages; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
} else {
|
||||
// در صفحات وسط
|
||||
pageNumbers.push("...");
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
pageNumbers.push("...");
|
||||
pageNumbers.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pageNumbers;
|
||||
};
|
||||
return pageNumbers;
|
||||
};
|
||||
|
||||
const pageNumbers = getPageNumbers();
|
||||
const pageNumbers = getPageNumbers();
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 text-xs justify-center items-center mt-4">
|
||||
{/* دکمه قبلی */}
|
||||
{/* <button
|
||||
return (
|
||||
<div className="flex flex-wrap gap-4 text-xs justify-center items-center mt-4">
|
||||
{onLimitChange && (
|
||||
<label className="flex items-center gap-2 text-gray-600">
|
||||
<span>تعداد در صفحه</span>
|
||||
<select
|
||||
aria-label="تعداد آیتم در صفحه"
|
||||
className="h-8 rounded-md border border-[#EAECF5] bg-white px-2 outline-none focus:border-primary"
|
||||
value={limit}
|
||||
onChange={handleLimitChange}
|
||||
>
|
||||
{limitOptions.map((option) => (
|
||||
<option
|
||||
key={option}
|
||||
value={option}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* دکمه قبلی */}
|
||||
{/* <button
|
||||
className={`px-3 py-1 rounded-md ${currentPage === 1
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-blue-600 hover:bg-gray-100"
|
||||
@@ -69,28 +95,28 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
قبلی
|
||||
</button> */}
|
||||
|
||||
{/* شماره صفحات */}
|
||||
{pageNumbers.map((page, index) =>
|
||||
typeof page === "number" ? (
|
||||
<button
|
||||
key={`page-${page}`}
|
||||
className={`size-8 rounded-md ${currentPage === page
|
||||
? "bg-primary text-white"
|
||||
: "text-primary bg-[#EAECF5] hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() => onPageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
) : (
|
||||
<span key={`ellipsis-${index}`} className="px-3 py-1 text-gray-500">
|
||||
{page}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{/* شماره صفحات */}
|
||||
{pageNumbers.map((page, index) =>
|
||||
typeof page === "number" ? (
|
||||
<button
|
||||
key={`page-${page}`}
|
||||
className={`size-8 rounded-md ${currentPage === page ? "bg-primary text-white" : "text-primary bg-[#EAECF5] hover:bg-gray-100"}`}
|
||||
onClick={() => onPageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
key={`ellipsis-${index}`}
|
||||
className="px-3 py-1 text-gray-500"
|
||||
>
|
||||
{page}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* دکمه بعدی */}
|
||||
{/* <button
|
||||
{/* دکمه بعدی */}
|
||||
{/* <button
|
||||
className={`px-3 py-1 rounded-md ${currentPage === totalPages
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-blue-600 hover:bg-gray-100"
|
||||
@@ -100,8 +126,9 @@ const Pagination: React.FC<PaginationProps> = ({
|
||||
>
|
||||
بعدی
|
||||
</button> */}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
export default Pagination;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { type FC } from "react";
|
||||
|
||||
type Props = {
|
||||
value: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ProgressBar: FC<Props> = ({ value, className = "" }) => {
|
||||
const progress = Math.min(100, Math.max(0, value));
|
||||
|
||||
return (
|
||||
<div className={`flex-1 h-[5px] rounded-full bg-white/40 flex overflow-hidden ${className}`}>
|
||||
<div className="h-full rounded-full bg-black" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressBar;
|
||||
+39
-56
@@ -1,62 +1,45 @@
|
||||
import { FC, SelectHTMLAttributes } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
import { ArrowDown2 } from 'iconsax-react'
|
||||
import Error from './Error'
|
||||
import { ArrowDown2 } from "iconsax-react";
|
||||
import { FC, SelectHTMLAttributes } from "react";
|
||||
import { clx } from "../helpers/utils";
|
||||
import Error from "./Error";
|
||||
|
||||
export type ItemsSelectType = {
|
||||
value: string,
|
||||
label: string,
|
||||
}
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
type Props = {
|
||||
className?: string,
|
||||
items: ItemsSelectType[],
|
||||
error_text?: string,
|
||||
placeholder?: string,
|
||||
label?: string,
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||
className?: string;
|
||||
items: ItemsSelectType[];
|
||||
error_text?: string;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
labelClassName?: string;
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>;
|
||||
|
||||
const Select: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='w-full relative'>
|
||||
{
|
||||
props.label &&
|
||||
<label className='text-sm'>
|
||||
{props.label}
|
||||
</label>
|
||||
}
|
||||
<select {...props} className={clx(
|
||||
'w-full text-black block border appearance-none border-border px-2.5 h-10 text-sm rounded-2.5 bg-gray',
|
||||
props.className,
|
||||
props.label && 'mt-1'
|
||||
)}>
|
||||
{
|
||||
props.placeholder &&
|
||||
<option value="" disabled selected={!props.value}>{props.placeholder}</option>
|
||||
}
|
||||
{
|
||||
props.items?.map((item) => {
|
||||
return (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<ArrowDown2 size={16} color='black' className={clx(
|
||||
'absolute z-0 top-3 left-2',
|
||||
props.label && 'top-10'
|
||||
)} />
|
||||
{
|
||||
props.error_text && props.error_text !== '' ?
|
||||
<Error
|
||||
errorText={props.error_text}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
return (
|
||||
<div className="w-full relative">
|
||||
{props.label && <label className={clx("text-sm", props.labelClassName)}>{props.label}</label>}
|
||||
<div className={clx("relative", props.label && "mt-1")}>
|
||||
<select {...props} className={clx("w-full text-black block border appearance-none border-border px-2.5 h-10 text-sm rounded-2.5 bg-gray", props.className)}>
|
||||
{props.placeholder && (
|
||||
<option value="" disabled selected={!props.value}>
|
||||
{props.placeholder}
|
||||
</option>
|
||||
)}
|
||||
{props.items?.map((item) => {
|
||||
return (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<ArrowDown2 size={16} color="black" className="absolute z-0 top-0 bottom-0 my-auto left-2" />
|
||||
</div>
|
||||
{props.error_text && props.error_text !== "" ? <Error errorText={props.error_text} /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Select
|
||||
export default Select;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type?: 'success' | 'error' | 'info';
|
||||
}
|
||||
|
||||
let addToast: (toast: Toast) => void = () => {};
|
||||
|
||||
const ToastContainer: React.FC = () => {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
const showToast = (toastItem: Toast) => {
|
||||
setToasts((prev) => [...prev, toastItem]);
|
||||
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== toastItem.id));
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
addToast = showToast;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
|
||||
{toasts.map((toastItem) => (
|
||||
<div
|
||||
key={toastItem.id}
|
||||
className="px-4 flex items-center gap-2 backdrop-blur-2xl h-16 min-w-[300px] rounded-2xl shadow-md transition-transform bg-white/70 text-black"
|
||||
>
|
||||
{toastItem.type === 'success' ? (
|
||||
<TickCircle className="size-5" color="green" />
|
||||
) : toastItem.type === 'error' ? (
|
||||
<CloseCircle className="size-5" color="red" />
|
||||
) : (
|
||||
<InfoCircle className="size-5" color="blue" />
|
||||
)}
|
||||
{toastItem.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const toast = (message: string | undefined, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
if (!message) return;
|
||||
addToast({ id: `${Date.now()}-${Math.random()}`, message, type });
|
||||
};
|
||||
|
||||
export default ToastContainer;
|
||||
@@ -1,30 +1,23 @@
|
||||
import { Trash } from 'iconsax-react'
|
||||
import { FC, useState } from 'react'
|
||||
import ModalConfrim from './ModalConfrim'
|
||||
import { Trash } from "iconsax-react";
|
||||
import { FC, useState } from "react";
|
||||
import ModalConfrim from "./ModalConfrim";
|
||||
|
||||
interface Props {
|
||||
onDelete: () => void,
|
||||
isLoading?: boolean
|
||||
onDelete: () => void;
|
||||
isLoading?: boolean;
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const TrashWithConfrim: FC<Props> = ({ onDelete, isLoading = false }) => {
|
||||
const [isConfirm, setIsConfirm] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
<Trash
|
||||
onClick={() => setIsConfirm(true)}
|
||||
className='size-5'
|
||||
color='#888'
|
||||
/>
|
||||
const TrashWithConfrim: FC<Props> = ({ onDelete, isLoading = false, size = 20, color = "#888" }) => {
|
||||
const [isConfirm, setIsConfirm] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<Trash onClick={() => setIsConfirm(true)} color={color} size={size} />
|
||||
|
||||
<ModalConfrim
|
||||
isOpen={isConfirm}
|
||||
close={() => setIsConfirm(false)}
|
||||
onConfrim={() => onDelete()}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ModalConfrim isOpen={isConfirm} close={() => setIsConfirm(false)} onConfrim={() => onDelete()} isLoading={isLoading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrashWithConfrim
|
||||
export default TrashWithConfrim;
|
||||
|
||||
@@ -12,6 +12,34 @@ type Props = {
|
||||
accept?: string;
|
||||
}
|
||||
|
||||
const MIME_TO_EXTENSIONS: Record<string, string[]> = {
|
||||
'image/png': ['.png'],
|
||||
'image/svg+xml': ['.svg'],
|
||||
'image/jpeg': ['.jpg', '.jpeg'],
|
||||
'image/gif': ['.gif'],
|
||||
'image/webp': ['.webp'],
|
||||
}
|
||||
|
||||
const buildAcceptTypes = (accept?: string) => {
|
||||
if (!accept) return undefined
|
||||
|
||||
return accept.split(',').reduce<Record<string, string[]>>((acc, type) => {
|
||||
const trimmed = type.trim()
|
||||
if (trimmed.startsWith('.')) {
|
||||
const ext = trimmed.toLowerCase()
|
||||
const mimeEntry = Object.entries(MIME_TO_EXTENSIONS).find(([, exts]) => exts.includes(ext))
|
||||
if (mimeEntry) {
|
||||
acc[mimeEntry[0]] = mimeEntry[1]
|
||||
}
|
||||
} else if (MIME_TO_EXTENSIONS[trimmed]) {
|
||||
acc[trimmed] = MIME_TO_EXTENSIONS[trimmed]
|
||||
} else {
|
||||
acc[trimmed] = []
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const UploadBox: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
@@ -19,8 +47,7 @@ const UploadBox: FC<Props> = (props: Props) => {
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
const array = [...files, ...acceptedFiles]
|
||||
setFiles(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array);
|
||||
@@ -33,9 +60,11 @@ const UploadBox: FC<Props> = (props: Props) => {
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files])
|
||||
const acceptTypes = buildAcceptTypes(props.accept)
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop,
|
||||
accept: props.accept ? { [props.accept]: [] } : undefined
|
||||
accept: acceptTypes,
|
||||
multiple: props.isMultiple,
|
||||
})
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
|
||||
@@ -36,6 +36,9 @@ export const Pages = {
|
||||
create: "/tickets/create",
|
||||
detail: "/tickets/messages/",
|
||||
category: "/tickets/category",
|
||||
masters: "/tickets/masters",
|
||||
mastersCreate: "/tickets/masters/create",
|
||||
mastersUpdate: "/tickets/masters/update/",
|
||||
},
|
||||
announcement: {
|
||||
list: "/announcement",
|
||||
@@ -81,6 +84,7 @@ export const Pages = {
|
||||
detail: "/users/detail/",
|
||||
roleList: "/users/roles-all",
|
||||
roleCreate: "/users/roles-add",
|
||||
roleUpdate: "/users/roles-edit/",
|
||||
groupAdd: "/users/group-add",
|
||||
groupList: "/users/groups",
|
||||
},
|
||||
@@ -168,5 +172,11 @@ export const Pages = {
|
||||
},
|
||||
taskmanager: {
|
||||
workspace: "/taskmanager/workspace/",
|
||||
workspaceList: "/workspace/list",
|
||||
createWorkspace: "/workspace/create",
|
||||
updateWorkspace: "/workspace/update/",
|
||||
createProject: "/project/create",
|
||||
updateProject: "/project/update/",
|
||||
projectList: "/project/list/",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
export const SideBarItemHasSubMenu = [
|
||||
"services",
|
||||
"customers",
|
||||
"receipts",
|
||||
"representatives",
|
||||
"users",
|
||||
"tickets",
|
||||
"learning",
|
||||
"blog",
|
||||
"taskmanager",
|
||||
"content",
|
||||
"financial",
|
||||
"support",
|
||||
"accessLogs",
|
||||
"other",
|
||||
"products",
|
||||
];
|
||||
|
||||
const pathSegmentToSubMenu: Record<string, string> = {
|
||||
services: "services",
|
||||
customers: "customers",
|
||||
representatives: "representatives",
|
||||
reseller: "representatives",
|
||||
users: "users",
|
||||
ads: "content",
|
||||
announcement: "content",
|
||||
blog: "content",
|
||||
learning: "content",
|
||||
sliders: "content",
|
||||
payments: "financial",
|
||||
receipts: "financial",
|
||||
transactions: "financial",
|
||||
discounts: "financial",
|
||||
"card-bank": "financial",
|
||||
support: "support",
|
||||
tickets: "support",
|
||||
feedback: "other",
|
||||
"access-logs": "other",
|
||||
criticisms: "other",
|
||||
messages: "other",
|
||||
dmenu: "products",
|
||||
dkala: "products",
|
||||
dpage: "products",
|
||||
dmail: "products",
|
||||
};
|
||||
|
||||
export const getSubMenuNameFromPath = (pathname: string): string => {
|
||||
if (pathname.startsWith("/customers/subscriptions")) {
|
||||
return "customers";
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/workspace") || pathname.startsWith("/project") || pathname.startsWith("/taskmanager")) {
|
||||
return "taskmanager";
|
||||
}
|
||||
|
||||
const segment = pathname.split("/")[1] ?? "";
|
||||
return pathSegmentToSubMenu[segment] ?? "";
|
||||
};
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
export enum PermissionGroupEnum {
|
||||
SERVICES = "services",
|
||||
CUSTOMERS = "customers",
|
||||
USERS = "users",
|
||||
FINANCIAL = "financial",
|
||||
CONTENT = "content",
|
||||
SUPPORT = "support",
|
||||
OTHER = "other",
|
||||
PRODUCTS = "products",
|
||||
REPRESENTATIVES = "representatives",
|
||||
TASK_MANAGER = "task_manager",
|
||||
}
|
||||
|
||||
export const PERMISSION_GROUP_ORDER: PermissionGroupEnum[] = [
|
||||
PermissionGroupEnum.SERVICES,
|
||||
PermissionGroupEnum.CUSTOMERS,
|
||||
PermissionGroupEnum.USERS,
|
||||
PermissionGroupEnum.FINANCIAL,
|
||||
PermissionGroupEnum.CONTENT,
|
||||
PermissionGroupEnum.SUPPORT,
|
||||
PermissionGroupEnum.OTHER,
|
||||
PermissionGroupEnum.PRODUCTS,
|
||||
PermissionGroupEnum.REPRESENTATIVES,
|
||||
PermissionGroupEnum.TASK_MANAGER,
|
||||
];
|
||||
|
||||
export enum PermissionEnum {
|
||||
SERVICES_CREATE = "services_create",
|
||||
SERVICES_READ = "services_read",
|
||||
SERVICES_UPDATE = "services_update",
|
||||
SERVICES_DELETE = "services_delete",
|
||||
|
||||
CUSTOMERS_CREATE = "customers_create",
|
||||
CUSTOMERS_READ = "customers_read",
|
||||
CUSTOMERS_UPDATE = "customers_update",
|
||||
CUSTOMERS_DELETE = "customers_delete",
|
||||
|
||||
AGENTS_CREATE = "agents_create",
|
||||
AGENTS_READ = "agents_read",
|
||||
AGENTS_UPDATE = "agents_update",
|
||||
AGENTS_DELETE = "agents_delete",
|
||||
|
||||
DEVELOPERS_CREATE = "developers_create",
|
||||
DEVELOPERS_READ = "developers_read",
|
||||
DEVELOPERS_UPDATE = "developers_update",
|
||||
DEVELOPERS_DELETE = "developers_delete",
|
||||
|
||||
INVOICES_CREATE = "invoices_create",
|
||||
INVOICES_READ = "invoices_read",
|
||||
INVOICES_UPDATE = "invoices_update",
|
||||
INVOICES_DELETE = "invoices_delete",
|
||||
|
||||
TRANSACTIONS_CREATE = "transactions_create",
|
||||
TRANSACTIONS_READ = "transactions_read",
|
||||
TRANSACTIONS_UPDATE = "transactions_update",
|
||||
TRANSACTIONS_DELETE = "transactions_delete",
|
||||
|
||||
DISCOUNTS_CREATE = "discounts_create",
|
||||
DISCOUNTS_READ = "discounts_read",
|
||||
DISCOUNTS_UPDATE = "discounts_update",
|
||||
DISCOUNTS_DELETE = "discounts_delete",
|
||||
|
||||
ADMINS_CREATE = "admins_create",
|
||||
ADMINS_READ = "admins_read",
|
||||
ADMINS_UPDATE = "admins_update",
|
||||
ADMINS_DELETE = "admins_delete",
|
||||
|
||||
TICKETS_CREATE = "tickets_create",
|
||||
TICKETS_READ = "tickets_read",
|
||||
TICKETS_UPDATE = "tickets_update",
|
||||
TICKETS_DELETE = "tickets_delete",
|
||||
|
||||
CRITICISMS_CREATE = "criticisms_create",
|
||||
CRITICISMS_READ = "criticisms_read",
|
||||
CRITICISMS_UPDATE = "criticisms_update",
|
||||
CRITICISMS_DELETE = "criticisms_delete",
|
||||
|
||||
CONTACTS_US_CREATE = "contacts_us_create",
|
||||
CONTACTS_US_READ = "contacts_us_read",
|
||||
CONTACTS_US_UPDATE = "contacts_us_update",
|
||||
CONTACTS_US_DELETE = "contacts_us_delete",
|
||||
|
||||
ADVERTISEMENTS_CREATE = "advertisements_create",
|
||||
ADVERTISEMENTS_READ = "advertisements_read",
|
||||
ADVERTISEMENTS_UPDATE = "advertisements_update",
|
||||
ADVERTISEMENTS_DELETE = "advertisements_delete",
|
||||
|
||||
ANNOUNCEMENTS_CREATE = "announcements_create",
|
||||
ANNOUNCEMENTS_READ = "announcements_read",
|
||||
ANNOUNCEMENTS_UPDATE = "announcements_update",
|
||||
ANNOUNCEMENTS_DELETE = "announcements_delete",
|
||||
|
||||
BLOGS_CREATE = "blogs_create",
|
||||
BLOGS_READ = "blogs_read",
|
||||
BLOGS_UPDATE = "blogs_update",
|
||||
BLOGS_DELETE = "blogs_delete",
|
||||
|
||||
BLOG_CATEGORIES_CREATE = "blog_categories_create",
|
||||
BLOG_CATEGORIES_READ = "blog_categories_read",
|
||||
BLOG_CATEGORIES_UPDATE = "blog_categories_update",
|
||||
BLOG_CATEGORIES_DELETE = "blog_categories_delete",
|
||||
|
||||
BLOG_COMMENTS_CREATE = "blog_comments_create",
|
||||
BLOG_COMMENTS_READ = "blog_comments_read",
|
||||
BLOG_COMMENTS_UPDATE = "blog_comments_update",
|
||||
BLOG_COMMENTS_DELETE = "blog_comments_delete",
|
||||
|
||||
SLIDERS_CREATE = "sliders_create",
|
||||
SLIDERS_READ = "sliders_read",
|
||||
SLIDERS_UPDATE = "sliders_update",
|
||||
SLIDERS_DELETE = "sliders_delete",
|
||||
|
||||
LEARNINGS_CREATE = "learnings_create",
|
||||
LEARNINGS_READ = "learnings_read",
|
||||
LEARNINGS_UPDATE = "learnings_update",
|
||||
LEARNINGS_DELETE = "learnings_delete",
|
||||
|
||||
SETTINGS_CREATE = "settings_create",
|
||||
SETTINGS_READ = "settings_read",
|
||||
SETTINGS_UPDATE = "settings_update",
|
||||
SETTINGS_DELETE = "settings_delete",
|
||||
|
||||
BANK_ACCOUNTS_CREATE = "bank_accounts_create",
|
||||
BANK_ACCOUNTS_READ = "bank_accounts_read",
|
||||
BANK_ACCOUNTS_UPDATE = "bank_accounts_update",
|
||||
BANK_ACCOUNTS_DELETE = "bank_accounts_delete",
|
||||
|
||||
PAYMENTS_CREATE = "payments_create",
|
||||
PAYMENTS_READ = "payments_read",
|
||||
PAYMENTS_UPDATE = "payments_update",
|
||||
PAYMENTS_DELETE = "payments_delete",
|
||||
|
||||
SUPPORT_PLAN_CREATE = "support_plan_create",
|
||||
SUPPORT_PLAN_READ = "support_plan_read",
|
||||
SUPPORT_PLAN_UPDATE = "support_plan_update",
|
||||
SUPPORT_PLAN_DELETE = "support_plan_delete",
|
||||
|
||||
RESELLER_CREATE = "reseller_create",
|
||||
RESELLER_READ = "reseller_read",
|
||||
RESELLER_UPDATE = "reseller_update",
|
||||
RESELLER_DELETE = "reseller_delete",
|
||||
|
||||
WORKSPACE_CREATE = "workspace_create",
|
||||
WORKSPACE_READ = "workspace_read",
|
||||
WORKSPACE_UPDATE = "workspace_update",
|
||||
WORKSPACE_DELETE = "workspace_delete",
|
||||
|
||||
PROJECT_CREATE = "project_create",
|
||||
PROJECT_READ = "project_read",
|
||||
PROJECT_UPDATE = "project_update",
|
||||
PROJECT_DELETE = "project_delete",
|
||||
|
||||
TASK_CREATE = "task_create",
|
||||
TASK_READ = "task_read",
|
||||
TASK_UPDATE = "task_update",
|
||||
TASK_DELETE = "task_delete",
|
||||
|
||||
TASK_PHASE_CREATE = "task_phase_create",
|
||||
TASK_PHASE_READ = "task_phase_read",
|
||||
TASK_PHASE_UPDATE = "task_phase_update",
|
||||
TASK_PHASE_DELETE = "task_phase_delete",
|
||||
|
||||
TICKET_CATEGORIES_CREATE = "ticket_categories_create",
|
||||
TICKET_CATEGORIES_READ = "ticket_categories_read",
|
||||
TICKET_CATEGORIES_UPDATE = "ticket_categories_update",
|
||||
TICKET_CATEGORIES_DELETE = "ticket_categories_delete",
|
||||
|
||||
TICKET_MASTERS_CREATE = "ticket_masters_create",
|
||||
TICKET_MASTERS_READ = "ticket_masters_read",
|
||||
TICKET_MASTERS_UPDATE = "ticket_masters_update",
|
||||
TICKET_MASTERS_DELETE = "ticket_masters_delete",
|
||||
|
||||
VIEW_ALL_WORKSPACES = "view_all_workspaces",
|
||||
VIEW_ALL_PROJECTS = "view_all_projects",
|
||||
|
||||
LOGS = "logs",
|
||||
MANAGE_SSO_CLIENTS = "manage_sso_clients",
|
||||
DMENU = "dmenu",
|
||||
DKALA = "dkala",
|
||||
DPAGE = "dpage",
|
||||
DMAIL = "dmail",
|
||||
}
|
||||
|
||||
export type CrudAction = "create" | "read" | "update" | "delete";
|
||||
|
||||
const CRUD_ACTIONS: readonly CrudAction[] = ["create", "read", "update", "delete"];
|
||||
|
||||
export const CONTENT_PERMISSION_RESOURCES = [
|
||||
"blogs",
|
||||
"blog_categories",
|
||||
"blog_comments",
|
||||
"sliders",
|
||||
"learnings",
|
||||
"advertisements",
|
||||
"announcements",
|
||||
] as const;
|
||||
export const FINANCIAL_PERMISSION_RESOURCES = ["payments", "invoices", "transactions", "discounts", "bank_accounts"] as const;
|
||||
export const SUPPORT_PERMISSION_RESOURCES = [
|
||||
"support_plan",
|
||||
"tickets",
|
||||
"ticket_categories",
|
||||
"ticket_masters",
|
||||
] as const;
|
||||
export const TASK_MANAGER_PERMISSION_RESOURCES = [
|
||||
"workspace",
|
||||
"project",
|
||||
"task",
|
||||
"task_phase",
|
||||
"view_all_workspaces",
|
||||
"view_all_projects",
|
||||
] as const;
|
||||
export const USERS_SIDEBAR_PERMISSION_RESOURCES = ["admins"] as const;
|
||||
export const PRODUCT_PERMISSION_RESOURCES = ["dmenu", "dkala", "dpage", "dmail"] as const;
|
||||
|
||||
type PermissionEntry = { name: string };
|
||||
|
||||
export const getPermissionNames = (permissions: PermissionEntry[] | undefined): string[] =>
|
||||
permissions?.map((permission) => permission.name) ?? [];
|
||||
|
||||
export const hasPermission = (
|
||||
permissions: PermissionEntry[] | undefined,
|
||||
resourceOrPermission: string,
|
||||
): boolean => {
|
||||
const names = getPermissionNames(permissions);
|
||||
|
||||
if (names.includes(resourceOrPermission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return CRUD_ACTIONS.some((action) => names.includes(`${resourceOrPermission}_${action}`));
|
||||
};
|
||||
|
||||
export const hasActionPermission = (
|
||||
permissions: PermissionEntry[] | undefined,
|
||||
resource: string,
|
||||
action: CrudAction,
|
||||
): boolean => {
|
||||
const names = getPermissionNames(permissions);
|
||||
|
||||
if (names.includes(resource)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return names.includes(`${resource}_${action}`);
|
||||
};
|
||||
|
||||
export const canCreate = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||
hasActionPermission(permissions, resource, "create");
|
||||
|
||||
export const canRead = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||
hasActionPermission(permissions, resource, "read");
|
||||
|
||||
export const canUpdate = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||
hasActionPermission(permissions, resource, "update");
|
||||
|
||||
export const canDelete = (permissions: PermissionEntry[] | undefined, resource: string) =>
|
||||
hasActionPermission(permissions, resource, "delete");
|
||||
|
||||
export const hasAnyPermission = (
|
||||
permissions: PermissionEntry[] | undefined,
|
||||
resources: readonly string[],
|
||||
): boolean => resources.some((resource) => hasPermission(permissions, resource));
|
||||
|
||||
export const canAccessMenuItem = (
|
||||
permissions: PermissionEntry[] | undefined,
|
||||
activeName?: string,
|
||||
activeNames?: readonly string[],
|
||||
): boolean => {
|
||||
if (activeNames && activeNames.length > 0) {
|
||||
return hasAnyPermission(permissions, activeNames);
|
||||
}
|
||||
|
||||
if (!activeName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hasPermission(permissions, activeName);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
canCreate,
|
||||
canDelete,
|
||||
canRead,
|
||||
canUpdate,
|
||||
CrudAction,
|
||||
hasActionPermission,
|
||||
hasAnyPermission,
|
||||
hasPermission,
|
||||
} from "../helpers/permissions";
|
||||
import { useGetAdminPermissions } from "../pages/users/hooks/useUserData";
|
||||
|
||||
export const usePermissions = () => {
|
||||
const { data, isPending, isLoading } = useGetAdminPermissions();
|
||||
const permissions = data?.data?.permissions;
|
||||
|
||||
return {
|
||||
permissions,
|
||||
isPending: isPending || isLoading,
|
||||
can: (resource: string, action: CrudAction) => hasActionPermission(permissions, resource, action),
|
||||
canCreate: (resource: string) => canCreate(permissions, resource),
|
||||
canRead: (resource: string) => canRead(permissions, resource),
|
||||
canUpdate: (resource: string) => canUpdate(permissions, resource),
|
||||
canDelete: (resource: string) => canDelete(permissions, resource),
|
||||
has: (resourceOrPermission: string) => hasPermission(permissions, resourceOrPermission),
|
||||
hasAny: (resources: readonly string[]) => hasAnyPermission(permissions, resources),
|
||||
};
|
||||
};
|
||||
@@ -72,6 +72,82 @@ tbody tr {
|
||||
.rmdp-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-wrapper {
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-calendar {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-header {
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-header-values {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #292d32;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-arrow {
|
||||
border-color: #292d32;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-week-day {
|
||||
font-size: 11px;
|
||||
color: #8c90a3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day span {
|
||||
font-size: 11px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-deactive span {
|
||||
color: #c4c4c4;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-today span {
|
||||
background-color: transparent;
|
||||
color: #292d32;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day:not(.rmdp-disabled):not(.rmdp-day-hidden) span:hover {
|
||||
background-color: #f0edf5;
|
||||
color: #292d32;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-selected span:not(.highlight) {
|
||||
background-color: #292d32;
|
||||
color: #fff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-range {
|
||||
background-color: #f0edf5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-range.start,
|
||||
.task-detail-date-calendar .rmdp-day.rmdp-range.end {
|
||||
background-color: transparent;
|
||||
}
|
||||
.dltr {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
+146
-8
@@ -35,7 +35,16 @@
|
||||
"enter_password_step3": "رمز عبور را وارد کنید",
|
||||
"enter_your_password": "رمز عبور خود را وارد کنید.",
|
||||
"forgot_password": "فراموشی رمز عبور",
|
||||
"login_with_otp": "ورود با رمز عبور یکبار مصرف"
|
||||
"forgot_password_description": "ایمیل خود را وارد کنید تا کد بازیابی برای شما ارسال شود.",
|
||||
"reset_password": "بازیابی رمز عبور",
|
||||
"new_password": "رمز عبور جدید",
|
||||
"confirm_password": "تکرار رمز عبور",
|
||||
"password_mismatch": "رمز عبور و تکرار آن یکسان نیستند",
|
||||
"change_email": "تغییر ایمیل",
|
||||
"resend_code": "ارسال مجدد کد",
|
||||
"back_to_login": "بازگشت به صفحه ورود",
|
||||
"login_with_otp": "ورود با رمز عبور یکبار مصرف",
|
||||
"otp_sent": "رمز یک بار مصرف برای شما ارسال شد"
|
||||
},
|
||||
"errors": {
|
||||
"required": "این فیلد اجباری می باشد",
|
||||
@@ -59,7 +68,7 @@
|
||||
"mainpage": "صفحه اصلی",
|
||||
"myservice": "سرویس های من",
|
||||
"other_service": "سایر سرویس ها",
|
||||
"receipt_list": "لیست صورتحساب",
|
||||
"receipt_list": "صورتحساب ها",
|
||||
"transactions": "تراکنش ها",
|
||||
"other": "سایر",
|
||||
"ticket": "تیکت ها",
|
||||
@@ -72,8 +81,9 @@
|
||||
"payments": "پرداخت ها",
|
||||
"sliders": "اسلایدر",
|
||||
"support": "پلن پشتیبانی",
|
||||
"support_section": "پشتیبانی",
|
||||
"feedback": "گزارش ها",
|
||||
"accessLogs": "لاگهای دسترسی",
|
||||
"accessLogs": "لگ دسترسی",
|
||||
"icons": "آیکون ها",
|
||||
"dmenu": "دی منو",
|
||||
"dkala": "دی کالا",
|
||||
@@ -86,12 +96,13 @@
|
||||
"sms_count": "تعداد پیامک",
|
||||
"subscriptions": "اشتراک ها",
|
||||
"financial": "مالی",
|
||||
"products": "محصولات",
|
||||
"dmail": "دی میل",
|
||||
"domain": "دامنه",
|
||||
"reseller": "نماینده",
|
||||
"resellers": "نمایندگان",
|
||||
"withdraw": "درخواست های برداشت",
|
||||
"taskmanager": "مدیریت تسک ها"
|
||||
"taskmanager": "مدیریت تسک"
|
||||
},
|
||||
"reseller": {
|
||||
"list": "لیست نمایندکان",
|
||||
@@ -129,7 +140,10 @@
|
||||
"service_category": "دسته بندی سرویس ها",
|
||||
"customer_list": "لیست مشتریان",
|
||||
"create_customer": "افزودن مشتری",
|
||||
"representative_list": "لیست نمایندگان",
|
||||
"ticket_list": "لیست تیکت ها",
|
||||
"ticket_category": "دسته بندی تیکت ها",
|
||||
"ticket_masters": "مستر",
|
||||
"send_ticket": "ارسال تیکت",
|
||||
"category": "دسته بندی",
|
||||
"user_list": "لیست کاربران",
|
||||
@@ -141,9 +155,20 @@
|
||||
"plan_users": "کاربران پلن",
|
||||
"plan_list": "لیست پلن ها",
|
||||
"guide": "راهنمای سرویس",
|
||||
"blog_list": "لیست بلاگ",
|
||||
"blog_comments": "نظرات بلاگ",
|
||||
"blog_category": "دسته بندی بلاگ",
|
||||
"learning_list": "لیست آموزش ها",
|
||||
"learning_category": "دسته بندی آموزش ها",
|
||||
"access_logs_list": "لیست لاگها",
|
||||
"access_logs_stats": "آمار لاگها",
|
||||
"access_logs_errors": "لاگهای خطا"
|
||||
"access_logs_errors": "لاگهای خطا",
|
||||
"access_logs_permissions": "لاگهای مجوز",
|
||||
"taskmanager_workspace": "فضای کاری",
|
||||
"taskmanager_workspace_list": "لیست فضاهای کاری",
|
||||
"taskmanager_create_workspace": "ساخت فضای کاری جدید",
|
||||
"taskmanager_project_list": "لیست پروژهها",
|
||||
"taskmanager_create_project": "افزودن پروژه"
|
||||
},
|
||||
"guide": {
|
||||
"list_guide": "لیست راهنمای سرویس",
|
||||
@@ -344,7 +369,19 @@
|
||||
"assign_to": "کارشناس پاسخگویی",
|
||||
"user_plan": "پلن کاربر",
|
||||
"refer_ticket_success": "تیکت با موفقیت ارجاع شد",
|
||||
"refer_ticket_error": "تیکت ارجاع نشد"
|
||||
"refer_ticket_error": "تیکت ارجاع نشد",
|
||||
"create_task": "ساخت تسک",
|
||||
"create_task_modal_title": "ساخت تسک از تیکت",
|
||||
"create_task_success": "تسک با موفقیت ساخته شد",
|
||||
"select_project": "انتخاب پروژه",
|
||||
"select_column": "انتخاب ستون",
|
||||
"column": "ستون",
|
||||
"select_workspace_required": "فضای کاری را انتخاب کنید",
|
||||
"select_project_required": "پروژه را انتخاب کنید",
|
||||
"select_column_required": "ستون را انتخاب کنید",
|
||||
"masters": "مستر",
|
||||
"add_master": "افزودن مستر",
|
||||
"edit_master": "ویرایش مستر"
|
||||
},
|
||||
"active": "فعال",
|
||||
"inactive": "غیرفعال",
|
||||
@@ -696,12 +733,25 @@
|
||||
"image": "تصویر حساب کاربری",
|
||||
"roles_list": "لیست نقش ها",
|
||||
"add_role": "افزودن نقش",
|
||||
"update_role": "ویرایش نقش",
|
||||
"role_search": "جستجو در نقش ها",
|
||||
"title": "عنوان",
|
||||
"user": "کاربر",
|
||||
"submit_role": "ثبت نقش",
|
||||
"title_role": "عنوان نقش",
|
||||
"permissions": "دسترسی ها",
|
||||
"permission_groups": {
|
||||
"services": "سرویسها",
|
||||
"customers": "مشتریان",
|
||||
"users": "کاربران",
|
||||
"financial": "مالی",
|
||||
"content": "محتوا",
|
||||
"support": "پشتیبانی",
|
||||
"other": "سایر",
|
||||
"products": "محصولات",
|
||||
"representatives": "نمایندگان",
|
||||
"task_manager": "مدیریت وظایف"
|
||||
},
|
||||
"status_role": "وضعیت نقش",
|
||||
"error_count_permission": "حداقل به دسترسی رو باید انتخاب کنید",
|
||||
"is_admin": "آیا ادمین است؟",
|
||||
@@ -711,6 +761,7 @@
|
||||
"group_add": "ساخت گروه",
|
||||
"submit_group": "ثبت گروه",
|
||||
"error_count_users": "حداقل یک کاربر باید انتخاب شود",
|
||||
"error_count_roles": "حداقل یک نقش باید انتخاب شود",
|
||||
"list_user_group": "لیست گروه های کاربری",
|
||||
"groups": "گروه ها",
|
||||
"group_name": "نام گروه",
|
||||
@@ -747,7 +798,10 @@
|
||||
"confrim_email": "تایید ایمیل",
|
||||
"confrim": "تایید صورتحساب ",
|
||||
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
|
||||
"email_not_verified": "ایمیل تایید نشده"
|
||||
"email_not_verified": "ایمیل تایید نشده",
|
||||
"resend_email": "ارسال مجدد تایید",
|
||||
"first_name": "نام",
|
||||
"last_name": "نام خانوادگی"
|
||||
},
|
||||
"email": "ایمیل",
|
||||
"customer": {
|
||||
@@ -965,5 +1019,89 @@
|
||||
"days": "روز",
|
||||
"id": "شناسه"
|
||||
},
|
||||
"cancel": "لغو"
|
||||
"cancel": "لغو",
|
||||
"taskmanager": {
|
||||
"workspace_list": "لیست فضاهای کاری",
|
||||
"new_workspace": "ساخت فضای کاری جدید",
|
||||
"submit_workspace": "ثبت فضای کاری",
|
||||
"workspace_deleted": "فضای کاری با موفقیت حذف شد",
|
||||
"workspacename": "نام",
|
||||
"workspacetype": "نوع",
|
||||
"workspace_info": "اطلاعات",
|
||||
"workspace_description": "توضیحات",
|
||||
"workspace_status": "وضعیت فضای کار",
|
||||
"choose_color": "انتخاب رنگ دلخواه",
|
||||
"users": "کاربران",
|
||||
"select_user": "انتخاب کاربر",
|
||||
"select_workspace": "انتخاب فضای کاری",
|
||||
"no_users_added": "هنوز کاربری اضافه نکردی",
|
||||
"new_project": "افزودن پروژه",
|
||||
"submit_project": "ثبت پروژه",
|
||||
"project_info": "اطلاعات پروژه",
|
||||
"project_name": "نام پروژه",
|
||||
"project_start_date": "تاریخ شروع پروژه",
|
||||
"employer_name": "نام کارفرما",
|
||||
"project_status": "وضعیت فضای کار",
|
||||
"project_settings": "تنظیمات پروژه",
|
||||
"background": "پس زمینه",
|
||||
"background_image": "تصویر پس زمینه",
|
||||
"upload_background": "تصویر مورد نظر را دراپ کنید",
|
||||
"select_date": "انتخاب تاریخ",
|
||||
"projects": "پروژهها",
|
||||
"delete_project": "پاک کردن",
|
||||
"project_deleted": "پروژه با موفقیت حذف شد",
|
||||
"delete_confirm_message": "برای حذف «{{name}}»، نام آن را در کادر زیر وارد کنید.",
|
||||
"delete_confirm_placeholder": "نام را وارد کنید",
|
||||
"add_new_column": "اضافه کردن ستون جدید",
|
||||
"add_column": "اضافه کردن",
|
||||
"column_name": "نام ستون",
|
||||
"delete_column": "حذف ستون",
|
||||
"edit_column": "ویرایش نام",
|
||||
"delete_column_confirm": "آیا از حذف این ستون اطمینان دارید؟",
|
||||
"column_deleted": "ستون با موفقیت حذف شد",
|
||||
"column_updated": "ستون با موفقیت ویرایش شد",
|
||||
"add_new_task": "اضافه کردن تسک",
|
||||
"add_task": "اضافه کردن",
|
||||
"task_title": "عنوان تسک",
|
||||
"delete_task": "حذف تسک",
|
||||
"delete_task_confirm": "آیا از حذف این تسک اطمینان دارید؟",
|
||||
"task_deleted": "تسک با موفقیت حذف شد",
|
||||
"task_detail": {
|
||||
"labels": "برچسب ها",
|
||||
"user_management": "مدیریت کاربران",
|
||||
"checklist": "چک لیست",
|
||||
"attachment": "ضمیمه",
|
||||
"date": "تاریخ",
|
||||
"date_settings": "تنظیمات تاریخ",
|
||||
"description": "توضیحات",
|
||||
"description_placeholder": "توضیحات خود را اضافه کنید",
|
||||
"new_label": "برچسب جدید",
|
||||
"label_title": "عنوان",
|
||||
"choose_custom_color": "انتخاب رنگ دلخواه",
|
||||
"select_cover_color": "انتخاب رنگ کاور",
|
||||
"members": "اعضا",
|
||||
"copy_from": "کپی از",
|
||||
"none": "هیچ کدام",
|
||||
"link": "لینک",
|
||||
"files": "فایل ها",
|
||||
"links": "لینک ها",
|
||||
"or": "یا",
|
||||
"insert": "درج",
|
||||
"start_date": "تاریخ شروع",
|
||||
"end_date": "تاریخ پایان",
|
||||
"time_tracking": "ثبت زمان",
|
||||
"start_tracking": "شروع",
|
||||
"stop_tracking": "پایان",
|
||||
"time_history": "سابقه",
|
||||
"no_time_entries": "هنوز زمانی ثبت نشده",
|
||||
"tracking_active": "در حال اجرا",
|
||||
"comments": "کامنتها",
|
||||
"add_comment": "ثبت کامنت",
|
||||
"comment_placeholder": "کامنت خود را بنویسید...",
|
||||
"no_comments": "هنوز کامنتی ثبت نشده",
|
||||
"unknown_user": "کاربر",
|
||||
"related_ticket": "تیکت مرتبط",
|
||||
"view_ticket": "مشاهده تیکت"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-15
@@ -14,6 +14,7 @@ import moment from 'moment-jalaali'
|
||||
import ToggleStatusAds from './components/ToggleStatus'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Pagination from '../../components/Pagination'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
const AddList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
@@ -23,6 +24,7 @@ const AddList: FC = () => {
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const getAds = useGetAdsList(page, search, isActive, since)
|
||||
const deleteAds = useDeleteAds()
|
||||
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteAds.mutate(id, {
|
||||
@@ -39,18 +41,20 @@ const AddList: FC = () => {
|
||||
{t('ads.ads')}
|
||||
</div>
|
||||
|
||||
<Link to={Pages.ads.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('ads.new_ads')}
|
||||
{canCreate('advertisements') && (
|
||||
<Link to={Pages.ads.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('ads.new_ads')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-4'>
|
||||
<div className='flex flex-col xl:flex-row justify-between items-center xl:items-end'>
|
||||
@@ -130,10 +134,14 @@ const AddList: FC = () => {
|
||||
</Td>
|
||||
<Td text={''}>
|
||||
<div className='flex gap-3'>
|
||||
<Link to={Pages.ads.update + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' />
|
||||
{canUpdate('advertisements') && (
|
||||
<Link to={Pages.ads.update + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
)}
|
||||
{canDelete('advertisements') && (
|
||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
|
||||
@@ -13,7 +13,7 @@ import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { useCreateAds } from './hooks/useAdsData'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { clx } from '../../helpers/utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
@@ -54,21 +54,21 @@ const CreateAd: FC = () => {
|
||||
values.imageUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
|
||||
createAds.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.ads.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error(t('ads.file_error'))
|
||||
toast(t('ads.file_error'), 'error')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { useGetDetailAds, useUpdateAds } from './hooks/useAdsData'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { clx } from '../../helpers/utils'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
@@ -56,18 +56,18 @@ const UpdateAds: FC = () => {
|
||||
values.imageUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateAds.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.ads.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as Yup from 'yup'
|
||||
import { ServiceItemType } from '../service/types/ServiceTypes'
|
||||
import { useCreateAnnoncement, useGetCustomersService } from './hooks/useAnnoncementData'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import moment from 'moment-jalaali'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
@@ -57,11 +57,11 @@ const Create: FC = () => {
|
||||
createAnnoncement.mutate(values, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.announcement.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ import { Add, Edit, Trash } from 'iconsax-react'
|
||||
import Td from '../../components/Td'
|
||||
import moment from 'moment-jalaali'
|
||||
import Pagination from '../../components/Pagination'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
|
||||
const AnnouncementtList: FC = () => {
|
||||
|
||||
@@ -18,15 +19,16 @@ const AnnouncementtList: FC = () => {
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const getAnnoncements = useGetAnnoncements(page, '', '')
|
||||
const deleteAnnoncement = useDeleteAnnoncement()
|
||||
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteAnnoncement.mutate(id, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
getAnnoncements.refetch()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -38,21 +40,23 @@ const AnnouncementtList: FC = () => {
|
||||
<div>
|
||||
{t('announcement.announcements')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.announcement.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('announcement.add_announcement')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{canCreate('announcements') && (
|
||||
<div>
|
||||
<Link to={Pages.announcement.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('announcement.add_announcement')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -88,17 +92,21 @@ const AnnouncementtList: FC = () => {
|
||||
<Td text={moment(item.publishAt).format('jYYYY-jMM-jDD')} />
|
||||
<Td text={''}>
|
||||
<div className='flex gap-2'>
|
||||
<Link to={Pages.announcement.detail + item.id}>
|
||||
<Edit
|
||||
{canUpdate('announcements') && (
|
||||
<Link to={Pages.announcement.detail + item.id}>
|
||||
<Edit
|
||||
className='size-5'
|
||||
color='#888'
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
{canDelete('announcements') && (
|
||||
<Trash
|
||||
className='size-5'
|
||||
color='#888'
|
||||
onClick={() => handleDelete(item.id)}
|
||||
/>
|
||||
</Link>
|
||||
<Trash
|
||||
className='size-5'
|
||||
color='#888'
|
||||
onClick={() => handleDelete(item.id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as Yup from 'yup'
|
||||
import { ServiceItemType } from '../service/types/ServiceTypes'
|
||||
import { useGetAnnoncementDetail, useGetCustomersService, useUpdateAnnoncement } from './hooks/useAnnoncementData'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import moment from 'moment-jalaali'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
@@ -59,11 +59,11 @@ const Update: FC = () => {
|
||||
updateAnnoncement.mutate(values, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.announcement.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { FC, useState } from 'react'
|
||||
import LogoImage from '../../assets/images/logo.svg'
|
||||
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ForgotPasswordStep1 from './components/ForgotPasswordStep1'
|
||||
import ForgotPasswordStep2 from './components/ForgotPasswordStep2'
|
||||
|
||||
const ForgotPassword: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [step, setStep] = useState(1)
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||
<div className='flex-1 h-full flex flex-col'>
|
||||
<img src={LogoSmallImage} className='w-8' />
|
||||
|
||||
<div className='flex flex-1 flex-col h-full justify-center'>
|
||||
{
|
||||
step === 1 ?
|
||||
<ForgotPasswordStep1 onSuccess={() => setStep(2)} />
|
||||
:
|
||||
<ForgotPasswordStep2 onBack={() => setStep(1)} />
|
||||
}
|
||||
|
||||
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.back_to_login')}
|
||||
</p>
|
||||
<Link to={Pages.auth.login}>
|
||||
<div>{t('auth.login')}</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||
<img src={LogoImage} className='h-20' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPassword
|
||||
@@ -0,0 +1,88 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForgotPasswordType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { useForgotPassword } from '../hooks/useAuthData'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { isEmail } from '../../../config/func'
|
||||
|
||||
type Props = {
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
const ForgotPasswordStep1: FC<Props> = ({ onSuccess }) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { email, setEmail } = useAuthStore()
|
||||
const forgotPassword = useForgotPassword()
|
||||
|
||||
const formik = useFormik<ForgotPasswordType>({
|
||||
initialValues: {
|
||||
email: email,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
email: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
.test('is-email', t('errors.required'), (value) => !!value && isEmail(value)),
|
||||
}),
|
||||
onSubmit(values) {
|
||||
setEmail(values.email)
|
||||
forgotPassword.mutate({ email: values.email }, {
|
||||
onSuccess() {
|
||||
toast(t('auth.otp_sent'), 'success')
|
||||
onSuccess()
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.forgot_password')}
|
||||
</h2>
|
||||
<p className='text-description text-sm mt-2'>
|
||||
{t('auth.forgot_password_description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<Input
|
||||
label={t('auth.email')}
|
||||
placeholder={t('auth.enter_email')}
|
||||
type='text'
|
||||
dir='ltr'
|
||||
name='email'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.email}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.touched.email && formik.errors.email &&
|
||||
<Error
|
||||
errorText={formik.errors.email}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={forgotPassword.isPending}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPasswordStep1
|
||||
@@ -0,0 +1,172 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ResetPasswordType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Button from '../../../components/Button'
|
||||
import Input from '../../../components/Input'
|
||||
import Error from '../../../components/Error'
|
||||
import OTPInput from 'react-otp-input'
|
||||
import { useCountDown } from '../../../hooks/useCountDown'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import { useForgotPassword, useResetPassword } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
type Props = {
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const ForgotPasswordStep2: FC<Props> = ({ onBack }) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const navigate = useNavigate()
|
||||
const { email } = useAuthStore()
|
||||
const { value, reset } = useCountDown(2, true)
|
||||
const resetPassword = useResetPassword()
|
||||
const forgotPassword = useForgotPassword()
|
||||
|
||||
const formik = useFormik<ResetPasswordType & { confirmPassword: string }>({
|
||||
initialValues: {
|
||||
email: email,
|
||||
code: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
code: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
newPassword: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
confirmPassword: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
.oneOf([Yup.ref('newPassword')], t('auth.password_mismatch')),
|
||||
}),
|
||||
onSubmit(values) {
|
||||
resetPassword.mutate({
|
||||
email,
|
||||
code: values.code,
|
||||
newPassword: values.newPassword,
|
||||
}, {
|
||||
onSuccess() {
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.auth.login)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleResend = () => {
|
||||
forgotPassword.mutate({ email }, {
|
||||
onSuccess() {
|
||||
reset()
|
||||
toast(t('auth.otp_sent'), 'success')
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='lg:text-2xl font-bold'>
|
||||
{t('auth.reset_password')}
|
||||
</h2>
|
||||
<p className='text-description flex lg:flex-row flex-col lg:gap-1 gap-2 text-sm lg:mt-2 mt-3'>
|
||||
<div className='flex gap-1 flex-wrap'>
|
||||
<div>{t('auth.verfify_code_text_1')}</div>
|
||||
<div>{email}</div>
|
||||
<div>{t('auth.verfify_code_text_2')}</div>
|
||||
</div>
|
||||
<div onClick={onBack} className='text-black cursor-pointer'>
|
||||
{t('auth.change_email')}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<div className='text-sm'>
|
||||
{t('auth.verify_code')}
|
||||
</div>
|
||||
<div className='mt-2 w-full flex justify-center dltr otp'>
|
||||
<OTPInput
|
||||
shouldAutoFocus
|
||||
value={formik.values.code}
|
||||
numInputs={5}
|
||||
inputType='tel'
|
||||
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
||||
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
formik.touched.code && formik.errors.code &&
|
||||
<div className='text-xs mt-2 text-red-500'>{formik.errors.code}</div>
|
||||
}
|
||||
|
||||
<div className='flex mt-4 justify-between items-center'>
|
||||
<div
|
||||
onClick={handleResend}
|
||||
className='text-xs text-black cursor-pointer'
|
||||
>
|
||||
{t('auth.resend_code')}
|
||||
</div>
|
||||
<div className='flex gap-1 text-description text-xs'>
|
||||
<div>{value}</div>
|
||||
<div>{t('auth.counter_otp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label={t('auth.new_password')}
|
||||
type='password'
|
||||
className='text-right'
|
||||
name='newPassword'
|
||||
dir='ltr'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.newPassword}
|
||||
error_text={formik.touched.newPassword && formik.errors.newPassword ? formik.errors.newPassword : ''}
|
||||
/>
|
||||
{
|
||||
formik.touched.newPassword && formik.errors.newPassword &&
|
||||
<Error errorText={formik.errors.newPassword} />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
<Input
|
||||
label={t('auth.confirm_password')}
|
||||
type='password'
|
||||
className='text-right'
|
||||
name='confirmPassword'
|
||||
dir='ltr'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.confirmPassword}
|
||||
error_text={formik.touched.confirmPassword && formik.errors.confirmPassword ? formik.errors.confirmPassword : ''}
|
||||
/>
|
||||
{
|
||||
formik.touched.confirmPassword && formik.errors.confirmPassword &&
|
||||
<Error errorText={formik.errors.confirmPassword} />
|
||||
}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={t('auth.reset_password')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={resetPassword.isPending}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPasswordStep2
|
||||
@@ -9,7 +9,7 @@ import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { isEmail } from '../../../config/func'
|
||||
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
const LoginStep1: FC = () => {
|
||||
@@ -35,7 +35,7 @@ const LoginStep1: FC = () => {
|
||||
setStepLogin(2)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -43,10 +43,10 @@ const LoginStep1: FC = () => {
|
||||
loginWithOtp.mutate({ phone: values.phone_email }, {
|
||||
onSuccess() {
|
||||
setStepLogin(2)
|
||||
toast.success(t('auth.otp_sent'))
|
||||
toast(t('auth.otp_sent'), 'success')
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -69,7 +69,7 @@ const LoginStep1: FC = () => {
|
||||
label={t('auth.mobile_or_email')}
|
||||
placeholder={t('auth.enter_mobile_or_email')}
|
||||
type='text'
|
||||
className='text-right'
|
||||
dir='ltr'
|
||||
name='phone_email'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.phone_email}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCountDown } from '../../../hooks/useCountDown'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import { useOtpVerify } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { setToken, setRefreshToken } from '../../../config/func'
|
||||
|
||||
const LoginStep2: FC = () => {
|
||||
@@ -42,7 +42,7 @@ const LoginStep2: FC = () => {
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -78,7 +78,13 @@ const LoginStep2: FC = () => {
|
||||
value={formik.values.code}
|
||||
numInputs={5}
|
||||
inputType='tel'
|
||||
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
||||
onChange={(otp: string) => {
|
||||
formik.setFieldValue('code', otp).then(() => {
|
||||
if (otp.length === 5 && !otpVerify.isPending) {
|
||||
formik.submitForm()
|
||||
}
|
||||
})
|
||||
}}
|
||||
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||
import { useLoginWithPassword } from '../hooks/useAuthData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { setRefreshToken } from '../../../config/func'
|
||||
import { setToken } from '../../../config/func'
|
||||
@@ -39,7 +39,7 @@ const LoginStep3: FC = () => {
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
|
||||
@@ -63,6 +63,7 @@ const LoginStep3: FC = () => {
|
||||
type='password'
|
||||
className='text-right'
|
||||
name='password'
|
||||
dir='ltr'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.password}
|
||||
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { useCheckHasAccountRegister, useLoginWithOtp } from '../hooks/useAuthData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
const RegisterStep1: FC = () => {
|
||||
@@ -33,7 +33,7 @@ const RegisterStep1: FC = () => {
|
||||
setStepLogin(2)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ import DatePickerComponent from '../../../components/DatePicker'
|
||||
import Button from '../../../components/Button'
|
||||
import { useRegister } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { Pages } from '../../../config/Pages'
|
||||
|
||||
const RegisterStep2: FC = () => {
|
||||
@@ -52,7 +52,7 @@ const RegisterStep2: FC = () => {
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import * as api from '../service/AuthService'
|
||||
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
|
||||
import { CheckHasAccountPhoneType, CheckHasAccountType, ForgotPasswordType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType, ResetPasswordType } from '../types/AuthTypes';
|
||||
|
||||
export const useLoginWithPassword = () => {
|
||||
return useMutation({
|
||||
@@ -36,4 +36,16 @@ export const useRegister = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: RegisterType) => api.register(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useForgotPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: ForgotPasswordType) => api.forgotPassword(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: ResetPasswordType) => api.resetPassword(variables),
|
||||
});
|
||||
};
|
||||
@@ -2,11 +2,13 @@ import axios from "../../../config/axios";
|
||||
import {
|
||||
CheckHasAccountPhoneType,
|
||||
CheckHasAccountType,
|
||||
ForgotPasswordType,
|
||||
LoginWithOtpType,
|
||||
LoginWithPasswordType,
|
||||
OtpVerifyType,
|
||||
RefreshTokenType,
|
||||
RegisterType,
|
||||
ResetPasswordType,
|
||||
} from "../types/AuthTypes";
|
||||
|
||||
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
||||
@@ -43,3 +45,13 @@ export const refreshToken = async (params: RefreshTokenType) => {
|
||||
const { data } = await axios.post(`/auth/refresh`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const forgotPassword = async (params: ForgotPasswordType) => {
|
||||
const { data } = await axios.post(`/auth/forgot-password`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const resetPassword = async (params: ResetPasswordType) => {
|
||||
const { data } = await axios.post(`/auth/reset-password`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -52,3 +52,13 @@ export type RegisterType = {
|
||||
export type RefreshTokenType = {
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export type ForgotPasswordType = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type ResetPasswordType = {
|
||||
email: string;
|
||||
code: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
@@ -15,8 +15,9 @@ import * as Yup from 'yup'
|
||||
import Textarea from '../../components/Textarea'
|
||||
import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import UpdateCategory from './components/UpdateCategory'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
const BlogCategory: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
@@ -25,6 +26,7 @@ const BlogCategory: FC = () => {
|
||||
const singleUpload = useSingleUpload()
|
||||
const createBlogCategory = useCreateBlogCategory()
|
||||
const deleteBlogCategory = useDeleteBlogCategory()
|
||||
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||
const formik = useFormik<CreateBlogCategoryType>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
@@ -38,7 +40,7 @@ const BlogCategory: FC = () => {
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (!file) {
|
||||
toast.error('فایل را انتخاب کنید')
|
||||
toast('فایل را انتخاب کنید', 'error')
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
@@ -51,7 +53,7 @@ const BlogCategory: FC = () => {
|
||||
|
||||
createBlogCategory.mutateAsync(values, {
|
||||
onSuccess: () => {
|
||||
toast.success('دسته بندی با موفقیت ثبت شد')
|
||||
toast('دسته بندی با موفقیت ثبت شد', 'success')
|
||||
setFile(undefined)
|
||||
formik.resetForm()
|
||||
getBlogCategories.refetch()
|
||||
@@ -102,15 +104,19 @@ const BlogCategory: FC = () => {
|
||||
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||
<Td text={t('')}>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<UpdateCategory id={item.id} />
|
||||
<Trash size={20} color='red' onClick={() => {
|
||||
deleteBlogCategory.mutateAsync(item.id, {
|
||||
onSuccess: () => {
|
||||
toast.success('دسته بندی با موفقیت حذف شد')
|
||||
getBlogCategories.refetch()
|
||||
}
|
||||
})
|
||||
}} />
|
||||
{canUpdate('blog_categories') && (
|
||||
<UpdateCategory id={item.id} />
|
||||
)}
|
||||
{canDelete('blog_categories') && (
|
||||
<Trash size={20} color='red' onClick={() => {
|
||||
deleteBlogCategory.mutateAsync(item.id, {
|
||||
onSuccess: () => {
|
||||
toast('دسته بندی با موفقیت حذف شد', 'success')
|
||||
getBlogCategories.refetch()
|
||||
}
|
||||
})
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
@@ -123,59 +129,61 @@ const BlogCategory: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative bg-white min-h-[calc(100vh-185px)] w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||
<p className='text-md'>{t('blog.add_category')}</p>
|
||||
<div className='text-sm flex items-center justify-between mt-4'>
|
||||
<p>
|
||||
{t('blog.category_status')}
|
||||
</p>
|
||||
<div className='flex gap-1 text-xs items-center text-description'>
|
||||
<div>
|
||||
{t('ads.deactive')}
|
||||
{canCreate('blog_categories') && (
|
||||
<div className='relative bg-white min-h-[calc(100vh-185px)] w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
|
||||
<p className='text-md'>{t('blog.add_category')}</p>
|
||||
<div className='text-sm flex items-center justify-between mt-4'>
|
||||
<p>
|
||||
{t('blog.category_status')}
|
||||
</p>
|
||||
<div className='flex gap-1 text-xs items-center text-description'>
|
||||
<div>
|
||||
{t('ads.deactive')}
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
/>
|
||||
</div>
|
||||
<SwitchComponent
|
||||
active={formik.values.isActive}
|
||||
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label='عنوان'
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label='عنوان'
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Textarea
|
||||
label='توضیحات'
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<UploadBoxDraggble
|
||||
label='آپلود آیکون'
|
||||
onChange={(file) => setFile(file[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='w-[172px] absolute bottom-4 left-4'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createBlogCategory.isPending || singleUpload.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
ذخیره
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<Textarea
|
||||
label='توضیحات'
|
||||
{...formik.getFieldProps('description')}
|
||||
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<UploadBoxDraggble
|
||||
label='آپلود آیکون'
|
||||
onChange={(file) => setFile(file[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className='w-[172px] absolute bottom-4 left-4'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createBlogCategory.isPending || singleUpload.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
ذخیره
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+32
-24
@@ -11,6 +11,7 @@ import { BlogItemType } from './types/BlogTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||
import Pagination from '../../components/Pagination'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
const BlogList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
@@ -18,6 +19,7 @@ const BlogList: FC = () => {
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const getBlogs = useGetBlogs(search, page);
|
||||
const deleteBlog = useDeleteBlog()
|
||||
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||
|
||||
return (
|
||||
<div className='mt-4 min-h-[500px]'>
|
||||
@@ -26,18 +28,20 @@ const BlogList: FC = () => {
|
||||
{t('blog.blog')}
|
||||
</div>
|
||||
|
||||
<Link to={Pages.blog.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('blog.new_blog')}
|
||||
{canCreate('blogs') && (
|
||||
<Link to={Pages.blog.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('blog.new_blog')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -139,19 +143,23 @@ const BlogList: FC = () => {
|
||||
</Td> */}
|
||||
<Td text={''}>
|
||||
<div className='flex gap-2'>
|
||||
<Link to={Pages.blog.detail + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
<TrashWithConfrim
|
||||
onDelete={() => {
|
||||
deleteBlog.mutate(item.id, {
|
||||
onSuccess: () => {
|
||||
getBlogs.refetch()
|
||||
}
|
||||
})
|
||||
}}
|
||||
isLoading={deleteBlog.isPending}
|
||||
/>
|
||||
{canUpdate('blogs') && (
|
||||
<Link to={Pages.blog.detail + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
)}
|
||||
{canDelete('blogs') && (
|
||||
<TrashWithConfrim
|
||||
onDelete={() => {
|
||||
deleteBlog.mutate(item.id, {
|
||||
onSuccess: () => {
|
||||
getBlogs.refetch()
|
||||
}
|
||||
})
|
||||
}}
|
||||
isLoading={deleteBlog.isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
|
||||
@@ -6,9 +6,11 @@ import { BlogCommentType } from './types/BlogTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
import { CloseCircle, TickCircle } from 'iconsax-react'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
const Comments: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const { canUpdate } = usePermissions()
|
||||
const getBlogComments = useGetBlogComments()
|
||||
const changeStatusComment = useChangeStatusComment()
|
||||
const handleChangeStatus = (id: string, status: string) => {
|
||||
@@ -17,7 +19,7 @@ const Comments: FC = () => {
|
||||
getBlogComments.refetch()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -49,7 +51,7 @@ const Comments: FC = () => {
|
||||
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||
<Td text=''>
|
||||
{
|
||||
item.status === 'PENDING' &&
|
||||
item.status === 'PENDING' && canUpdate('blog_comments') &&
|
||||
<div className='flex items-center gap-2'>
|
||||
<TickCircle onClick={() => handleChangeStatus(item.id, 'APPROVED')} color='green' size={20} />
|
||||
<CloseCircle onClick={() => handleChangeStatus(item.id, 'REJECTED')} color='red' size={20} />
|
||||
|
||||
@@ -12,7 +12,7 @@ import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
||||
import QuillEditor from '../../components/QuillEditor'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -57,7 +57,7 @@ const CreateBlog: FC = () => {
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (!file) {
|
||||
toast.error(t('errors.required'))
|
||||
toast(t('errors.required'), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ const CreateBlog: FC = () => {
|
||||
createBlog.mutate(values, {
|
||||
onSuccess() {
|
||||
setIsSaved(true)
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.blog.list)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
||||
import QuillEditor from '../../components/QuillEditor'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
@@ -87,11 +87,11 @@ const UpdateBlog: FC = () => {
|
||||
updateBlog.mutate({ id: id || '', params: values }, {
|
||||
onSuccess() {
|
||||
setIsSaved(true)
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.blog.list)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import UploadBox from '../../../components/UploadBox'
|
||||
import { useSingleUpload } from '../../service/hooks/useServiceData'
|
||||
import SwitchComponent from '../../../components/Switch'
|
||||
import Button from '../../../components/Button'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
interface Props {
|
||||
id: string
|
||||
@@ -42,12 +42,12 @@ const UpdateCategory: FC<Props> = ({ id }) => {
|
||||
}
|
||||
updateBlogCategory.mutateAsync({ id: id, params: values }, {
|
||||
onSuccess: () => {
|
||||
toast.success('دسته بندی با موفقیت ویرایش شد')
|
||||
toast('دسته بندی با موفقیت ویرایش شد', 'success')
|
||||
setIsOpen(false)
|
||||
window.location.reload()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCreateBank } from './hooks/useCardBankData'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
|
||||
const CreateBankCard: FC = () => {
|
||||
|
||||
@@ -40,7 +40,7 @@ const CreateBankCard: FC = () => {
|
||||
navigate(Pages.cardBank.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useGetCardBankDetail, useUpdateBank } from './hooks/useCardBankData'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
|
||||
const EditBankCard: FC = () => {
|
||||
|
||||
@@ -42,7 +42,7 @@ const EditBankCard: FC = () => {
|
||||
navigate(Pages.cardBank.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+22
-16
@@ -8,10 +8,12 @@ import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Eye } from 'iconsax-react'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
|
||||
const CardBankList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { canCreate, canUpdate } = usePermissions()
|
||||
const getCardBanks = useGetCardBanks()
|
||||
|
||||
return (
|
||||
@@ -23,19 +25,21 @@ const CardBankList: FC = () => {
|
||||
{t('cardBank.manage_banks')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.cardBank.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('cardBank.add_account')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
{canCreate('bank_accounts') &&
|
||||
<Link to={Pages.cardBank.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('cardBank.add_account')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
@@ -63,9 +67,11 @@ const CardBankList: FC = () => {
|
||||
<Td text={item.cardNumber} />
|
||||
<Td text={item.IBan} />
|
||||
<Td text={''}>
|
||||
<Link to={Pages.cardBank.detail + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
{canUpdate('bank_accounts') &&
|
||||
<Link to={Pages.cardBank.detail + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
}
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -9,9 +9,11 @@ import { CriticismsItemTypes } from './types/CriticismsTypes'
|
||||
import moment from 'moment-jalaali'
|
||||
import ModalConfrim from '../../components/ModalConfrim'
|
||||
import Pagination from '../../components/Pagination'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
|
||||
const CriticismsList: FC = () => {
|
||||
|
||||
const { canDelete } = usePermissions()
|
||||
const [itemId, setItemId] = useState<string>('')
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const [openConfrim, setOpenConfrim] = useState(false)
|
||||
@@ -69,7 +71,9 @@ const CriticismsList: FC = () => {
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text=''>
|
||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' />
|
||||
{canDelete('criticisms') &&
|
||||
<Trash onClick={() => handleDelete(item.id)} size={20} color='#8C90A3' className='cursor-pointer' />
|
||||
}
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useFormik } from 'formik';
|
||||
import { CreateCustomerType, ProvinesItemType } from './types/CustomerTypes';
|
||||
import { useCreateCustomer, useGetCities, useGetProvines } from './hooks/useCustomerData';
|
||||
import PageLoading from '../../components/PageLoading';
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types';
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -32,11 +32,11 @@ const AddCustomer: FC = () => {
|
||||
const handleCreateCustomer = (values: CreateCustomerType) => {
|
||||
createCustomer.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.customer.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -88,7 +88,7 @@ const AddCustomer: FC = () => {
|
||||
values.profilePic = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+121
-127
@@ -1,134 +1,128 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../components/Button'
|
||||
import { Add, Eye } from 'iconsax-react'
|
||||
import Select from '../../components/Select'
|
||||
import Input from '../../components/Input'
|
||||
import Td from '../../components/Td'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import { useGetCustomers } from './hooks/useCustomerData'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { CustomerItemType } from './types/CustomerTypes'
|
||||
import Pagination from '../../components/Pagination'
|
||||
import { Add, Eye } from "iconsax-react";
|
||||
import { FC, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import Button from "../../components/Button";
|
||||
import Input from "../../components/Input";
|
||||
import PageLoading from "../../components/PageLoading";
|
||||
import Pagination from "../../components/Pagination";
|
||||
import Td from "../../components/Td";
|
||||
import { Pages } from "../../config/Pages";
|
||||
import { usePermissions } from "../../hooks/usePermissions";
|
||||
import { useGetCustomers } from "./hooks/useCustomerData";
|
||||
import { CustomerItemType } from "./types/CustomerTypes";
|
||||
|
||||
const CustomerList: FC = () => {
|
||||
const { t } = useTranslation("global");
|
||||
const { canCreate, canUpdate } = usePermissions();
|
||||
const [page, setPage] = useState(1);
|
||||
const [limit, setLimit] = useState(10);
|
||||
const [search, setSearch] = useState("");
|
||||
const getCustomers = useGetCustomers(page, limit, false, search);
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [page, setPage] = useState(1)
|
||||
const getCustomers = useGetCustomers(page)
|
||||
const handleSearch = (value: string) => {
|
||||
if (value !== search) {
|
||||
setPage(1);
|
||||
}
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
<div className='flex w-full justify-between items-center'>
|
||||
<div>
|
||||
{t('customer.customer_list')}
|
||||
</div>
|
||||
<Link to={Pages.customer.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('customer.add_cutomer')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<div>{t("customer.customer_list")}</div>
|
||||
{canCreate("customers") && (
|
||||
<Link to={Pages.customer.create}>
|
||||
<Button className="px-5">
|
||||
<div className="flex gap-2">
|
||||
<Add
|
||||
className="size-5"
|
||||
color="#fff"
|
||||
/>
|
||||
<div>{t("customer.add_cutomer")}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between items-center mt-12'>
|
||||
<div className='flex gap-4'>
|
||||
<Select
|
||||
className='w-36'
|
||||
items={[
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Inactive', value: 'inactive' },
|
||||
]}
|
||||
placeholder={t('service.category')}
|
||||
/>
|
||||
<Select
|
||||
className='w-36'
|
||||
items={[
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Active', value: 'active' },
|
||||
{ label: 'Inactive', value: 'inactive' },
|
||||
]}
|
||||
placeholder={t('service.status')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('service.search')}
|
||||
className='bg-white border border-border'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
getCustomers.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
|
||||
<table className='w-full text-sm '>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={t('customer.name')} />
|
||||
<Td text={t('customer.company')} />
|
||||
<Td text={t('customer.email')} />
|
||||
<Td text={t('customer.active_services')} />
|
||||
<Td text={t('customer.factor')} />
|
||||
<Td text={t('customer.tickets')} />
|
||||
<Td text={t('customer.detail')} />
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||
return (
|
||||
<tr key={item.id} className='tr'>
|
||||
<Td text={item.firstName + ' ' + item.lastName} />
|
||||
<Td text={item?.legalUser?.registrationName} />
|
||||
<Td text={item.email ? item.email : ''} />
|
||||
<Td text={item.subscriptionsCount} />
|
||||
<Td text={''}>
|
||||
<Link to={Pages.receipts.index + `?user=${item.id}`}>
|
||||
<div className='text-[#0047FF]'>{item.invoicesCount} صورت حساب</div>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text={''}>
|
||||
<Link to={Pages.ticket.list + `?user=${item.id}`}>
|
||||
<div className='text-[#0047FF]'>{item.ticketsCount} تیکت</div>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text={''}>
|
||||
<Link to={Pages.customer.update + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text={''} />
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination
|
||||
totalPages={getCustomers.data?.data?.pager?.totalPages}
|
||||
currentPage={page}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className="flex justify-end items-center mt-12">
|
||||
<div>
|
||||
<Input
|
||||
variant="search"
|
||||
placeholder={t("search")}
|
||||
className="bg-white border border-border"
|
||||
onChangeSearchFinal={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
export default CustomerList
|
||||
{getCustomers.isPending ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
<div className="relative overflow-x-auto rounded-3xl mt-9 w-full">
|
||||
<table className="w-full text-sm ">
|
||||
<thead className="thead">
|
||||
<tr>
|
||||
<Td text={t("customer.name")} />
|
||||
<Td text={t("customer.company")} />
|
||||
<Td text={t("customer.email")} />
|
||||
<Td text={t("customer.active_services")} />
|
||||
<Td text={t("customer.factor")} />
|
||||
<Td text={t("customer.tickets")} />
|
||||
<Td text={t("customer.detail")} />
|
||||
<Td text={""} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
|
||||
return (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="tr"
|
||||
>
|
||||
<Td text={item.firstName + " " + item.lastName} />
|
||||
<Td text={item?.legalUser?.registrationName} />
|
||||
<Td text={item.email ? item.email : ""} />
|
||||
<Td text={item.subscriptionsCount} />
|
||||
<Td text={""}>
|
||||
<Link to={Pages.receipts.index + `?user=${item.id}`}>
|
||||
<div className="text-[#0047FF]">{item.invoicesCount} صورت حساب</div>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text={""}>
|
||||
<Link to={Pages.ticket.list + `?user=${item.id}`}>
|
||||
<div className="text-[#0047FF]">{item.ticketsCount} تیکت</div>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text={""}>
|
||||
{canUpdate("customers") && (
|
||||
<Link to={Pages.customer.update + item.id}>
|
||||
<Eye
|
||||
size={20}
|
||||
color="#8C90A3"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</Td>
|
||||
<Td text={""} />
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Pagination
|
||||
totalPages={getCustomers.data?.data?.pager?.totalPages}
|
||||
currentPage={page}
|
||||
onPageChange={setPage}
|
||||
limit={limit}
|
||||
onLimitChange={setLimit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerList;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useFormik } from 'formik';
|
||||
import { CreateCustomerType, ProvinesItemType } from './types/CustomerTypes';
|
||||
import { useGetCities, useGetCustomerDetail, useGetProvines, useUpdateCustomer } from './hooks/useCustomerData';
|
||||
import PageLoading from '../../components/PageLoading';
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types';
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -34,11 +34,11 @@ const UpdateCustomer: FC = () => {
|
||||
const handleUpdateCustomer = (values: CreateCustomerType) => {
|
||||
updateCustomer.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.customer.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -62,40 +62,33 @@ const UpdateCustomer: FC = () => {
|
||||
profilePic: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone: Yup.string().required(t('errors.required')),
|
||||
firstName: Yup.string().required(t('errors.required')),
|
||||
lastName: Yup.string().required(t('errors.required')),
|
||||
email: Yup.string().email(t('errors.email')).required(t('errors.required')),
|
||||
birthDate: Yup.string().required(t('errors.required')),
|
||||
nationalCode: Yup.string().required(t('errors.required')),
|
||||
economicCode: Yup.string().required(t('errors.required')),
|
||||
registrationName: Yup.string().required(t('errors.required')),
|
||||
registrationCode: Yup.string().required(t('errors.required')),
|
||||
nationalIdentity: Yup.string().required(t('errors.required')),
|
||||
postalCode: Yup.string().required(t('errors.required')),
|
||||
address: Yup.string().required(t('errors.required')),
|
||||
cityId: Yup.string().required(t('errors.required')),
|
||||
email: Yup.string().test(
|
||||
'email',
|
||||
t('errors.email'),
|
||||
(value) => !value || Yup.string().email().isValidSync(value)
|
||||
),
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
values.cityId = +values.cityId
|
||||
if (values.password === '') values.password = undefined
|
||||
const payload: Partial<CreateCustomerType> = Object.fromEntries(
|
||||
Object.entries(values).filter(([, value]) => value !== '' && value !== undefined && value !== null)
|
||||
) as Partial<CreateCustomerType>
|
||||
|
||||
if (payload.cityId !== undefined) payload.cityId = +payload.cityId
|
||||
|
||||
if (file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.profilePic = data?.data?.url
|
||||
payload.profilePic = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
|
||||
handleUpdateCustomer(values)
|
||||
} else {
|
||||
values.profilePic = undefined
|
||||
handleUpdateCustomer(values)
|
||||
}
|
||||
|
||||
handleUpdateCustomer(payload as CreateCustomerType)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as api from "../service/CustomerService";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as api from "../service/CustomerService";
|
||||
import { CreateCustomerType } from "../types/CustomerTypes";
|
||||
|
||||
export const useGetCustomers = (page: number, withoutPaginate?: boolean) => {
|
||||
export const useGetCustomers = (page: number, limit: number, withoutPaginate?: boolean, search?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["customers", page],
|
||||
queryFn: () => api.getCustomers(page, withoutPaginate),
|
||||
queryKey: ["customers", page, limit, withoutPaginate, search],
|
||||
queryFn: () => api.getCustomers(page, limit, withoutPaginate, search),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -27,8 +27,7 @@ export const useGetCities = (provincesId: string) => {
|
||||
export const useCreateCustomer = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateCustomerType) =>
|
||||
api.createCustomer(variables),
|
||||
mutationFn: (variables: CreateCustomerType) => api.createCustomer(variables),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["customers"],
|
||||
@@ -48,8 +47,7 @@ export const useGetCustomerDetail = (id?: string) => {
|
||||
export const useUpdateCustomer = (id: string) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (variables: CreateCustomerType) =>
|
||||
api.updateCustomer(id, variables),
|
||||
mutationFn: (variables: CreateCustomerType) => api.updateCustomer(id, variables),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["customers"],
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import axios from "../../../config/axios";
|
||||
import { CreateCustomerType } from "../types/CustomerTypes";
|
||||
|
||||
export const getCustomers = async (page: number, withoutPaginate?: boolean) => {
|
||||
const { data } = await axios.get(
|
||||
`/users/customers?page=${page}&paginate=${withoutPaginate ? "0" : "1"}`
|
||||
);
|
||||
export const getCustomers = async (page: number, limit: number, withoutPaginate?: boolean, search?: string) => {
|
||||
const { data } = await axios.get(`/users/customers?page=${page}&limit=${limit}&paginate=${withoutPaginate ? "0" : "1"}&q=${encodeURIComponent(search ?? "")}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -28,10 +26,7 @@ export const getDetailCustomer = async (id: string) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateCustomer = async (
|
||||
id: string,
|
||||
params: CreateCustomerType
|
||||
) => {
|
||||
export const updateCustomer = async (id: string, params: CreateCustomerType) => {
|
||||
const { data } = await axios.patch(`/users/customers/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as Yup from 'yup'
|
||||
import { useGetAllServices } from '../service/hooks/useServiceData'
|
||||
import { ServiceItemType } from '../service/types/ServiceTypes'
|
||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { useCreateDiscount } from './hooks/useDiscountData'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
@@ -64,11 +64,11 @@ const CreateDiscount: FC = () => {
|
||||
}
|
||||
createDiscount.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.discount.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -12,12 +12,14 @@ import moment from 'moment-jalaali'
|
||||
import ToggleStatusDiscount from './components/ToggleStatusDiscount'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { NumberFormat } from '../../config/func'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
|
||||
const DiscountList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const getDiscounts = useGetDiscounts(search)
|
||||
const deleteDiscount = useDeleteDiscount()
|
||||
@@ -26,10 +28,10 @@ const DiscountList: FC = () => {
|
||||
deleteDiscount.mutate(id, {
|
||||
onSuccess: () => {
|
||||
getDiscounts.refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -40,18 +42,20 @@ const DiscountList: FC = () => {
|
||||
{t('discount.list')}
|
||||
</div>
|
||||
|
||||
<Link to={Pages.discount.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('discount.add_new_discount')}
|
||||
{canCreate('discounts') &&
|
||||
<Link to={Pages.discount.create}>
|
||||
<Button
|
||||
className='w-[172px]'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Add size={20} color='white' />
|
||||
<div>
|
||||
{t('discount.add_new_discount')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -114,17 +118,21 @@ const DiscountList: FC = () => {
|
||||
</Td>
|
||||
<Td text={t('')}>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Link
|
||||
to={Pages.discount.detail + item.id}
|
||||
>
|
||||
<Edit size={20} color='#888888' />
|
||||
</Link>
|
||||
{canUpdate('discounts') &&
|
||||
<Link
|
||||
to={Pages.discount.detail + item.id}
|
||||
>
|
||||
<Edit size={20} color='#888888' />
|
||||
</Link>
|
||||
}
|
||||
|
||||
<Trash
|
||||
size={20}
|
||||
color='#888888'
|
||||
onClick={() => handleDelete(item.id)}
|
||||
/>
|
||||
{canDelete('discounts') &&
|
||||
<Trash
|
||||
size={20}
|
||||
color='#888888'
|
||||
onClick={() => handleDelete(item.id)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as Yup from 'yup'
|
||||
import { useGetAllServices } from '../service/hooks/useServiceData'
|
||||
import { ServiceItemType } from '../service/types/ServiceTypes'
|
||||
import CheckBoxComponent from '../../components/CheckBoxComponent'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { useGetDiscountDetail, useUpdateDiscount } from './hooks/useDiscountData'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
@@ -66,11 +66,11 @@ const UpdateDiscount: FC = () => {
|
||||
}
|
||||
updateDiscount.mutate({ id: id || '', params }, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.discount.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import Td from '../../../components/Td'
|
||||
import Input from '../../../components/Input'
|
||||
import PageLoading from '../../../components/PageLoading'
|
||||
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { GroupIconType } from '../types/Types'
|
||||
|
||||
@@ -29,10 +29,10 @@ const GroupIconList: FC = () => {
|
||||
deleteGroupIcon.mutate(id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Td from '../../../components/Td'
|
||||
import Input from '../../../components/Input'
|
||||
import PageLoading from '../../../components/PageLoading'
|
||||
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { IconType } from '../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
@@ -30,10 +30,10 @@ const IconsList: FC = () => {
|
||||
deleteIcon.mutate(id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import DefaulModal from '../../../../components/DefaulModal'
|
||||
import Input from '../../../../components/Input'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useCreateGroupIcon } from '../../hooks/useIconData'
|
||||
import { CreateGroupIconType } from '../../types/Types'
|
||||
@@ -34,11 +34,11 @@ const CreateGroupIcon: FC<CreateGroupIconProps> = ({ open, close, onSuccess }) =
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
close()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
onSuccess?.()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import UploadBox from '../../../../components/UploadBox'
|
||||
import Select from '../../../../components/Select'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData'
|
||||
import { useSingleUpload } from '../../../service/hooks/useServiceData'
|
||||
import { useMultiUpload } from '../../../service/hooks/useServiceData'
|
||||
import { CreateIconType, GroupIconType } from '../../types/Types'
|
||||
import { ErrorType } from '../../../../helpers/types'
|
||||
|
||||
@@ -21,9 +21,10 @@ interface CreateIconProps {
|
||||
|
||||
const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const createIcon = useCreateIcon()
|
||||
const singleUpload = useSingleUpload()
|
||||
const multiUpload = useMultiUpload()
|
||||
const { data: groupIcons } = useGetGroupIcons()
|
||||
|
||||
const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || []
|
||||
@@ -32,21 +33,6 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
label: group.name
|
||||
}))
|
||||
|
||||
const handleCreateIcon = (values: CreateIconType) => {
|
||||
createIcon.mutate(values, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
setFile(undefined)
|
||||
close()
|
||||
toast.success(t('success'))
|
||||
onSuccess?.()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formik = useFormik<CreateIconType>({
|
||||
initialValues: {
|
||||
url: '',
|
||||
@@ -57,27 +43,44 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (!file) {
|
||||
toast.error(t('errors.required'))
|
||||
if (files.length === 0) {
|
||||
toast(t('errors.required'), 'error')
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.url = data?.data?.url
|
||||
handleCreateIcon(values)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
files.forEach((file) => {
|
||||
formData.append('files', file)
|
||||
})
|
||||
const uploadResult = await multiUpload.mutateAsync(formData)
|
||||
const uploadedUrls: string[] = uploadResult.data.map((item: { url: string }) => item.url)
|
||||
|
||||
for (const url of uploadedUrls) {
|
||||
await createIcon.mutateAsync({
|
||||
url,
|
||||
groupId: values.groupId
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
formik.resetForm()
|
||||
setFiles([])
|
||||
close()
|
||||
toast(t('success'), 'success')
|
||||
onSuccess?.()
|
||||
} catch (error) {
|
||||
const err = error as ErrorType
|
||||
toast(err.response?.data?.error?.message?.[0], 'error')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
formik.resetForm()
|
||||
setFile(undefined)
|
||||
setFiles([])
|
||||
close()
|
||||
}
|
||||
|
||||
@@ -91,9 +94,10 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
<div className='mt-6'>
|
||||
<UploadBox
|
||||
label={t('icon.url')}
|
||||
onChange={(files) => setFile(files[0])}
|
||||
onChange={(selectedFiles) => setFiles(selectedFiles)}
|
||||
isReset={!open}
|
||||
accept="image/svg+xml"
|
||||
isMultiple
|
||||
accept="image/svg+xml,.svg,image/png,.png"
|
||||
/>
|
||||
|
||||
<div className='mt-4'>
|
||||
@@ -117,7 +121,7 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
|
||||
<Button
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createIcon.isPending || singleUpload.isPending}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<TickCircle
|
||||
|
||||
@@ -6,7 +6,7 @@ import Td from '../../../components/Td'
|
||||
import { ShopType, ShopsResponse } from '../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import Button from '../../../components/Button'
|
||||
import ShopAdminsModal from './components/ShopAdminsModal'
|
||||
import Pagination from '../../../components/Pagination'
|
||||
@@ -56,11 +56,11 @@ const ShopsList: FC = () => {
|
||||
const handleDeleteShop = (shopId: string) => {
|
||||
deleteShop.mutate(shopId, {
|
||||
onSuccess: () => {
|
||||
toast.success('فروشگاه با موفقیت حذف شد')
|
||||
toast('فروشگاه با موفقیت حذف شد', 'success')
|
||||
queryClient.invalidateQueries({ queryKey: ['shops'] })
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('خطا در حذف فروشگاه')
|
||||
toast('خطا در حذف فروشگاه', 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwitchComponent from '../../../components/Switch'
|
||||
import Button from '../../../components/Button'
|
||||
import DatePickerComponent from '../../../components/DatePicker'
|
||||
import PageLoading from '../../../components/PageLoading'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
@@ -57,13 +57,13 @@ const UpdateShop: FC = () => {
|
||||
{ id, params },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
queryClient.invalidateQueries({ queryKey: ['shops'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['shop', id] })
|
||||
navigate(Pages.dkala.shops.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message?.[0] || 'خطا در بروزرسانی فروشگاه')
|
||||
toast(error.response?.data?.error?.message?.[0] || 'خطا در بروزرسانی فروشگاه', 'error')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import Button from '../../../../components/Button'
|
||||
import { useAddAdminShop, useGetSystemRoles } from '../../hooks/useIconData'
|
||||
import { SystemRoleType } from '../../types/Types'
|
||||
import { ErrorType } from '../../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
|
||||
interface AddShopAdminFormProps {
|
||||
@@ -46,13 +46,13 @@ const AddShopAdminForm: FC<AddShopAdminFormProps> = ({
|
||||
onSubmit: (values) => {
|
||||
addAdmin(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
formik.resetForm()
|
||||
onSuccess()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Td from '../../../../components/Td'
|
||||
import { ShopAdminType } from '../../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
import TrashWithConfrim from '../../../../components/TrashWithConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { ErrorType } from '../../../../helpers/types'
|
||||
import Button from '../../../../components/Button'
|
||||
import AddShopAdminForm from './AddShopAdminForm'
|
||||
@@ -31,10 +31,10 @@ const ShopAdminsModal: FC<ShopAdminsModalProps> = ({ open, close, shopId }) => {
|
||||
deleteAdmin({ shopId, adminId }, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Td from '../../../components/Td'
|
||||
import Input from '../../../components/Input'
|
||||
import PageLoading from '../../../components/PageLoading'
|
||||
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { GroupIconType } from '../types/Types'
|
||||
|
||||
@@ -29,10 +29,10 @@ const GroupIconList: FC = () => {
|
||||
deleteGroupIcon.mutate(id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Td from '../../../components/Td'
|
||||
import Input from '../../../components/Input'
|
||||
import PageLoading from '../../../components/PageLoading'
|
||||
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { IconType } from '../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
@@ -30,10 +30,10 @@ const IconsList: FC = () => {
|
||||
deleteIcon.mutate(id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import DefaulModal from '../../../../components/DefaulModal'
|
||||
import Input from '../../../../components/Input'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useCreateGroupIcon } from '../../hooks/useIconData'
|
||||
import { CreateGroupIconType } from '../../types/Types'
|
||||
@@ -34,11 +34,11 @@ const CreateGroupIcon: FC<CreateGroupIconProps> = ({ open, close, onSuccess }) =
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
close()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
onSuccess?.()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import UploadBox from '../../../../components/UploadBox'
|
||||
import Select from '../../../../components/Select'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData'
|
||||
import { useSingleUpload } from '../../../service/hooks/useServiceData'
|
||||
import { useMultiUpload } from '../../../service/hooks/useServiceData'
|
||||
import { CreateIconType, GroupIconType } from '../../types/Types'
|
||||
import { ErrorType } from '../../../../helpers/types'
|
||||
|
||||
@@ -21,9 +21,10 @@ interface CreateIconProps {
|
||||
|
||||
const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const createIcon = useCreateIcon()
|
||||
const singleUpload = useSingleUpload()
|
||||
const multiUpload = useMultiUpload()
|
||||
const { data: groupIcons } = useGetGroupIcons()
|
||||
|
||||
const groups = (groupIcons as unknown as { data?: GroupIconType[] })?.data || []
|
||||
@@ -32,21 +33,6 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
label: group.name
|
||||
}))
|
||||
|
||||
const handleCreateIcon = (values: CreateIconType) => {
|
||||
createIcon.mutate(values, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
setFile(undefined)
|
||||
close()
|
||||
toast.success(t('success'))
|
||||
onSuccess?.()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formik = useFormik<CreateIconType>({
|
||||
initialValues: {
|
||||
url: '',
|
||||
@@ -57,27 +43,44 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (!file) {
|
||||
toast.error(t('errors.required'))
|
||||
if (files.length === 0) {
|
||||
toast(t('errors.required'), 'error')
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
await singleUpload.mutateAsync(formData, {
|
||||
onSuccess: (data) => {
|
||||
values.url = data?.data?.url
|
||||
handleCreateIcon(values)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
files.forEach((file) => {
|
||||
formData.append('files', file)
|
||||
})
|
||||
const uploadResult = await multiUpload.mutateAsync(formData)
|
||||
const uploadedUrls: string[] = uploadResult.data.map((item: { url: string }) => item.url)
|
||||
|
||||
for (const url of uploadedUrls) {
|
||||
await createIcon.mutateAsync({
|
||||
url,
|
||||
groupId: values.groupId
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
formik.resetForm()
|
||||
setFiles([])
|
||||
close()
|
||||
toast(t('success'), 'success')
|
||||
onSuccess?.()
|
||||
} catch (error) {
|
||||
const err = error as ErrorType
|
||||
toast(err.response?.data?.error?.message?.[0], 'error')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
formik.resetForm()
|
||||
setFile(undefined)
|
||||
setFiles([])
|
||||
close()
|
||||
}
|
||||
|
||||
@@ -91,9 +94,10 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
<div className='mt-6'>
|
||||
<UploadBox
|
||||
label={t('icon.url')}
|
||||
onChange={(files) => setFile(files[0])}
|
||||
onChange={(selectedFiles) => setFiles(selectedFiles)}
|
||||
isReset={!open}
|
||||
accept="image/svg+xml"
|
||||
isMultiple
|
||||
accept="image/svg+xml,.svg,image/png,.png"
|
||||
/>
|
||||
|
||||
<div className='mt-4'>
|
||||
@@ -117,7 +121,7 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
|
||||
|
||||
<Button
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createIcon.isPending || singleUpload.isPending}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<TickCircle
|
||||
|
||||
@@ -6,7 +6,7 @@ import Td from '../../../components/Td'
|
||||
import { RestaurantType, RestaurantsResponse } from '../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import Button from '../../../components/Button'
|
||||
import RestaurantAdminsModal from './components/RestaurantAdminsModal'
|
||||
import Pagination from '../../../components/Pagination'
|
||||
@@ -56,11 +56,11 @@ const RestaurantsList: FC = () => {
|
||||
const handleDeleteRestaurant = (restaurantId: string) => {
|
||||
deleteRestaurant.mutate(restaurantId, {
|
||||
onSuccess: () => {
|
||||
toast.success('رستوران با موفقیت حذف شد')
|
||||
toast('رستوران با موفقیت حذف شد', 'success')
|
||||
queryClient.invalidateQueries({ queryKey: ['restaurants'] })
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('خطا در حذف رستوران')
|
||||
toast('خطا در حذف رستوران', 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwitchComponent from '../../../components/Switch'
|
||||
import Button from '../../../components/Button'
|
||||
import DatePickerComponent from '../../../components/DatePicker'
|
||||
import PageLoading from '../../../components/PageLoading'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
@@ -57,13 +57,13 @@ const UpdateRestaurant: FC = () => {
|
||||
{ id, params },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
queryClient.invalidateQueries({ queryKey: ['restaurants'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['restaurant', id] })
|
||||
navigate(Pages.dmenu.restaurants.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message?.[0] || 'خطا در بروزرسانی رستوران')
|
||||
toast(error.response?.data?.error?.message?.[0] || 'خطا در بروزرسانی رستوران', 'error')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import Button from '../../../../components/Button'
|
||||
import { useAddAdminRestaurant, useGetSystemRoles } from '../../hooks/useIconData'
|
||||
import { SystemRoleType } from '../../types/Types'
|
||||
import { ErrorType } from '../../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
|
||||
interface AddRestaurantAdminFormProps {
|
||||
@@ -46,13 +46,13 @@ const AddRestaurantAdminForm: FC<AddRestaurantAdminFormProps> = ({
|
||||
onSubmit: (values) => {
|
||||
addAdmin(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
formik.resetForm()
|
||||
onSuccess()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Td from '../../../../components/Td'
|
||||
import { RestaurantAdminType } from '../../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
import TrashWithConfrim from '../../../../components/TrashWithConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { ErrorType } from '../../../../helpers/types'
|
||||
import Button from '../../../../components/Button'
|
||||
import AddRestaurantAdminForm from './AddRestaurantAdminForm'
|
||||
@@ -31,10 +31,10 @@ const RestaurantAdminsModal: FC<RestaurantAdminsModalProps> = ({ open, close, re
|
||||
deleteAdmin({ restaurantId, adminId }, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Td from '../../../components/Td'
|
||||
import Input from '../../../components/Input'
|
||||
import PageLoading from '../../../components/PageLoading'
|
||||
import TrashWithConfrim from '../../../components/TrashWithConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { StickerType } from '../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
@@ -30,10 +30,10 @@ const StickersList: FC = () => {
|
||||
deleteSticker.mutate(id, {
|
||||
onSuccess: () => {
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import UploadBox from '../../../../components/UploadBox'
|
||||
import Select from '../../../../components/Select'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../../components/Toast';
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useCreateSticker, useGetGroupStickers } from '../../hooks/useStickerData'
|
||||
import { useSingleUpload } from '../../../service/hooks/useServiceData'
|
||||
@@ -38,11 +38,11 @@ const CreateSticker: FC<CreateStickerProps> = ({ open, close, onSuccess }) => {
|
||||
formik.resetForm()
|
||||
setFile(undefined)
|
||||
close()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
onSuccess?.()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -58,7 +58,7 @@ const CreateSticker: FC<CreateStickerProps> = ({ open, close, onSuccess }) => {
|
||||
}),
|
||||
onSubmit: async (values) => {
|
||||
if (!file) {
|
||||
toast.error(t('errors.required'))
|
||||
toast(t('errors.required'), 'error')
|
||||
return
|
||||
}
|
||||
const formData = new FormData()
|
||||
@@ -69,7 +69,7 @@ const CreateSticker: FC<CreateStickerProps> = ({ open, close, onSuccess }) => {
|
||||
handleCreateSticker(values)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Textarea from '../../components/Textarea'
|
||||
import Switch from '../../components/Switch'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useCreateGuide } from './hooks/useGuideData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
@@ -50,11 +50,11 @@ const Create: FC = () => {
|
||||
serviceId: id || '',
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.services.guide + id)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message?.[0] || t('error'))
|
||||
toast(error.response?.data?.error?.message?.[0] || t('error'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+30
-22
@@ -12,6 +12,7 @@ import Td from '../../components/Td'
|
||||
import StatusWithText from '../../components/StatusWithText'
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||
import { useDeleteGuide } from './hooks/useGuideData'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
|
||||
type GuideItem = {
|
||||
id: string;
|
||||
@@ -24,6 +25,7 @@ type GuideItem = {
|
||||
|
||||
const List: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const { canCreate, canUpdate, canDelete } = usePermissions()
|
||||
const { id } = useParams()
|
||||
const getGuides = useGetGuides(id || '')
|
||||
const deleteGuide = useDeleteGuide(id || '')
|
||||
@@ -36,21 +38,23 @@ const List: FC = () => {
|
||||
<div>
|
||||
{t('guide.list_guide')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.services.guide + id + '/create'}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('guide.submit_guide')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{canCreate('services') && (
|
||||
<div>
|
||||
<Link to={Pages.services.guide + id + '/create'}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('guide.submit_guide')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -85,13 +89,17 @@ const List: FC = () => {
|
||||
</Td>
|
||||
<Td text=''>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Link to={Pages.services.guide + id + '/' + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
<TrashWithConfrim
|
||||
onDelete={() => deleteGuide.mutate(item.id)}
|
||||
isLoading={deleteGuide.isPending}
|
||||
/>
|
||||
{canUpdate('services') && (
|
||||
<Link to={Pages.services.guide + id + '/' + item.id}>
|
||||
<Eye size={20} color='#8C90A3' />
|
||||
</Link>
|
||||
)}
|
||||
{canDelete('services') && (
|
||||
<TrashWithConfrim
|
||||
onDelete={() => deleteGuide.mutate(item.id)}
|
||||
isLoading={deleteGuide.isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
|
||||
@@ -9,7 +9,7 @@ import Textarea from '../../components/Textarea'
|
||||
import Switch from '../../components/Switch'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { useGetGuideDetail, useUpdateGuide } from './hooks/useGuideData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
@@ -53,11 +53,11 @@ const Update: FC = () => {
|
||||
}
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.services.guide + id)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message?.[0] || t('error'))
|
||||
toast(error.response?.data?.error?.message?.[0] || t('error'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
icon: ReactNode,
|
||||
color: string,
|
||||
count: number,
|
||||
description: string,
|
||||
to?: string
|
||||
}
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
color: string;
|
||||
count: number;
|
||||
description: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
const ItemDashboard: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className="p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl">
|
||||
<div className="size-10 rounded-full bg-[#EEF0F7] flex justify-center items-center">{props.icon}</div>
|
||||
<div className="mt-5">{props.title}</div>
|
||||
|
||||
return (
|
||||
<Link to={props.to ? props.to : ''} className='p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl'>
|
||||
<div className='size-10 rounded-full bg-[#EEF0F7] flex justify-center items-center'>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
{props.title}
|
||||
</div>
|
||||
<div className="mt-5 text-xs text-description flex gap-1.5 items-center">
|
||||
<div
|
||||
style={{ background: props.color }}
|
||||
className="size-1.5 rounded-full"
|
||||
></div>
|
||||
<div className="flex gap-0.5">
|
||||
<div>{props.count}</div>
|
||||
<div className="whitespace-nowrap">{props.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
<div className='mt-5 text-xs text-description flex gap-1.5 items-center'>
|
||||
<div style={{ background: props.color }} className='size-1.5 rounded-full'></div>
|
||||
<div className='flex gap-0.5'>
|
||||
<div>{props.count}</div>
|
||||
<div className='whitespace-nowrap'>
|
||||
{props.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default ItemDashboard
|
||||
export default ItemDashboard;
|
||||
|
||||
@@ -6,6 +6,7 @@ import Pagination from '../../components/Pagination'
|
||||
import { useGetCategory } from './hooks/useLearningData'
|
||||
import { CategoryLearningType } from './types/LearningTypes'
|
||||
import CreateCategory from './components/CreateCategory'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
|
||||
|
||||
const LearningCategory: FC = () => {
|
||||
@@ -13,6 +14,7 @@ const LearningCategory: FC = () => {
|
||||
const { t } = useTranslation('global')
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const getCategory = useGetCategory()
|
||||
const { canCreate } = usePermissions()
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
@@ -65,7 +67,9 @@ const LearningCategory: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateCategory />
|
||||
{canCreate('learnings') && (
|
||||
<CreateCategory />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ import Textarea from '../../components/Textarea'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import CreateLearningSidebar from './components/CreateLearningSidebar'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast';
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
@@ -52,7 +52,7 @@ const CreateLearning: FC = () => {
|
||||
values.videoUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -63,21 +63,21 @@ const CreateLearning: FC = () => {
|
||||
values.coverUrl = data?.data?.url
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
|
||||
createLearning.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
navigate(Pages.learning.list)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error(t('learning.error_video_cover_required'))
|
||||
toast(t('learning.error_video_cover_required'), 'error')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+19
-15
@@ -9,11 +9,13 @@ import PageLoading from '../../components/PageLoading'
|
||||
import Td from '../../components/Td'
|
||||
import { LearningItemType } from './types/LearningTypes'
|
||||
import Pagination from '../../components/Pagination'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
const LearningList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const getLearning = useGetLearning(page)
|
||||
const { canCreate } = usePermissions()
|
||||
|
||||
return (
|
||||
<div className='mt-4'>
|
||||
@@ -21,21 +23,23 @@ const LearningList: FC = () => {
|
||||
<div>
|
||||
{t('learning.list_learning')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to={Pages.learning.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('learning.submit_learning')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{canCreate('learnings') && (
|
||||
<div>
|
||||
<Link to={Pages.learning.create}>
|
||||
<Button
|
||||
className='px-5'
|
||||
>
|
||||
<div className='flex gap-2'>
|
||||
<Add
|
||||
className='size-5'
|
||||
color='#fff'
|
||||
/>
|
||||
<div>{t('learning.submit_learning')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ import Button from '../../../components/Button'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import * as Yup from 'yup'
|
||||
import { useFormik } from 'formik'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { CreateCategoryLearningType } from '../types/LearningTypes'
|
||||
import { useCreateCategory, useGetCategory } from '../hooks/useLearningData'
|
||||
@@ -29,10 +29,10 @@ const CreateCategory: FC = () => {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
getCategory.refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { timeAgo } from '../../config/func';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import MoonLoader from "react-spinners/MoonLoader"
|
||||
import Button from '../../components/Button';
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast } from '../../components/Toast';
|
||||
// import { useGetDashboardSummary } from '../home/hooks/useHomeData';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Pages } from '../../config/Pages';
|
||||
@@ -33,7 +33,7 @@ const Notifications: FC = () => {
|
||||
onSuccess: () => {
|
||||
getDashboard.refetch()
|
||||
refetch()
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import Pagination from '../../components/Pagination'
|
||||
import moment from 'moment-jalaali'
|
||||
import Accept from './components/Accept'
|
||||
import Reject from './components/Reject'
|
||||
import { usePermissions } from '../../hooks/usePermissions'
|
||||
|
||||
const PaymentList: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { canUpdate } = usePermissions()
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const [pageGateWay, setPageGateWay] = useState<number>(1)
|
||||
const [activeTab, setActiveTab] = useState<PaymentStatusType>('GATEWAY')
|
||||
@@ -140,7 +142,7 @@ const PaymentList: FC = () => {
|
||||
<Td text={t(`payment.${item.status}`)} />
|
||||
<Td text={''}>
|
||||
{
|
||||
item.status === 'PENDING' &&
|
||||
item.status === 'PENDING' && canUpdate('payments') &&
|
||||
<Fragment>
|
||||
<div className='flex gap-2'>
|
||||
<Accept
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../../components/Button'
|
||||
import { useApproveTransfer } from '../hooks/usePaymentData'
|
||||
import ModalConfrim from '../../../components/ModalConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
@@ -19,12 +19,12 @@ const Accept: FC<Props> = (props: Props) => {
|
||||
const handleApproveTransfer = () => {
|
||||
approveTransfer.mutate(props.id, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
setOpenConfrimModal(false)
|
||||
window.location.reload()
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Button from '../../../components/Button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRejectTransfer } from '../hooks/usePaymentData'
|
||||
import ModalConfrim from '../../../components/ModalConfrim'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
@@ -20,16 +20,16 @@ const Reject: FC<Props> = (props: Props) => {
|
||||
if (text && text.length > 0) {
|
||||
rejectTransfer.mutate({ id: props.id, comment: text }, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
setOpenConfrimModal(false)
|
||||
window.location.reload()
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
toast(error?.response?.data?.error?.message[0], 'error')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
toast.error(t('payment.reject_comment_error'))
|
||||
toast(t('payment.reject_comment_error'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-127
@@ -1,70 +1,31 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AvatarImage from '../../assets/images/avatar_image.png'
|
||||
import Button from '../../components/Button'
|
||||
import { DocumentUpload } from 'iconsax-react'
|
||||
import Input from '../../components/Input'
|
||||
import DatePickerComponent from '../../components/DatePicker'
|
||||
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
||||
import Username from './components/Username'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../components/Toast'
|
||||
import { ErrorType } from '../../helpers/types'
|
||||
import { UpdateProfileType } from './types/ProfileTypes'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import Email from './components/Email'
|
||||
import Phone from './components/Phone'
|
||||
import { useGetProvines } from '../customer/hooks/useCustomerData'
|
||||
import FirstName from './components/FirstName'
|
||||
import LastName from './components/LastName'
|
||||
import BirthDate from './components/BirthDate'
|
||||
import NationalCode from './components/NationalCode'
|
||||
import { useSingleUpload } from '../service/hooks/useServiceData'
|
||||
|
||||
const Profile: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [file, setFile] = useState<File>()
|
||||
const getProvines = useGetProvines()
|
||||
const getProfile = useGetProfile()
|
||||
const singleUpload = useSingleUpload()
|
||||
const updateProfile = useUpdateProfile()
|
||||
|
||||
const formik = useFormik<UpdateProfileType>({
|
||||
initialValues: {
|
||||
cityId: '',
|
||||
userAddress: '',
|
||||
postalCode: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
cityId: Yup.string().required(t('errors.required')),
|
||||
userAddress: Yup.string().required(t('errors.required')),
|
||||
postalCode: Yup.string().required(t('errors.required')),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
updateProfile.mutate(values, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('success'))
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (getProfile.data) {
|
||||
formik.setValues({
|
||||
cityId: getProfile.data.data.user.city?.id || '',
|
||||
userAddress: getProfile.data.data.user.userAddress || '',
|
||||
postalCode: getProfile.data.data.user.postalCode || '',
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getProfile.data])
|
||||
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
|
||||
if (acceptedFiles.length > 0) {
|
||||
@@ -80,19 +41,19 @@ const Profile: FC = () => {
|
||||
}
|
||||
updateProfile.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('profile.image_uploaded_successfully'))
|
||||
toast(t('profile.image_uploaded_successfully'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error?.message[0])
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error(t('errors.is_not_image'))
|
||||
toast(t('errors.is_not_image'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +68,7 @@ const Profile: FC = () => {
|
||||
</div>
|
||||
|
||||
{
|
||||
getProfile.isPending || getProvines.isPending ?
|
||||
getProfile.isPending ?
|
||||
<PageLoading />
|
||||
:
|
||||
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
|
||||
@@ -175,87 +136,15 @@ const Profile: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<DatePickerComponent
|
||||
label={t('profile.date_of_birth')}
|
||||
onChange={() => { }}
|
||||
placeholder=''
|
||||
defaulValue={getProfile.data?.data?.user?.birthDate}
|
||||
/>
|
||||
|
||||
<FirstName firstName={getProfile.data?.data?.user?.firstName} />
|
||||
<LastName lastName={getProfile.data?.data?.user?.lastName} />
|
||||
<div className='xl:mt-7 mt-4'>
|
||||
<Input
|
||||
label={t('profile.national_code')}
|
||||
value={getProfile.data?.data?.user?.nationalCode}
|
||||
/>
|
||||
<BirthDate birthDate={getProfile.data?.data?.user?.birthDate} />
|
||||
</div>
|
||||
<NationalCode nationalCode={getProfile.data?.data?.user?.nationalCode} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className='mt-8 flex xl:flex-row flex-col xl:gap-0 gap-7'>
|
||||
<div className='flex-1'>
|
||||
<div>
|
||||
{t('profile.address')}
|
||||
</div>
|
||||
<div className='text-description text-xs mt-2'>
|
||||
{t('profile.address_live')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='rowTwoInput'>
|
||||
<Select
|
||||
label={t('profile.province')}
|
||||
items={getProvines.data?.data?.provinces.map((item: ProvinesItemType) => ({ label: item.name, value: item.id }))}
|
||||
onChange={(e) => setProvinesId(e.target.value)}
|
||||
className='bg-white border'
|
||||
placeholder={t('select')}
|
||||
value={provinesId}
|
||||
/>
|
||||
<Select
|
||||
label={t('profile.city')}
|
||||
items={getCities.data?.data?.cities ? getCities.data?.data?.cities.map((item: ProvinesItemType) => ({ label: item.name, value: item.id })) : []}
|
||||
placeholder={t('select')}
|
||||
{...formik.getFieldProps('cityId')}
|
||||
error_text={formik.touched.cityId && formik.errors.cityId ? formik.errors.cityId : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='xl:mt-7 mt-4'>
|
||||
<Input
|
||||
label={t('profile.postal_code')}
|
||||
{...formik.getFieldProps('postalCode')}
|
||||
error_text={formik.touched.postalCode && formik.errors.postalCode ? formik.errors.postalCode : ''}
|
||||
value={formik.values.postalCode}
|
||||
/>
|
||||
</div>
|
||||
<div className='xl:mt-7 mt-4'>
|
||||
<Textarea
|
||||
label={t('profile.address')}
|
||||
defaultValue={formik.values.userAddress}
|
||||
{...formik.getFieldProps('userAddress')}
|
||||
error_text={formik.touched.userAddress && formik.errors.userAddress ? formik.errors.userAddress : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end mt-5'>
|
||||
<Button
|
||||
className='xl:w-fit px-10'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={updateProfile.isPending}
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle size={20} color='white' />
|
||||
<div>
|
||||
{t('save')}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div> */}
|
||||
|
||||
<div className='h-20 xl:hidden'></div>
|
||||
</div>
|
||||
}
|
||||
@@ -264,4 +153,4 @@ const Profile: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Profile
|
||||
export default Profile
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import Button from '../../../components/Button'
|
||||
import DatePickerComponent from '../../../components/DatePicker'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||
import { toast } from '../../../components/Toast'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
birthDate: string | null | undefined
|
||||
}
|
||||
|
||||
const BirthDate: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [value, setValue] = useState<string>(props.birthDate || '')
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||
const updateProfile = useUpdateProfile()
|
||||
|
||||
useEffect(() => {
|
||||
setValue(props.birthDate || '')
|
||||
}, [props.birthDate])
|
||||
|
||||
const handleSave = () => {
|
||||
const params: UpdateProfileType = { birthDate: value }
|
||||
updateProfile.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast(t('success'), 'success')
|
||||
setIsReadOnly(true)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isDisabled = !value || value === (props.birthDate || '')
|
||||
|
||||
return (
|
||||
<div className='flex items-end xl:gap-6 gap-3'>
|
||||
{
|
||||
isReadOnly ? (
|
||||
<Input
|
||||
label={t('profile.date_of_birth')}
|
||||
value={value}
|
||||
readOnly
|
||||
/>
|
||||
) : (
|
||||
<DatePickerComponent
|
||||
label={t('profile.date_of_birth')}
|
||||
onChange={setValue}
|
||||
placeholder=''
|
||||
defaulValue={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isReadOnly ? (
|
||||
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||
<div>{t('edit')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
label={t('save')}
|
||||
className='px-4 w-fit'
|
||||
disabled={isDisabled}
|
||||
onClick={handleSave}
|
||||
isLoading={updateProfile.isPending}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BirthDate
|
||||
@@ -1,14 +1,12 @@
|
||||
import { FC, Fragment, useState } from 'react'
|
||||
import { FC, Fragment, useEffect, useState } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { Edit, TickCircle } from 'iconsax-react'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../../components/Button'
|
||||
import { isEmail } from '../../../config/func'
|
||||
import DefaulModal from '../../../components/DefaulModal'
|
||||
import OTPInput from 'react-otp-input'
|
||||
import { useUpdateEmail } from '../hooks/useProfileData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast'
|
||||
|
||||
type Props = {
|
||||
email: string | null,
|
||||
@@ -20,17 +18,20 @@ const Email: FC<Props> = (props: Props) => {
|
||||
const { t } = useTranslation('global')
|
||||
const [email, setEmail] = useState<string>(props.email ? props.email : '')
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||
const [showModal, setShowModal] = useState<boolean>(false)
|
||||
const [code, setCode] = useState<string>('')
|
||||
const updateEmail = useUpdateEmail()
|
||||
|
||||
const handleShowModal = () => {
|
||||
updateEmail.mutate({ email: email }, {
|
||||
useEffect(() => {
|
||||
setEmail(props.email ? props.email : '')
|
||||
}, [props.email])
|
||||
|
||||
const handleSendVerification = () => {
|
||||
updateEmail.mutate({ email }, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('profile.link_email'))
|
||||
toast(t('profile.link_email'), 'success')
|
||||
setIsReadOnly(true)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -45,17 +46,30 @@ const Email: FC<Props> = (props: Props) => {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
{
|
||||
!props.isVerified &&
|
||||
!props.isVerified && isReadOnly &&
|
||||
<div className='text-[10px] absolute left-24 text-red-400 top-[34px]'>
|
||||
{t('profile.email_not_verified')}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
isReadOnly ?
|
||||
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||
<div>
|
||||
{t('edit')}
|
||||
<div className='flex items-center gap-3 relative -top-3'>
|
||||
{
|
||||
!props.isVerified && !!email &&
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleSendVerification}
|
||||
disabled={updateEmail.isPending}
|
||||
className='text-[#0047FF] text-xs whitespace-nowrap disabled:opacity-50'
|
||||
>
|
||||
{t('profile.resend_email')}
|
||||
</button>
|
||||
}
|
||||
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer gap-1 text-[#0047FF] text-xs items-center'>
|
||||
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||
<div>
|
||||
{t('edit')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
@@ -63,65 +77,13 @@ const Email: FC<Props> = (props: Props) => {
|
||||
label={t('save')}
|
||||
className='px-4 w-fit'
|
||||
disabled={!isEmail(email)}
|
||||
onClick={handleShowModal}
|
||||
onClick={handleSendVerification}
|
||||
isLoading={updateEmail.isPending}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<DefaulModal
|
||||
open={showModal}
|
||||
close={() => setShowModal(false)}
|
||||
isHeader
|
||||
title_header={t('profile.confrim_email')}
|
||||
>
|
||||
<div className='mt-7'>
|
||||
<div className='text-xs text-center'>
|
||||
کد تایید به ایمیل {email} ارسال شده است.برای تایید کد مربوطه را وارد کنید.
|
||||
</div>
|
||||
|
||||
<div className='mt-10'>کد تایید</div>
|
||||
<div className='mt-2 w-full flex justify-center dltr otp'>
|
||||
<OTPInput
|
||||
value={code}
|
||||
onChange={setCode}
|
||||
shouldAutoFocus
|
||||
renderInput={(props) =>
|
||||
<input
|
||||
{...props}
|
||||
type='tel'
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
className='w-full h-[50px] flex-1 mx-2 bg-white bg-opacity-30 border rounded-2.5' />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-14 flex justify-end border-t border-border pt-8'>
|
||||
<div className='flex gap-5'>
|
||||
<Button
|
||||
className='bg-white bg-opacity-40 text-description w-[150px] text-xs'
|
||||
label='لغو'
|
||||
onClick={() => setShowModal(false)}
|
||||
/>
|
||||
<Button
|
||||
className='w-[150px] text-xs'
|
||||
>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<TickCircle
|
||||
size={20}
|
||||
color='white'
|
||||
/>
|
||||
<div>{t('wallet.submit_slip')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</DefaulModal>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default Email
|
||||
export default Email
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import Button from '../../../components/Button'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||
import { toast } from '../../../components/Toast'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
firstName: string | null | undefined
|
||||
}
|
||||
|
||||
const FirstName: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [value, setValue] = useState<string>(props.firstName || '')
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||
const updateProfile = useUpdateProfile()
|
||||
|
||||
useEffect(() => {
|
||||
setValue(props.firstName || '')
|
||||
}, [props.firstName])
|
||||
|
||||
const handleSave = () => {
|
||||
const params: UpdateProfileType = { firstName: value.trim() }
|
||||
updateProfile.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast(t('success'), 'success')
|
||||
setIsReadOnly(true)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isDisabled = value.trim().length < 2 || value.trim() === (props.firstName || '')
|
||||
|
||||
return (
|
||||
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
|
||||
<Input
|
||||
label={t('profile.first_name')}
|
||||
value={value}
|
||||
readOnly={isReadOnly}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
{
|
||||
isReadOnly ? (
|
||||
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||
<div>{t('edit')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
label={t('save')}
|
||||
className='px-4 w-fit'
|
||||
disabled={isDisabled}
|
||||
onClick={handleSave}
|
||||
isLoading={updateProfile.isPending}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FirstName
|
||||
@@ -0,0 +1,69 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import Button from '../../../components/Button'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||
import { toast } from '../../../components/Toast'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
lastName: string | null | undefined
|
||||
}
|
||||
|
||||
const LastName: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [value, setValue] = useState<string>(props.lastName || '')
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||
const updateProfile = useUpdateProfile()
|
||||
|
||||
useEffect(() => {
|
||||
setValue(props.lastName || '')
|
||||
}, [props.lastName])
|
||||
|
||||
const handleSave = () => {
|
||||
const params: UpdateProfileType = { lastName: value.trim() }
|
||||
updateProfile.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast(t('success'), 'success')
|
||||
setIsReadOnly(true)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isDisabled = value.trim().length < 2 || value.trim() === (props.lastName || '')
|
||||
|
||||
return (
|
||||
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
|
||||
<Input
|
||||
label={t('profile.last_name')}
|
||||
value={value}
|
||||
readOnly={isReadOnly}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
{
|
||||
isReadOnly ? (
|
||||
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||
<div>{t('edit')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
label={t('save')}
|
||||
className='px-4 w-fit'
|
||||
disabled={isDisabled}
|
||||
onClick={handleSave}
|
||||
isLoading={updateProfile.isPending}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LastName
|
||||
@@ -0,0 +1,74 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import Button from '../../../components/Button'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUpdateProfile } from '../hooks/useProfileData'
|
||||
import { UpdateProfileType } from '../types/ProfileTypes'
|
||||
import { toast } from '../../../components/Toast'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
nationalCode: string | null | undefined
|
||||
}
|
||||
|
||||
const NationalCode: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [value, setValue] = useState<string>(props.nationalCode || '')
|
||||
const [isReadOnly, setIsReadOnly] = useState<boolean>(true)
|
||||
const updateProfile = useUpdateProfile()
|
||||
|
||||
useEffect(() => {
|
||||
setValue(props.nationalCode || '')
|
||||
}, [props.nationalCode])
|
||||
|
||||
const handleSave = () => {
|
||||
const params: UpdateProfileType = { nationalCode: value }
|
||||
updateProfile.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast(t('success'), 'success')
|
||||
setIsReadOnly(true)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isDisabled = value.length !== 10 || value === (props.nationalCode || '')
|
||||
|
||||
return (
|
||||
<div className='flex items-end xl:gap-6 gap-3 xl:mt-7 mt-4'>
|
||||
<Input
|
||||
label={t('profile.national_code')}
|
||||
value={value}
|
||||
readOnly={isReadOnly}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value.replace(/\D/g, '').slice(0, 10)
|
||||
setValue(next)
|
||||
}}
|
||||
inputMode='numeric'
|
||||
maxLength={10}
|
||||
/>
|
||||
{
|
||||
isReadOnly ? (
|
||||
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
|
||||
<Edit className='xl:size-[18px] size-4' color='#0047FF' />
|
||||
<div>{t('edit')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
label={t('save')}
|
||||
className='px-4 w-fit'
|
||||
disabled={isDisabled}
|
||||
onClick={handleSave}
|
||||
isLoading={updateProfile.isPending}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NationalCode
|
||||
@@ -7,7 +7,7 @@ import DefaulModal from '../../../components/DefaulModal'
|
||||
import OTPInput from 'react-otp-input'
|
||||
import { useUpdatePhone, useVerifyPhone } from '../hooks/useProfileData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { VerifyPhoneType } from '../types/ProfileTypes'
|
||||
|
||||
type Props = {
|
||||
@@ -30,7 +30,7 @@ const Phone: FC<Props> = (props: Props) => {
|
||||
setShowModal(true)
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -45,10 +45,10 @@ const Phone: FC<Props> = (props: Props) => {
|
||||
onSuccess: () => {
|
||||
setShowModal(false)
|
||||
setIsReadOnly(true)
|
||||
toast.success(t('success'))
|
||||
toast(t('success'), 'success')
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
toast.error(error.response?.data?.error.message[0])
|
||||
toast(error.response?.data?.error.message[0], 'error')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '../../../components/Button'
|
||||
import { useCheckUserName, useUpdateProfile } from '../hooks/useProfileData'
|
||||
import { CheckUserNameType, UpdateProfileType } from '../types/ProfileTypes'
|
||||
import { toast } from 'react-toastify'
|
||||
import { toast } from '../../../components/Toast';
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
type Props = {
|
||||
@@ -54,13 +54,13 @@ const Username: FC<Props> = (props: Props) => {
|
||||
|
||||
updateProfile.mutate(params, {
|
||||
onSuccess: () => {
|
||||
toast.success(t('profile.username_save_success'))
|
||||
toast(t('profile.username_save_success'), 'success')
|
||||
setCanEdit(false)
|
||||
setCanSave(false)
|
||||
setStatus('')
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error.response?.data?.error?.message?.[0])
|
||||
toast(error.response?.data?.error?.message?.[0], 'error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import * as api from '../service/ProfileService'
|
||||
import { CheckUserNameType, UpdateEmailType, UpdatePhoneType, UpdateProfileType, VerifyPhoneType } from '../types/ProfileTypes';
|
||||
|
||||
@@ -16,14 +16,22 @@ export const useCheckUserName = () => {
|
||||
};
|
||||
|
||||
export const useUpdateProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (variables: UpdateProfileType) => api.updateProfile(variables),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateEmail = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (variables: UpdateEmailType) => api.updateEmail(variables),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,7 +42,11 @@ export const useUpdatePhone = () => {
|
||||
};
|
||||
|
||||
export const useVerifyPhone = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (variables: VerifyPhoneType) => api.verifyPhone(variables),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ export type UpdateProfileType = {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
birthDate?: string;
|
||||
nationalCode?: string;
|
||||
userName?: string;
|
||||
profilePic?: string;
|
||||
cityId?: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user