document seller

This commit is contained in:
hamid zarghami
2025-10-13 17:26:46 +03:30
parent d1fcb2b79a
commit 48d79ac3cb
6 changed files with 316 additions and 56 deletions
+1
View File
@@ -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
+76 -56
View File
@@ -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>
</>
)
}
+17
View File
@@ -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
View File
@@ -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;
};
+5
View File
@@ -380,3 +380,8 @@ export interface UpdateContractType {
id: string;
content: string;
}
export interface ChangeStatusDocumentType {
docsId: string;
status: "Approved" | "REJECTED";
}