Compare commits

..

124 Commits

Author SHA1 Message Date
hamid zarghami 6b96ecf9d9 refer ticket
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 14:57:27 +03:30
hamid zarghami 4bb164be0f fix render column
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 14:39:55 +03:30
hamid zarghami 2f71b4c96c edit check list item 2026-07-26 14:38:46 +03:30
hamid zarghami cabf248c0e workspace users
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 10:05:58 +03:30
hamid zarghami 1990e73368 permisisions
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-26 08:59:39 +03:30
hamid zarghami 185961c162 permission 2026-07-26 08:45:44 +03:30
hamid zarghami b60a38e21d ticket master 2026-07-25 16:36:35 +03:30
hamid zarghami f53b5d8fe1 fix design bug 2026-07-25 15:53:16 +03:30
hamid zarghami f5da44d9cf attachment comment 2026-07-25 15:49:02 +03:30
hamid zarghami 77475f09cd project users
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-25 15:28:09 +03:30
hamid zarghami 9413cf03c9 out side click fix 2026-07-25 12:52:49 +03:30
hamid zarghami b2f9166c22 tooltip 2026-07-25 12:46:57 +03:30
hamid zarghami 7fedfdfbfd padding left for task description 2026-07-25 12:25:48 +03:30
hamid zarghami 71e7d4eac2 forgot password
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-25 11:47:21 +03:30
hamid zarghami c9713a93ac remove remark
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-25 11:19:15 +03:30
hamid zarghami 42528f4d5c edit profile 2026-07-25 11:19:15 +03:30
hamid zarghami 52bb36ac3c space between permision 2026-07-25 11:19:15 +03:30
hamid zarghami f7871db066 fix check list ticked 2026-07-25 11:19:15 +03:30
hamid zarghami 153547d098 logo otp auto 2026-07-25 11:19:15 +03:30
hamid zarghami 09feb73331 changed text management attach to view projects 2026-07-25 11:19:15 +03:30
hamid zarghami 9a7858a4ec fix a problem error 2026-07-25 11:19:14 +03:30
hamid zarghami df2f800b54 delete and update a attachment in task 2026-07-25 11:19:14 +03:30
hamid zarghami 3537094e7c middle in mmobile 2026-07-25 11:19:14 +03:30
hamid zarghami fa55fd0f02 fix design bug in description task 2026-07-25 11:19:14 +03:30
morteza d43774a8a1 improvement
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-25 10:33:09 +03:30
hamid zarghami 13781df281 show attachment in task card and show check list item in task card
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-25 09:20:48 +03:30
hamid zarghami 086db34c12 preview image 2026-07-25 09:13:54 +03:30
hamid zarghami b5b8344888 fix pagination bug
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-22 16:11:27 +03:30
hamid zarghami 76a68adc1c fix build
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-22 15:59:15 +03:30
hamid zarghami 24171dbaf8 customer search
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-22 15:46:12 +03:30
hamid zarghami 9aadb8e6ad pagination with limit 2026-07-22 15:39:20 +03:30
hamid zarghami 96d750fc88 fix deploy
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-22 15:20:50 +03:30
morteza f1173adc6d multiple role
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-22 12:11:44 +03:30
hamid zarghami a2d2234a28 customer update optional fields
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-22 10:11:34 +03:30
hamid zarghami 81353fc281 base url
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-21 14:35:58 +03:30
hamid zarghami 402354e122 profile picture in any task item 2026-07-21 14:35:36 +03:30
hamid zarghami 9004413a33 delete a check list item 2026-07-21 14:27:34 +03:30
hamid zarghami 6b5ac30f32 split tasks and user
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-21 12:29:11 +03:30
hamid zarghami 09adc8bdd0 permission fix
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-21 10:26:50 +03:30
hamid zarghami bddf675d66 remove color in task
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-21 09:02:03 +03:30
hamid zarghami dd9bc0ddca base url
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-20 21:22:31 +03:30
hamid zarghami 65154a1aab create task by ticket 2026-07-20 15:38:08 +03:30
hamid zarghami 2af7323a6a permission group 2026-07-20 15:10:30 +03:30
hamid zarghami 70e159cefc update permion server 2026-07-20 11:45:21 +03:30
hamid zarghami 6609666279 fix design bug 2026-07-20 11:39:25 +03:30
hamid zarghami a3ce347f02 confrim before delete a workspace or a project
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-20 11:37:43 +03:30
hamid zarghami d3fd21177b change toast
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-20 10:55:33 +03:30
hamid zarghami feed194673 setting project in workspacce a project
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-20 10:37:00 +03:30
hamid zarghami f2cfbf6a5b assign user in create task 2026-07-20 10:21:26 +03:30
hamid zarghami 94639858f2 profile picture 2026-07-20 10:11:43 +03:30
hamid zarghami bd65d28ea3 update background color 2026-07-20 09:56:04 +03:30
hamid zarghami ee792b69bf fix home icon
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-19 12:08:00 +03:30
hamid zarghami 89d9ad7557 grediend in workspace
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-19 11:49:38 +03:30
hamid zarghami 3eae768d5a fix build 2026-07-19 11:43:35 +03:30
hamid zarghami b847dbca69 update column name 2026-07-19 11:39:31 +03:30
hamid zarghami 44fd61c039 update sidebar 2026-07-19 11:32:33 +03:30
hamid zarghami 2b338d53b1 change sidebar
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-19 11:09:22 +03:30
hamid zarghami 1fb0091aa2 sidebar change
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-19 09:17:12 +03:30
hamid zarghami a335f2b66c fix save desciption
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-18 12:07:29 +03:30
hamid zarghami 1d56aae0cb fix users list
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-18 11:44:14 +03:30
hamid zarghami ac3e8f5af9 outside click tab
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-18 08:49:24 +03:30
hamid zarghami 8cd652fece responsive and move to dnd 2026-07-17 23:16:52 +03:30
hamid zarghami 6206b4be17 fix build
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-15 11:37:50 +03:30
hamid zarghami d5d161c7f9 fix base url
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-15 11:00:01 +03:30
hamid zarghami 1413744b6f Create task in tocker 2026-07-15 10:48:27 +03:30
hamid zarghami 443d97f898 track time 2026-07-15 10:24:05 +03:30
hamid zarghami a4b263cfd7 workspace name 2026-07-15 10:00:33 +03:30
hamid zarghami 59048e9b5f update sidebar for task manager
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-15 09:13:46 +03:30
hamid zarghami f654ec61ef change base url
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-14 14:58:12 +03:30
hamid zarghami 46ac8ca0ca change priority
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-14 14:57:43 +03:30
hamid zarghami a8830ea70d cursor pagination 2026-07-14 14:52:40 +03:30
hamid zarghami 1d92a954d0 fix build
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-14 11:45:40 +03:30
hamid zarghami d6955187cc update env
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-14 11:29:46 +03:30
hamid zarghami 1963a351d0 color task
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-14 11:28:10 +03:30
hamid zarghami a356a3fcbd change status in modal
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-14 11:18:20 +03:30
hamid zarghami 95177b529e update task phase
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-12 12:29:52 +03:30
hamid zarghami fd545231d3 Fix + button 2026-07-12 12:17:43 +03:30
hamid zarghami e1fe030316 setting date 2026-07-12 12:15:55 +03:30
hamid zarghami c11c972c7c attachment 2026-07-12 12:13:31 +03:30
hamid zarghami 7c2d6c465e change status checklist 2026-07-12 12:00:08 +03:30
hamid zarghami 50d4ff9281 check list 2026-07-12 11:56:46 +03:30
hamid zarghami 61282e1d1b manager users 2026-07-12 11:43:56 +03:30
hamid zarghami ee8da0b88c Remark 2026-07-11 12:22:15 +03:30
hamid zarghami c4150ab8ac delete task and update task 2026-07-11 11:58:49 +03:30
hamid zarghami 833ac2446f task list and create 2026-07-11 11:54:23 +03:30
hamid zarghami 2bc7a37082 taskphase crate and delete 2026-07-11 11:45:18 +03:30
hamid zarghami 9c84048730 get project 2026-07-11 11:40:55 +03:30
hamid zarghami 651fb16feb CREATE AND DELETE column 2026-07-11 11:06:43 +03:30
hamid zarghami 1618b5fb57 update project 2026-07-11 11:01:01 +03:30
hamid zarghami 47691a649f project list 2026-07-11 10:19:39 +03:30
hamid zarghami b0737ea6ad create project 2026-07-09 11:42:41 +03:30
hamid zarghami 21a99d9ec1 edit and delete workspace 2026-07-09 10:17:33 +03:30
hamid zarghami b965dd0a6d list of workspace 2026-07-09 10:12:55 +03:30
hamid zarghami 43dc693061 workspace create 2026-07-09 10:09:47 +03:30
hamid zarghami 9043cd4cb2 base url
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-29 16:30:17 +03:30
hamid zarghami d0a1d79fad update role + and fox
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-29 16:28:26 +03:30
hamid zarghami f3153c6793 moving column
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-20 10:22:06 +03:30
hamid zarghami f836abd8f4 responsive task manager 2026-06-20 09:21:13 +03:30
hamid zarghami 19d60a9225 update style default modal + added date in modal
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-17 16:40:08 +03:30
hamid zarghami 73d1ad3492 attachments list 2026-06-17 16:33:01 +03:30
hamid zarghami 9c6aa47cf7 Setting design
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-17 16:14:18 +03:30
hamid zarghami 92033fb94c task detail attachment form 2026-06-17 15:34:47 +03:30
hamid zarghami 707908fe03 check list 2026-06-17 12:20:54 +03:30
hamid zarghami c62e958e91 Create check list 2026-06-17 10:57:52 +03:30
hamid zarghami 908f5e72ed manage users 2026-06-17 10:40:59 +03:30
hamid zarghami a03a147d12 lebal 2026-06-17 09:46:57 +03:30
hamid zarghami 021285bc09 task detail Labels 2026-06-17 09:45:04 +03:30
hamid zarghami c6559b3054 detail modal task 2026-06-16 16:31:49 +03:30
hamid zarghami 8518662eee Create project fix design select project 2026-06-16 14:39:56 +03:30
hamid zarghami 90cc91ac86 project list 2026-06-16 14:35:37 +03:30
hamid zarghami ad523d848f create project 2026-06-16 14:05:57 +03:30
hamid zarghami 82b2138027 create workspace 2026-06-16 10:20:25 +03:30
hamid zarghami 35c1c1c8a9 multi upload icons
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-14 09:51:57 +03:30
hamid zarghami 328402fd7f drag and drop
deploy to danak / build_and_deploy (push) Has been cancelled
2026-06-11 12:30:45 +03:30
hamid zarghami eb2f23c403 add new columg design 2026-06-11 12:30:45 +03:30
hamid zarghami f3d7296b06 height fit 2026-06-11 12:30:45 +03:30
hamid zarghami d1f28367d9 add task design 2026-06-11 12:30:45 +03:30
hamid zarghami e2e96a03d3 Columnn page 2026-06-11 12:30:45 +03:30
hamid zarghami 1bef61588b Task + column scroll 2026-06-11 12:30:45 +03:30
hamid zarghami cccc0896b5 add task 2026-06-11 12:30:45 +03:30
hamid zarghami b7b3d0e9e9 base columnn component 2026-06-11 12:30:45 +03:30
hamid zarghami 284b69e298 move component 2026-06-11 12:30:45 +03:30
hamid zarghami 8d66437e7b base design workspace 2026-06-11 12:30:45 +03:30
hamid zarghami 9d2401d951 Add to sidebar 2026-06-11 12:30:45 +03:30
265 changed files with 14345 additions and 4461 deletions
+111
View File
@@ -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
+83
View File
@@ -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 features 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
+2 -1
View File
@@ -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.
+4
View File
@@ -0,0 +1,4 @@
{
"printWidth": 200,
"singleAttributePerLine": true
}
+112 -74
View File
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@headlessui/react": "^2.2.0",
"@hello-pangea/dnd": "^18.0.1",
"@material-tailwind/react": "^2.1.10",
"@tanstack/react-query": "^5.62.8",
"axios": "^1.7.9",
@@ -31,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",
@@ -113,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",
@@ -1568,7 +1569,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1585,7 +1585,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1602,7 +1601,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1619,7 +1617,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1636,7 +1633,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1653,7 +1649,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1670,7 +1665,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1687,7 +1681,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1704,7 +1697,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1721,7 +1713,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1738,7 +1729,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1755,7 +1745,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1772,7 +1761,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1789,7 +1777,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1806,7 +1793,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1823,7 +1809,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1840,7 +1825,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1857,7 +1841,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1874,7 +1857,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1891,7 +1873,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1908,7 +1889,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1925,7 +1905,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1942,7 +1921,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1959,7 +1937,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1976,7 +1953,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2215,6 +2191,23 @@
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@hello-pangea/dnd": {
"version": "18.0.1",
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
"integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.26.7",
"css-box-model": "^1.2.1",
"raf-schd": "^4.0.3",
"react-redux": "^9.2.0",
"redux": "^5.0.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -2420,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"
},
@@ -2432,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"
@@ -2747,7 +2742,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2761,7 +2755,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2775,7 +2768,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2789,7 +2781,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2803,7 +2794,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2817,7 +2807,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2831,7 +2820,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2845,7 +2833,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2859,7 +2846,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2873,7 +2859,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2887,7 +2872,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2901,7 +2885,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2915,7 +2898,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2929,7 +2911,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2943,7 +2924,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2957,7 +2937,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2971,7 +2950,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2985,7 +2963,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2999,7 +2976,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3013,7 +2989,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3098,8 +3073,9 @@
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
@@ -3112,7 +3088,7 @@
"version": "7.6.8",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz",
"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
@@ -3122,7 +3098,7 @@
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
@@ -3133,7 +3109,7 @@
"version": "7.20.6",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
"integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.20.7"
@@ -3201,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"
@@ -3235,6 +3212,12 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz",
@@ -3271,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",
@@ -3479,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"
},
@@ -3834,6 +3819,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -4136,6 +4122,15 @@
"node": ">=8"
}
},
"node_modules/css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
"license": "MIT",
"dependencies": {
"tiny-invariant": "^1.0.6"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4491,7 +4486,6 @@
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -4556,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",
@@ -5470,6 +5465,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
},
@@ -6050,7 +6046,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -6434,7 +6430,6 @@
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -6786,7 +6781,6 @@
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6802,6 +6796,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -6968,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",
@@ -7045,6 +7041,12 @@
"node": ">= 12.0.0"
}
},
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
"license": "MIT"
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -7097,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"
}
@@ -7112,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"
},
@@ -7226,6 +7230,29 @@
"react-dom": ">=16.8.6 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-redux": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -7284,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",
@@ -7320,6 +7334,13 @@
"node": ">=8.10.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -7496,8 +7517,8 @@
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
@@ -7840,7 +7861,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -8352,6 +8372,12 @@
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@@ -8396,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"
},
@@ -8557,8 +8584,9 @@
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8745,6 +8773,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -8756,8 +8793,8 @@
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -8861,7 +8898,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -8879,8 +8915,8 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -9171,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",
@@ -9232,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"
},
@@ -9477,7 +9515,7 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
"integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
"dev": true,
"devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
+1 -1
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@headlessui/react": "^2.2.0",
"@hello-pangea/dnd": "^18.0.1",
"@material-tailwind/react": "^2.1.10",
"@tanstack/react-query": "^5.62.8",
"axios": "^1.7.9",
@@ -33,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
View File
@@ -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

