income price

This commit is contained in:
hamid zarghami
2025-09-30 16:49:35 +03:30
parent 5f5efbd063
commit ab5c7cc3c3
8 changed files with 478 additions and 2 deletions
+127
View File
@@ -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
+7
View File
@@ -22,3 +22,10 @@ export enum SiteSettingPageEnum {
returnRulesContent = "returnRulesContent",
jobsContent = "jobsContent",
}
export enum PricingPageEnum {
insertionFee = "insertionFee",
photographyFee = "photographyFee",
unboxingVideoFee = "unboxingVideoFee",
expertReviewsFee = "expertReviewsFee",
}
+37 -1
View File
@@ -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;
};
+25 -1
View File
@@ -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;
}
+2
View File
@@ -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>