income price
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { useGetPricing, useDeletePricing } from './hooks/useSettingData'
|
||||
import { type PricingItem } from './types/Types'
|
||||
import PageLoading from '../../components/PageLoading'
|
||||
import Error from '../../components/Error'
|
||||
import Td from '../../components/Td'
|
||||
import Button from '../../components/Button'
|
||||
import AddPricingModal from './components/AddPrcingModal'
|
||||
import EditPricingModal from './components/EditPricingModal'
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim'
|
||||
import { Edit } from 'iconsax-react'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
const Pricing: FC = () => {
|
||||
const { data, isLoading, error } = useGetPricing();
|
||||
const deletePricingMutation = useDeletePricing();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [selectedPricingItem, setSelectedPricingItem] = useState<PricingItem | null>(null);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<PageLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<Error errorText="خطا در بارگذاری اطلاعات قیمتگذاری" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const pricingItems = data?.results?.pricing || [];
|
||||
|
||||
const handleEditPricing = (pricingItem: PricingItem) => {
|
||||
setSelectedPricingItem(pricingItem);
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeletePricing = async (id: string) => {
|
||||
try {
|
||||
await deletePricingMutation.mutateAsync(id);
|
||||
toast.success('قیمتگذاری با موفقیت حذف شد');
|
||||
} catch (error) {
|
||||
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'خطا در حذف قیمتگذاری';
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="">مدیریت قیمتگذاری</h1>
|
||||
<Button
|
||||
variant="primary"
|
||||
className='w-fit'
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
افزودن قیمتگذاری جدید
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={'نوع'} />
|
||||
<Td text={'قیمت'} />
|
||||
<Td text={'تاریخ ایجاد'} />
|
||||
<Td text={'تاریخ بروزرسانی'} />
|
||||
<Td text={'عملیات'} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pricingItems.length === 0 ? (
|
||||
<tr className='tr'>
|
||||
<td colSpan={4} className="text-center py-8 text-gray-500">
|
||||
هیچ آیتم قیمتگذاری یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
pricingItems.map((item: PricingItem) => (
|
||||
<tr key={item._id} className='tr'>
|
||||
<Td text={item.type} />
|
||||
<Td text={`${item.price.toLocaleString('fa-IR')} تومان`} />
|
||||
<Td text={new Date(item.createdAt).toLocaleDateString('fa-IR')} />
|
||||
<Td text={new Date(item.updatedAt).toLocaleDateString('fa-IR')} />
|
||||
<Td text={''}>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Edit
|
||||
onClick={() => handleEditPricing(item)}
|
||||
color='#8C90A3'
|
||||
size={16}
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
/>
|
||||
<TrashWithConfrim
|
||||
onDelete={() => handleDeletePricing(item._id)}
|
||||
isLoading={deletePricingMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</Td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<AddPricingModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
|
||||
<EditPricingModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={() => setIsEditModalOpen(false)}
|
||||
pricingItem={selectedPricingItem}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pricing
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { useState } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import Button from '../../../components/Button'
|
||||
import Input from '../../../components/Input'
|
||||
import Select from '../../../components/Select'
|
||||
import DefaulModal from '../../../components/DefaulModal'
|
||||
import { PricingPageEnum } from '../enum/Enum'
|
||||
import { useCreatePricing } from '../hooks/useSettingData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
|
||||
interface AddPricingModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const pricingOptions = [
|
||||
{ value: PricingPageEnum.insertionFee, label: 'هزینه درج محصول' },
|
||||
{ value: PricingPageEnum.photographyFee, label: 'هزینه عکاسی' },
|
||||
{ value: PricingPageEnum.unboxingVideoFee, label: 'هزینه ویدیو باز کردن بسته' },
|
||||
{ value: PricingPageEnum.expertReviewsFee, label: 'هزینه بررسی کارشناسان' },
|
||||
]
|
||||
|
||||
const AddPricingModal: FC<AddPricingModalProps> = ({ isOpen, onClose }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
type: '' as PricingPageEnum | '',
|
||||
price: '',
|
||||
})
|
||||
|
||||
const createPricingMutation = useCreatePricing()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!formData.type || !formData.price) {
|
||||
toast.error('لطفا تمام فیلدها را پر کنید')
|
||||
return
|
||||
}
|
||||
|
||||
const price = parseInt(formData.price.replace(/,/g, ''))
|
||||
if (isNaN(price) || price <= 0) {
|
||||
toast.error('قیمت باید یک عدد مثبت باشد')
|
||||
return
|
||||
}
|
||||
|
||||
await createPricingMutation.mutateAsync({
|
||||
type: formData.type as PricingPageEnum,
|
||||
price,
|
||||
}, {
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
toast.success('قیمتگذاری با موفقیت اضافه شد')
|
||||
setFormData({ type: '', price: '' })
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setFormData({ type: '', price: '' })
|
||||
onClose()
|
||||
}
|
||||
|
||||
const formatPrice = (value: string) => {
|
||||
const numericValue = value.replace(/\D/g, '')
|
||||
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaulModal open={isOpen} close={handleClose} isHeader={true} title_header="افزودن قیمتگذاری جدید">
|
||||
|
||||
<form onSubmit={handleSubmit} className="w-[400px] mt-4 space-y-4">
|
||||
<div>
|
||||
<Select
|
||||
label='نوع قیمتگذاری'
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value as PricingPageEnum }))}
|
||||
items={pricingOptions}
|
||||
placeholder="نوع قیمتگذاری را انتخاب کنید"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label='قیمت (تومان)'
|
||||
type="text"
|
||||
value={formData.price}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
price: formatPrice(e.target.value)
|
||||
}))}
|
||||
placeholder="مثال: ۱۰۰,۰۰۰"
|
||||
className="text-left"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
disabled={createPricingMutation.isPending}
|
||||
isLoading={createPricingMutation.isPending}
|
||||
>
|
||||
{createPricingMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={handleClose}
|
||||
disabled={createPricingMutation.isPending}
|
||||
>
|
||||
لغو
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DefaulModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPricingModal
|
||||
@@ -0,0 +1,136 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import Button from '../../../components/Button'
|
||||
import Input from '../../../components/Input'
|
||||
import Select from '../../../components/Select'
|
||||
import DefaulModal from '../../../components/DefaulModal'
|
||||
import { PricingPageEnum } from '../enum/Enum'
|
||||
import { useUpdatePricing } from '../hooks/useSettingData'
|
||||
import { type PricingItem } from '../types/Types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
|
||||
interface EditPricingModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
pricingItem: PricingItem | null
|
||||
}
|
||||
|
||||
const pricingOptions = [
|
||||
{ value: PricingPageEnum.insertionFee, label: 'هزینه درج محصول' },
|
||||
{ value: PricingPageEnum.photographyFee, label: 'هزینه عکاسی' },
|
||||
{ value: PricingPageEnum.unboxingVideoFee, label: 'هزینه ویدیو باز کردن بسته' },
|
||||
{ value: PricingPageEnum.expertReviewsFee, label: 'هزینه بررسی کارشناسان' },
|
||||
]
|
||||
|
||||
const EditPricingModal: FC<EditPricingModalProps> = ({ isOpen, onClose, pricingItem }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
type: '' as PricingPageEnum | '',
|
||||
price: '',
|
||||
})
|
||||
|
||||
const updatePricingMutation = useUpdatePricing()
|
||||
|
||||
useEffect(() => {
|
||||
if (pricingItem && isOpen) {
|
||||
setFormData({
|
||||
type: pricingItem.type as PricingPageEnum,
|
||||
price: pricingItem.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','),
|
||||
})
|
||||
}
|
||||
}, [pricingItem, isOpen])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!formData.type || !formData.price || !pricingItem) {
|
||||
toast.error('لطفا تمام فیلدها را پر کنید')
|
||||
return
|
||||
}
|
||||
|
||||
const price = parseInt(formData.price.replace(/,/g, ''))
|
||||
if (isNaN(price) || price <= 0) {
|
||||
toast.error('قیمت باید یک عدد مثبت باشد')
|
||||
return
|
||||
}
|
||||
|
||||
await updatePricingMutation.mutateAsync({
|
||||
id: pricingItem._id,
|
||||
params: {
|
||||
type: formData.type as PricingPageEnum,
|
||||
price,
|
||||
}
|
||||
}, {
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
toast.success('قیمتگذاری با موفقیت بروزرسانی شد')
|
||||
onClose()
|
||||
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setFormData({ type: '', price: '' })
|
||||
onClose()
|
||||
}
|
||||
|
||||
const formatPrice = (value: string) => {
|
||||
const numericValue = value.replace(/\D/g, '')
|
||||
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaulModal open={isOpen} close={handleClose} isHeader={true} title_header="ویرایش قیمتگذاری">
|
||||
|
||||
<form onSubmit={handleSubmit} className="w-[400px] mt-4 space-y-4">
|
||||
<div>
|
||||
<Select
|
||||
label='نوع قیمتگذاری'
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, type: e.target.value as PricingPageEnum }))}
|
||||
items={pricingOptions}
|
||||
placeholder="نوع قیمتگذاری را انتخاب کنید"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
label='قیمت (تومان)'
|
||||
type="text"
|
||||
value={formData.price}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
price: formatPrice(e.target.value)
|
||||
}))}
|
||||
placeholder="مثال: ۱۰۰,۰۰۰"
|
||||
className="text-left"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
disabled={updatePricingMutation.isPending}
|
||||
isLoading={updatePricingMutation.isPending}
|
||||
>
|
||||
{updatePricingMutation.isPending ? 'در حال ذخیره...' : 'بروزرسانی'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={handleClose}
|
||||
disabled={updatePricingMutation.isPending}
|
||||
>
|
||||
لغو
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DefaulModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditPricingModal
|
||||
@@ -22,3 +22,10 @@ export enum SiteSettingPageEnum {
|
||||
returnRulesContent = "returnRulesContent",
|
||||
jobsContent = "jobsContent",
|
||||
}
|
||||
|
||||
export enum PricingPageEnum {
|
||||
insertionFee = "insertionFee",
|
||||
photographyFee = "photographyFee",
|
||||
unboxingVideoFee = "unboxingVideoFee",
|
||||
expertReviewsFee = "expertReviewsFee",
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as api from "../service/SettingService";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum";
|
||||
import { type CreateFaqType } from "../types/Types";
|
||||
import { type CreateFaqType, type CreatePricingItem } from "../types/Types";
|
||||
|
||||
export const useGetAboutUs = () => {
|
||||
return useQuery({
|
||||
@@ -92,3 +92,39 @@ export const useDeleteDocument = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPricing = () => {
|
||||
return useQuery({
|
||||
queryKey: ["pricing"],
|
||||
queryFn: api.getPricing,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreatePricing = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: api.createPricing,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["pricing"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdatePricing = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({id, params}: {id: string, params: CreatePricingItem}) => api.updatePricing(id, params),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["pricing"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeletePricing = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: api.deletePricing,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["pricing"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -4,9 +4,11 @@ import type {
|
||||
CreateAboutUs,
|
||||
CreateDocumentType,
|
||||
CreateFaqType,
|
||||
CreatePricingItem,
|
||||
DocumentTypesResponse,
|
||||
FaqDetailResponse,
|
||||
FaqResponse,
|
||||
PricingResponse,
|
||||
UpdateSiteSetting,
|
||||
} from "../types/Types";
|
||||
import { FaqPageEnum, SiteSettingPageEnum } from "../enum/Enum";
|
||||
@@ -81,3 +83,23 @@ export const deleteDocument = async (id: string) => {
|
||||
const { data } = await axios.delete(`/admin/settings/document-types/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getPricing = async (): Promise<PricingResponse> => {
|
||||
const { data } = await axios.get(`/admin/financial/pricing`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createPricing = async (params: CreatePricingItem) => {
|
||||
const { data } = await axios.post("/admin/financial/pricing", params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updatePricing = async (id: string, params: CreatePricingItem) => {
|
||||
const { data } = await axios.patch(`/admin/financial/pricing/${id}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deletePricing = async (id: string) => {
|
||||
const { data } = await axios.delete(`/admin/financial/pricing/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FaqPageEnum } from "../enum/Enum";
|
||||
import type { FaqPageEnum, PricingPageEnum } from "../enum/Enum";
|
||||
|
||||
export interface AboutUsItem {
|
||||
_id: string;
|
||||
@@ -137,3 +137,27 @@ export interface DocumentTypesResponse {
|
||||
success: boolean;
|
||||
results: DocumentTypesResults;
|
||||
}
|
||||
|
||||
export interface PricingItem {
|
||||
_id: string;
|
||||
type: string;
|
||||
price: number;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PricingResults {
|
||||
pricing: PricingItem[];
|
||||
}
|
||||
|
||||
export interface PricingResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: PricingResults;
|
||||
}
|
||||
|
||||
export interface CreatePricingItem {
|
||||
type: PricingPageEnum;
|
||||
price: number;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import PrivacyPage from '@/pages/setting/PrivacyPage'
|
||||
import ReturnPolicyPage from '@/pages/setting/ReturnPolicyPage'
|
||||
import JobsPage from '@/pages/setting/JobsPage'
|
||||
import ManageDocument from '@/pages/setting/ManageDocument'
|
||||
import Pricing from '@/pages/setting/Pricing'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
@@ -133,6 +134,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={Pages.pages.returnPolicy} element={<ReturnPolicyPage />} />
|
||||
<Route path={Pages.pages.jobs} element={<JobsPage />} />
|
||||
<Route path={Pages.pages.documents} element={<ManageDocument />} />
|
||||
<Route path={Pages.pages.incomeTariff} element={<Pricing />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user