Compare commits

...

51 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
205 changed files with 6420 additions and 3037 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ export const createCustomer = async (params: CreateCustomerType) => {
**List** — header + optional filters + `PageLoading` while pending + `<table>` with `Td` + `Pagination`. Link actions via `Pages.{feature}.*`.
**Create / Update** — Formik + Yup validation, `toast.success(t('success'))` on success, `toast.error(error.response?.data?.error.message[0])` on error (`ErrorType` from `src/helpers/types.ts`). Navigate back to list with `useNavigate()` + `Pages.*`.
**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.
+1 -1
View File
@@ -2,4 +2,4 @@ VITE_TOKEN_NAME = 'admin_token'
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
VITE_BASE_URL = 'https://api.danakcorp.com'
#VITE_BASE_URL = 'http://192.168.1.107:3500'
# VITE_BASE_URL = 'http://192.168.99.131:3500'
Binary file not shown.
+23 -15
View File
@@ -32,7 +32,6 @@
"react-otp-input": "^3.1.1",
"react-router-dom": "^7.1.0",
"react-spinners": "^0.15.0",
"react-toastify": "^11.0.2",
"swiper": "^11.2.1",
"tailwind-merge": "^2.5.5",
"vite-plugin-pwa": "^0.21.1",
@@ -114,6 +113,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0",
@@ -2413,6 +2413,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -2425,6 +2426,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
@@ -3073,6 +3075,7 @@
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
@@ -3174,6 +3177,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -3250,6 +3254,7 @@
"integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.18.1",
"@typescript-eslint/types": "8.18.1",
@@ -3458,6 +3463,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3813,6 +3819,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -4543,6 +4550,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5457,6 +5465,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
},
@@ -6787,6 +6796,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -6953,6 +6963,7 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -7088,6 +7099,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7103,6 +7115,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.25.0"
},
@@ -7298,19 +7311,6 @@
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-toastify": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.2.tgz",
"integrity": "sha512-GjHuGaiXMvbls3ywqv8XdWONwrcO4DXCJIY1zVLkHU73gEElKvTTXNI5Vom3s/k/M8hnkrfsqgBSX3OwmlonbA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -7338,7 +7338,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
@@ -7517,6 +7518,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
@@ -8420,6 +8422,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -8583,6 +8586,7 @@
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8790,6 +8794,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -8911,6 +8916,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -9201,6 +9207,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -9262,6 +9269,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
-1
View File
@@ -34,7 +34,6 @@
"react-otp-input": "^3.1.1",
"react-router-dom": "^7.1.0",
"react-spinners": "^0.15.0",
"react-toastify": "^11.0.2",
"swiper": "^11.2.1",
"tailwind-merge": "^2.5.5",
"vite-plugin-pwa": "^0.21.1",
+41 -54
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;
+19 -2
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,7 +29,14 @@ 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-hidden 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
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;
+7 -5
View File
@@ -1,5 +1,5 @@
import { SearchNormal } from "iconsax-react";
import { FC, InputHTMLAttributes, useEffect, useState } from "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";
@@ -30,6 +30,8 @@ const Input: FC<Props> = (props: Props) => {
const [showPassword, setShowPassword] = useState<boolean>(false);
const [search, setSearch] = useState<string>("");
const onChangeSearchFinalRef = useRef(props.onChangeSearchFinal);
onChangeSearchFinalRef.current = props.onChangeSearchFinal;
const inputClass = clx(
"w-full h-10 text-black block px-4 text-xs rounded-xl border border-border",
@@ -68,13 +70,13 @@ const Input: FC<Props> = (props: Props) => {
}, [props.value]);
useEffect(() => {
if (props.variant === "search" && props.onChangeSearchFinal) {
if (props.variant !== "search" || !onChangeSearchFinalRef.current) return;
const timeout = setTimeout(() => {
props.onChangeSearchFinal?.(search);
onChangeSearchFinalRef.current?.(search);
}, 1000);
return () => clearTimeout(timeout);
}
}, [search, props]);
}, [search, props.variant]);
return (
<div className="w-full">
+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,
)
}
+39 -12
View File
@@ -4,13 +4,17 @@ interface PaginationProps {
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 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);
};
const getPageNumbers = () => {
const pageNumbers: (number | string)[] = [];
const maxVisiblePages = 5;
@@ -56,7 +60,29 @@ const Pagination: React.FC<PaginationProps> = ({
const pageNumbers = getPageNumbers();
return (
<div className="flex gap-2 text-xs justify-center items-center mt-4">
<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
@@ -74,19 +100,19 @@ const Pagination: React.FC<PaginationProps> = ({
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"
}`}
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">
<span
key={`ellipsis-${index}`}
className="px-3 py-1 text-gray-500"
>
{page}
</span>
)
),
)}
{/* دکمه بعدی */}
@@ -101,6 +127,7 @@ const Pagination: React.FC<PaginationProps> = ({
بعدی
</button> */}
</div>
</div>
);
};
+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;
+3
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",
+2 -1
View File
@@ -3,6 +3,7 @@ export const SideBarItemHasSubMenu = [
"customers",
"representatives",
"users",
"taskmanager",
"content",
"financial",
"support",
@@ -44,7 +45,7 @@ export const getSubMenuNameFromPath = (pathname: string): string => {
}
if (pathname.startsWith("/workspace") || pathname.startsWith("/project") || pathname.startsWith("/taskmanager")) {
return "users";
return "taskmanager";
}
const segment = pathname.split("/")[1] ?? "";
+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),
};
};
+44 -4
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": "این فیلد اجباری می باشد",
@@ -134,6 +143,7 @@
"representative_list": "لیست نمایندگان",
"ticket_list": "لیست تیکت ها",
"ticket_category": "دسته بندی تیکت ها",
"ticket_masters": "مستر",
"send_ticket": "ارسال تیکت",
"category": "دسته بندی",
"user_list": "لیست کاربران",
@@ -368,7 +378,10 @@
"column": "ستون",
"select_workspace_required": "فضای کاری را انتخاب کنید",
"select_project_required": "پروژه را انتخاب کنید",
"select_column_required": "ستون را انتخاب کنید"
"select_column_required": "ستون را انتخاب کنید",
"masters": "مستر",
"add_master": "افزودن مستر",
"edit_master": "ویرایش مستر"
},
"active": "فعال",
"inactive": "غیرفعال",
@@ -727,6 +740,18 @@
"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": "آیا ادمین است؟",
@@ -736,6 +761,7 @@
"group_add": "ساخت گروه",
"submit_group": "ثبت گروه",
"error_count_users": "حداقل یک کاربر باید انتخاب شود",
"error_count_roles": "حداقل یک نقش باید انتخاب شود",
"list_user_group": "لیست گروه های کاربری",
"groups": "گروه ها",
"group_name": "نام گروه",
@@ -772,7 +798,10 @@
"confrim_email": "تایید ایمیل",
"confrim": "تایید صورتحساب ",
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
"email_not_verified": "ایمیل تایید نشده"
"email_not_verified": "ایمیل تایید نشده",
"resend_email": "ارسال مجدد تایید",
"first_name": "نام",
"last_name": "نام خانوادگی"
},
"email": "ایمیل",
"customer": {
@@ -1013,12 +1042,16 @@
"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": "نام ستون",
@@ -1061,7 +1094,14 @@
"stop_tracking": "پایان",
"time_history": "سابقه",
"no_time_entries": "هنوز زمانی ثبت نشده",
"tracking_active": "در حال اجرا"
"tracking_active": "در حال اجرا",
"comments": "کامنت‌ها",
"add_comment": "ثبت کامنت",
"comment_placeholder": "کامنت خود را بنویسید...",
"no_comments": "هنوز کامنتی ثبت نشده",
"unknown_user": "کاربر",
"related_ticket": "تیکت مرتبط",
"view_ticket": "مشاهده تیکت"
}
}
}
+8
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,6 +41,7 @@ const AddList: FC = () => {
{t('ads.ads')}
</div>
{canCreate('advertisements') && (
<Link to={Pages.ads.create}>
<Button
className='w-[172px]'
@@ -51,6 +54,7 @@ const AddList: FC = () => {
</div>
</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'>
{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')
}
})
}
+11 -3
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,6 +40,7 @@ const AnnouncementtList: FC = () => {
<div>
{t('announcement.announcements')}
</div>
{canCreate('announcements') && (
<div>
<Link to={Pages.announcement.create}>
<Button
@@ -53,6 +56,7 @@ const AnnouncementtList: FC = () => {
</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'>
{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)}
/>
)}
</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;
};
+12 -4
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'>
{canUpdate('blog_categories') && (
<UpdateCategory id={item.id} />
)}
{canDelete('blog_categories') && (
<Trash size={20} color='red' onClick={() => {
deleteBlogCategory.mutateAsync(item.id, {
onSuccess: () => {
toast.success('دسته بندی با موفقیت حذف شد')
toast('دسته بندی با موفقیت حذف شد', 'success')
getBlogCategories.refetch()
}
})
}} />
)}
</div>
</Td>
</tr>
@@ -123,6 +129,7 @@ const BlogCategory: FC = () => {
</div>
</div>
</div>
{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'>
@@ -176,6 +183,7 @@ const BlogCategory: FC = () => {
</div>
</Button>
</div>
)}
</div>
</div>
)
+8
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,6 +28,7 @@ const BlogList: FC = () => {
{t('blog.blog')}
</div>
{canCreate('blogs') && (
<Link to={Pages.blog.create}>
<Button
className='w-[172px]'
@@ -38,6 +41,7 @@ const BlogList: FC = () => {
</div>
</Button>
</Link>
)}
</div>
@@ -139,9 +143,12 @@ const BlogList: FC = () => {
</Td> */}
<Td text={''}>
<div className='flex gap-2'>
{canUpdate('blogs') && (
<Link to={Pages.blog.detail + item.id}>
<Eye size={20} color='#8C90A3' />
</Link>
)}
{canDelete('blogs') && (
<TrashWithConfrim
onDelete={() => {
deleteBlog.mutate(item.id, {
@@ -152,6 +159,7 @@ const BlogList: FC = () => {
}}
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')
}
})
}
+6
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,6 +25,7 @@ const CardBankList: FC = () => {
{t('cardBank.manage_banks')}
</div>
<div>
{canCreate('bank_accounts') &&
<Link to={Pages.cardBank.create}>
<Button
className='px-5'
@@ -36,6 +39,7 @@ const CardBankList: FC = () => {
</div>
</Button>
</Link>
}
</div>
</div>
{
@@ -63,9 +67,11 @@ const CardBankList: FC = () => {
<Td text={item.cardNumber} />
<Td text={item.IBan} />
<Td text={''}>
{canUpdate('bank_accounts') &&
<Link to={Pages.cardBank.detail + item.id}>
<Eye size={20} color='#8C90A3' />
</Link>
}
</Td>
</tr>
)
+4
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=''>
{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')
}
})
+80 -86
View File
@@ -1,122 +1,114 @@
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>
<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'>
<Button className="px-5">
<div className="flex gap-2">
<Add
className='size-5'
color='#fff'
className="size-5"
color="#fff"
/>
<div>{t('customer.add_cutomer')}</div>
<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 className="flex justify-end items-center mt-12">
<div>
<Input
variant='search'
placeholder={t('service.search')}
className='bg-white border border-border'
variant="search"
placeholder={t("search")}
className="bg-white border border-border"
onChangeSearchFinal={handleSearch}
/>
</div>
</div>
{
getCustomers.isPending ?
{getCustomers.isPending ? (
<PageLoading />
:
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
<table className='w-full text-sm '>
<thead className='thead'>
) : (
<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={''} />
<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) => {
{getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
return (
<tr key={item.id} className='tr'>
<Td text={item.firstName + ' ' + item.lastName} />
<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.email ? item.email : ""} />
<Td text={item.subscriptionsCount} />
<Td text={''}>
<Td text={""}>
<Link to={Pages.receipts.index + `?user=${item.id}`}>
<div className='text-[#0047FF]'>{item.invoicesCount} صورت حساب</div>
<div className="text-[#0047FF]">{item.invoicesCount} صورت حساب</div>
</Link>
</Td>
<Td text={''}>
<Td text={""}>
<Link to={Pages.ticket.list + `?user=${item.id}`}>
<div className='text-[#0047FF]'>{item.ticketsCount} تیکت</div>
<div className="text-[#0047FF]">{item.ticketsCount} تیکت</div>
</Link>
</Td>
<Td text={''}>
<Td text={""}>
{canUpdate("customers") && (
<Link to={Pages.customer.update + item.id}>
<Eye size={20} color='#8C90A3' />
<Eye
size={20}
color="#8C90A3"
/>
</Link>
)}
</Td>
<Td text={''} />
<Td text={""} />
</tr>
)
})
}
);
})}
</tbody>
</table>
@@ -124,11 +116,13 @@ const CustomerList: FC = () => {
totalPages={getCustomers.data?.data?.pager?.totalPages}
currentPage={page}
onPageChange={setPage}
limit={limit}
onLimitChange={setLimit}
/>
</div>
}
)}
</div>
)
}
);
};
export default CustomerList
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')
}
})
},
+11 -3
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,6 +42,7 @@ const DiscountList: FC = () => {
{t('discount.list')}
</div>
{canCreate('discounts') &&
<Link to={Pages.discount.create}>
<Button
className='w-[172px]'
@@ -52,6 +55,7 @@ const DiscountList: FC = () => {
</div>
</Button>
</Link>
}
</div>
@@ -114,17 +118,21 @@ const DiscountList: FC = () => {
</Td>
<Td text={t('')}>
<div className='flex gap-2 items-center'>
{canUpdate('discounts') &&
<Link
to={Pages.discount.detail + item.id}
>
<Edit size={20} color='#888888' />
</Link>
}
{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')
}
})
}
@@ -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 { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData'
import { useMultiUpload } from '../../../service/hooks/useServiceData'
@@ -44,7 +44,7 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
}),
onSubmit: async (values) => {
if (files.length === 0) {
toast.error(t('errors.required'))
toast(t('errors.required'), 'error')
return
}
@@ -67,11 +67,11 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
formik.resetForm()
setFiles([])
close()
toast.success(t('success'))
toast(t('success'), 'success')
onSuccess?.()
} catch (error) {
const err = error as ErrorType
toast.error(err.response?.data?.error?.message?.[0])
toast(err.response?.data?.error?.message?.[0], 'error')
} finally {
setIsSubmitting(false)
}
+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')
}
})
}
@@ -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 { useCreateIcon, useGetGroupIcons } from '../../hooks/useIconData'
import { useMultiUpload } from '../../../service/hooks/useServiceData'
@@ -44,7 +44,7 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
}),
onSubmit: async (values) => {
if (files.length === 0) {
toast.error(t('errors.required'))
toast(t('errors.required'), 'error')
return
}
@@ -67,11 +67,11 @@ const CreateIcon: FC<CreateIconProps> = ({ open, close, onSuccess }) => {
formik.resetForm()
setFiles([])
close()
toast.success(t('success'))
toast(t('success'), 'success')
onSuccess?.()
} catch (error) {
const err = error as ErrorType
toast.error(err.response?.data?.error?.message?.[0])
toast(err.response?.data?.error?.message?.[0], 'error')
} finally {
setIsSubmitting(false)
}
+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'))
}
})
}
+8
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,6 +38,7 @@ const List: FC = () => {
<div>
{t('guide.list_guide')}
</div>
{canCreate('services') && (
<div>
<Link to={Pages.services.guide + id + '/create'}>
<Button
@@ -51,6 +54,7 @@ const List: FC = () => {
</Button>
</Link>
</div>
)}
</div>
{
@@ -85,13 +89,17 @@ const List: FC = () => {
</Td>
<Td text=''>
<div className='flex items-center gap-2'>
{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'))
}
})
}
+21 -26
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 (
<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="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 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 className="whitespace-nowrap">{props.description}</div>
</div>
</div>
</div>
</Link>
)
}
);
};
export default ItemDashboard
export default ItemDashboard;
+4
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>
{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')
}
}
})
+4
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,6 +23,7 @@ const LearningList: FC = () => {
<div>
{t('learning.list_learning')}
</div>
{canCreate('learnings') && (
<div>
<Link to={Pages.learning.create}>
<Button
@@ -36,6 +39,7 @@ const LearningList: FC = () => {
</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')
}
}
+16 -127
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,86 +136,14 @@ 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>
{/* <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
+28 -66
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,81 +46,42 @@ 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'>
<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>
:
<Button
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;
+248 -283
View File
@@ -1,62 +1,64 @@
import { ChangeEvent, FC, Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../../components/Button'
import { Add, TickCircle, TickSquare, Trash } from 'iconsax-react'
import Select from '../../components/Select'
import Input from '../../components/Input'
import { CreateReceiptType, ReceiptCreateItemsType } from './types/ReceiptTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { useGetCustomers } from '../customer/hooks/useCustomerData'
import { CustomerItemType } from '../customer/types/CustomerTypes'
import PageLoading from '../../components/PageLoading'
import { useCreateInvoice } from './hooks/useReceiptData'
import { useNavigate } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { ErrorType } from '../../helpers/types'
import { RecurringPeriodEnum } from './enum/ReceipEnum'
import SwitchComponent from '../../components/Switch'
import { useFormik } from "formik";
import { Add, TickCircle, TickSquare, Trash } from "iconsax-react";
import { ChangeEvent, FC, Fragment, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import * as Yup from "yup";
import Button from "../../components/Button";
import Input from "../../components/Input";
import PageLoading from "../../components/PageLoading";
import Select from "../../components/Select";
import SwitchComponent from "../../components/Switch";
import { toast } from "../../components/Toast";
import { Pages } from "../../config/Pages";
import { ErrorType } from "../../helpers/types";
import { useGetCustomers } from "../customer/hooks/useCustomerData";
import { CustomerItemType } from "../customer/types/CustomerTypes";
import { RecurringPeriodEnum } from "./enum/ReceipEnum";
import { useCreateInvoice } from "./hooks/useReceiptData";
import { CreateReceiptType, ReceiptCreateItemsType } from "./types/ReceiptTypes";
const CreateReceipt: FC = () => {
const navigate = useNavigate()
const { t } = useTranslation('global')
const [items, setItems] = useState<ReceiptCreateItemsType[]>([])
const [customer, setCustomer] = useState<CustomerItemType>()
const [isRecurring, setIsRecurring] = useState<boolean>(false)
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY)
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0)
const getCustomers = useGetCustomers(1, true)
const createInvoice = useCreateInvoice()
const navigate = useNavigate();
const { t } = useTranslation("global");
const [items, setItems] = useState<ReceiptCreateItemsType[]>([]);
const [customer, setCustomer] = useState<CustomerItemType>();
const [isRecurring, setIsRecurring] = useState<boolean>(false);
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY);
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0);
const getCustomers = useGetCustomers(1, 50, true);
const createInvoice = useCreateInvoice();
const formik = useFormik<ReceiptCreateItemsType>({
initialValues: {
name: '',
count: '',
unitPrice: '',
discount: ''
name: "",
count: "",
unitPrice: "",
discount: "",
},
validationSchema: Yup.object({
name: Yup.string().required(t('errors.required')),
count: Yup.string().required(t('errors.required')),
unitPrice: Yup.string().required(t('errors.required')),
name: Yup.string().required(t("errors.required")),
count: Yup.string().required(t("errors.required")),
unitPrice: Yup.string().required(t("errors.required")),
}),
onSubmit: (values) => {
setItems((prev) => [...prev, {
setItems((prev) => [
...prev,
{
count: +values.count,
discount: +values.discount,
name: values.name,
unitPrice: +values.unitPrice
}])
formik.resetForm()
}
})
unitPrice: +values.unitPrice,
},
]);
formik.resetForm();
},
});
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value)
setCustomer(customer)
}
const value = e.target.value;
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value);
setCustomer(customer);
};
const handleSubmit = () => {
if (customer) {
@@ -65,406 +67,369 @@ const CreateReceipt: FC = () => {
items: items,
isRecurring: isRecurring,
recurringPeriod: isRecurring ? +type : undefined,
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined
}
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined,
};
createInvoice.mutate(params, {
onSuccess: () => {
toast.success(t('success'))
navigate(Pages.receipts.index)
toast(t("success"), "success");
navigate(Pages.receipts.index);
},
onError(error: ErrorType) {
toast.error(error.response?.data?.error.message[0])
toast(error.response?.data?.error.message[0], "error");
},
})
}
});
}
};
const handleRemove = (index: number) => {
setItems((prev) => prev.filter((_, i) => i !== index))
}
setItems((prev) => prev.filter((_, i) => i !== index));
};
return (
<div className='mt-4'>
{
getCustomers.isPending ?
<div className="mt-4">
{getCustomers.isPending ? (
<PageLoading />
:
) : (
<Fragment>
<div className='flex justify-between items-center'>
<div>
{t('receip.add_receip')}
</div>
<div className="flex justify-between items-center">
<div>{t("receip.add_receip")}</div>
<div>
<Button
className='px-5'
className="px-5"
onClick={handleSubmit}
isLoading={createInvoice.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('receip.submit_receip')}
</div>
<div className="flex gap-2 items-center">
<TickCircle
size={20}
color="white"
/>
<div>{t("receip.submit_receip")}</div>
</div>
</Button>
</div>
</div>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div>
{t('receip.customer_information')}
</div>
<div className="flex gap-6 xl:mt-8 mt-4">
<div className="flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl">
<div>{t("receip.customer_information")}</div>
<div className='mt-8'>
<div className='w-full xl:w-1/2'>
<div className="mt-8">
<div className="w-full xl:w-1/2">
<Select
label={t('receip.customer')}
placeholder={t('select')}
label={t("receip.customer")}
placeholder={t("select")}
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
return {
label: item.firstName + ' ' + item.lastName,
value: item.id
}
label: item.firstName + " " + item.lastName,
value: item.id,
};
})}
onChange={handleChangeCustomer}
/>
</div>
</div>
{
customer &&
<div className='mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm'>
<div className='flex justify-between items-center border-b border-border border-dashed pb-7'>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.type_person')}</div>
<div>{customer.legalUser ? 'حقوقی' : 'حقیقی'}</div>
{customer && (
<div className="mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm">
<div className="flex justify-between items-center border-b border-border border-dashed pb-7">
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.type_person")}</div>
<div>{customer.legalUser ? "حقوقی" : "حقیقی"}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.representativeـname')}</div>
<div>{customer.firstName + ' ' + customer.lastName}</div>
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.representativeـname")}</div>
<div>{customer.firstName + " " + customer.lastName}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.company_name')}</div>
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.company_name")}</div>
<div>{customer?.legalUser?.registrationName}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.registration_number')}</div>
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.registration_number")}</div>
<div>{customer?.legalUser?.registrationCode}</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.tel_company')}</div>
<div className="flex justify-between items-center mt-7">
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.tel_company")}</div>
<div>{customer?.realUser?.phone || customer?.legalUser?.phone}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.national_code')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.national_code")}</div>
<div>{customer.nationalCode}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.postal_code')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.postal_code")}</div>
<div>{customer?.realUser?.address?.postalCode || customer?.legalUser?.address?.postalCode}</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.state')}</div>
<div className="flex justify-between items-center mt-7">
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.state")}</div>
<div>{customer?.realUser?.address?.city?.province?.name || customer?.legalUser?.address?.city?.province?.name}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.city')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.city")}</div>
<div>{customer?.realUser?.address?.city?.name || customer?.legalUser?.address?.city?.name}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.address')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.address")}</div>
<div>{customer?.realUser?.address?.fullAddress || customer?.legalUser?.address?.fullAddress}</div>
</div>
</div>
</div>
}
)}
<div className='mt-10'>
<div className="mt-10">
<SwitchComponent
label={t('receip.recurring')}
label={t("receip.recurring")}
active={isRecurring}
onChange={(value) => setIsRecurring(value)}
/>
</div>
{
isRecurring &&
<div className='mt-10'>
<div>
{t('receip.select_type')}
</div>
{isRecurring && (
<div className="mt-10">
<div>{t("receip.select_type")}</div>
<div className='mt-4 rowTwoInput'>
<div className="mt-4 rowTwoInput">
<Select
label={t('select')}
label={t("select")}
items={[
{ label: t('receip.WEEKLY'), value: RecurringPeriodEnum.WEEKLY.toString() },
{ label: t('receip.MONTHLY'), value: RecurringPeriodEnum.MONTHLY.toString() },
{ label: t('receip.QUARTERLY'), value: RecurringPeriodEnum.QUARTERLY.toString() },
{ label: t('receip.SEMIANNUALLY'), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
{ label: t('receip.ANNUALLY'), value: RecurringPeriodEnum.ANNUALLY.toString() }
{ label: t("receip.WEEKLY"), value: RecurringPeriodEnum.WEEKLY.toString() },
{ label: t("receip.MONTHLY"), value: RecurringPeriodEnum.MONTHLY.toString() },
{ label: t("receip.QUARTERLY"), value: RecurringPeriodEnum.QUARTERLY.toString() },
{ label: t("receip.SEMIANNUALLY"), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
{ label: t("receip.ANNUALLY"), value: RecurringPeriodEnum.ANNUALLY.toString() },
]}
onChange={(e) => setType(e.target.value as unknown as RecurringPeriodEnum)}
/>
<Input
label={t('receip.maxRecurringCycles')}
name='maxRecurringCycles'
label={t("receip.maxRecurringCycles")}
name="maxRecurringCycles"
value={maxRecurringCycles}
onChange={(e) => setMaxRecurringCycles(+e.target.value)}
/>
</div>
</div>
}
)}
<div className='mt-10'>
<div>{t('receip.receipt_information')}</div>
<div className="mt-10">
<div>{t("receip.receipt_information")}</div>
</div>
<div className='mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description'>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.number')}
</div>
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
{items.length + 1}
</div>
<div className="mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description">
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.number")}</div>
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{items.length + 1}</div>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.product_name')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.product_name")}</div>
<Input
className='text-center'
name='name'
className="text-center"
name="name"
value={formik.values.name}
onChange={formik.handleChange}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.count')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.count")}</div>
<Input
type='number'
className='w-16 text-center'
name='count'
type="number"
className="w-16 text-center"
name="count"
value={formik.values.count}
onChange={formik.handleChange}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.unit_amount')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.unit_amount")}</div>
<Input
className='text-center'
name='unitPrice'
className="text-center"
name="unitPrice"
value={formik.values.unitPrice}
onChange={(e) => formik.setFieldValue('unitPrice', e.target.value)}
onChange={(e) => formik.setFieldValue("unitPrice", e.target.value)}
seprator
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.discount')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.discount")}</div>
<Input
type='number'
className='w-16 text-center'
name='discount'
type="number"
className="w-16 text-center"
name="discount"
value={formik.values.discount}
onChange={formik.handleChange}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.total_amount')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.total_amount")}</div>
<Input
className='text-center bg-[#EBEDF5]'
className="text-center bg-[#EBEDF5]"
readOnly
/>
</div>
<div onClick={() => {
<div
onClick={() => {
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
toast.error(t('receip.error_empty'))
toast(t("receip.error_empty"), "error");
} else {
formik.handleSubmit()
formik.handleSubmit();
}
}} className='size-10 border border-border rounded-xl flex justify-center items-center'>
<Add size={20} color='black' />
}}
className="size-10 border border-border rounded-xl flex justify-center items-center"
>
<Add
size={20}
color="black"
/>
</div>
</div>
</div>
{
items.map((item, index: number) => {
{items.map((item, index: number) => {
return (
<div key={item.name} className='mt-6 flex items-end justify-between text-xs text-description'>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.number')}
<div
key={item.name}
className="mt-6 flex items-end justify-between text-xs text-description"
>
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.number")}</div>}
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{index + 1}</div>
</div>
}
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
{index + 1}
</div>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.product_name')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.product_name")}</div>}
<Input
className='text-center'
name='name'
className="text-center"
name="name"
readOnly
value={item.name}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.count')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.count")}</div>}
<Input
type='number'
className='w-16 text-center'
name='count'
type="number"
className="w-16 text-center"
name="count"
readOnly
value={item.count}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.unit_amount')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.unit_amount")}</div>}
<Input
className='text-center'
name='unitPrice'
className="text-center"
name="unitPrice"
readOnly
value={item.unitPrice}
seprator
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.discount')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.discount")}</div>}
<Input
type='number'
className='w-16 text-center'
name='discount'
type="number"
className="w-16 text-center"
name="discount"
readOnly
value={item.discount}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.total_amount')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.total_amount")}</div>}
<Input
className='text-center bg-[#EBEDF5]'
className="text-center bg-[#EBEDF5]"
readOnly
value={((Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0)).toFixed(1)}
seprator
/>
</div>
<div onClick={() => handleRemove(index)} className='size-10 border border-border rounded-xl flex justify-center items-center'>
<Trash size={20} color='black' />
<div
onClick={() => handleRemove(index)}
className="size-10 border border-border rounded-xl flex justify-center items-center"
>
<Trash
size={20}
color="black"
/>
</div>
</div>
)
})
}
);
})}
<div className='mt-8 flex flex-col gap-4 items-end'>
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
<div>
{t('receip.tax')}
</div>
<div className='text-black'>
{((items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0)) * 0.10).toFixed(1)}
</div>
</div>
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
<div>
{t('receip.total')}
</div>
<div className='text-black'>
<div className="mt-8 flex flex-col gap-4 items-end">
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
<div>{t("receip.tax")}</div>
<div className="text-black">
{(
items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0) * 1.10
items.reduce(
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
0,
) * 0.1
).toFixed(1)}
</div>
</div>
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
<div>{t("receip.total")}</div>
<div className="text-black">
{(
items.reduce(
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
0,
) * 1.1
).toFixed(1)}
</div>
</div>
</div>
</div>
<div className='bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
{t('ticket.title_hint')}
</div>
<div className="bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl">
<div className="text-sm">{t("ticket.title_hint")}</div>
<div className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div className="mt-6">
<div className="flex items-start gap-2 border-b pb-5">
<div>
<TickSquare size={20} color='black' variant='Bold' />
<TickSquare
size={20}
color="black"
variant="Bold"
/>
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
<div className="text-description text-xs leading-5">سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.</div>
</div>
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div className="flex items-start gap-2 mt-6 border-b pb-5">
<div>
<TickSquare size={20} color='black' variant='Bold' />
<TickSquare
size={20}
color="black"
variant="Bold"
/>
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
<div className="text-description text-xs leading-5">لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.</div>
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div className="flex items-start gap-2 mt-6">
<div>
<TickSquare size={20} color='black' variant='Bold' />
<TickSquare
size={20}
color="black"
variant="Bold"
/>
</div>
<div className='text-description text-xs leading-5'>
<div className="text-description text-xs leading-5">
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
@@ -472,9 +437,9 @@ const CreateReceipt: FC = () => {
</div>
</div>
</Fragment>
}
)}
</div>
)
}
);
};
export default CreateReceipt
export default CreateReceipt;
+263 -296
View File
@@ -1,22 +1,22 @@
import { ChangeEvent, FC, Fragment, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../../components/Button'
import { Add, TickCircle, TickSquare, Trash } from 'iconsax-react'
import Select from '../../components/Select'
import Input from '../../components/Input'
import { CreateReceiptType, ReceiptCreateItemsType } from './types/ReceiptTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { useGetCustomers } from '../customer/hooks/useCustomerData'
import { CustomerItemType } from '../customer/types/CustomerTypes'
import PageLoading from '../../components/PageLoading'
import { useGetInvoiceById, useUpdateInvoice } from './hooks/useReceiptData'
import { useNavigate, useParams } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { ErrorType } from '../../helpers/types'
import { RecurringPeriodEnum } from './enum/ReceipEnum'
import SwitchComponent from '../../components/Switch'
import { useFormik } from "formik";
import { Add, TickCircle, TickSquare, Trash } from "iconsax-react";
import { ChangeEvent, FC, Fragment, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import * as Yup from "yup";
import Button from "../../components/Button";
import Input from "../../components/Input";
import PageLoading from "../../components/PageLoading";
import Select from "../../components/Select";
import SwitchComponent from "../../components/Switch";
import { toast } from "../../components/Toast";
import { Pages } from "../../config/Pages";
import { ErrorType } from "../../helpers/types";
import { useGetCustomers } from "../customer/hooks/useCustomerData";
import { CustomerItemType } from "../customer/types/CustomerTypes";
import { RecurringPeriodEnum } from "./enum/ReceipEnum";
import { useGetInvoiceById, useUpdateInvoice } from "./hooks/useReceiptData";
import { CreateReceiptType, ReceiptCreateItemsType } from "./types/ReceiptTypes";
// Add interface for invoice item from API
interface InvoiceItemType {
@@ -32,47 +32,49 @@ interface InvoiceItemType {
}
const CreateReceipt: FC = () => {
const navigate = useNavigate()
const { id } = useParams()
const { t } = useTranslation('global')
const { data: invoice, isPending: isLoading } = useGetInvoiceById(id as string)
const updateInvoice = useUpdateInvoice()
const [items, setItems] = useState<ReceiptCreateItemsType[]>([])
const [customer, setCustomer] = useState<CustomerItemType>()
const [isRecurring, setIsRecurring] = useState<boolean>(false)
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY)
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0)
const getCustomers = useGetCustomers(1, true)
const navigate = useNavigate();
const { id } = useParams();
const { t } = useTranslation("global");
const { data: invoice, isPending: isLoading } = useGetInvoiceById(id as string);
const updateInvoice = useUpdateInvoice();
const [items, setItems] = useState<ReceiptCreateItemsType[]>([]);
const [customer, setCustomer] = useState<CustomerItemType>();
const [isRecurring, setIsRecurring] = useState<boolean>(false);
const [type, setType] = useState<RecurringPeriodEnum>(RecurringPeriodEnum.WEEKLY);
const [maxRecurringCycles, setMaxRecurringCycles] = useState<number>(0);
const getCustomers = useGetCustomers(1, 50, true);
const formik = useFormik<ReceiptCreateItemsType>({
initialValues: {
name: '',
count: '',
unitPrice: '',
discount: ''
name: "",
count: "",
unitPrice: "",
discount: "",
},
validationSchema: Yup.object({
name: Yup.string().required(t('errors.required')),
count: Yup.string().required(t('errors.required')),
unitPrice: Yup.string().required(t('errors.required')),
name: Yup.string().required(t("errors.required")),
count: Yup.string().required(t("errors.required")),
unitPrice: Yup.string().required(t("errors.required")),
}),
onSubmit: (values) => {
setItems((prev) => [...prev, {
setItems((prev) => [
...prev,
{
count: +values.count,
discount: +values.discount,
name: values.name,
unitPrice: +values.unitPrice
}])
formik.resetForm()
}
})
unitPrice: +values.unitPrice,
},
]);
formik.resetForm();
},
});
const handleChangeCustomer = (e: ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value)
setCustomer(customer)
}
const value = e.target.value;
const customer = getCustomers.data?.data?.customers?.find((item: CustomerItemType) => item.id === value);
setCustomer(customer);
};
const handleSubmit = () => {
if (customer) {
@@ -81,28 +83,30 @@ const CreateReceipt: FC = () => {
items: items,
isRecurring: isRecurring,
recurringPeriod: isRecurring ? +type : undefined,
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined
}
maxRecurringCycles: isRecurring ? maxRecurringCycles : undefined,
};
updateInvoice.mutate({ id: id as string, params }, {
updateInvoice.mutate(
{ id: id as string, params },
{
onSuccess: () => {
toast.success(t('success'))
navigate(Pages.receipts.index)
toast(t("success"), "success");
navigate(Pages.receipts.index);
},
onError(error: ErrorType) {
toast.error(error.response?.data?.error.message[0])
toast(error.response?.data?.error.message[0], "error");
},
})
}
},
);
}
};
const handleRemove = (index: number) => {
setItems((prev) => prev.filter((_, i) => i !== index))
}
setItems((prev) => prev.filter((_, i) => i !== index));
};
// Prefill form when editing an invoice
useEffect(() => {
// Check if we need to access invoice.data or directly invoice
const invoiceData = invoice?.data?.invoice || invoice;
@@ -111,9 +115,7 @@ const CreateReceipt: FC = () => {
// 1. If there's a user, find matching customer
if (invoiceData.user && getCustomers.data?.data?.customers) {
const foundCustomer = getCustomers.data.data.customers.find(
(item: CustomerItemType) => item.id === invoiceData.user.id
);
const foundCustomer = getCustomers.data.data.customers.find((item: CustomerItemType) => item.id === invoiceData.user.id);
setCustomer(foundCustomer);
console.log("Found customer:", foundCustomer);
setCustomer(foundCustomer);
@@ -123,20 +125,21 @@ const CreateReceipt: FC = () => {
if (invoiceData.items && invoiceData.items.length > 0) {
console.log("Setting items:", invoiceData.items);
// Map API items to the format expected by our component
setItems(invoiceData.items.map((item: InvoiceItemType) => ({
setItems(
invoiceData.items.map((item: InvoiceItemType) => ({
name: item.name,
count: item.count,
unitPrice: item.unitPrice,
discount: item.discount || 0
})));
discount: item.discount || 0,
})),
);
// 3. Set formik values from first item
}
// 4. Set recurring settings
setIsRecurring(Boolean(invoiceData.isRecurring));
if (typeof invoiceData.recurringPeriod === 'number') {
if (typeof invoiceData.recurringPeriod === "number") {
setType(invoiceData.recurringPeriod.toString());
}
setMaxRecurringCycles(invoiceData.maxRecurringCycles || 0);
@@ -149,8 +152,8 @@ const CreateReceipt: FC = () => {
// Trigger the onChange manually with a synthetic event
const event = {
target: {
value: invoice.user.id
}
value: invoice.user.id,
},
} as ChangeEvent<HTMLSelectElement>;
handleChangeCustomer(event);
@@ -166,387 +169,351 @@ const CreateReceipt: FC = () => {
}, [customer, invoice]);
return (
<div className='mt-4'>
{
getCustomers.isPending || isLoading ?
<div className="mt-4">
{getCustomers.isPending || isLoading ? (
<PageLoading />
:
) : (
<Fragment>
<div className='flex justify-between items-center'>
<div>
{t('receip.add_receip')}
</div>
<div className="flex justify-between items-center">
<div>{t("receip.add_receip")}</div>
<div>
<Button
className='px-5'
className="px-5"
onClick={handleSubmit}
isLoading={updateInvoice.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('receip.submit_receip')}
</div>
<div className="flex gap-2 items-center">
<TickCircle
size={20}
color="white"
/>
<div>{t("receip.submit_receip")}</div>
</div>
</Button>
</div>
</div>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div>
{t('receip.customer_information')}
</div>
<div className="flex gap-6 xl:mt-8 mt-4">
<div className="flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl">
<div>{t("receip.customer_information")}</div>
<div className='mt-8'>
<div className='w-full xl:w-1/2'>
<div className="mt-8">
<div className="w-full xl:w-1/2">
<Select
label={t('receip.customer')}
placeholder={t('select')}
label={t("receip.customer")}
placeholder={t("select")}
value={(invoice?.data?.invoice || invoice)?.user?.id}
items={getCustomers.data?.data?.customers?.map((item: CustomerItemType) => {
return {
label: item.firstName + ' ' + item.lastName,
value: item.id
}
label: item.firstName + " " + item.lastName,
value: item.id,
};
})}
onChange={handleChangeCustomer}
/>
</div>
</div>
{
customer &&
<div className='mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm'>
<div className='flex justify-between items-center border-b border-border border-dashed pb-7'>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.type_person')}</div>
<div>{customer.legalUser ? 'حقوقی' : 'حقیقی'}</div>
{customer && (
<div className="mt-10 bg-[#F6F7FB] p-8 rounded-3xl text-sm">
<div className="flex justify-between items-center border-b border-border border-dashed pb-7">
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.type_person")}</div>
<div>{customer.legalUser ? "حقوقی" : "حقیقی"}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.representativeـname')}</div>
<div>{customer.firstName + ' ' + customer.lastName}</div>
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.representativeـname")}</div>
<div>{customer.firstName + " " + customer.lastName}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.company_name')}</div>
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.company_name")}</div>
<div>{customer?.legalUser?.registrationName}</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-description'>{t('receip.registration_number')}</div>
<div className="flex items-center gap-2">
<div className="text-description">{t("receip.registration_number")}</div>
<div>{customer?.legalUser?.registrationCode}</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.tel_company')}</div>
<div className="flex justify-between items-center mt-7">
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.tel_company")}</div>
<div>{customer?.realUser?.phone || customer?.legalUser?.phone}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.national_code')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.national_code")}</div>
<div>{customer.nationalCode}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.postal_code')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.postal_code")}</div>
<div>{customer?.realUser?.address?.postalCode || customer?.legalUser?.address?.postalCode}</div>
</div>
</div>
<div className='flex justify-between items-center mt-7'>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.state')}</div>
<div className="flex justify-between items-center mt-7">
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.state")}</div>
<div>{customer?.realUser?.address?.city?.province?.name || customer?.legalUser?.address?.city?.province?.name}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.city')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.city")}</div>
<div>{customer?.realUser?.address?.city?.name || customer?.legalUser?.address?.city?.name}</div>
</div>
<div className='flex items-center gap-2 flex-1'>
<div className='text-description'>{t('receip.address')}</div>
<div className="flex items-center gap-2 flex-1">
<div className="text-description">{t("receip.address")}</div>
<div>{customer?.realUser?.address?.fullAddress || customer?.legalUser?.address?.fullAddress}</div>
</div>
</div>
</div>
}
)}
<div className='mt-10'>
<div className="mt-10">
<SwitchComponent
label={t('receip.recurring')}
label={t("receip.recurring")}
active={isRecurring}
onChange={(value) => setIsRecurring(value)}
/>
</div>
{
isRecurring &&
<div className='mt-10'>
<div>
{t('receip.select_type')}
</div>
{isRecurring && (
<div className="mt-10">
<div>{t("receip.select_type")}</div>
<div className='mt-4 rowTwoInput'>
<div className="mt-4 rowTwoInput">
<Select
label={t('select')}
label={t("select")}
items={[
{ label: t('receip.WEEKLY'), value: RecurringPeriodEnum.WEEKLY.toString() },
{ label: t('receip.MONTHLY'), value: RecurringPeriodEnum.MONTHLY.toString() },
{ label: t('receip.QUARTERLY'), value: RecurringPeriodEnum.QUARTERLY.toString() },
{ label: t('receip.SEMIANNUALLY'), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
{ label: t('receip.ANNUALLY'), value: RecurringPeriodEnum.ANNUALLY.toString() }
{ label: t("receip.WEEKLY"), value: RecurringPeriodEnum.WEEKLY.toString() },
{ label: t("receip.MONTHLY"), value: RecurringPeriodEnum.MONTHLY.toString() },
{ label: t("receip.QUARTERLY"), value: RecurringPeriodEnum.QUARTERLY.toString() },
{ label: t("receip.SEMIANNUALLY"), value: RecurringPeriodEnum.SEMIANNUALLY.toString() },
{ label: t("receip.ANNUALLY"), value: RecurringPeriodEnum.ANNUALLY.toString() },
]}
value={type}
onChange={(e) => setType(e.target.value as unknown as RecurringPeriodEnum)}
/>
<Input
label={t('receip.maxRecurringCycles')}
name='maxRecurringCycles'
label={t("receip.maxRecurringCycles")}
name="maxRecurringCycles"
value={maxRecurringCycles}
onChange={(e) => setMaxRecurringCycles(+e.target.value)}
/>
</div>
</div>
}
)}
<div className='mt-10'>
<div>{t('receip.receipt_information')}</div>
<div className="mt-10">
<div>{t("receip.receipt_information")}</div>
</div>
<div className='mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description'>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.number')}
</div>
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
{items.length + 1}
</div>
<div className="mt-8 border-b border-dashed border-border pb-7 flex items-end justify-between text-xs text-description">
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.number")}</div>
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{items.length + 1}</div>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.product_name')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.product_name")}</div>
<Input
className='text-center'
name='name'
className="text-center"
name="name"
value={formik.values.name}
onChange={formik.handleChange}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.count')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.count")}</div>
<Input
type='number'
className='w-16 text-center'
name='count'
type="number"
className="w-16 text-center"
name="count"
value={formik.values.count}
onChange={formik.handleChange}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.unit_amount')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.unit_amount")}</div>
<Input
className='text-center'
name='unitPrice'
className="text-center"
name="unitPrice"
value={formik.values.unitPrice}
onChange={(e) => formik.setFieldValue('unitPrice', e.target.value)}
onChange={(e) => formik.setFieldValue("unitPrice", e.target.value)}
seprator
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.discount')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.discount")}</div>
<Input
type='number'
className='w-16 text-center'
name='discount'
type="number"
className="w-16 text-center"
name="discount"
value={formik.values.discount}
onChange={formik.handleChange}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
<div>
{t('receip.total_amount')}
</div>
<div className="flex flex-col gap-2 items-center">
<div>{t("receip.total_amount")}</div>
<Input
className='text-center bg-[#EBEDF5]'
className="text-center bg-[#EBEDF5]"
readOnly
/>
</div>
<div onClick={() => {
<div
onClick={() => {
if (formik.errors.name || formik.errors.count || formik.errors.unitPrice || formik.errors.discount) {
toast.error(t('receip.error_empty'))
toast(t("receip.error_empty"), "error");
} else {
formik.handleSubmit()
formik.handleSubmit();
}
}} className='size-10 border border-border rounded-xl flex justify-center items-center'>
<Add size={20} color='black' />
}}
className="size-10 border border-border rounded-xl flex justify-center items-center"
>
<Add
size={20}
color="black"
/>
</div>
</div>
</div>
{
items.map((item, index: number) => {
{items.map((item, index: number) => {
return (
<div key={item.name} className='mt-6 flex items-end justify-between text-xs text-description'>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.number')}
<div
key={item.name}
className="mt-6 flex items-end justify-between text-xs text-description"
>
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.number")}</div>}
<div className="size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center">{index + 1}</div>
</div>
}
<div className='size-10 bg-[#EBEDF5] rounded-xl flex justify-center items-center'>
{index + 1}
</div>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.product_name')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.product_name")}</div>}
<Input
className='text-center'
name='name'
className="text-center"
name="name"
readOnly
value={item.name}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.count')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.count")}</div>}
<Input
type='number'
className='w-16 text-center'
name='count'
type="number"
className="w-16 text-center"
name="count"
readOnly
value={item.count}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.unit_amount')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.unit_amount")}</div>}
<Input
className='text-center'
name='unitPrice'
className="text-center"
name="unitPrice"
readOnly
value={item.unitPrice}
seprator
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.discount')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.discount")}</div>}
<Input
type='number'
className='w-16 text-center'
name='discount'
type="number"
className="w-16 text-center"
name="discount"
readOnly
value={item.discount}
/>
</div>
<div className='flex flex-col gap-2 items-center'>
{
index === 0 &&
<div>
{t('receip.total_amount')}
</div>
}
<div className="flex flex-col gap-2 items-center">
{index === 0 && <div>{t("receip.total_amount")}</div>}
<Input
className='text-center bg-[#EBEDF5]'
className="text-center bg-[#EBEDF5]"
readOnly
value={((Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0)).toFixed(1)}
seprator
/>
</div>
<div onClick={() => handleRemove(index)} className='size-10 border border-border rounded-xl flex justify-center items-center'>
<Trash size={20} color='black' />
<div
onClick={() => handleRemove(index)}
className="size-10 border border-border rounded-xl flex justify-center items-center"
>
<Trash
size={20}
color="black"
/>
</div>
</div>
)
})
}
);
})}
<div className='mt-8 flex flex-col gap-4 items-end'>
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
<div>
{t('receip.tax')}
</div>
<div className='text-black'>
{(items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0) * 0.10).toFixed(1)}
</div>
</div>
<div className='xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center'>
<div>
{t('receip.total')}
</div>
<div className='text-black'>
<div className="mt-8 flex flex-col gap-4 items-end">
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
<div>{t("receip.tax")}</div>
<div className="text-black">
{(
items.reduce((acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0), 0) * 1.10
items.reduce(
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
0,
) * 0.1
).toFixed(1)}
</div>
</div>
<div className="xl:w-1/2 h-12 w-full rounded-xl flex px-4 bg-[#EBEDF5] text-sm text-description justify-between items-center">
<div>{t("receip.total")}</div>
<div className="text-black">
{(
items.reduce(
(acc, item) => acc + (Number(item.unitPrice) || 0) * (Number(item.count) || 0) - (Number(item.unitPrice) || 0) * (Number(item.count) || 0) * (Number(item.discount) / 100 || 0),
0,
) * 1.1
).toFixed(1)}
</div>
</div>
</div>
</div>
<div className='bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
{t('ticket.title_hint')}
</div>
<div className="bg-white w-sidebar 3xl:block hidden py-10 px-5 h-fit rounded-3xl">
<div className="text-sm">{t("ticket.title_hint")}</div>
<div className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div className="mt-6">
<div className="flex items-start gap-2 border-b pb-5">
<div>
<TickSquare size={20} color='black' variant='Bold' />
<TickSquare
size={20}
color="black"
variant="Bold"
/>
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
<div className="text-description text-xs leading-5">سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.</div>
</div>
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div className="flex items-start gap-2 mt-6 border-b pb-5">
<div>
<TickSquare size={20} color='black' variant='Bold' />
<TickSquare
size={20}
color="black"
variant="Bold"
/>
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
<div className="text-description text-xs leading-5">لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.</div>
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div className="flex items-start gap-2 mt-6">
<div>
<TickSquare size={20} color='black' variant='Bold' />
<TickSquare
size={20}
color="black"
variant="Bold"
/>
</div>
<div className='text-description text-xs leading-5'>
<div className="text-description text-xs leading-5">
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
@@ -554,9 +521,9 @@ const CreateReceipt: FC = () => {
</div>
</div>
</Fragment>
}
)}
</div>
)
}
);
};
export default CreateReceipt
export default CreateReceipt;
+9 -3
View File
@@ -16,11 +16,13 @@ import Pagination from '../../components/Pagination'
import { Pages } from '../../config/Pages'
import TrashWithConfrim from '../../components/TrashWithConfrim'
import { ErrorType } from '../../helpers/types'
import { toast } from 'react-toastify'
import { toast } from '../../components/Toast';
import { usePermissions } from '../../hooks/usePermissions'
const ReceiptsList: FC = () => {
const { t } = useTranslation('global')
const { canUpdate, canDelete } = usePermissions()
const [page, setPage] = useState<number>(1)
const [searchParams] = useSearchParams()
const [activeTab, setActiveTab] = useState<InvoiceStatus>('PENDING')
@@ -34,11 +36,11 @@ const ReceiptsList: FC = () => {
const handleDeleteInvoice = (id: string) => {
deleteInvoice.mutate(id, {
onSuccess: () => {
toast.success(t('success'))
toast(t('success'), 'success')
getInvoices.refetch()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
@@ -177,13 +179,17 @@ const ReceiptsList: FC = () => {
</Td>
<Td text={''}>
<div className='flex gap-2'>
{canUpdate('invoices') &&
<Link to={Pages.receipts.detail + item.id}>
<Edit size={20} color='#888' />
</Link>
}
{canDelete('invoices') &&
<TrashWithConfrim
onDelete={() => handleDeleteInvoice(item.id)}
isLoading={deleteInvoice.isPending}
/>
}
</div>
</Td>
</tr>
+2 -2
View File
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import { useCreateResellers } from './hooks/useResellerData'
import { useNavigate } from 'react-router-dom'
import { ErrorType } from '../../helpers/types'
import { toast } from 'react-toastify'
import { toast } from '../../components/Toast';
import Input from '../../components/Input'
const CreateReseller: FC = () => {
@@ -32,7 +32,7 @@ const CreateReseller: FC = () => {
navigate(-1)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
+4
View File
@@ -8,10 +8,12 @@ import { useGetResellers } from './hooks/useResellerData'
import PageLoading from '../../components/PageLoading'
import Td from '../../components/Td'
import { ResellerItemType } from './types/Types'
import { usePermissions } from '../../hooks/usePermissions'
const ResellerList: FC = () => {
const { t } = useTranslation('global')
const { canCreate } = usePermissions()
const getReseller = useGetResellers()
return (
@@ -21,6 +23,7 @@ const ResellerList: FC = () => {
{t('reseller.list')}
</div>
{canCreate('reseller') && (
<Link to={Pages.reseller.create}>
<Button
className='w-[172px]'
@@ -33,6 +36,7 @@ const ResellerList: FC = () => {
</div>
</Button>
</Link>
)}
</div>
{
+3 -3
View File
@@ -3,7 +3,7 @@ import Button from '../../../components/Button'
import DefaulModal from '../../../components/DefaulModal'
import { usePayment } from '../hooks/useResellerData'
import Input from '../../../components/Input'
import { toast } from 'react-toastify'
import { toast } from '../../../components/Toast';
import { ErrorType } from '../../../helpers/types'
type Props = {
@@ -26,11 +26,11 @@ const Payment: FC<Props> = ({ id, refetch }) => {
refetch()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
toast(error.response?.data?.error.message[0], 'error')
}
})
} else {
toast.error('شماره تراکنش رو پر کنید')
toast('شماره تراکنش رو پر کنید', 'error')
}
}
+8 -8
View File
@@ -13,7 +13,7 @@ import * as Yup from 'yup'
import { useCreateService, useMultiUpload, useSingleUpload } from './hooks/useServiceData';
import moment from 'moment-jalaali';
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';
@@ -98,7 +98,7 @@ const AddService: FC = () => {
values.icon = data?.data?.url
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
@@ -111,7 +111,7 @@ const AddService: FC = () => {
values.images = await data.data.map((item: { url: string }) => item?.url)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
@@ -126,7 +126,7 @@ const AddService: FC = () => {
values.audios = await data.data.map((item: { url: string }) => item?.url)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
} else {
@@ -144,7 +144,7 @@ const AddService: FC = () => {
values.videos = await data.data.map((item: { url: string }) => item?.url)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
} else {
@@ -159,15 +159,15 @@ const AddService: FC = () => {
createService.mutate(params, {
onSuccess: () => {
formik.resetForm()
toast.success(t('success'))
toast(t('success'), 'success')
navigate(Pages.services.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
} else {
toast.error(t('errors.upload_image'))
toast(t('errors.upload_image'), 'error')
}
},
})
+7 -1
View File
@@ -11,11 +11,13 @@ import StatusCategory from './components/StatusCategory'
import Pagination from '../../components/Pagination'
import UpdateCategory from './components/UpdateCategory'
import { Trash } from 'iconsax-react'
import { usePermissions } from '../../hooks/usePermissions'
const Category: FC = () => {
const { t } = useTranslation('global')
const { canCreate, canUpdate, canDelete } = usePermissions()
const [search, setSearch] = useState<string>('')
const [page, setPage] = useState<number>(1)
const [isActive, setIsActive] = useState<'active' | 'deactive' | ''>('')
@@ -102,12 +104,16 @@ const Category: FC = () => {
</Td>
<Td text={''}>
<div className='flex gap-2'>
{canUpdate('services') && (
<UpdateCategory refetch={() => getCategory.refetch()} id={item.id} />
)}
{canDelete('services') && (
<Trash
size={20}
color='#888888'
onClick={() => handleDelete(item.id)}
/>
)}
</div>
</Td>
</tr>
@@ -130,7 +136,7 @@ const Category: FC = () => {
</div>
</div>
<CreateCategory />
{canCreate('services') && <CreateCategory />}
</div>
</div>
)

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