request + detail + change status request
This commit is contained in:
@@ -3,6 +3,7 @@ VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token'
|
|||||||
# VITE_BASE_URL = 'http://192.168.1.113:4001'
|
# VITE_BASE_URL = 'http://192.168.1.113:4001'
|
||||||
VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
||||||
# VITE_DANAK_BASE_URL ='http://192.168.1.113:4000'
|
# VITE_DANAK_BASE_URL ='http://192.168.1.113:4000'
|
||||||
|
VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
|
||||||
VITE_SERVICE_ID = '7e3c2f08-b7e7-4402-ae5f-fea99cff56bd'
|
VITE_SERVICE_ID = '7e3c2f08-b7e7-4402-ae5f-fea99cff56bd'
|
||||||
VITE_WORKSPACE_ID = 'workspace_id'
|
VITE_WORKSPACE_ID = 'workspace_id'
|
||||||
VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
|
VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const Pages = {
|
|||||||
},
|
},
|
||||||
requests: {
|
requests: {
|
||||||
list: "/requests/list",
|
list: "/requests/list",
|
||||||
|
detail: "/requests/detail/",
|
||||||
},
|
},
|
||||||
dashboard: "/dashboard",
|
dashboard: "/dashboard",
|
||||||
services: {
|
services: {
|
||||||
|
|||||||
@@ -843,5 +843,66 @@
|
|||||||
"select_company": "انتخاب شرکت",
|
"select_company": "انتخاب شرکت",
|
||||||
"register_date": "تاریخ ثبت نام",
|
"register_date": "تاریخ ثبت نام",
|
||||||
"company": "شرکت"
|
"company": "شرکت"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"request_list": "لیست درخواست ها",
|
||||||
|
"details": "جزئیات درخواست",
|
||||||
|
"basic_info": "اطلاعات پایه",
|
||||||
|
"company_info": "اطلاعات شرکت",
|
||||||
|
"user_info": "اطلاعات کاربر",
|
||||||
|
"additional_info": "اطلاعات تکمیلی",
|
||||||
|
"id": "شناسه",
|
||||||
|
"created_at": "تاریخ ایجاد",
|
||||||
|
"updated_at": "تاریخ بروزرسانی",
|
||||||
|
"status": "وضعیت",
|
||||||
|
"reject_reason": "دلیل رد",
|
||||||
|
"company_name": "نام شرکت",
|
||||||
|
"ceo": "مدیرعامل",
|
||||||
|
"email": "ایمیل",
|
||||||
|
"phone": "شماره تماس",
|
||||||
|
"address": "آدرس",
|
||||||
|
"identification_number": "شماره شناسایی",
|
||||||
|
"date_of_establishment": "تاریخ تاسیس",
|
||||||
|
"description": "توضیحات",
|
||||||
|
"company_status": "وضعیت شرکت",
|
||||||
|
"business": "کسب و کار",
|
||||||
|
"invoice_count": "تعداد فاکتور",
|
||||||
|
"website": "وبسایت",
|
||||||
|
"map_location": "موقعیت مکانی",
|
||||||
|
"view_on_map": "مشاهده روی نقشه",
|
||||||
|
"profile_image": "تصویر پروفایل",
|
||||||
|
"cover_image": "تصویر کاور",
|
||||||
|
"view_image": "مشاهده تصویر",
|
||||||
|
"products": "محصولات",
|
||||||
|
"services": "خدمات",
|
||||||
|
"actions": "عملیات",
|
||||||
|
"approve": "تایید",
|
||||||
|
"reject": "رد",
|
||||||
|
"approving": "در حال تایید",
|
||||||
|
"rejecting": "در حال رد",
|
||||||
|
"enter_reject_reason": "دلیل رد درخواست را وارد کنید",
|
||||||
|
"approve_success": "درخواست با موفقیت تایید شد",
|
||||||
|
"reject_success": "درخواست با موفقیت رد شد",
|
||||||
|
"approve_error": "خطا در تایید درخواست",
|
||||||
|
"reject_error": "خطا در رد درخواست",
|
||||||
|
"pending": "در انتظار بررسی",
|
||||||
|
"approved": "تایید شده",
|
||||||
|
"rejected": "رد شده",
|
||||||
|
"industry": "صنعت",
|
||||||
|
"full_name": "نام و نام خانوادگی",
|
||||||
|
"national_code": "کد ملی",
|
||||||
|
"role": "نقش",
|
||||||
|
"customer": "مشتری",
|
||||||
|
"date_receipt": "تاریخ درخواست",
|
||||||
|
"last_date_receipt": "آخرین تاریخ درخواست",
|
||||||
|
"total": "مبلغ کل",
|
||||||
|
"lateFee": "جریمه دیرکرد",
|
||||||
|
"service": "سرویس",
|
||||||
|
"status_paid": "وضعیت پرداخت",
|
||||||
|
"recurring": "تکرار شونده",
|
||||||
|
"maxRecurringCycles": "حداکثر تعداد تکرار",
|
||||||
|
"paid": "پرداخت شده",
|
||||||
|
"recurringPeriod": "دوره تکرار",
|
||||||
|
"user": "کاربر"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import { useChangeRequestStatus, useGetRequestDetail } from './hooks/useRequestData'
|
||||||
|
import ModalConfrim from '../../components/ModalConfrim'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../helpers/types'
|
||||||
|
interface Product {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
imageUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Service {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
imageUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Business {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Company {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
chiefExecutiveOfficer: string
|
||||||
|
email: string
|
||||||
|
phone: string
|
||||||
|
identificationNumber: string
|
||||||
|
dateOfEstablishment: string
|
||||||
|
address: string
|
||||||
|
mapAddressLink: string
|
||||||
|
description: string
|
||||||
|
websiteUrl: string
|
||||||
|
profileImageUrl: string
|
||||||
|
coverImageUrl: string
|
||||||
|
status: string
|
||||||
|
industry: string
|
||||||
|
business: string
|
||||||
|
products: Product[]
|
||||||
|
services: Service[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
id: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
rejectReason: string | null
|
||||||
|
requestStatus: string
|
||||||
|
company: Company
|
||||||
|
business: Business
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequestDetail: FC = () => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { id } = useParams()
|
||||||
|
const { data: response, isLoading } = useGetRequestDetail(id || '')
|
||||||
|
const [rejectReason, setRejectReason] = useState('')
|
||||||
|
const [showRejectModal, setShowRejectModal] = useState(false)
|
||||||
|
const { mutate: changeRequestStatus, isPending } = useChangeRequestStatus()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className='mt-5'>
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response?.data?.request) {
|
||||||
|
return <div>Request not found</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const request: Request = response.data.request
|
||||||
|
|
||||||
|
const handleApprove = () => {
|
||||||
|
if (id) {
|
||||||
|
changeRequestStatus({
|
||||||
|
id,
|
||||||
|
status: 'APPROVED',
|
||||||
|
rejectReason: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReject = (rejectReason: string) => {
|
||||||
|
if (id) {
|
||||||
|
changeRequestStatus({
|
||||||
|
id,
|
||||||
|
status: 'REJECTED',
|
||||||
|
rejectReason: rejectReason
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t('success'))
|
||||||
|
window.location.reload()
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div className='flex items-center gap-4 mb-6'>
|
||||||
|
<h1 className='text-lg font-bold'>{t('request.details')}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='bg-white rounded-lg shadow p-6'>
|
||||||
|
<div className='grid grid-cols-2 gap-6'>
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<div className='bg-gray-50 p-4 rounded-lg'>
|
||||||
|
<h2 className='text-base font-semibold mb-4'>{t('request.basic_info')}</h2>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.id')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.id}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.created_at')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{moment(request.createdAt).format('jYYYY/jMM/jDD HH:mm')}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.updated_at')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{moment(request.updatedAt).format('jYYYY/jMM/jDD HH:mm')}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.status')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.requestStatus ? t(`request.${request.requestStatus.toLowerCase()}`) : '-'}</span>
|
||||||
|
</div>
|
||||||
|
{request.rejectReason && (
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.reject_reason')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.rejectReason}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='bg-gray-50 p-4 rounded-lg'>
|
||||||
|
<h2 className='text-base font-semibold mb-4'>{t('request.company_info')}</h2>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.company_name')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.name || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.ceo')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.chiefExecutiveOfficer || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.email')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.email || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.phone')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.phone || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.address')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.address || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.identification_number')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.identificationNumber || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.date_of_establishment')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.dateOfEstablishment ? moment(request.company.dateOfEstablishment).format('jYYYY/jMM/jDD') : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.description')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.description || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.company_status')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.company.status ? t(`request.${request.company.status.toLowerCase()}`) : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.business')}:</span>
|
||||||
|
<span className='text-sm font-medium'>{request.business.name || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.invoice_count')}:</span>
|
||||||
|
<span className='text-sm font-medium'>0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-6'>
|
||||||
|
<div className='bg-gray-50 p-4 rounded-lg'>
|
||||||
|
<h2 className='text-base font-semibold mb-4'>{t('request.additional_info')}</h2>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
{request.company.websiteUrl && (
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.website')}:</span>
|
||||||
|
<a
|
||||||
|
href={request.company.websiteUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className='text-sm text-blue-600 hover:underline'
|
||||||
|
>
|
||||||
|
{request.company.websiteUrl}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{request.company.mapAddressLink && (
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.map_location')}:</span>
|
||||||
|
<a
|
||||||
|
href={request.company.mapAddressLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className='text-sm text-blue-600 hover:underline'
|
||||||
|
>
|
||||||
|
{t('request.view_on_map')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{request.company.profileImageUrl && (
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.profile_image')}:</span>
|
||||||
|
<a
|
||||||
|
href={request.company.profileImageUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className='text-sm text-blue-600 hover:underline'
|
||||||
|
>
|
||||||
|
{t('request.view_image')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{request.company.coverImageUrl && (
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<span className='text-sm text-gray-600'>{t('request.cover_image')}:</span>
|
||||||
|
<a
|
||||||
|
href={request.company.coverImageUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className='text-sm text-blue-600 hover:underline'
|
||||||
|
>
|
||||||
|
{t('request.view_image')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{request.company.products && request.company.products.length > 0 && (
|
||||||
|
<div className='bg-gray-50 p-4 rounded-lg'>
|
||||||
|
<h2 className='text-base font-semibold mb-4'>{t('request.products')}</h2>
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
{request.company.products.map((product: Product) => (
|
||||||
|
<div key={product.id} className='bg-white p-3 rounded-lg shadow-sm'>
|
||||||
|
<div className='aspect-video mb-2 overflow-hidden rounded-lg'>
|
||||||
|
<img
|
||||||
|
src={product.imageUrl}
|
||||||
|
alt={product.title}
|
||||||
|
className='w-full h-full object-cover'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className='text-sm font-medium'>{product.title}</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{request.company.services && request.company.services.length > 0 && (
|
||||||
|
<div className='bg-gray-50 p-4 rounded-lg'>
|
||||||
|
<h2 className='text-base font-semibold mb-4'>{t('request.services')}</h2>
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
{request.company.services.map((service: Service) => (
|
||||||
|
<div key={service.id} className='bg-white p-3 rounded-lg shadow-sm'>
|
||||||
|
<div className='aspect-video mb-2 overflow-hidden rounded-lg'>
|
||||||
|
<img
|
||||||
|
src={service.imageUrl}
|
||||||
|
alt={service.title}
|
||||||
|
className='w-full h-full object-cover'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className='text-sm font-medium'>{service.title}</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{request.requestStatus === 'PENDING' && (
|
||||||
|
<div className='bg-gray-50 p-4 rounded-lg'>
|
||||||
|
<h2 className='text-base font-semibold mb-4'>{t('request.actions')}</h2>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<button
|
||||||
|
onClick={handleApprove}
|
||||||
|
disabled={isPending}
|
||||||
|
className='flex-1 bg-green-500 text-white py-2 px-4 rounded-lg hover:bg-green-600 transition-colors disabled:opacity-50 text-sm'
|
||||||
|
>
|
||||||
|
{isPending ? t('request.approving') : t('request.approve')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowRejectModal(true)}
|
||||||
|
disabled={isPending}
|
||||||
|
className='flex-1 bg-red-500 text-white py-2 px-4 rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50 text-sm'
|
||||||
|
>
|
||||||
|
{isPending ? t('request.rejecting') : t('request.reject')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalConfrim
|
||||||
|
isHasDescription
|
||||||
|
isOpen={showRejectModal}
|
||||||
|
close={() => setShowRejectModal(false)}
|
||||||
|
label={t('request.reject_reason')}
|
||||||
|
onConfrim={(text) => handleReject(text || '')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showRejectModal && (
|
||||||
|
<div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'>
|
||||||
|
<div className='bg-white p-6 rounded-lg w-96'>
|
||||||
|
<h3 className='text-base font-semibold mb-4'>{t('request.reject_reason')}</h3>
|
||||||
|
<textarea
|
||||||
|
value={rejectReason}
|
||||||
|
onChange={(e) => setRejectReason(e.target.value)}
|
||||||
|
className='w-full p-2 border rounded-lg mb-4 text-sm'
|
||||||
|
rows={4}
|
||||||
|
placeholder={t('request.enter_reject_reason')}
|
||||||
|
/>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowRejectModal(false)}
|
||||||
|
className='flex-1 bg-gray-500 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition-colors text-sm'
|
||||||
|
>
|
||||||
|
{t('cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleReject(rejectReason)}
|
||||||
|
disabled={!rejectReason || isPending}
|
||||||
|
className='flex-1 bg-red-500 text-white py-2 px-4 rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50 text-sm'
|
||||||
|
>
|
||||||
|
{isPending ? t('request.rejecting') : t('request.reject')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RequestDetail
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useGetRequests } from './hooks/useRequestData'
|
||||||
|
import Pagination from '../../components/Pagination'
|
||||||
|
import Td from '../../components/Td'
|
||||||
|
import PageLoading from '../../components/PageLoading'
|
||||||
|
import { RequestItemType } from './types/Types'
|
||||||
|
import moment from 'moment-jalaali'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Eye } from 'iconsax-react'
|
||||||
|
import { Pages } from '../../config/Pages'
|
||||||
|
|
||||||
|
const RequestsList: FC = () => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const { data, isLoading } = useGetRequests(page)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleRowClick = (id: string) => {
|
||||||
|
navigate(Pages.requests.detail + id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<div>
|
||||||
|
{t('request.request_list')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoading ?
|
||||||
|
<div className='mt-5'>
|
||||||
|
<PageLoading />
|
||||||
|
</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('request.user')} />
|
||||||
|
<Td text={t('request.date_receipt')} />
|
||||||
|
<Td text={t('request.status')} />
|
||||||
|
<Td text={t('request.business')} />
|
||||||
|
<Td text={t('request.user_info')} />
|
||||||
|
<Td text={t('detail')} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
data?.data?.requests?.map((item: RequestItemType, index: number) => {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={item.id}
|
||||||
|
className='tr hover:bg-gray-50'
|
||||||
|
>
|
||||||
|
<Td text={String(index + 1)} />
|
||||||
|
<Td text={`${item.user.firstName} ${item.user.lastName}`} />
|
||||||
|
<Td text={moment(item.createdAt).format('jYYYY/jMM/jDD')} />
|
||||||
|
<Td text={t(`request.${item.requestStatus.toLowerCase()}`)} />
|
||||||
|
<Td text={item.company?.name} />
|
||||||
|
<Td text={item.company.name} />
|
||||||
|
|
||||||
|
<Td text=''>
|
||||||
|
<Eye
|
||||||
|
size={20}
|
||||||
|
color='#888'
|
||||||
|
onClick={() => handleRowClick(item.id)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
totalPages={data?.data?.pager?.totalPages}
|
||||||
|
currentPage={page}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RequestsList
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
approveRequest,
|
||||||
|
getRequestDetail,
|
||||||
|
getRequests,
|
||||||
|
rejectRequest,
|
||||||
|
} from "../service/RequestService";
|
||||||
|
|
||||||
|
export const useGetRequests = (page: number = 1) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["requests", page],
|
||||||
|
queryFn: () => getRequests(page),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetRequestDetail = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["request", id],
|
||||||
|
queryFn: () => getRequestDetail(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useApproveRequest = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => approveRequest(id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useChangeRequestStatus = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
rejectReason,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
rejectReason: string;
|
||||||
|
}) => {
|
||||||
|
if (status === "APPROVED") {
|
||||||
|
return approveRequest(id);
|
||||||
|
} else {
|
||||||
|
return rejectRequest(id, status, rejectReason);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
|
||||||
|
export const getRequests = async (page: number = 1) => {
|
||||||
|
const { data } = await axios.get(`/companies/requests?page=${page}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequestDetail = async (id: string) => {
|
||||||
|
const { data } = await axios.get(`/companies/requests/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const approveRequest = async (id: string) => {
|
||||||
|
const { data } = await axios.patch(`/companies/requests/${id}/approve`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rejectRequest = async (
|
||||||
|
id: string,
|
||||||
|
status: string,
|
||||||
|
rejectReason: string
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.patch(
|
||||||
|
`/companies/requests/${id}/change-status`,
|
||||||
|
{
|
||||||
|
requestStatus: status,
|
||||||
|
rejectReason,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
export type ApiResponse<T> = {
|
||||||
|
statusCode: number;
|
||||||
|
success: boolean;
|
||||||
|
data: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestResponse = {
|
||||||
|
request: RequestItemType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestItemType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
rejectReason: string | null;
|
||||||
|
requestStatus: string;
|
||||||
|
company: {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
chiefExecutiveOfficer: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
identificationNumber: string;
|
||||||
|
dateOfEstablishment: string;
|
||||||
|
address: string;
|
||||||
|
mapAddressLink: string;
|
||||||
|
description: string;
|
||||||
|
websiteUrl: string;
|
||||||
|
profileImageUrl: string;
|
||||||
|
coverImageUrl: string;
|
||||||
|
isActive: boolean;
|
||||||
|
status: string;
|
||||||
|
industry: string;
|
||||||
|
business: string;
|
||||||
|
user: string;
|
||||||
|
invoiceCount: number;
|
||||||
|
};
|
||||||
|
business: string;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -17,7 +17,6 @@ import { ErrorType } from '../../helpers/types'
|
|||||||
import { clx } from '../../helpers/utils'
|
import { clx } from '../../helpers/utils'
|
||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
import { useMultiUpload } from '../service/hooks/useServiceData'
|
import { useMultiUpload } from '../service/hooks/useServiceData'
|
||||||
import ReferTicket from './components/ReferTicket'
|
|
||||||
|
|
||||||
const TicketDetail: FC = () => {
|
const TicketDetail: FC = () => {
|
||||||
|
|
||||||
@@ -99,22 +98,11 @@ const TicketDetail: FC = () => {
|
|||||||
<div className='flex xl:flex-row flex-col-reverse gap-6 xl:mt-8 mt-6'>
|
<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='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
|
||||||
<div className='gap-6 rowTwoInput'>
|
<div className='gap-6 rowTwoInput'>
|
||||||
<Input
|
|
||||||
label={t('ticket.select_your_service')}
|
|
||||||
value={getMessages.data?.data?.ticket?.danakService?.name || '-'}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
label={t('ticket.user_name')}
|
label={t('ticket.user_name')}
|
||||||
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
|
value={getMessages.data?.data?.ticket?.user?.firstName + ' ' + getMessages.data?.data?.ticket?.user?.lastName}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
|
||||||
label={t('ticket.user_plan')}
|
|
||||||
value={getMessages.data?.data?.supportPlan?.userSupportPlan?.supportPlan?.name}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 xl:text-sm text-xs'>
|
<div className='mt-8 xl:text-sm text-xs'>
|
||||||
@@ -123,7 +111,7 @@ const TicketDetail: FC = () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
getMessages.data?.data?.messages.map((item: TicketMessageType) => {
|
getMessages.data?.data?.messages.map((item: TicketMessageType) => {
|
||||||
if (item?.author?.roles[0]?.name === 'user') {
|
if (item?.author?.role?.name === 'USER') {
|
||||||
return (
|
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 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'>
|
<div className='leading-7'>
|
||||||
@@ -309,10 +297,6 @@ const TicketDetail: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
|
||||||
<ReferTicket refetch={getMessages.refetch} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ export type TicketMessageType = {
|
|||||||
id: string;
|
id: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
roles: {
|
role: {
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
};
|
||||||
};
|
};
|
||||||
attachments: {
|
attachments: {
|
||||||
attachmentUrl: string;
|
attachmentUrl: string;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ import CreateCompany from '../pages/company/Create'
|
|||||||
import Industries from '../pages/company/components/Industries'
|
import Industries from '../pages/company/components/Industries'
|
||||||
import CompanyList from '../pages/company/List'
|
import CompanyList from '../pages/company/List'
|
||||||
import UpdateCompany from '../pages/company/Update'
|
import UpdateCompany from '../pages/company/Update'
|
||||||
|
import RequestsList from '../pages/requests/List'
|
||||||
|
import RequestDetail from '../pages/requests/Detail'
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
const { hasSubMenu } = useSharedStore()
|
const { hasSubMenu } = useSharedStore()
|
||||||
@@ -112,6 +114,8 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Pages.company.industries} element={<Industries />} />
|
<Route path={Pages.company.industries} element={<Industries />} />
|
||||||
<Route path={Pages.company.list} element={<CompanyList />} />
|
<Route path={Pages.company.list} element={<CompanyList />} />
|
||||||
<Route path={Pages.company.detail + ':id'} element={<UpdateCompany />} />
|
<Route path={Pages.company.detail + ':id'} element={<UpdateCompany />} />
|
||||||
|
<Route path={Pages.requests.list} element={<RequestsList />} />
|
||||||
|
<Route path={Pages.requests.detail + ':id'} element={<RequestDetail />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import { useSharedStore } from './store/sharedStore'
|
|||||||
import { useGetProfile } from '../pages/profile/hooks/useProfileData'
|
import { useGetProfile } from '../pages/profile/hooks/useProfileData'
|
||||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
||||||
import { removeRefreshToken, removeToken } from '../config/func'
|
import { removeRefreshToken, removeToken } from '../config/func'
|
||||||
|
import { clx } from '../helpers/utils'
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [popoverKey, setPopoverKey] = useState(0);
|
const [popoverKey, setPopoverKey] = useState(0);
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { setOpenSidebar, openSidebar } = useSharedStore()
|
const { setOpenSidebar, openSidebar, hasSubMenu } = useSharedStore()
|
||||||
const { data } = useGetProfile()
|
const { data } = useGetProfile()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,8 +30,13 @@ const Header: FC = () => {
|
|||||||
window.location.href = Pages.auth.login
|
window.location.href = Pages.auth.login
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSubMenuOpen = hasSubMenu && (openSidebar || window.innerWidth > 1139)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
|
<div className={clx(
|
||||||
|
'fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]',
|
||||||
|
hasSubMenuOpen && '!right-[320px] !w-[calc(100%-340px)]'
|
||||||
|
)}>
|
||||||
|
|
||||||
<div className='min-w-[270px] hidden xl:block'>
|
<div className='min-w-[270px] hidden xl:block'>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
Reference in New Issue
Block a user