From ab5c7cc3c3fa729d68cb8e7d8a84b1229438d218 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 30 Sep 2025 16:49:35 +0330 Subject: [PATCH] income price --- src/pages/setting/Pricing.tsx | 127 ++++++++++++++++ .../setting/components/AddPrcingModal.tsx | 122 ++++++++++++++++ .../setting/components/EditPricingModal.tsx | 136 ++++++++++++++++++ src/pages/setting/enum/Enum.ts | 7 + src/pages/setting/hooks/useSettingData.ts | 38 ++++- src/pages/setting/service/SettingService.ts | 22 +++ src/pages/setting/types/Types.ts | 26 +++- src/router/Main.tsx | 2 + 8 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 src/pages/setting/Pricing.tsx create mode 100644 src/pages/setting/components/AddPrcingModal.tsx create mode 100644 src/pages/setting/components/EditPricingModal.tsx diff --git a/src/pages/setting/Pricing.tsx b/src/pages/setting/Pricing.tsx new file mode 100644 index 0000000..bdfbb8d --- /dev/null +++ b/src/pages/setting/Pricing.tsx @@ -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(null); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ +
+ ); + } + + 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 ( +
+
+

مدیریت قیمت‌گذاری

+ +
+ +
+ + + + + + + {pricingItems.length === 0 ? ( + + + + ) : ( + pricingItems.map((item: PricingItem) => ( + + + + )) + )} + +
+ + + + +
+ هیچ آیتم قیمت‌گذاری یافت نشد +
+ + + + +
+ handleEditPricing(item)} + color='#8C90A3' + size={16} + className="cursor-pointer hover:text-blue-500" + /> + handleDeletePricing(item._id)} + isLoading={deletePricingMutation.isPending} + /> +
+
+
+ + setIsModalOpen(false)} + /> + + setIsEditModalOpen(false)} + pricingItem={selectedPricingItem} + /> +
+ ) +} + +export default Pricing \ No newline at end of file diff --git a/src/pages/setting/components/AddPrcingModal.tsx b/src/pages/setting/components/AddPrcingModal.tsx new file mode 100644 index 0000000..fd518a4 --- /dev/null +++ b/src/pages/setting/components/AddPrcingModal.tsx @@ -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 = ({ 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 ( + + +
+
+ setFormData(prev => ({ + ...prev, + price: formatPrice(e.target.value) + }))} + placeholder="مثال: ۱۰۰,۰۰۰" + className="text-left" + /> +
+ +
+ + +
+
+
+ ) +} + +export default AddPricingModal \ No newline at end of file diff --git a/src/pages/setting/components/EditPricingModal.tsx b/src/pages/setting/components/EditPricingModal.tsx new file mode 100644 index 0000000..da91963 --- /dev/null +++ b/src/pages/setting/components/EditPricingModal.tsx @@ -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 = ({ 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 ( + + +
+
+ setFormData(prev => ({ + ...prev, + price: formatPrice(e.target.value) + }))} + placeholder="مثال: ۱۰۰,۰۰۰" + className="text-left" + /> +
+ +
+ + +
+
+
+ ) +} + +export default EditPricingModal diff --git a/src/pages/setting/enum/Enum.ts b/src/pages/setting/enum/Enum.ts index 75775ce..d433506 100644 --- a/src/pages/setting/enum/Enum.ts +++ b/src/pages/setting/enum/Enum.ts @@ -22,3 +22,10 @@ export enum SiteSettingPageEnum { returnRulesContent = "returnRulesContent", jobsContent = "jobsContent", } + +export enum PricingPageEnum { + insertionFee = "insertionFee", + photographyFee = "photographyFee", + unboxingVideoFee = "unboxingVideoFee", + expertReviewsFee = "expertReviewsFee", +} diff --git a/src/pages/setting/hooks/useSettingData.ts b/src/pages/setting/hooks/useSettingData.ts index dc4cc65..3290604 100644 --- a/src/pages/setting/hooks/useSettingData.ts +++ b/src/pages/setting/hooks/useSettingData.ts @@ -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"] }); + }, + }); +}; \ No newline at end of file diff --git a/src/pages/setting/service/SettingService.ts b/src/pages/setting/service/SettingService.ts index c524130..8c72e1d 100644 --- a/src/pages/setting/service/SettingService.ts +++ b/src/pages/setting/service/SettingService.ts @@ -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 => { + 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; +}; diff --git a/src/pages/setting/types/Types.ts b/src/pages/setting/types/Types.ts index 17c4914..c6e07de 100644 --- a/src/pages/setting/types/Types.ts +++ b/src/pages/setting/types/Types.ts @@ -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; +} diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 6c838be..51f57da 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -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 = () => { } /> } /> } /> + } />