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={'قرارداد'} /> <Td text={'قرارداد'} />
<Td text={'مدارک'} />
<Td text={'فعال/غیرفعال'} /> <Td text={'فعال/غیرفعال'} />
</tr> </tr>
</thead> </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 { type Seller } from '../types/Types'
import StatusWithText from '../../../components/StatusWithText' import StatusWithText from '../../../components/StatusWithText'
import Td from '../../../components/Td' import Td from '../../../components/Td'
import SellerToggle from './SellerToggle' import SellerToggle from './SellerToggle'
import DocumentsModal from './DocumentsModal'
import { Calendar, Call, Card, User as UserIcon, Shop, Eye } from 'iconsax-react' import { Calendar, Call, Card, User as UserIcon, Shop, Eye } from 'iconsax-react'
interface Props { interface Props {
@@ -11,6 +12,8 @@ interface Props {
} }
const SellerTableRow: FC<Props> = ({ seller, onViewContract }) => { const SellerTableRow: FC<Props> = ({ seller, onViewContract }) => {
const [documentsModalOpen, setDocumentsModalOpen] = useState(false)
const getStatusVariant = (status: string) => { const getStatusVariant = (status: string) => {
switch (status) { switch (status) {
case 'Approved': return 'success'; case 'Approved': return 'success';
@@ -33,64 +36,81 @@ const SellerTableRow: FC<Props> = ({ seller, onViewContract }) => {
}; };
return ( return (
<tr key={seller._id} className='tr'> <>
<Td text=""> <DocumentsModal
<div className="flex items-center gap-1"> open={documentsModalOpen}
<UserIcon size={16} color="#8C90A3" /> close={() => setDocumentsModalOpen(false)}
<span>{seller.fullName || '-'}</span> documents={seller.documents || []}
</div> sellerId={seller._id}
</Td> />
<Td text=""> <tr key={seller._id} className='tr'>
<div className="flex items-center gap-1"> <Td text="">
<Shop size={16} color="#8C90A3" /> <div className="flex items-center gap-1">
<span>{seller.shop?.shopName || '-'}</span> <UserIcon size={16} color="#8C90A3" />
</div> <span>{seller.fullName || '-'}</span>
</Td> </div>
<Td text=""> </Td>
<div className="flex items-center gap-1"> <Td text="">
<Card size={16} color="#8C90A3" /> <div className="flex items-center gap-1">
<span>{seller.email || '-'}</span> <Shop size={16} color="#8C90A3" />
</div> <span>{seller.shop?.shopName || '-'}</span>
</Td> </div>
<Td text=""> </Td>
<div className="flex items-center gap-1"> <Td text="">
<Call size={16} color="#8C90A3" /> <div className="flex items-center gap-1">
<span>{seller.phoneNumber}</span> <Card size={16} color="#8C90A3" />
</div> <span>{seller.email || '-'}</span>
</Td> </div>
<Td text=""> </Td>
<StatusWithText <Td text="">
variant={getStatusVariant(seller.accountStatus)} <div className="flex items-center gap-1">
text={getStatusText(seller.accountStatus)} <Call size={16} color="#8C90A3" />
/> <span>{seller.phoneNumber}</span>
</Td> </div>
<Td text={seller.businessType || '-'} /> </Td>
<Td text=""> <Td text="">
<div className="flex items-center gap-1"> <StatusWithText
<Calendar size={16} color="#8C90A3" /> variant={getStatusVariant(seller.accountStatus)}
<span>{formatDate(seller.createdAt)}</span> text={getStatusText(seller.accountStatus)}
</div> />
</Td> </Td>
<Td text=""> <Td text={seller.businessType || '-'} />
{seller.contracts ? ( <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 <button
onClick={() => onViewContract(seller.contracts)} onClick={() => setDocumentsModalOpen(true)}
className="flex items-center gap-2 px-3 py-1 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors" className="flex items-center gap-1 hover:bg-gray-50 px-2 py-1 rounded transition-colors"
> >
<Eye color="#8C90A3" size={16} /> <Card size={16} color="#8C90A3" />
<span className="text-sm">مشاهده</span> <span>مدارک ({seller.documents?.length || 0})</span>
</button> </button>
) : ( </Td>
<span className="text-gray-400 text-sm">-</span> <Td text="">
)} <SellerToggle
</Td> sellerId={seller._id}
<Td text=""> isActive={seller.accountStatus === 'Approved'}
<SellerToggle />
sellerId={seller._id} </Td>
isActive={seller.accountStatus === 'Approved'} </tr>
/> </>
</Td>
</tr>
) )
} }
+17
View File
@@ -1,6 +1,7 @@
import * as api from "../service/SellerService"; import * as api from "../service/SellerService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { import type {
ChangeStatusDocumentType,
CreateCategoryLearning, CreateCategoryLearning,
CreateLearningType, CreateLearningType,
} from "../types/Types"; } from "../types/Types";
@@ -146,3 +147,19 @@ export const useUpdateContract = () => {
mutationFn: api.updateContract, 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, GetLearningResponse,
CreateLearningType, CreateLearningType,
UpdateContractType, UpdateContractType,
ChangeStatusDocumentType,
} from "../types/Types"; } from "../types/Types";
export const getSellers = async ( export const getSellers = async (
@@ -158,3 +159,14 @@ export const updateContract = async (params: UpdateContractType) => {
); );
return data; 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; id: string;
content: string; content: string;
} }
export interface ChangeStatusDocumentType {
docsId: string;
status: "Approved" | "REJECTED";
}