+21 -4
View File
@@ -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>
)
}
+74
View File
@@ -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
View File
@@ -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;
+6 -3
View File
@@ -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,
)
}
+100 -73
View File
@@ -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;
+18
View File
@@ -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
View File
@@ -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;
+54
View File
@@ -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;
+17 -24
View File
@@ -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;
+32 -3
View File
@@ -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) => {
+13
View File
@@ -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",
},
@@ -166,4 +170,13 @@ export const Pages = {
create: "/reseller/create",
withdraw: "/reseller/withdraw",
},
taskmanager: {
workspace: "/taskmanager/workspace/",
workspaceList: "/workspace/list",
createWorkspace: "/workspace/create",
updateWorkspace: "/workspace/update/",
createProject: "/project/create",
updateProject: "/project/update/",
projectList: "/project/list/",
},
};
+47 -5
View File
@@ -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] ?? "";
};
+278
View File
@@ -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);
};
+28
View File
@@ -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),
};
};
+78
View File
@@ -4,6 +4,7 @@
html {
direction: rtl;
min-height: 100%;
}
body {
@@ -16,6 +17,7 @@ body {
background: #eceef6;
font-weight: 300;
overflow: hidden;
min-height: 100%;
}
html,
body,
@@ -70,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;
}
+147 -8
View File
@@ -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,11 +96,13 @@
"sms_count": "تعداد پیامک",
"subscriptions": "اشتراک ها",
"financial": "مالی",
"products": "محصولات",
"dmail": "دی میل",
"domain": "دامنه",
"reseller": "نماینده",
"resellers": "نمایندگان",
"withdraw": "درخواست های برداشت"
"withdraw": "درخواست های برداشت",
"taskmanager": "مدیریت تسک"
},
"reseller": {
"list": "لیست نمایندکان",
@@ -128,7 +140,10 @@
"service_category": "دسته بندی سرویس ها",
"customer_list": "لیست مشتریان",
"create_customer": "افزودن مشتری",
"representative_list": "لیست نمایندگان",
"ticket_list": "لیست تیکت ها",
"ticket_category": "دسته بندی تیکت ها",
"ticket_masters": "مستر",
"send_ticket": "ارسال تیکت",
"category": "دسته بندی",
"user_list": "لیست کاربران",
@@ -140,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": "لیست راهنمای سرویس",
@@ -343,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": "غیرفعال",
@@ -695,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": "آیا ادمین است؟",
@@ -710,6 +761,7 @@
"group_add": "ساخت گروه",
"submit_group": "ثبت گروه",
"error_count_users": "حداقل یک کاربر باید انتخاب شود",
"error_count_roles": "حداقل یک نقش باید انتخاب شود",
"list_user_group": "لیست گروه های کاربری",
"groups": "گروه ها",
"group_name": "نام گروه",
@@ -746,7 +798,10 @@
"confrim_email": "تایید ایمیل",
"confrim": "تایید صورتحساب ",
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
"email_not_verified": "ایمیل تایید نشده"
"email_not_verified": "ایمیل تایید نشده",
"resend_email": "ارسال مجدد تایید",
"first_name": "نام",
"last_name": "نام خانوادگی"
},
"email": "ایمیل",
"customer": {
@@ -964,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
View File
@@ -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>
+5 -5
View File
@@ -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')
}
}
})
+4 -4
View File
@@ -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')
}
})
+3 -3
View File
@@ -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')
}
})
}
+34 -26
View File
@@ -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>
+3 -3
View File
@@ -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')
}
})
}
+50
View File
@@ -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
+5 -5
View File
@@ -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}
+9 -3
View File
@@ -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>
+3 -2
View File
@@ -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 : ''}
+2 -2
View File
@@ -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')
},
})
},
+2 -2
View File
@@ -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')
},
})
},
+13 -1
View File
@@ -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({
@@ -37,3 +37,15 @@ export const useRegister = () => {
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),
});
};
+12
View File
@@ -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;
};
+10
View File
@@ -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;
};
+68 -60
View File
@@ -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
View File
@@ -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>
+5 -3
View File
@@ -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} />
+4 -4
View File
@@ -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')
}
})
},
+3 -3
View File
@@ -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')
}
})
},
+3 -3
View File
@@ -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')
}
})
}
+2 -2
View File
@@ -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')
}
})
}
+2 -2
View File
@@ -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
View File
@@ -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>
)
+5 -1
View File
@@ -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>
)
+4 -4
View File
@@ -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
View File
@@ -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;
+18 -25
View File
@@ -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)
}
})
+6 -8
View File
@@ -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;
};
+3 -3
View File
@@ -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')
}
})
},
+32 -24
View File
@@ -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>
+3 -3
View File
@@ -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')
}
})
},
+3 -3
View File
@@ -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')
}
})
}
+3 -3
View File
@@ -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')
}
})
}
+39 -35
View File
@@ -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
+3 -3
View File
@@ -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')
}
})
}
+3 -3
View File
@@ -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')
}
})
}
+3 -3
View File
@@ -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')
}
})
}
+3 -3
View File
@@ -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')
}
})
}
+39 -35
View File
@@ -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
+3 -3
View File
@@ -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')
}
})
}
+3 -3
View File
@@ -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')
}
})
}
+3 -3
View File
@@ -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')
}
})
}
+3 -3
View File
@@ -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
View File
@@ -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>
+3 -3
View File
@@ -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'))
}
})
}
+26 -31
View File
@@ -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;
+5 -1
View File
@@ -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>
)
+6 -6
View File
@@ -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
View File
@@ -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')
}
})
}
+2 -2
View File
@@ -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')
}
})
}
+3 -1
View File
@@ -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 -3
View File
@@ -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')
},
})
}
+4 -4
View File
@@ -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')
}
}
+15 -126
View File
@@ -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>
}
@@ -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
+31 -69
View File
@@ -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,63 +77,11 @@ 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>
)
}
@@ -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
+69
View File
@@ -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
+4 -4
View File
@@ -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')
}
})
+3 -3
View File
@@ -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')
},
})
}
+13 -1
View File
@@ -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"] });
},
});
};
+1
View File
@@ -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