This commit is contained in:
hamid zarghami
2025-05-19 16:13:42 +03:30
parent f7e4548c96
commit 3d3ffd05f4
34 changed files with 1809 additions and 408 deletions
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 201.8 46.7" xml:space="preserve" class="logo-shape dark" style="enable-background:new 0 0 201.8 46.7;"><g><g><path d="M51,38.6h-1.9v5.1h-0.8v-5.1h-1.9V38H51L51,38.6L51,38.6z" class="st0"></path> <path d="M57.6,43.8c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.3-0.6
c0.6,0,1,0.2,1.3,0.5s0.4,0.8,0.4,1.4v0.4h-2.7l0,0c0,0.4,0.1,0.8,0.3,1c0.2,0.3,0.5,0.4,0.9,0.4c0.3,0,0.5,0,0.7-0.1
c0.2-0.1,0.4-0.2,0.5-0.3l0.3,0.5c-0.1,0.1-0.4,0.3-0.6,0.4C58.3,43.7,58,43.8,57.6,43.8z M57.5,40c-0.3,0-0.5,0.1-0.7,0.3
c-0.2,0.2-0.3,0.5-0.3,0.8l0,0h1.9V41c0-0.3-0.1-0.6-0.2-0.8S57.8,40,57.5,40z" class="st0"></path> <path d="M66.3,43.2c0.2,0,0.5-0.1,0.6-0.2s0.3-0.4,0.3-0.6h0.7l0,0c0,0.4-0.1,0.7-0.5,1c-0.3,0.3-0.7,0.4-1.1,0.4
c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.4-0.6c0.5,0,0.9,0.1,1.2,0.4
c0.3,0.3,0.5,0.7,0.4,1.1l0,0h-0.7c0-0.3-0.1-0.5-0.3-0.7S66.5,40,66.3,40c-0.4,0-0.7,0.1-0.8,0.4c-0.2,0.3-0.3,0.6-0.3,1.1v0.2
c0,0.4,0.1,0.8,0.3,1.1C65.6,43,65.9,43.2,66.3,43.2z" class="st0"></path> <path d="M74,40c0.1-0.2,0.3-0.4,0.5-0.5s0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3v2.7H76V41
c0-0.4-0.1-0.6-0.2-0.8C75.6,40.1,75.3,40,75,40c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-6.1H74L74,40L74,40z" class="st0"></path> <path d="M83,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
v2.7H85V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L83,39.4
L83,39.4z" class="st0"></path> <path d="M91.2,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6c-0.6,0-1-0.2-1.4-0.6C91.4,42.8,91.2,42.3,91.2,41.5L91.2,41.5z M92,41.6
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1
c-0.2-0.3-0.5-0.5-0.9-0.5s-0.6,0.1-0.8,0.5S92,41.1,92,41.6L92,41.6z" class="st0"></path> <path d="M101.3,43.7h-0.8v-6.1h0.8V43.7z" class="st0"></path> <path d="M106.8,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C107,42.8,106.8,42.3,106.8,41.5L106.8,41.5z M107.6,41.6
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
s-0.6,0.1-0.8,0.5S107.6,41.1,107.6,41.6L107.6,41.6z" class="st0"></path> <path d="M115.9,41.7c0-0.7,0.1-1.2,0.4-1.7s0.7-0.6,1.2-0.6c0.3,0,0.5,0,0.7,0.2c0.2,0.1,0.4,0.3,0.5,0.5l0.1-0.6h0.6
v4.3c0,0.5-0.2,1-0.5,1.3s-0.8,0.4-1.4,0.4c-0.2,0-0.4,0-0.7-0.1c-0.2-0.1-0.5-0.1-0.6-0.2l0.1-0.6c0.1,0.1,0.3,0.1,0.5,0.2
c0.2,0,0.4,0.1,0.6,0.1c0.4,0,0.6-0.1,0.8-0.3c0.2-0.2,0.3-0.4,0.3-0.8v-0.5c-0.1,0.2-0.3,0.3-0.5,0.4c-0.2,0.1-0.4,0.1-0.7,0.1
c-0.5,0-0.9-0.2-1.2-0.6S115.9,42.4,115.9,41.7L115.9,41.7L115.9,41.7z M116.7,41.7c0,0.4,0.1,0.8,0.3,1s0.5,0.4,0.8,0.4
c0.2,0,0.4,0,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.5v-2c-0.1-0.2-0.2-0.3-0.4-0.4c-0.2-0.1-0.4-0.2-0.6-0.2c-0.4,0-0.6,0.1-0.8,0.5
S116.7,41.2,116.7,41.7L116.7,41.7L116.7,41.7z" class="st0"></path> <path d="M126.4,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8c-0.2,0.2-0.5,0.3-0.8,0.3
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0c0.1,0,0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2c0.1-0.1,0.2-0.3,0.3-0.5
l0.2-0.5l-1.6-4.2h0.9L126.4,42.1z" class="st0"></path> <path d="M144.4,41.9L144.4,41.9c0,0.6-0.2,1-0.6,1.4s-0.9,0.5-1.5,0.5c-0.6,0-1.2-0.2-1.6-0.7s-0.6-1-0.6-1.8v-1
c0-0.7,0.2-1.3,0.6-1.8s0.9-0.7,1.6-0.7s1.2,0.2,1.5,0.5c0.4,0.3,0.6,0.8,0.6,1.4l0,0h-0.8c0-0.4-0.1-0.7-0.4-1
c-0.2-0.2-0.6-0.4-1-0.4s-0.8,0.2-1,0.5c-0.3,0.4-0.4,0.8-0.4,1.3v1c0,0.5,0.1,1,0.4,1.3s0.6,0.5,1,0.5c0.4,0,0.8-0.1,1-0.4
s0.4-0.5,0.4-1L144.4,41.9L144.4,41.9z" class="st0"></path> <path d="M149.6,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C149.8,42.8,149.6,42.3,149.6,41.5L149.6,41.5z M150.4,41.6
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
s-0.6,0.1-0.8,0.5S150.4,41.1,150.4,41.6L150.4,41.6z" class="st0"></path> <path d="M159.5,39.4l0,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.8-0.2s0.5,0.1,0.7,0.2s0.4,0.3,0.5,0.6
c0.1-0.3,0.3-0.4,0.5-0.6c0.2-0.1,0.5-0.2,0.8-0.2c0.4,0,0.8,0.1,1,0.5c0.3,0.3,0.4,0.8,0.4,1.3v2.5H164v-2.6
c0-0.4-0.1-0.7-0.2-0.9c-0.1-0.2-0.4-0.3-0.6-0.3c-0.3,0-0.5,0.1-0.6,0.3c-0.2,0.2-0.3,0.4-0.3,0.7v0v2.7h-0.8v-2.6
c0-0.4-0.1-0.7-0.2-0.9s-0.4-0.3-0.6-0.3c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L159.5,39.4L159.5,39.4z" class="st0"></path> <path d="M174,41.7c0,0.6-0.1,1.1-0.4,1.5s-0.7,0.6-1.2,0.6c-0.3,0-0.5,0-0.7-0.1s-0.4-0.2-0.5-0.4v2.1h-0.8v-5.9h0.6
l0.1,0.5c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.9,0.2,1.2,0.6S174,41,174,41.7L174,41.7L174,41.7z M173.2,41.7
c0-0.5-0.1-0.9-0.3-1.2s-0.5-0.5-0.8-0.5c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v2.1c0.1,0.2,0.2,0.3,0.4,0.4s0.4,0.1,0.6,0.1
c0.4,0,0.6-0.1,0.8-0.4C173.1,42.5,173.2,42.2,173.2,41.7L173.2,41.7L173.2,41.7z" class="st0"></path> <path d="M182.1,43.7c0-0.1,0-0.2-0.1-0.3s0-0.2,0-0.3c-0.1,0.2-0.3,0.4-0.6,0.5s-0.5,0.2-0.8,0.2
c-0.4,0-0.8-0.1-1-0.3s-0.4-0.5-0.4-0.9c0-0.4,0.2-0.7,0.5-1c0.3-0.2,0.8-0.3,1.3-0.3h0.8v-0.4c0-0.3-0.1-0.5-0.2-0.6
s-0.4-0.2-0.6-0.2c-0.3,0-0.5,0.1-0.6,0.2c-0.2,0.1-0.2,0.3-0.2,0.5l-0.7,0l0,0c0-0.3,0.1-0.6,0.4-0.9s0.7-0.4,1.2-0.4
c0.5,0,0.9,0.1,1.2,0.4c0.3,0.3,0.4,0.6,0.4,1.1v2c0,0.1,0,0.3,0,0.4c0,0.1,0,0.3,0.1,0.4L182.1,43.7L182.1,43.7z M180.8,43.1
c0.3,0,0.5-0.1,0.8-0.2c0.2-0.1,0.4-0.3,0.4-0.5v-0.7h-0.9c-0.3,0-0.6,0.1-0.8,0.2s-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.5
S180.5,43.1,180.8,43.1z" class="st0"></path> <path d="M189.1,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
v2.7H191V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3
L189.1,39.4L189.1,39.4z" class="st0"></path> <path d="M198.7,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8s-0.5,0.3-0.8,0.3c-0.1,0-0.1,0-0.2,0
c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0s0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2s0.2-0.3,0.3-0.5l0.2-0.5l-1.6-4.2h0.9L198.7,42.1z" class="st0"></path></g></g> <g><path d="M46.4,31.9V9.9h6.2c1.9,0,3.6,0.4,5.1,1.3c1.5,0.9,2.6,2.1,3.4,3.6c0.8,1.6,1.2,3.4,1.2,5.4v1.4
c0,2.1-0.4,3.9-1.2,5.5c-0.8,1.6-2,2.8-3.4,3.6s-3.2,1.3-5.2,1.3H46.4z M49.3,12.2v17.3h3.1c2.2,0,4-0.7,5.2-2.1
c1.3-1.4,1.9-3.4,1.9-6v-1.3c0-2.5-0.6-4.5-1.8-5.9s-2.9-2.1-5-2.1L49.3,12.2L49.3,12.2z" class="st0"></path> <path d="M131.9,31.9H129l-11.1-17v17h-2.9V9.9h2.9L129,26.9V9.9h2.9L131.9,31.9L131.9,31.9z" class="st0"></path> <path d="M189.6,21.7l-2.7,2.8v7.5H184V9.8h2.9v10.9l9.8-10.9h3.5l-8.7,9.7l9.4,12.3h-3.5L189.6,21.7z" class="st0"></path> <polygon points="88.9,13.4 95.6,31.9 98.6,31.9 90.1,9.8 87.6,9.8 79.2,31.9 82.2,31.9 " class="st0"></polygon> <polygon points="157.9,13.4 164.7,31.9 167.6,31.9 159.2,9.8 156.7,9.8 148.2,31.9 151.2,31.9 " class="st0"></polygon></g> <path d="M27.5,30.2c0,0-5.5,5.3-10.1,9.8v6.1c-0.5,0.4,16.4-15.9,16.4-15.9L17.4,13v6.5L27.5,30.2z" class="st0"></path> <g><rect x="9.1" y="13.1" transform="matrix(0.7298 -0.6837 0.6837 0.7298 -14.5845 13.0725)" width="0.3" height="23.8" class="st0"></rect> <rect x="9.9" y="13.4" transform="matrix(0.7287 -0.6849 0.6849 0.7287 -13.8472 13.4064)" width="0.3" height="21.6" class="st0"></rect> <polygon points="17.3,30.5 4,16.4 4.2,16.2 17.5,30.3 " class="st0"></polygon> <polygon points="17.3,28.9 5.5,16.5 5.7,16.3 17.5,28.7 " class="st0"></polygon> <polygon points="17.2,27.2 7,16.5 7.2,16.3 17.5,27.1 " class="st0"></polygon></g> <g><polygon points="1.3,16.5 1.1,16.3 17.3,0.5 17.5,0.7 " class="st0"></polygon> <polygon points="2.8,16.5 2.6,16.3 17.3,2 17.4,2.2 " class="st0"></polygon> <polygon points="4.3,16.5 4.1,16.3 17.2,3.5 17.4,3.7 " class="st0"></polygon> <polygon points="5.8,16.5 5.6,16.3 17.2,5 17.4,5.2 " class="st0"></polygon> <polygon points="7.4,16.5 7.2,16.3 17.2,6.5 17.4,6.7 " class="st0"></polygon></g></svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

