document seller
This commit is contained in:
@@ -53,6 +53,7 @@ const SellerList: FC = () => {
|
||||
<Td text={'نوع کسبوکار'} />
|
||||
<Td text={'تاریخ ایجاد'} />
|
||||
<Td text={'قرارداد'} />
|
||||
<Td text={'مدارک'} />
|
||||
<Td text={'فعال/غیرفعال'} />
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import DefaulModal from '../../../components/DefaulModal'
|
||||
import Button from '../../../components/Button'
|
||||
import StatusWithText from '../../../components/StatusWithText'
|
||||
import { type SellerDocument } from '../types/Types'
|
||||
import { useChangeStatusDocument } from '../hooks/useSellerData'
|
||||
import { Eye, Import, CloseCircle } from 'iconsax-react'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
close: () => void
|
||||
documents: SellerDocument[]
|
||||
sellerId: string
|
||||
}
|
||||
|
||||
const DocumentsModal: FC<Props> = ({ open, close, documents, sellerId }) => {
|
||||
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
||||
const changeStatusMutation = useChangeStatusDocument()
|
||||
|
||||
const getStatusVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Approved': return 'success'
|
||||
case 'Pending': return 'warning'
|
||||
default: return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Approved': return 'تایید شده'
|
||||
case 'Pending': return 'در انتظار'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
const isImage = (url: string) => {
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']
|
||||
const lowerUrl = url.toLowerCase()
|
||||
return imageExtensions.some(ext => lowerUrl.includes(ext))
|
||||
}
|
||||
|
||||
const handleDownload = (url: string, fileName: string) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
const handleApprove = async (documentId: string) => {
|
||||
try {
|
||||
await changeStatusMutation.mutateAsync({
|
||||
sellerId,
|
||||
params: {
|
||||
docsId: documentId,
|
||||
status: 'Approved'
|
||||
}
|
||||
})
|
||||
// Refresh documents or update local state
|
||||
} catch (error) {
|
||||
console.error('خطا در تایید مدرک:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReject = async (documentId: string) => {
|
||||
try {
|
||||
await changeStatusMutation.mutateAsync({
|
||||
sellerId,
|
||||
params: {
|
||||
docsId: documentId,
|
||||
status: 'REJECTED'
|
||||
}
|
||||
})
|
||||
// Refresh documents or update local state
|
||||
} catch (error) {
|
||||
console.error('خطا در رد مدرک:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaulModal
|
||||
open={open}
|
||||
close={close}
|
||||
isHeader
|
||||
title_header="مدارک فروشنده"
|
||||
width={600}
|
||||
>
|
||||
<div className="space-y-4 mt-5">
|
||||
{documents.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
هیچ مدرکی یافت نشد
|
||||
</div>
|
||||
) : (
|
||||
documents.map((document) => (
|
||||
<div
|
||||
key={document._id}
|
||||
className="border border-gray-200 rounded-lg p-4 space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{document.type}</span>
|
||||
<StatusWithText
|
||||
variant={getStatusVariant(document.status)}
|
||||
text={getStatusText(document.status)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{isImage(document.docsUrl) && (
|
||||
<button
|
||||
onClick={() => setSelectedImage(document.docsUrl)}
|
||||
className="flex items-center gap-2 px-3 py-1 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<Eye color="#8C90A3" size={16} />
|
||||
<span className="text-sm">مشاهده</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleDownload(document.docsUrl, `${document.type}_${document._id}`)}
|
||||
className="flex items-center gap-2 px-3 py-1 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<Import color="#8C90A3" size={16} />
|
||||
<span className="text-sm">دانلود</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isImage(document.docsUrl) && (
|
||||
<div className="mt-3">
|
||||
<img
|
||||
src={document.docsUrl}
|
||||
alt={document.type}
|
||||
className="w-full max-h-48 object-contain rounded-lg border"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{document.comment && (
|
||||
<div className="text-sm text-gray-600 bg-gray-50 p-2 rounded">
|
||||
{document.comment}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
ایجاد شده: {new Date(document.createdAt).toLocaleDateString('fa-IR')}
|
||||
</div>
|
||||
|
||||
{document.status === 'Pending' && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={() => handleApprove(document._id)}
|
||||
isLoading={changeStatusMutation.isPending}
|
||||
className="flex-1"
|
||||
>
|
||||
تایید
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => handleReject(document._id)}
|
||||
isLoading={changeStatusMutation.isPending}
|
||||
className="flex-1"
|
||||
>
|
||||
رد
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image Preview Modal */}
|
||||
{selectedImage && (
|
||||
<DefaulModal
|
||||
open={!!selectedImage}
|
||||
close={() => setSelectedImage(null)}
|
||||
width={800}
|
||||
>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setSelectedImage(null)}
|
||||
className="absolute top-2 right-2 z-10 bg-black/50 text-white rounded-full p-2 hover:bg-black/70 transition-colors"
|
||||
>
|
||||
<CloseCircle size={24} />
|
||||
</button>
|
||||
<img
|
||||
src={selectedImage}
|
||||
alt="پیشنمایش مدرک"
|
||||
className="w-full max-h-[70vh] object-contain rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
)}
|
||||
</DefaulModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentsModal
|
||||
@@ -1,8 +1,9 @@
|
||||
import { type FC } from 'react'
|
||||
import { type FC, useState } from 'react'
|
||||
import { type Seller } from '../types/Types'
|
||||
import StatusWithText from '../../../components/StatusWithText'
|
||||
import Td from '../../../components/Td'
|
||||
import SellerToggle from './SellerToggle'
|
||||
import DocumentsModal from './DocumentsModal'
|
||||
import { Calendar, Call, Card, User as UserIcon, Shop, Eye } from 'iconsax-react'
|
||||
|
||||
interface Props {
|
||||
@@ -11,6 +12,8 @@ interface Props {
|
||||
}
|
||||
|
||||
const SellerTableRow: FC<Props> = ({ seller, onViewContract }) => {
|
||||
const [documentsModalOpen, setDocumentsModalOpen] = useState(false)
|
||||
|
||||
const getStatusVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Approved': return 'success';
|
||||
@@ -33,64 +36,81 @@ const SellerTableRow: FC<Props> = ({ seller, onViewContract }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<tr key={seller._id} className='tr'>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserIcon size={16} color="#8C90A3" />
|
||||
<span>{seller.fullName || '-'}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Shop size={16} color="#8C90A3" />
|
||||
<span>{seller.shop?.shopName || '-'}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Card size={16} color="#8C90A3" />
|
||||
<span>{seller.email || '-'}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Call size={16} color="#8C90A3" />
|
||||
<span>{seller.phoneNumber}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<StatusWithText
|
||||
variant={getStatusVariant(seller.accountStatus)}
|
||||
text={getStatusText(seller.accountStatus)}
|
||||
/>
|
||||
</Td>
|
||||
<Td text={seller.businessType || '-'} />
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar size={16} color="#8C90A3" />
|
||||
<span>{formatDate(seller.createdAt)}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
{seller.contracts ? (
|
||||
<>
|
||||
<DocumentsModal
|
||||
open={documentsModalOpen}
|
||||
close={() => setDocumentsModalOpen(false)}
|
||||
documents={seller.documents || []}
|
||||
sellerId={seller._id}
|
||||
/>
|
||||
<tr key={seller._id} className='tr'>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserIcon size={16} color="#8C90A3" />
|
||||
<span>{seller.fullName || '-'}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Shop size={16} color="#8C90A3" />
|
||||
<span>{seller.shop?.shopName || '-'}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Card size={16} color="#8C90A3" />
|
||||
<span>{seller.email || '-'}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Call size={16} color="#8C90A3" />
|
||||
<span>{seller.phoneNumber}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<StatusWithText
|
||||
variant={getStatusVariant(seller.accountStatus)}
|
||||
text={getStatusText(seller.accountStatus)}
|
||||
/>
|
||||
</Td>
|
||||
<Td text={seller.businessType || '-'} />
|
||||
<Td text="">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar size={16} color="#8C90A3" />
|
||||
<span>{formatDate(seller.createdAt)}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text="">
|
||||
{seller.contracts ? (
|
||||
<button
|
||||
onClick={() => onViewContract(seller.contracts)}
|
||||
className="flex items-center gap-2 px-3 py-1 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<Eye color="#8C90A3" size={16} />
|
||||
<span className="text-sm">مشاهده</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-gray-400 text-sm">-</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td text="">
|
||||
<button
|
||||
onClick={() => onViewContract(seller.contracts)}
|
||||
className="flex items-center gap-2 px-3 py-1 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
onClick={() => setDocumentsModalOpen(true)}
|
||||
className="flex items-center gap-1 hover:bg-gray-50 px-2 py-1 rounded transition-colors"
|
||||
>
|
||||
<Eye color="#8C90A3" size={16} />
|
||||
<span className="text-sm">مشاهده</span>
|
||||
<Card size={16} color="#8C90A3" />
|
||||
<span>مدارک ({seller.documents?.length || 0})</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-gray-400 text-sm">-</span>
|
||||
)}
|
||||
</Td>
|
||||
<Td text="">
|
||||
<SellerToggle
|
||||
sellerId={seller._id}
|
||||
isActive={seller.accountStatus === 'Approved'}
|
||||
/>
|
||||
</Td>
|
||||
</tr>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<SellerToggle
|
||||
sellerId={seller._id}
|
||||
isActive={seller.accountStatus === 'Approved'}
|
||||
/>
|
||||
</Td>
|
||||
</tr>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as api from "../service/SellerService";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
ChangeStatusDocumentType,
|
||||
CreateCategoryLearning,
|
||||
CreateLearningType,
|
||||
} from "../types/Types";
|
||||
@@ -146,3 +147,19 @@ export const useUpdateContract = () => {
|
||||
mutationFn: api.updateContract,
|
||||
});
|
||||
};
|
||||
|
||||
export const useChangeStatusDocument = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
sellerId,
|
||||
params,
|
||||
}: {
|
||||
sellerId: string;
|
||||
params: ChangeStatusDocumentType;
|
||||
}) => api.changeStatusDocument(sellerId, params),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["sellers"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
GetLearningResponse,
|
||||
CreateLearningType,
|
||||
UpdateContractType,
|
||||
ChangeStatusDocumentType,
|
||||
} from "../types/Types";
|
||||
|
||||
export const getSellers = async (
|
||||
@@ -158,3 +159,14 @@ export const updateContract = async (params: UpdateContractType) => {
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const changeStatusDocument = async (
|
||||
sellerId: string,
|
||||
params: ChangeStatusDocumentType
|
||||
) => {
|
||||
const { data } = await axios.patch(
|
||||
`/admin/sellers/document/${sellerId}`,
|
||||
params
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -380,3 +380,8 @@ export interface UpdateContractType {
|
||||
id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ChangeStatusDocumentType {
|
||||
docsId: string;
|
||||
status: "Approved" | "REJECTED";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user