feat: add shipment management modal for admin

- Add ShipmentManagementModal component to manage shop shipment methods
- Add button to Shop settings page to open shipment management modal
- Update useShipmentData hooks to invalidate queries after add/remove operations
- Modal shows active and available shipment methods for admin to manage
This commit is contained in:
hamid zarghami
2025-10-27 11:14:13 +03:30
parent 4ed09f494f
commit cf3e2b7d4b
6 changed files with 260 additions and 3 deletions
+19 -2
View File
@@ -8,10 +8,11 @@ import Button from '../../components/Button'
import Input from '../../components/Input'
import Textarea from '../../components/Textarea'
import UploadBox from '../../components/UploadBox'
import { TickCircle } from 'iconsax-react'
import { TickCircle, Truck } from 'iconsax-react'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/helpers/utils'
import { useSingleUpload } from '../category'
import ShipmentManagementModal from './components/ShipmentManagementModal'
// Validation schema برای UpdateShopType
const validationSchema = Yup.object().shape({
@@ -26,6 +27,7 @@ const Shop: FC = () => {
const updateShopMutation = useUpdateShop()
const uploadSingle = useSingleUpload()
const [logoFile, setLogoFile] = useState<File[]>([])
const [isShipmentModalOpen, setIsShipmentModalOpen] = useState(false)
const formik = useFormik<UpdateShopType>({
initialValues: {
@@ -91,7 +93,17 @@ const Shop: FC = () => {
<div className='flex items-center gap-3'>
<h1>ویرایش اطلاعات شاپ</h1>
</div>
<div>
<div className='flex gap-2'>
<Button
variant='outline'
className='px-6'
onClick={() => setIsShipmentModalOpen(true)}
>
<div className='flex gap-2 items-center'>
<Truck size={16} />
<div>مدیریت روش ارسال</div>
</div>
</Button>
<Button
className='px-6'
onClick={() => formik.handleSubmit()}
@@ -160,6 +172,11 @@ const Shop: FC = () => {
</div>
</div>
</div>
<ShipmentManagementModal
open={isShipmentModalOpen}
close={() => setIsShipmentModalOpen(false)}
/>
</div>
)
}
@@ -0,0 +1,154 @@
import { type FC, useState } from 'react'
import DefaulModal from '@/components/DefaulModal'
import Button from '@/components/Button'
import { useGetShipment } from '../../shipment/hooks/useShipmentData'
import { useGetAdminShop, useAddShipmentToAdmin, useRemoveShipmentFromAdmin } from '../../shipment/hooks/useShipmentData'
import PageLoading from '@/components/PageLoading'
import Error from '@/components/Error'
import { Add, Trash } from 'iconsax-react'
import { toast } from 'react-toastify'
interface Props {
open: boolean
close: () => void
}
const ShipmentManagementModal: FC<Props> = ({ open, close }) => {
const [currentPage] = useState(1)
const { data: shipmentsData, isLoading: shipmentsLoading, error: shipmentsError } = useGetShipment(currentPage)
const { data: adminShopData, isLoading: adminShopLoading, error: adminShopError, refetch: refetchAdminShop } = useGetAdminShop()
const addShipmentMutation = useAddShipmentToAdmin()
const removeShipmentMutation = useRemoveShipmentFromAdmin()
if (shipmentsLoading || adminShopLoading) {
return (
<DefaulModal open={open} close={close} isHeader title_header="مدیریت روش‌های ارسال">
<div className="flex justify-center items-center min-h-[200px]">
<PageLoading />
</div>
</DefaulModal>
)
}
if (shipmentsError || adminShopError) {
return (
<DefaulModal open={open} close={close} isHeader title_header="مدیریت روش‌های ارسال">
<div className="flex justify-center items-center min-h-[200px]">
<Error errorText="خطا در بارگذاری اطلاعات" />
</div>
</DefaulModal>
)
}
const allShipments = shipmentsData?.results?.shipper || []
const shopShipments = adminShopData?.results?.data?.shipmentMethod || []
const shopShipmentIds = shopShipments.map(s => s._id)
const availableShipments = allShipments.filter(shipment => !shopShipmentIds.includes(shipment._id))
const getDeliveryTypeText = (type: string) => {
switch (type) {
case 'Standard':
return 'معمولی'
case 'SameDay':
return 'همان روز'
case 'Express':
return 'اکسپرس'
default:
return type
}
}
const handleAddShipment = async (shipmentId: number) => {
try {
await addShipmentMutation.mutateAsync(shipmentId.toString())
toast.success('روش ارسال با موفقیت اضافه شد')
refetchAdminShop()
} catch {
toast.error('خطا در اضافه کردن روش ارسال')
}
}
const handleRemoveShipment = async (shipmentId: number) => {
try {
await removeShipmentMutation.mutateAsync(shipmentId.toString())
toast.success('روش ارسال با موفقیت حذف شد')
refetchAdminShop()
} catch {
toast.error('خطا در حذف روش ارسال')
}
}
return (
<DefaulModal open={open} close={close} isHeader title_header="مدیریت روش‌های ارسال" width={800}>
<div className="p-4">
{/* روش‌های ارسال فروشگاه */}
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3 text-green-600">روشهای ارسال فعال فروشگاه</h3>
{shopShipments.length === 0 ? (
<div className="text-center py-4 text-gray-500 bg-gray-50 rounded-lg">
هیچ روش ارسالی فعالی وجود ندارد
</div>
) : (
<div className="space-y-2 max-h-40 overflow-y-auto">
{shopShipments.map((shipment) => (
<div key={shipment._id} className="flex items-center justify-between bg-green-50 p-3 rounded-lg border">
<div className="flex-1">
<div className="font-medium">{shipment.name}</div>
<div className="text-sm text-gray-600">
{getDeliveryTypeText(shipment.deliveryType)} - {shipment.deliveryTime} روز
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleRemoveShipment(shipment._id)}
isLoading={removeShipmentMutation.isPending}
className="text-red-600 hover:text-red-700 border-red-300 hover:border-red-400"
>
<Trash size={16} />
</Button>
</div>
))}
</div>
)}
</div>
{/* روش‌های ارسال موجود برای اضافه کردن */}
<div>
<h3 className="text-lg font-semibold mb-3 text-blue-600">روشهای ارسال قابل اضافه کردن</h3>
{availableShipments.length === 0 ? (
<div className="text-center py-4 text-gray-500 bg-gray-50 rounded-lg">
همه روشهای ارسال اضافه شدهاند
</div>
) : (
<div className="space-y-2 max-h-40 overflow-y-auto">
{availableShipments.map((shipment) => (
<div key={shipment._id} className="flex items-center justify-between bg-blue-50 p-3 rounded-lg border">
<div className="flex-1">
<div className="font-medium">{shipment.name}</div>
<div className="text-sm text-gray-600">
{getDeliveryTypeText(shipment.deliveryType)} - {shipment.deliveryTime} روز
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => handleAddShipment(shipment._id)}
isLoading={addShipmentMutation.isPending}
className="text-green-600 hover:text-green-700 border-green-300 hover:border-green-400"
>
<Add size={16} />
</Button>
</div>
))}
</div>
)}
</div>
</div>
</DefaulModal>
)
}
export default ShipmentManagementModal
+3 -1
View File
@@ -1,6 +1,6 @@
import { type FC, useState } from 'react'
import PageTitle from '../../components/PageTitle'
import { useGetShipment, useDeleteShipment } from './hooks/useShipmentData'
import { useGetShipment, useDeleteShipment, useGetAdminShop } from './hooks/useShipmentData'
import { type Shipper } from './types/Types'
import PageLoading from '@/components/PageLoading';
import Error from '@/components/Error';
@@ -14,7 +14,9 @@ import { Pages } from '@/config/Pages';
import { useNavigate } from 'react-router-dom';
const ShippingList: FC = () => {
const [currentPage, setCurrentPage] = useState(1);
useGetAdminShop()
const navigate = useNavigate();
const { data, isLoading, error, refetch } = useGetShipment(currentPage);
const deleteShipmentMutation = useDeleteShipment();
@@ -46,3 +46,30 @@ export const useUpdateShipment = () => {
},
});
};
export const useRemoveShipmentFromAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.removeShipmentFromAdmin(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["admin-shop"] });
},
});
};
export const useAddShipmentToAdmin = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.addShipmentToAdmin(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["admin-shop"] });
},
});
};
export const useGetAdminShop = () => {
return useQuery({
queryKey: ["admin-shop"],
queryFn: () => api.getAdminShop(),
});
};
+20
View File
@@ -4,6 +4,7 @@ import type {
CreateShipper,
Shipper,
ShipmentByIdResponse,
GetAdminShopResponse,
} from "../types/Types";
export const getShipment = async (
@@ -37,3 +38,22 @@ export const updateShipment = async (
const { data } = await axios.patch<Shipper>(`/admin/shipment/${id}`, params);
return data;
};
export const removeShipmentFromAdmin = async (id: string): Promise<void> => {
const { data } = await axios.post(`/admin/shipment/shipment/deactivate`, {
shipmentId: id,
});
return data;
};
export const addShipmentToAdmin = async (id: string): Promise<void> => {
const { data } = await axios.post(`/admin/shipment/shipment/activate`, {
shipmentId: id,
});
return data;
};
export const getAdminShop = async (): Promise<GetAdminShopResponse> => {
const { data } = await axios.get<GetAdminShopResponse>(`/admin/shop`);
return data;
};
+37
View File
@@ -54,3 +54,40 @@ export interface ShipmentByIdResponse {
shipper: Shipper;
};
}
export interface ShipmentMethod {
_id: number;
name: string;
description: string;
costs: Cost[];
deliveryType: string;
deliveryTime: number;
deleted: boolean;
createdAt: string;
updatedAt: string;
}
export interface ShopData {
_id: string;
shopName: string;
shopDescription: string;
telephoneNumber: string;
shopPostalCode: string;
address: string | null;
logo: string;
shipmentMethod: ShipmentMethod[];
isChatActive: boolean;
owner: string;
ownerRef: string;
createdAt: string;
updatedAt: string;
shopCode: number;
}
export interface GetAdminShopResponse {
status: number;
success: boolean;
results: {
data: ShopData;
};
}