+68
View File
@@ -0,0 +1,68 @@
import { FC, useState } from 'react'
import DefaulModal from './DefaulModal'
import { useTranslation } from 'react-i18next'
import Button from './Button'
import Textarea from './Textarea'
type Props = {
isLoading?: boolean,
close: () => void,
isOpen: boolean,
onConfrim: (text?: string) => void,
label?: string,
isHasDescription?: boolean
}
const ModalConfrim: FC<Props> = (props: Props) => {
const { t } = useTranslation('global')
const [description, setDescription] = useState<string>('')
return (
<DefaulModal
open={props.isOpen}
close={props.close}
title_header={t('confrim.subject')}
isHeader
>
<div className='mt-6'>
<div className='text-sm text-center'>
{
props.label ?
props.label
:
t('confrim.content')
}
{
props.isHasDescription &&
<div className='mt-4'>
<Textarea
placeholder={t('description')}
onChange={(e) => setDescription(e.target.value)}
value={description}
className='bg-transparent border border-gray-500 rounded-xl w-[300px] p-2'
label=''
/>
</div>
}
</div>
<div className='flex gap-4 justify-center mt-10'>
<Button
label={t('confrim.yes')}
onClick={() => props.onConfrim(props.isHasDescription ? description : undefined)}
isLoading={props.isLoading}
/>
<Button
label={t('confrim.cancel')}
className='bg-transparent text-black border border-primary'
onClick={props.close}
/>
</div>
</div>
</DefaulModal>
)
}
export default ModalConfrim
+19
View File
@@ -0,0 +1,19 @@
import { FC } from 'react'
import Logo from '../assets/images/logo_orig.svg'
import { useTranslation } from 'react-i18next'
const PageLoading: FC = () => {
const { t } = useTranslation('global')
return (
<div className='flex bg-transparent w-fit mx-auto flex-col gap-4 items-center'>
<img src={Logo} alt='logo' className='w-28' />
<div className='text-xs text-gray-700'>
{t('loading')}
</div>
</div>
)
}
export default PageLoading
+1
View File
@@ -2,6 +2,7 @@ import { FC, InputHTMLAttributes } from 'react'
type Props = {
label: string,
error_text?: string,
} & InputHTMLAttributes<HTMLTextAreaElement>
const Textarea: FC<Props> = (props: Props) => {
+70 -7
View File
@@ -99,7 +99,10 @@
"home": {
"last_announcement": "آخرین اطلاعیه ها",
"see_all": "دیدن همه",
"companies": "شرکت ها"
"companies": "شرکت ها",
"request_pending": "درخواست شما برای ثبت شرکت، هنوز تایید نشده است",
"request_approved": "درخواست شما برای ثبت شرکت، تایید شد.",
"request_rejected": "درخواست شما برای ثبت شرکت، رد شده است."
},
"all": "همه",
"service": {
@@ -172,12 +175,27 @@
"send_answer": "ارسال پاسخ",
"close_ticket": "بستن تیکت",
"info_ticket": "اطلاعات تیکت",
"update_sms": "به روزرسانی این تیکت از طریق پیامک اطلاع داده میشود"
"update_sms": "به روزرسانی این تیکت از طریق پیامک اطلاع داده میشود",
"low": "پایین",
"medium": "متوسط",
"high": "بالا",
"category": "دسته بندی",
"asignto": "کارشناس پاسخگویی",
"MEDIUM": "متوسط",
"LOW": "پایین",
"HIGH": "بالا",
"PENDING": "در انتظار",
"ANSWERED": "پاسخ داده شده",
"CLOSED": "بسته شده",
"service": "سرویس",
"all": "همه"
},
"date": "تاریخ",
"success": "عملیات با موفقیت انجام شد",
"select": "انتخاب",
"receip": {
"receip": "صورتحساب",
"receip_list": "لیست صورتحساب ها",
"user_list": "لیست کاربران",
"Language_learners_list": "لیست زبان آموزان",
"dont_pay": "پرداخت نشده",
"dont_confrim": "تایید نشده",
"paid": "پرداخت شده",
@@ -185,6 +203,8 @@
"returned": "پس داده شده",
"its_time": "سررسیده شده",
"archive": "بایگانی شده",
"overdue": "سررسید شده",
"late_fee": "جریمه دیرکرد",
"detail_account": "جزییات صورتحساب ",
"personal_information": "اطلاعات شخصی",
"type_person": "نوع شخص : ",
@@ -193,11 +213,11 @@
"company_name": "نام شرکت/ سازمان :",
"registration_number": "شماره ثبت :",
"tel_company": "تلفن شرکت :",
"national_code": "کد ملی نماینده :",
"national_code": " شناسه ملی :",
"postal_code": "کد پستی :",
"state": "استان :",
"city": "شهر :",
"address": "آدرس :",
"address": "آدرس ثبتی :",
"detail_receip": "مشخصات صورتحساب",
"not_paid": "پرداخت نشده",
"description": "توضیحات",
@@ -206,8 +226,51 @@
"date_factor": "تاریخ صدور فاکتور",
"due_date": "تاریخ سررسید فاکتور",
"factor_number": "شماره فاکتور",
"status_factor": "وضعیت فاکتور"
"status_factor": "وضعیت فاکتور",
"customer": "مشتری",
"receipt_information": "اطلاعات صورتحساب",
"number": "شماره",
"product_name": "نام کالا",
"count": "تعداد",
"unit_amount": "مبلغ واحد",
"discount": "تخفیف(٪)",
"total_amount": "مبلغ کل(تومان)",
"tax": "مالیات بر ارزش افزوده (۰.۹ درصد)",
"total": "جمع",
"error_empty": "همه فیلد ها را پر کنید",
"date_receipt": "تاریخ صورتحساب",
"last_date_receipt": "آخرین مهلت پرداخت",
"service": "سرویس",
"status": "وضعیت ",
"status_paid": "وضعیت پرداخت",
"PENDING": "در انتظار تایید ",
"PAID": "پرداخت شده",
"CANCELED": "لغو شده",
"EXPIRED": "منقضی شده",
"WAIT_PAYMENT": "در انتظار پرداخت",
"real_person": "شخص حقیقی",
"fullName": "نام و نام خانوادگی",
"compelete_financal_info_error": "برای دریافت فاکتور رسمی اطلاعات مالی خود را وارد کنید",
"compelete_financal_info": "تکمیل اطلاعات مالی",
"get_factor": "دریافت پیش فاکتور",
"pay": "پرداخت",
"confrim_factor": "تایید فاکتور",
"sure_confrim_factor": "برای تایید فاکتور مطمئن هستید؟",
"accepted": "تایید شده",
"sure_confrim_pay": "آیا مطمئن هستید که مبلغ صورتحساب از کیف پول شما کسر شود؟",
"thank_pay_1": "ضمن تشکر از خرید شما",
"thank_pay_2": "مبلغ این صورت حساب پرداخت شده است.",
"discount_code": "کد تخفیف",
"apply": "اعمال",
"discount_applied": "کد تخفیف با موفقیت اعمال شد",
"discount_error": "کد تخفیف معتبر نیست",
"pending": "در انتظار تایید",
"discount_removed": "کد تخفیف با موفقیت حذف شد",
"remove_discount": "حذف تخفیف",
"charge_wallet": "شارژ کیف پول",
"sure_charge_wallet": "آیا از شارژ کیف پول اطمینان دارید؟"
},
"loading": "در حال بارکذاری...",
"edit": "ویرایش",
"transaction": {
"transaction": "تراکنش ها",
+61
View File
@@ -0,0 +1,61 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import StatusWithText from '../../components/StatusWithText'
import { useNavigate, useParams } from 'react-router-dom'
import { ArrowLeft2 } from 'iconsax-react'
import { useGetAnnoncementDetail } from './hooks/useAnnoncementData'
import PageLoading from '../../components/PageLoading'
import { Helmet } from 'react-helmet-async'
const AnnouncementDetail: FC = () => {
const { id } = useParams()
const { t } = useTranslation('global')
const navigate = useNavigate()
const getAnnoncementDetail = useGetAnnoncementDetail(id ? id : '')
return (
<div className='mt-4'>
<Helmet>
<title>
داناک | اطلاعیه
</title>
</Helmet>
<div className='flex justify-between'>
<div>
{t('announcement.text_announcement')}
</div>
<div onClick={() => navigate(-1)} className='text-xs flex gap-0.5 items-center font-extralight'>
{t('back')}
<ArrowLeft2 size={12} color='black' />
</div>
</div>
{
getAnnoncementDetail.isPending ?
<PageLoading />
:
<div className='flex-1 mt-4 bg-white py-6 px-4 rounded-2xl'>
<div className='flex justify-between items-start'>
<div className='text-sm max-w-[70%] leading-67'>
{getAnnoncementDetail.data?.data?.announcement?.title}
</div>
{
getAnnoncementDetail.data?.data?.announcement.isImportant &&
<StatusWithText
variant='error'
text={t('announcement.important')}
/>
}
</div>
<div dangerouslySetInnerHTML={{ __html: getAnnoncementDetail.data?.data?.announcement?.content }} className='mt-5 leading-7 text-xs font-light'>
</div>
</div>
}
</div>
)
}
export default AnnouncementDetail
+151
View File
@@ -0,0 +1,151 @@
import { FC, Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../components/Input'
import StatusWithText from '../../components/StatusWithText'
import { clx } from '../../helpers/utils'
import { useNavigate } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { useGetAnnoncement } from './hooks/useAnnoncementData'
import { AnnoncementItemType } from './types/AnnoncementTypes'
import moment from 'moment-jalaali'
import { Helmet } from 'react-helmet-async'
import AnouncementSkeleton from './components/AnouncementSkeleton'
const AnnouncementtList: FC = () => {
const navigate = useNavigate()
const { t } = useTranslation('global')
const [search, setSearch] = useState<string>('')
const [itemSelected, setItemSelected] = useState<AnnoncementItemType>()
const getAnnoncements = useGetAnnoncement(search)
const handleChange = (item: AnnoncementItemType) => {
if (window.innerWidth < 1280) {
navigate(Pages.announcement.detail + item.announcement.id)
} else {
setItemSelected(item)
}
}
return (
<div className='mt-4 text-sm'>
<Helmet>
<title>
داناک | اطلاعیه ها
</title>
</Helmet>
<div className='text-base'>
{t('announcement.announcements')}
</div>
<div className='mt-10 flex gap-8'>
<div className='bg-white xl:w-[400px] w-full rounded-2xl p-6'>
<div className='flex gap-3 items-center'>
<div className='flex-1'>
<Input
variant='search'
placeholder={t('announcement.search')}
className='-mt-1'
onChangeSearchFinal={(value) => setSearch(value)}
/>
</div>
{/* <div>
<Select
items={[
{ value: '1', label: 'همه' },
{ value: '2', label: 'فعال' },
{ value: '3', label: 'غیر فعال' },
]}
className='bg-[#EEF0F7] w-[68px]'
/>
</div> */}
</div>
<Fragment>
<div className='mt-8'>
{
getAnnoncements.isPending ?
<Fragment>
<AnouncementSkeleton />
<AnouncementSkeleton />
<AnouncementSkeleton />
</Fragment>
:
getAnnoncements.data?.data?.announcements?.map((item: AnnoncementItemType) => {
return (
<div onClick={() => handleChange(item)} className={clx(
'p-4 border-b cursor-pointer',
item.id === itemSelected?.id && 'bg-[#F8F9FB]'
)}>
<div className='flex justify-between'>
<div className='leading-6 text-xs xl:text-[12.5px] max-w-[70%]'>
{item.announcement.title}
</div>
<div className='flex gap-1'>
{
item.announcement.isImportant &&
<StatusWithText
variant='error'
text={t('announcement.important')}
/>
}
{
!item.isRead &&
<StatusWithText
variant='success'
text={t('announcement.new')}
/>
}
</div>
</div>
<div className='m-2 flex justify-between items-center text-[11px]'>
<div dangerouslySetInnerHTML={{ __html: item.announcement.content }} className='max-w-[70%] truncate text-description'>
</div>
<div className='text-description'>
{moment(item.announcement.publishAt).format('jYYYY/jMM/jDD')}
</div>
</div>
</div>
)
})
}
</div>
</Fragment>
</div>
{
itemSelected &&
<div className='flex-1 hidden xl:block bg-white py-7 px-5 rounded-2xl'>
<div className='flex justify-between items-center'>
<div className='text-lg'>
متن اطلاعیه
</div>
{
itemSelected.announcement.isImportant &&
<StatusWithText
variant='error'
text={t('announcement.important')}
/>
}
</div>
<div className='mt-8'>
{itemSelected.announcement.title}
</div>
<div dangerouslySetInnerHTML={{ __html: itemSelected.announcement.content }} className='mt-5 leading-7 text-xs font-light'>
</div>
</div>
}
</div>
</div>
)
}
export default AnnouncementtList
@@ -0,0 +1,41 @@
import { FC } from 'react'
import Skeleton from 'react-loading-skeleton'
const AnouncementSkeleton: FC = () => {
return (
<div className={'p-4 border-b'}>
<div className='flex items-center justify-between'>
<div className='leading-6 text-xs xl:text-[12.5px] max-w-[70%]'>
<Skeleton
height={7}
width={100}
/>
</div>
<div className='flex gap-1'>
<Skeleton
className='!w-10 !h-6 !rounded-full'
/>
<Skeleton
className='!w-10 !h-6 !rounded-full'
/>
</div>
</div>
<div className='m-2 flex justify-between items-center text-[11px]'>
<Skeleton
height={7}
width={150}
/>
<div className='text-description'>
<Skeleton
height={7}
width={40}
/>
</div>
</div>
</div>
)
}
export default AnouncementSkeleton
@@ -0,0 +1,17 @@
import * as api from "../service/AnnoncementService";
import { useQuery } from "@tanstack/react-query";
export const useGetAnnoncement = (search: string) => {
return useQuery({
queryKey: ["announcements", search],
queryFn: () => api.getAnnoncements(search),
});
};
export const useGetAnnoncementDetail = (id: string) => {
return useQuery({
queryKey: ["announcement", id],
queryFn: () => api.getAnnoncementDetail(id),
enabled: !!id,
});
};
@@ -0,0 +1,11 @@
import axios from "../../../config/axios";
export const getAnnoncements = async (search: string) => {
const { data } = await axios.get(`/announcements/user?q=${search}`);
return data;
};
export const getAnnoncementDetail = async (id: string) => {
const { data } = await axios.get(`/announcements/${id}`);
return data;
};
@@ -0,0 +1,18 @@
export type AnnoncementItemType = {
id: string;
createdAt: string;
updatedAt: string;
announcement: {
id: string;
createdAt: string;
title: string;
content: string;
publishAt: string;
targetService: {
id: string;
name: string;
};
isImportant: boolean;
};
isRead: boolean;
};
+38 -10
View File
@@ -1,31 +1,59 @@
import { Building, InfoCircle } from 'iconsax-react'
import { Building, CloseCircle, InfoCircle, TickCircle } from 'iconsax-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { buildPath, clx } from '../../helpers/utils'
import StatusWithText from '../../components/StatusWithText'
import TitleSeeAll from '../../components/TitleSeeAll'
import { useGetCompanies, useGetIndustry } from './hooks/useHomeData'
import { useGetCompanies, useGetIndustry, useGetCompanyRequest } from './hooks/useHomeData'
import { CompanyItemType, IndustryItemType } from './types/HomeTypes'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { CompanyRequestStatus } from './enum/Enum'
const Home: FC = () => {
const { t } = useTranslation('global')
const [activeIndustry, setActiveIndustry] = useState<string>('')
const { data: industry } = useGetIndustry()
const { data: companies } = useGetCompanies(activeIndustry)
const { data: companyRequest } = useGetCompanyRequest()
return (
<div className='mt-4'>
<div className='w-full h-10 flex gap-2 items-center px-4 rounded-xl bg-[#FCE7CE] text-[#FF7B00]'>
<InfoCircle
size={24}
color='#FF7B00'
/>
<div className='text-xs'>
درخواست شما برای ثبت شرکت، هنوز تایید نشده است
{
companyRequest?.data?.requests &&
<div className={clx(
'w-full h-10 flex gap-2 items-center px-4 rounded-xl ',
companyRequest?.data?.requests?.requestStatus === CompanyRequestStatus.PENDING ? 'bg-[#FCE7CE] text-[#FF7B00]'
: companyRequest?.data?.requests?.requestStatus === CompanyRequestStatus.APPROVED ? 'bg-[#E6FFF4] text-[#039855]'
: 'bg-[#FFE6E6] text-[#FF0000]'
)}>
{
companyRequest?.data?.requests?.requestStatus === CompanyRequestStatus.PENDING ?
<InfoCircle
size={22}
color='#FF7B00'
/>
: companyRequest?.data?.requests?.requestStatus === CompanyRequestStatus.APPROVED ?
<TickCircle
size={22}
color='#039855'
/>
: companyRequest?.data?.requests?.requestStatus === CompanyRequestStatus.REJECTED ?
<CloseCircle
size={22}
color='#FF0000'
/>
: null
}
<div className="text-xs">
{companyRequest?.data?.requests?.requestStatus === CompanyRequestStatus.PENDING
? t('home.request_pending')
: companyRequest?.data?.requests?.requestStatus === CompanyRequestStatus.APPROVED
? t('home.request_approved')
: t('home.request_rejected')}
</div>
</div>
</div>
}
<div className='mt-6'>
<TitleSeeAll
+11
View File
@@ -0,0 +1,11 @@
export enum CompanyStatus {
APPROVED = "APPROVED",
PENDING = "PENDING",
REJECTED = "REJECTED",
}
export enum CompanyRequestStatus {
PENDING = "PENDING",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
}
+8
View File
@@ -4,6 +4,7 @@ import {
getIdWithSlug,
getIndustry,
getCompanyDetail,
getCompanyRequest,
} from "../service/HomeService";
export const useGetSlugId = () => {
@@ -33,3 +34,10 @@ export const useGetCompanyDetail = (id: string) => {
enabled: !!id,
});
};
export const useGetCompanyRequest = () => {
return useQuery({
queryKey: ["company-request"],
queryFn: () => getCompanyRequest(),
});
};
+5
View File
@@ -25,3 +25,8 @@ export const getCompanyDetail = async (id: string) => {
const { data } = await axios.get(`/companies/public/${id}`);
return data;
};
export const getCompanyRequest = async () => {
const { data } = await axios.get(`/companies/requests/user`);
return data;
};
+300 -225
View File
@@ -1,255 +1,330 @@
import { FC } from 'react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Edit } from 'iconsax-react'
import StatusCircle from '../../components/StatusCircle'
import SamanImage from '../../assets/images/saman.png'
import { useParams } from 'react-router-dom'
import { useApplyDiscount, useGetInvoiceDetail, useRemoveDiscount } from './hooks/useReceiptData'
import { ApplyDiscountType, ReceiptDetailItemType } from './types/ReceiptTypes'
import { NumberFormat } from '../../config/func'
import moment from 'moment-jalaali'
import LegalInfo from './components/LegalInfo'
import RealInfo from './components/RealInfo'
import RegisterInfo from './components/RegisterInfo'
import { clx } from '../../helpers/utils'
import Button from '../../components/Button'
import ApproveInvoice from './components/ApproveInvoice'
import PayInvoice from './components/PayInvoice'
import Input from '../../components/Input'
import { useGetProfile } from '../profile/hooks/useProfileData'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
// import PDFGenerator from '../../components/PDFGenerator'
// import { useGetGetWays } from '../wallet/hooks/useWalletData'
// import { GatewayItemType } from '../wallet/types/WalletTypes'
const ReceiptsDetail: FC = () => {
const { id } = useParams()
const { t } = useTranslation('global')
const [code, setCode] = useState<string>('')
const [gateWayId, setGateWayId] = useState<string>('')
const getProfile = useGetProfile()
const getInvoce = useGetInvoiceDetail(id ? id : '')
const applyDiscount = useApplyDiscount()
const removeDiscount = useRemoveDiscount()
// const getWays = useGetGetWays()
const handleApplyDiscount = () => {
const params: ApplyDiscountType = {
id: id ? id : '',
code: code
}
applyDiscount.mutate(params, {
onSuccess: () => {
toast.success(t('receip.discount_applied'))
getInvoce.refetch()
},
onError: (error: ErrorType) => {
toast.error(error?.response?.data?.error?.message[0])
}
})
}
const handleRemoveDiscount = () => {
removeDiscount.mutate({ id: id ? id : '' }, {
onSuccess: () => {
toast.success(t('receip.discount_removed'))
getInvoce.refetch()
},
onError: (error: ErrorType) => {
toast.error(error?.response?.data?.error?.message[0])
}
})
}
// useEffect(() => {
// if (getWays.data) {
// setGateWayId(getWays.data?.data?.paymentGateways?.[0]?.id)
// }
// }, [getWays.data])
return (
<div className='mt-4 pb-20'>
<div>
{t('receip.detail_account')}
۱۲۳۱۲
</div>
<div className='mt-4'>
<div className='flex xl:flex-row flex-col gap-4 items-center'>
<div className='flex gap-6 items-center'>
<div className='whitespace-nowrap'>{t('receip.detail_account')}</div>
<div className='flex xl:flex-row flex-col mt-8 gap-6'>
<div className='flex-1'>
<div className='w-full bg-white rounded-3xl p-8'>
<div className='flex justify-between items-center'>
<div>
{t('receip.personal_information')}
</div>
<div className='flex gap-1 items-center text-xs text-[#0047FF]'>
<Edit size={16} color='#0047FF' />
<div>
{t('edit')}
</div>
</div>
</div>
<ApproveInvoice status={getInvoce.data?.data?.invoice?.status} id={id ? id : ''} refetch={getInvoce.refetch} />
</div>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.type_person')}
</div>
<div>
{t('receip.company')}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.representativeـname')}
</div>
<div>
مهرداد مظفری
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.company_name')}
</div>
<div>
داناک
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.registration_number')}
</div>
<div>
۱۲۳۴
</div>
</div>
</div>
<div className='mt-8 border-t pt-7 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.tel_company')}
</div>
<div>
۰۰۱۲۳۴۵۶۷۸
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.national_code')}
</div>
<div>
۰۰۱۲۳۴۵۶۷۸
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.postal_code')}
</div>
<div>
۳۸۲۱۳۵۸۸۹۲
</div>
</div>
</div>
<div className='mt-6 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.state')}
</div>
<div>
مرکزی
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.state')}
</div>
<div>
مرکزی
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.city')}
</div>
<div>
اراک
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.address')}
</div>
<div>
اراک -خیابان شریعتی-خیابان جنت
</div>
</div>
</div>
</div>
<div className='w-full bg-white rounded-3xl mt-8 p-8'>
<div className='flex justify-between items-center'>
<div>
{t('receip.detail_receip')}
</div>
<div className='flex gap-2 text-xs items-center'>
<StatusCircle
color='#FF7B00'
{
!getInvoce.data?.data?.invoice?.discount && ['PENDING', 'APPROVED', 'WAIT_PAYMENT'].includes(getInvoce.data?.data?.invoice?.status) ?
<div className='flex -mt-1 gap-2 items-center w-full'>
<div className='w-full xl:w-fit'>
<Input
className='bg-transparent border border-border xl:max-w-40'
placeholder={t('receip.discount_code')}
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<div>
{t('receip.not_paid')}
</div>
</div>
</div>
<div className='mt-7 text-sm'>
<div className='flex justify-between items-center text-description h-14'>
<div>
{t('receip.description')}
</div>
<div>
{t('receip.price')}
</div>
</div>
<div className='flex justify-between items-center text-xs border-t border-[#EAEDF5] h-14'>
<div>
اکانت یکساله با دسترسی
</div>
<div>
۱۰,۰۰۰,۰۰۰ ریال
</div>
</div>
<div className='flex justify-between items-center text-xs border-t border-[#EAEDF5] h-14'>
<div>
۱۰٪ ارزش افزوده
</div>
<div>
۱۰,۰۰۰,۰۰۰ ریال
</div>
</div>
<div className='flex justify-between items-center text-xs border-t border-[#EAEDF5] h-14'>
<div>
جمع کل
</div>
<div>
۱۰,۰۰۰,۰۰۰ ریال
</div>
</div>
</div>
</div>
</div>
<div className='bg-white xl:w-sidebar text-sm h-fit rounded-3xl p-8'>
<div className='flex justify-between items-center'>
<div>
{t('receip.receip_information')}
</div>
<div className='flex gap-2 text-xs items-center'>
<StatusCircle
color='#FF7B00'
<Button
label={t('receip.apply')}
className='bg-transparent text-black border border-black mt-1 px-5 w-fit'
onClick={handleApplyDiscount}
isLoading={applyDiscount.isPending}
/>
<div>پرداخت نشده</div>
</div>
</div>
: getInvoce.data?.data?.invoice?.discount &&
<div className='flex -mt-1 gap-2 items-center w-full'>
<div className='w-full xl:w-fit'>
<Button
label={t('receip.remove_discount')}
className='bg-transparent text-black border border-black mt-1 px-5 w-fit'
onClick={handleRemoveDiscount}
/>
</div>
</div>
}
</div>
<div className='mt-6 text-xs font-extralight'>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.date_factor')}
{
<div className='flex xl:flex-row flex-col mt-8 gap-6'>
<div className='flex-1'>
<div id="receipt-details" className='w-full bg-white rounded-3xl p-8'>
<div className='flex justify-between items-center mb-6'>
<div className='text-lg font-medium'>
{t('receip.personal_information')}
</div>
</div>
<div>
۱۴۰۲/۱۰/۰۴
{/* {
getProfile.data?.data?.user?.financialType === 'LEGAL' ?
<LegalInfo />
:
getProfile.data?.data?.user?.financialType === 'REAL' ?
<RealInfo />
: <RegisterInfo data={getProfile.data?.data} />
} */}
<div className='mt-8'>
<div className='flex justify-between items-center mb-6'>
<div className='text-lg font-medium'>
{t('receip.detail_receip')}
</div>
</div>
<div className='mt-7 text-sm'>
<div className='flex justify-between items-center text-description h-14 font-medium'>
<div>
{t('receip.description')}
</div>
<div>
{t('receip.price')}
</div>
</div>
{
getInvoce.data?.data?.invoice?.items?.map((item: ReceiptDetailItemType) => {
return (
<div key={item.id} className='flex justify-between items-center text-xs border-t border-[#EAEDF5] h-14'>
<div className='flex gap-0.5'>
<span>{item.name}</span>
<span>{item.count}</span>
<span>عدد</span>
</div>
<div>
<span>{NumberFormat(item.totalPrice)}</span> تومان
</div>
</div>
)
})
}
<div className='flex justify-between items-center text-xs border-t border-[#EAEDF5] h-14'>
<div>
۱۰٪ ارزش افزوده
</div>
<div>
<span>{NumberFormat(getInvoce.data?.data?.invoice?.tax)}</span> تومان
</div>
</div>
<div className='flex justify-between gap-1 items-center text-xs border-t border-[#EAEDF5] h-14'>
<div>
جمع کل
</div>
<div className='flex gap-1'>
{
getInvoce.data?.data?.invoice?.originalPrice !== getInvoce.data?.data?.invoice?.totalPrice && (
<><span className='line-through'>{NumberFormat(getInvoce.data?.data?.invoice?.originalPrice)}</span></>
)
}
<span>{NumberFormat(getInvoce.data?.data?.invoice?.totalPrice)}</span> تومان
</div>
</div>
</div>
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.due_date')}
</div>
<div>
۱۴۰۲/۱۰/۰۴
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.factor_number')}
</div>
<div>
۱۲۳۱
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.status_factor')}
</div>
<div>
تایید شده
<div className='mt-8'>
<div className='flex justify-between items-center mb-6'>
<div className='text-lg font-medium'>
{t('receip.receip_information')}
</div>
</div>
<div className='mt-6 text-xs font-extralight'>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.date_factor')}
</div>
<div>
{moment(getInvoce.data?.data?.invoice?.createdAt).format('jYYYY-jMM-jDD')}
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.due_date')}
</div>
<div>
{moment(getInvoce.data?.data?.invoice?.dueDate).format('jYYYY-jMM-jDD')}
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.factor_number')}
</div>
<div>
{moment(getInvoce.data?.data?.invoice.createdAt).format('jYYYY') + getInvoce.data?.data?.invoice.numericId}
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.status_factor')}
</div>
<div>
{t(`receip.${getInvoce.data?.data?.invoice?.status}`)}
</div>
</div>
</div>
</div>
</div>
</div>
<div className='mt-5 flex justify-center gap-2'>
<div className='border size-[72px] flex-col text-[10px] justify-center items-center rounded-xl border-border flex'>
<img src={SamanImage} alt='saman' className='w-8 h-8' />
<div className='mt-2'>
بانک سامان
<div className='bg-white xl:w-sidebar text-sm h-fit rounded-3xl p-8'>
{
getInvoce.data?.data?.invoice?.status === 'PAID' &&
<div className='p-6 bg-green-50 rounded-2xl text-green-400 text-xs mb-6'>
<p>{t('receip.thank_pay_1')}</p>
<p className='mt-1'>{t('receip.thank_pay_2')}</p>
</div>
}
<div className='flex justify-between items-center'>
<div>
{t('receip.receip_information')}
</div>
<div className={clx(
'border border-orange-400 text-orange-400 rounded-lg text-[9px] h-6 flex items-center px-1',
getInvoce.data?.data?.invoice?.status === 'PAID' ? 'border-green-400 text-green-400' : getInvoce.data?.data?.invoice?.status !== 'PENDING' ? 'border-red-400 text-red-400' : 'border-orange-400 text-orange-400'
)}>
<div>{t(`receip.${getInvoce.data?.data?.invoice?.status}`)}</div>
</div>
</div>
<div className='border size-[72px] flex-col text-[10px] justify-center items-center rounded-xl border-border flex'>
<img src={SamanImage} alt='saman' className='w-8 h-8' />
<div className='mt-2'>
بانک سامان
<div className='mt-6 text-xs font-extralight'>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.date_factor')}
</div>
<div>
{moment(getInvoce.data?.data?.invoice?.createdAt).format('jYYYY-jMM-jDD')}
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.due_date')}
</div>
<div>
{moment(getInvoce.data?.data?.invoice?.dueDate).format('jYYYY-jMM-jDD')}
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.factor_number')}
</div>
<div>
{moment(getInvoce.data?.data?.invoice.createdAt).format('jYYYY') + getInvoce.data?.data?.invoice.numericId}
</div>
</div>
<div className='flex justify-between h-12 border-b items-center'>
<div className='text-description'>
{t('receip.status_factor')}
</div>
<div>
{t(`receip.${getInvoce.data?.data?.invoice?.status}`)}
</div>
</div>
{
getInvoce.data?.data?.invoice?.status === 'WAIT_PAYMENT' &&
<div className='flex gap-4 items-center mt-8'>
{/* <PDFGenerator
elementId="receipt-details"
fileName={`receipt-${getInvoce.data?.data?.invoice.numericId}`}
/> */}
<PayInvoice
id={id ? id : ''}
refetch={getInvoce.refetch}
gateWayId={gateWayId}
/>
</div>
}
{/* {
getInvoce.data?.data?.remainingToCharge > 0 &&
<div className='flex mt-6 justify-center gap-2'>
{
getWays.data?.data?.paymentGateways?.map((item: GatewayItemType) => {
return (
<div key={item.id} className={clx(
'size-[72px] rounded-xl border border-border flex flex-col justify-center items-center',
gateWayId === item.id && 'border-black'
)}>
<img src={item.logoUrl} className='w-8' />
<div className='mt-1 text-[10px]'>
{item.name}
</div>
</div>
)
})
}
</div>
} */}
</div>
</div>
</div>
</div>
}
</div>
)
+110 -92
View File
@@ -1,109 +1,127 @@
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Tabs from '../../components/Tabs'
import { Calendar, CloseCircle, Eye, FolderOpen, MoneyChange, WalletCheck, WalletRemove, WalletSearch } from 'iconsax-react'
import { Eye, FolderOpen, WalletCheck, WalletMinus, WalletRemove, WalletSearch } from 'iconsax-react'
import Td from '../../components/Td'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { useGetInvoices } from './hooks/useReceiptData'
import { NumberFormat } from '../../config/func'
import { ReceiptItemType } from './types/ReceiptTypes'
import moment from 'moment-jalaali'
import { InvoiceStatus } from './types/ReceiptTypes'
const ReceiptsList: FC = () => {
const { t } = useTranslation('global')
const [activeTab, setActiveTab] = useState<string>('unpaid')
const [activeTab, setActiveTab] = useState<InvoiceStatus>('PENDING')
const getInvoices = useGetInvoices(activeTab)
return (
<div className='flex justify-center'>
<div className='mt-4 w-full max-w-[1200px]'>
<div>
{t('receip.Language_learners_list')}
</div>
<div className='mt-4'>
<div>
{t('receip.receip_list')}
</div>
<div className='mt-14'>
<Tabs
active={activeTab}
items={[
{
icon: <WalletRemove color={activeTab === 'unpaid' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_pay'),
value: 'unpaid'
},
{
icon: <WalletSearch color={activeTab === 'not_confimed' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_confrim'),
value: 'not_confimed'
},
{
icon: <WalletCheck color={activeTab === 'paid' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.paid'),
value: 'paid'
},
{
icon: <CloseCircle color={activeTab === 'canceled' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.canceled'),
value: 'canceled'
},
{
icon: <MoneyChange color={activeTab === 'returned' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.returned'),
value: 'returned'
},
{
icon: <Calendar color={activeTab === 'time' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.its_time'),
value: 'time'
},
{
icon: <FolderOpen color={activeTab === 'archive' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.archive'),
value: 'archive'
},
]}
onChange={setActiveTab}
/>
</div>
<div className='mt-14'>
<Tabs
active={activeTab}
items={[
{
icon: <WalletSearch color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.pending'),
value: 'PENDING'
},
{
icon: <WalletRemove color={activeTab === 'WAIT_PAYMENT' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_pay'),
value: 'WAIT_PAYMENT'
},
{
icon: <WalletCheck color={activeTab === 'PAID' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.paid'),
value: 'PAID'
},
{
icon: <WalletMinus color={activeTab === 'OVERDUE' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.overdue'),
value: 'OVERDUE'
},
{
icon: <FolderOpen color={activeTab === 'ARCHIVED' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.archive'),
value: 'ARCHIVED'
},
]}
onChange={(value) => setActiveTab(value as InvoiceStatus)}
/>
</div>
<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('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''} />
</tr>
</thead>
<tbody>
<tr className='tr'>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''}>
<Link to={Pages.receipts.detail + '1'}>
<Eye size={20} color='black' />
</Link>
</Td>
</tr>
<tr className='tr'>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''} />
</tr>
</tbody>
</table>
</div>
<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('ticket.number')} />
{/* <Td text={t('receip.customer')} /> */}
<Td text={t('receip.date_receipt')} />
<Td text={t('receip.last_date_receipt')} />
<Td text={t('receip.total')} />
<Td text={t('receip.service')} />
<Td text={t('receip.late_fee')} />
<Td text={t('receip.status')} />
<Td text={t('receip.status_paid')} />
<Td text={''} />
</tr>
</thead>
<tbody>
{
getInvoices.data?.data?.invoices?.map((item: ReceiptItemType) => {
return (
<tr className='tr'>
<Td text={moment(item.createdAt).format('jYYYY') + item.numericId} />
{/* <Td text={item.user.firstName + ' ' + item.user.lastName} /> */}
<Td text={''}>
<div className='dltr text-right'>
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
</div>
</Td>
<Td text={''}>
<div className='dltr text-right'>
{moment(item.dueDate).format('jYYYY-jMM-jDD HH:mm')}
</div>
</Td>
<Td text={NumberFormat(item.totalPrice)} />
<Td text={''}>
<div className='flex flex-wrap gap-1.5'>
{
item.items.map((item => {
return (
<div className='bg-primary p-2 rounded-full text-white flex gap-1'><span>{item.name}</span> <span>({item.count})</span></div>
)
}))
}
</div>
</Td>
<Td text={NumberFormat(item.lateFee)} />
<Td text={t(`receip.${item.status}`)} />
<Td text={item.status === 'PAID' ? moment(item.paidAt).format('jYYYY-jMM-jDD') : t('receip.not_paid')} />
<Td text={''}>
<Link to={Pages.receipts.detail + item.id}>
<Eye size={20} color='black' />
</Link>
</Td>
</tr>
)
})
}
</tbody>
</table>
</div>
</div>
)
@@ -0,0 +1,165 @@
import { Fragment, FC, useState } from 'react'
import Button from '../../../components/Button'
import { TickCircle } from 'iconsax-react'
import { useTranslation } from 'react-i18next'
import { useApproveInvoice, useRequestApprove } from '../hooks/useReceiptData'
import { ErrorType } from '../../../helpers/types'
import { toast } from 'react-toastify'
import { clx } from '../../../helpers/utils'
import { useGetProfile } from '../../profile/hooks/useProfileData'
import { RequestApproveInvoiceType } from '../types/ReceiptTypes'
import DefaulModal from '../../../components/DefaulModal'
import OTPInput from 'react-otp-input'
import { useCountDown } from '../../../hooks/useCountDown'
type Props = {
id: string,
refetch: () => void,
status: string
}
const ApproveInvoice: FC<Props> = ({ id, refetch, status }) => {
const approveInvoce = useApproveInvoice()
const requestApprove = useRequestApprove()
const getProfile = useGetProfile()
const { value, reset } = useCountDown(2, true)
const [showModal, setShowModal] = useState<boolean>(false)
const { t } = useTranslation('global')
const [code, setCode] = useState<string>('')
const handleConfrim = () => {
approveInvoce.mutate({ code: code, id: id, phone: getProfile.data?.data?.user?.phone }, {
onSuccess: () => {
toast.success(t('success'))
refetch()
setShowModal(false)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
const handleShowModal = () => {
const params: RequestApproveInvoiceType = {
id: id,
phone: getProfile.data?.data?.user?.phone
}
requestApprove.mutate(params, {
onSuccess: () => {
setShowModal(true)
reset()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
}
if (status === 'PENDING' || status === 'WAIT_PAYMENT' && getProfile.isSuccess)
return (
<Fragment>
<Button
disabled={approveInvoce.isPending || requestApprove.isPending}
onClick={() => status === 'PENDING' ? handleShowModal() : null}
className={clx(
'px-9 w-fit bg-white bg-opacity-70 text-description',
status === 'WAIT_PAYMENT' && 'bg-green-300 text-[#00BA4B] bg-opacity-10'
)}
>
<div className='flex gap-2.5'>
<TickCircle
size={20}
color={status === 'PENDING' ? '#888888' : '#00BA4B'}
/>
<div className='whitespace-nowrap'>
{
status === 'WAIT_PAYMENT' ?
t('receip.accepted')
:
t('receip.confrim_factor')
}
</div>
</div>
</Button>
<DefaulModal
open={showModal}
close={() => setShowModal(false)}
isHeader
title_header={t('profile.confrim')}
>
<div className='mt-7'>
<div className='text-xs text-center'>
کد تایید به شماره {getProfile.data?.data?.user?.phone} ارسال شده است.برای تایید کد مربوطه را وارد کنید.
</div>
<div className='mt-10'>کد تایید</div>
<div className='mt-2 w-full flex justify-center dltr otp'>
<OTPInput
value={code}
onChange={setCode}
shouldAutoFocus
numInputs={5}
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='flex mt-7 justify-center'>
{
value !== '00:00' ?
<div className='flex gap-1 text-description text-xs'>
<div>{value}</div>
<div>{t('auth.counter_otp')}</div>
</div>
:
<div onClick={handleShowModal} className='flex cursor-pointer gap-1 pl-2 text-black text-sm'>
<div className=''>
{t('auth.try_again')}
</div>
</div>
}
</div>
<div className='mt-10 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'
onClick={handleConfrim}
disabled={code.length !== 5}
isLoading={approveInvoce.isPending || requestApprove.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle
size={20}
color='white'
/>
<div>{t('save')}</div>
</div>
</Button>
</div>
</div>
</div>
</DefaulModal>
</Fragment>
)
else return null
}
export default ApproveInvoice
+118
View File
@@ -0,0 +1,118 @@
import { FC, Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import { useGetLegalInfo } from '../../financial/hooks/useFinancialData'
import { useGetProfile } from '../../profile/hooks/useProfileData'
const LegalInfo: FC = () => {
const { t } = useTranslation('global')
const { data: d, isSuccess } = useGetLegalInfo()
const getProfile = useGetProfile()
const data = d?.data?.legalUser
if (isSuccess)
return (
<Fragment>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.type_person')}
</div>
<div>
{
t('receip.company')
}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.representativeـname')}
</div>
<div>
{getProfile.data?.data?.user?.firstName + ' ' + getProfile.data?.data?.user?.lastName}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.company_name')}
</div>
<div>
{data.registrationName}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.registration_number')}
</div>
<div>
{data.registrationCode}
</div>
</div>
</div>
<div className='mt-8 border-t pt-7 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.tel_company')}
</div>
<div>
{data.phone}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.national_code')}
</div>
<div>
{data?.nationalIdentity}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.postal_code')}
</div>
<div>
{data?.address?.postalCode}
</div>
</div>
</div>
<div className='mt-6 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.state')}
</div>
<div>
{data?.address?.city?.province?.name}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.city')}
</div>
<div>
{data?.address?.city?.name}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.address')}
</div>
<div>
{data?.address?.fullAddress}
</div>
</div>
</div>
</Fragment>
)
}
export default LegalInfo
@@ -0,0 +1,68 @@
import { FC, Fragment, useState } from 'react'
import Button from '../../../components/Button'
import { useTranslation } from 'react-i18next'
import { useGetInvoiceDetail, usePayInvoice } from '../hooks/useReceiptData'
import { ErrorType } from '../../../helpers/types'
import ModalConfrim from '../../../components/ModalConfrim'
// import { usePayment } from '../../wallet/hooks/useWalletData'
import { toast } from '../../../components/Toast'
type Props = {
id: string,
refetch: () => void,
gateWayId: string,
}
const PayInvoice: FC<Props> = ({ id, refetch, gateWayId }) => {
const { t } = useTranslation('global')
const [showModal, setShowModal] = useState<boolean>(false)
const getInvoce = useGetInvoiceDetail(id)
const payInvoice = usePayInvoice()
// const payment = usePayment()
const handleConfrim = () => {
payInvoice.mutate(id, {
onSuccess: () => {
toast(t('success'), 'success')
refetch()
setShowModal(false)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
}
})
}
const handleChartWallet = () => {
// payment.mutate({ amount: getInvoce.data?.data?.remainingToCharge, gatewayId: gateWayId, invoiceId: id }, {
// onSuccess: (data) => {
// window.location.href = data?.data?.redirectUrl
// toast(t('wallet.transfer_to_getway'), 'success')
// },
// onError: (error: ErrorType) => {
// toast(error.response?.data?.error.message[0], 'error')
// }
// })
}
return (
<Fragment>
<Button
label={getInvoce.data?.data?.remainingToCharge > 0 ? t('receip.charge_wallet') : t('receip.pay')}
onClick={() => setShowModal(true)}
/>
<ModalConfrim
isOpen={showModal}
close={() => setShowModal(false)}
onConfrim={getInvoce.data?.data?.remainingToCharge > 0 ? handleChartWallet : handleConfrim}
label={getInvoce.data?.data?.remainingToCharge > 0 ? t('receip.sure_charge_wallet') : t('receip.sure_confrim_pay')}
isLoading={payInvoice.isPending}
/>
</Fragment>
)
}
export default PayInvoice
+107
View File
@@ -0,0 +1,107 @@
import { FC, Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import { useGetRealInfo } from '../../financial/hooks/useFinancialData'
const RealInfo: FC = () => {
const { t } = useTranslation('global')
const { data: d } = useGetRealInfo()
const data = d?.data?.realUser
return (
<Fragment>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.type_person')}
</div>
<div>
{t('receip.real_person')}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.fullName')}
</div>
<div>
{data?.firstName + ' ' + data?.lastName}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('financial.father_name')}
</div>
<div>
{data?.fatherName}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('financial.sex')}
</div>
<div>
{data?.gender === 'MALE' ? 'مرد' : 'زن'}
</div>
</div>
</div>
<div className='mt-8 border-t pt-7 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('financial.phone')}
</div>
<div>
{data?.phone}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.postal_code')}
</div>
<div>
{data?.address?.postalCode}
</div>
</div>
</div>
<div className='mt-6 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.state')}
</div>
<div>
{data?.address?.city?.province?.name}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.city')}
</div>
<div>
{data?.address?.city?.name}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.address')}
</div>
<div>
{data?.address?.fullAddress}
</div>
</div>
</div>
</Fragment>
)
}
export default RealInfo
@@ -0,0 +1,70 @@
import { FC, Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import { FinancialDataType } from '../../financial/types/FinancialTypes'
import { InfoCircle } from 'iconsax-react'
import Button from '../../../components/Button'
import { Link } from 'react-router-dom'
import { Pages } from '../../../config/Pages'
type Props = {
data: FinancialDataType
}
const RegisterInfo: FC<Props> = ({ data }) => {
const { t } = useTranslation('global')
return (
<Fragment>
<div className='mt-8 xl:flex-row flex-col xl:h-10 py-4 xl:py-0 text-xs bg-[#FFF2F2] text-[#DB0105] rounded-xl px-4 flex gap-2 items-center'>
<InfoCircle
size={18}
color='#DB0105'
className='min-w-[18px]'
/>
<div className='text-center xl:text-right'>{t('receip.compelete_financal_info_error')}</div>
<div className='flex-1 flex xl:justify-end'>
<Link to={Pages.financial}>
<Button
label={t('receip.compelete_financal_info')}
className='w-fit whitespace-nowrap px-4 h-7 text-[10px]'
/>
</Link>
</div>
</div>
<div className='mt-8 flex xl:flex-row flex-col xl:gap-16 gap-4 xl:items-center text-xs'>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.type_person')}
</div>
<div>
{t('receip.real_person')}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('receip.fullName')}
</div>
<div>
{data.user?.firstName + ' ' + data.user?.lastName}
</div>
</div>
<div className='flex gap-2'>
<div className='text-description'>
{t('financial.phone')}
</div>
<div>
{data.user?.phone}
</div>
</div>
</div>
</Fragment>
)
}
export default RegisterInfo
@@ -0,0 +1,62 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/ReceiptService";
import {
ApplyDiscountType,
ApproveInvoiceType,
RemoveDiscountType,
RequestApproveInvoiceType,
} from "../types/ReceiptTypes";
export const useGetInvoices = (status: string) => {
return useQuery({
queryKey: ["invoices", status],
queryFn: () => api.getInvoice(status),
});
};
export const useGetInvoiceDetail = (id: string) => {
return useQuery({
queryKey: ["invoice", id],
queryFn: () => api.getInvoiceDetail(id),
enabled: !!id,
});
};
export const useApproveInvoice = () => {
return useMutation({
mutationFn: (variables: ApproveInvoiceType) =>
api.approveInvoice(variables),
});
};
export const usePayInvoice = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: string) => api.payInvoice(variables),
onSuccess: () => {
queryClient.refetchQueries({
queryKey: ["wallet-balance"],
});
},
});
};
export const useRequestApprove = () => {
return useMutation({
mutationFn: (variables: RequestApproveInvoiceType) =>
api.requestApprove(variables),
});
};
export const useApplyDiscount = () => {
return useMutation({
mutationFn: (variables: ApplyDiscountType) => api.applyDiscount(variables),
});
};
export const useRemoveDiscount = () => {
return useMutation({
mutationFn: (variables: RemoveDiscountType) =>
api.removeDiscount(variables.id),
});
};
@@ -0,0 +1,47 @@
import axios from "../../../config/axios";
import {
ApplyDiscountType,
ApproveInvoiceType,
RequestApproveInvoiceType,
} from "../types/ReceiptTypes";
export const getInvoice = async (status: string) => {
const { data } = await axios.get(`/invoices/user?status=${status}`);
return data;
};
export const getInvoiceDetail = async (id: string) => {
const { data } = await axios.get(`/invoices/${id}`);
return data;
};
export const requestApprove = async (params: RequestApproveInvoiceType) => {
const { data } = await axios.post(
`/invoices/${params.id}/approve/request`,
params
);
return data;
};
export const approveInvoice = async (params: ApproveInvoiceType) => {
const { data } = await axios.patch(`/invoices/${params.id}/approve`, params);
return data;
};
export const payInvoice = async (id: string) => {
const { data } = await axios.post(`/invoices/${id}/pay`);
return data;
};
export const applyDiscount = async (params: ApplyDiscountType) => {
const { data } = await axios.post(
`/invoices/${params.id}/apply-discount`,
params
);
return data;
};
export const removeDiscount = async (id: string) => {
const { data } = await axios.post(`/invoices/${id}/cancel-discount`);
return data;
};
+60
View File
@@ -0,0 +1,60 @@
export type InvoiceStatus =
| "PENDING"
| "WAIT_PAYMENT"
| "PAID"
| "ARCHIVED"
| "OVERDUE";
export type ReceiptItemType = {
id: string;
createdAt: string;
updatedAt: string;
name: string;
count: string | number;
unitPrice: string | number;
discount: string | number;
totalPrice: number;
subscriptionPlan?: string | null;
items: ReceiptItemType[];
user: {
firstName: string;
lastName: string;
};
dueDate: string;
status: InvoiceStatus;
paidAt: string;
numericId: string;
lateFee: number;
};
export type ReceiptDetailItemType = {
id: string;
createdAt: string;
updatedAt: string;
name: string;
count: string | number;
unitPrice: string | number;
discount: string | number;
totalPrice: number;
subscriptionPlan?: string | null;
};
export type RequestApproveInvoiceType = {
phone: string;
id: string;
};
export type ApproveInvoiceType = {
phone: string;
code: string;
id: string;
};
export type ApplyDiscountType = {
id: string;
code: string;
};
export type RemoveDiscountType = {
id: string;
};
+26 -28
View File
@@ -5,27 +5,33 @@ import Select from '../../components/Select'
import Textarea from '../../components/Textarea'
import UploadBox from '../../components/UploadBox'
import Button from '../../components/Button'
import { TickSquare } from 'iconsax-react'
import { TickCircle, TickSquare } from 'iconsax-react'
import { useCreateTicket, useGetCategoriesTicket, useMultiUpload } from './hooks/useTicketData'
import { useGetProfile } from '../profile/hooks/useProfileData'
import PageLoading from '../../components/PageLoading'
import { useFormik } from 'formik'
import { CreateTicketType } from './types/TicketTypes'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { toast } from '../../components/Toast'
import { ErrorType } from '../../helpers/types'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useParams } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import withAuth from '../../hoc/withAuth'
import { buildPath } from '../../helpers/utils'
// import { useGetMyServices } from '../service/hooks/useServiceData'
// import { MyServicesItem } from '../service/types/ServiecTypes'
const CreateTicket: FC = () => {
const { t } = useTranslation('global')
const { slug } = useParams()
const navigate = useNavigate()
const [files, setFiles] = useState<File[]>([])
const getProfile = useGetProfile()
const getCategories = useGetCategoriesTicket()
const multiUpload = useMultiUpload()
const createTicket = useCreateTicket()
// const getMyservice = useGetMyServices('')
const formik = useFormik<CreateTicketType>({
initialValues: {
@@ -33,7 +39,6 @@ const CreateTicket: FC = () => {
message: '',
categoryId: '',
priority: 'LOW',
danakServiceId: '',
title: '',
attachmentUrls: []
},
@@ -54,7 +59,7 @@ const CreateTicket: FC = () => {
values.attachmentUrls = 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 {
@@ -62,24 +67,23 @@ const CreateTicket: FC = () => {
}
createTicket.mutate(values, {
onSuccess: (data) => {
toast.success(t('success'))
toast(t('success'), 'success')
formik.resetForm()
navigate(Pages.ticket.detail + data.data?.ticketId)
navigate(buildPath(Pages.ticket.detail + data.data?.ticketId, slug))
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
})
console.log(formik.errors);
return (
<div className='mt-4'>
<div className='mt-4 pb-12'>
<div>
{t('ticket.new_ticket')}
</div>
@@ -143,18 +147,6 @@ const CreateTicket: FC = () => {
</div>
<div className='mt-6 rowTwoInput'>
<Select
label={t('ticket.select_your_service')}
items={[
{
label: '',
value: ''
}
]}
placeholder={t('ticket.not_once')}
className='border'
name='danakServiceId'
/>
<Select
label={t('ticket.category')}
items={getCategories.data?.data?.ticketCategories?.map((item: { id: string, title: string }) => ({
@@ -191,11 +183,17 @@ const CreateTicket: FC = () => {
<div className='mt-8 flex justify-end'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
className='xl:max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={createTicket.isPending || multiUpload.isPending}
/>
>
<div className='flex gap-2 items-center'>
<TickCircle size={20} color='white' />
<div>
{t('ticket.send')}
</div>
</div>
</Button>
</div>
</div>
@@ -226,7 +224,7 @@ const CreateTicket: FC = () => {
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
پاسخگویی 24 ساعته تلفنی را تنها از داناک می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
</div>
@@ -240,4 +238,4 @@ const CreateTicket: FC = () => {
)
}
export default CreateTicket
export default withAuth(CreateTicket)
+36 -25
View File
@@ -4,7 +4,7 @@ import Input from '../../components/Input'
import Textarea from '../../components/Textarea'
import UploadBox from '../../components/UploadBox'
import Button from '../../components/Button'
import { CloseCircle, InfoCircle, Paperclip2 } from 'iconsax-react'
import { CloseCircle, InfoCircle, Paperclip2, Send2 } from 'iconsax-react'
import { useNavigate, useParams } from 'react-router-dom'
import { useAddMessageTicket, useCloseTicket, useGetMessages, useMultiUpload } from './hooks/useTicketData'
import PageLoading from '../../components/PageLoading'
@@ -12,7 +12,7 @@ import moment from 'moment-jalaali'
import { useFormik } from 'formik'
import { AddMessageTicketType } from './types/TicketTypes'
import * as Yup from 'yup'
import { toast } from 'react-toastify'
import { toast } from '../../components/Toast'
import { ErrorType } from '../../helpers/types'
import { clx } from '../../helpers/utils'
import { Pages } from '../../config/Pages'
@@ -22,8 +22,8 @@ const TicketDetail: FC = () => {
const { t } = useTranslation('global')
const navigate = useNavigate()
const { id } = useParams()
const getMessages = useGetMessages(id ? id : '')
const [files, setFiles] = useState<File[]>([])
const getMessages = useGetMessages(id ? id : '')
const addMessage = useAddMessageTicket(id ? id : '')
const multiUpload = useMultiUpload()
const [isResetFiles, setIsResetFiles] = useState<boolean>(false)
@@ -48,7 +48,7 @@ const TicketDetail: FC = () => {
values.attachmentUrls = 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 {
@@ -57,7 +57,7 @@ const TicketDetail: FC = () => {
addMessage.mutate(values, {
onSuccess: () => {
toast.success(t('success'))
toast(t('success'), 'success')
formik.resetForm()
setFiles([])
setIsResetFiles(true)
@@ -66,7 +66,7 @@ const TicketDetail: FC = () => {
}, 1000);
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
},
@@ -75,20 +75,27 @@ const TicketDetail: FC = () => {
const hadleCloseTicket = () => {
closeTicket.mutate(id ? id : '', {
onSuccess: () => {
toast.success(t('success'))
toast(t('success'), 'success')
navigate(Pages.ticket.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message[0])
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
const handleSendAnswer = () => {
const messageEndRef = document.getElementById('content');
if (messageEndRef) {
messageEndRef.scrollIntoView({ behavior: 'smooth' });
messageEndRef.focus();
}
}
return (
<div className='mt-4'>
<div className='flex gap-1'>
<div>{t('ticket.ticket_number')}</div>
<div>12312</div>
</div>
{
getMessages.isPending ?
@@ -96,12 +103,12 @@ const TicketDetail: FC = () => {
:
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div className='gap-6 rowTwoInput'>
<Input
label={t('ticket.select_your_service')}
value={'دی منو'}
readOnly
/>
<Input
label={t('ticket.subject')}
value={getMessages.data?.data?.ticket?.subject}
readOnly
/>
<div className='gap-6 rowTwoInput mt-5'>
<Input
label={t('ticket.user_name')}
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
@@ -109,13 +116,9 @@ const TicketDetail: FC = () => {
/>
</div>
<div className='mt-8 xl:text-sm text-xs'>
{getMessages.data?.data?.ticket?.subject}
</div>
{
getMessages.data?.data?.messages.map((item: any) => {
if (item?.author?.role?.name === 'user') {
if (item?.author?.role?.name === 'USER') {
return (
<div key={item.id} className='mt-6 xl:text-sm text-xs bg-[#F6F7FA] p-6 rounded-3xl rounded-tr-none xl:max-w-[70%] max-w-[90%]'>
<div className='leading-7'>
@@ -154,8 +157,8 @@ const TicketDetail: FC = () => {
<div className='leading-7 mt-4'>
{item.content}
</div>
<div className='flex justify-end mt-6 text-xs text-description'>
10:07 | 1403/09/30
<div className='flex dltr end mt-6 text-xs text-description'>
{moment(item.createdAt).format('jYYYY-jMM-jDD HH:mm')}
</div>
</div>
@@ -186,6 +189,7 @@ const TicketDetail: FC = () => {
<Fragment>
<div className='mt-9'>
<Textarea
id='content'
label={t('ticket.answer')}
placeholder={t('ticket.your_description')}
value={formik.values.content}
@@ -206,11 +210,17 @@ const TicketDetail: FC = () => {
<div className='flex justify-end mt-6'>
<Button
label={t('ticket.send')}
className='max-w-[100px]'
className='xl:max-w-[100px]'
onClick={() => formik.handleSubmit()}
isLoading={addMessage.isPending || multiUpload.isPending}
/>
>
<div className='flex gap-2 items-center'>
<Send2 size={20} color='white' />
<div>
{t('ticket.send')}
</div>
</div>
</Button>
</div>
</Fragment>
}
@@ -223,6 +233,7 @@ const TicketDetail: FC = () => {
<Button
label={t('ticket.send_answer')}
className='text-xs xl:h-10 h-8'
onClick={handleSendAnswer}
/>
<Button
className='bg-[#D52903] xl:h-10 h-8'
+15 -12
View File
@@ -1,18 +1,19 @@
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '../../components/Button'
import { Add, Eye, MessageRemove, MessageTick, MessageTime } from 'iconsax-react'
import { Add, Eye, MessageRemove, Messages1, MessageTick, MessageTime } from 'iconsax-react'
import Tabs from '../../components/Tabs'
import Td from '../../components/Td'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { useGetTickets } from './hooks/useTicketData'
import moment from 'moment-jalaali'
import { buildPath } from '../../helpers/utils'
const TicketList: FC = () => {
const { t } = useTranslation('global')
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED'>('ANSWERED')
const [activeTab, setActiveTab] = useState<'ANSWERED' | 'PENDING' | 'CLOSED' | ''>('')
const getTickets = useGetTickets(activeTab)
return (
@@ -22,7 +23,7 @@ const TicketList: FC = () => {
{t('ticket.tickets')}
</div>
<Link to={Pages.ticket.create}>
<Link to={buildPath(Pages.ticket.create)}>
<Button
className='w-[172px]'
>
@@ -41,20 +42,20 @@ const TicketList: FC = () => {
active={activeTab}
items={[
{
icon: <MessageTick color={activeTab === 'ANSWERED' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.answered'),
value: 'ANSWERED'
icon: <Messages1 color={activeTab === '' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.all'),
value: ''
},
{
icon: <MessageTime color={activeTab === 'PENDING' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.checking'),
value: 'PENDING'
},
// {
// icon: <Message2 color={activeTab === 'wating' ? 'black' : '#8C90A3'} size={22} />,
// label: t('ticket.wait_answered'),
// value: 'wating'
// },
{
icon: <MessageTick color={activeTab === 'ANSWERED' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.answered'),
value: 'ANSWERED'
},
{
icon: <MessageRemove color={activeTab === 'CLOSED' ? 'black' : '#8C90A3'} size={22} />,
label: t('ticket.closed'),
@@ -71,7 +72,8 @@ const TicketList: FC = () => {
<tr>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.category')} />
<Td text={t('ticket.service')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
@@ -86,6 +88,7 @@ const TicketList: FC = () => {
<Td text={item.numericId} />
<Td text={item.subject} />
<Td text={item?.category?.title} />
<Td text={item?.danakService?.name} />
<Td text={''}>
<div className='dltr text-right'>
{moment(item.updatedAt).format('jYYYY-jMM-jDD HH:mm')}
+14 -4
View File
@@ -22,14 +22,15 @@ export const useGetCategoriesTicket = () => {
});
};
export const useSingleUpload = () => {
export const useSingleUpload = (callback?: (value: number) => void) => {
return useMutation({
mutationFn: (variables: FormData) => api.uploadSingle(variables),
mutationFn: (variables: FormData) => api.uploadSingle(variables, callback),
});
};
export const useMultiUpload = () => {
export const useMultiUpload = (callback?: (value: number) => void) => {
return useMutation({
mutationFn: (variables: FormData) => api.uploadMultiple(variables),
mutationFn: (variables: FormData) =>
api.uploadMultiple(variables, callback),
});
};
@@ -56,7 +57,16 @@ export const useAddMessageTicket = (id: string) => {
};
export const useCloseTicket = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.closeTicket(id),
onSuccess: () => {
queryClient.refetchQueries({
queryKey: ["tickets"],
});
queryClient.refetchQueries({
queryKey: ["ticket-messages"],
});
},
});
};
+41 -5
View File
@@ -2,7 +2,11 @@ import axios from "../../../config/axios";
import { AddMessageTicketType, CreateTicketType } from "../types/TicketTypes";
export const getTickets = async (status: string) => {
const { data } = await axios.get(`/tickets?status=${status}`);
let query = ``;
if (status) {
query = `?status=${status}`;
}
const { data } = await axios.get(`/tickets${query}`);
return data;
};
@@ -16,13 +20,45 @@ export const getCategoriesTicket = async () => {
return data;
};
export const uploadSingle = async (params: FormData) => {
const { data } = await axios.post(`/uploader/single-file`, params);
export const uploadSingle = async (
params: FormData,
callback?: (value: number) => void
) => {
const { data } = await axios.post(`/uploader/single-file`, params, {
onUploadProgress: (progressEvent: any) => {
if (progressEvent.bytes) {
if (callback) {
callback(
Math.round((progressEvent.loaded / progressEvent.total) * 100)
);
}
}
},
});
if (callback) {
callback(0);
}
return data;
};
export const uploadMultiple = async (params: FormData) => {
const { data } = await axios.post(`/uploader/multi-file`, params);
export const uploadMultiple = async (
params: FormData,
callback?: (value: number) => void
) => {
const { data } = await axios.post(`/uploader/multi-file`, params, {
onUploadProgress: (progressEvent: any) => {
if (progressEvent.bytes) {
if (callback) {
callback(
Math.round((progressEvent.loaded / progressEvent.total) * 100)
);
}
}
},
});
if (callback) {
callback(0);
}
return data;
};
+6
View File
@@ -22,6 +22,9 @@ import { useGetSlugId } from "../pages/home/hooks/useHomeData"
import { ErrorType } from "../helpers/types"
import { toast } from "react-toastify"
import Request from "../pages/request/Request"
import TicketList from "../pages/ticket/TicketList"
import TicketCreate from "../pages/ticket/CreateTicket"
import TicketDetail from "../pages/ticket/Detail"
const MainRouter = () => {
const { slug } = useParams()
const getSlugId = useGetSlugId()
@@ -71,6 +74,9 @@ const MainRouter = () => {
<Route path="reportCard/list" element={<ReportCardList />} />
<Route path="profile" element={<Profile />} />
<Route path="request" element={<Request />} />
<Route path="tickets" element={<TicketList />} />
<Route path="tickets/create" element={<TicketCreate />} />
<Route path="tickets/messages/:id" element={<TicketDetail />} />
</Routes>
</div>
</div>