diff --git a/src/App.tsx b/src/App.tsx index 8dcfeac..06c5577 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,6 +70,15 @@ const TicketsListPage = lazy(() => import('./pages/tickets/tickets-list/TicketsL const TicketDetailPage = lazy(() => import('./pages/tickets/ticket-detail/TicketDetailPage')); const TicketConfigPage = lazy(() => import('./pages/tickets/ticket-config/TicketConfigPage')); +// Payment IPG Page +const IPGListPage = lazy(() => import('./pages/payment-ipg/ipg-list/IPGListPage')); + +// Payment Card Page +const CardFormPage = lazy(() => import('./pages/payment-card/card-form/CardFormPage')); + +// Wallet Page +const WalletListPage = lazy(() => import('./pages/wallet/wallet-list/WalletListPage')); + const ProtectedRoute = ({ children }: { children: any }) => { const { user, isLoading } = useAuth(); @@ -158,6 +167,15 @@ const AppRoutes = () => { } /> } /> } /> + + {/* Payment IPG Route */} + } /> + + {/* Payment Card Route */} + } /> + + {/* Wallet Route */} + } /> ); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 7c336bb..99a554a 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -17,7 +17,9 @@ import { Users, Truck, X, - MessageSquare + MessageSquare, + CreditCard, + Wallet } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import { PermissionWrapper } from '../common/PermissionWrapper'; @@ -123,6 +125,21 @@ const menuItems: MenuItem[] = [ icon: Truck, path: '/shipping-methods', }, + { + title: 'درگاه‌های پرداخت', + icon: CreditCard, + path: '/payment-ipg', + }, + { + title: 'پرداخت کارت به کارت', + icon: CreditCard, + path: '/payment-card', + }, + { + title: 'مدیریت کیف پول', + icon: Wallet, + path: '/wallet', + }, ] } ]; diff --git a/src/components/ui/TagInput.tsx b/src/components/ui/TagInput.tsx index ee74737..2e76be9 100644 --- a/src/components/ui/TagInput.tsx +++ b/src/components/ui/TagInput.tsx @@ -68,14 +68,14 @@ export const TagInput: React.FC = ({ {values.map((value, index) => ( {value} {!disabled && ( diff --git a/src/constant/routes.ts b/src/constant/routes.ts index 61aa90b..b7e91b2 100644 --- a/src/constant/routes.ts +++ b/src/constant/routes.ts @@ -133,4 +133,16 @@ export const API_ROUTES = { CREATE_TICKET_SUBJECT: "tickets/config/subjects", UPDATE_TICKET_SUBJECT: (id: string) => `tickets/config/subjects/${id}`, DELETE_TICKET_SUBJECT: (id: string) => `tickets/config/subjects/${id}`, + + // Payment IPG APIs + GET_IPG_STATUS: "payment/ipg/status", + UPDATE_IPG_STATUS: "payment/ipg/status", + + // Payment Card APIs + GET_PAYMENT_CARD: "payment/card", + UPDATE_PAYMENT_CARD: "payment/card", + + // Wallet APIs + GET_WALLET_STATUS: "wallet/status", + UPDATE_WALLET_STATUS: "wallet/status", }; diff --git a/src/pages/orders/core/_models.ts b/src/pages/orders/core/_models.ts index c5c24c4..f5e6077 100644 --- a/src/pages/orders/core/_models.ts +++ b/src/pages/orders/core/_models.ts @@ -20,6 +20,7 @@ export interface OrderItem { product_id: number; product_name: string; product_image?: string; + image_url?: string; variant_id?: number; variant_name?: string; product_variant_id?: number; @@ -128,6 +129,12 @@ export interface Order { notes?: string; tracking_number?: string; estimated_delivery?: string; + shipping_method_id?: number; + selected_delivery_slot?: string | { + date?: string; + from_hour?: number; + to_hour?: number; + }; created_at: string; updated_at: string; }; diff --git a/src/pages/orders/order-detail/OrderDetailPage.tsx b/src/pages/orders/order-detail/OrderDetailPage.tsx index e595712..5e21b1a 100644 --- a/src/pages/orders/order-detail/OrderDetailPage.tsx +++ b/src/pages/orders/order-detail/OrderDetailPage.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useOrder, useUpdateOrderStatus } from '../core/_hooks'; import { OrderStatus } from '../core/_models'; +import { useShippingMethods } from '@/pages/shipping-methods/core/_hooks'; import { Button } from "@/components/ui/Button"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { Modal } from "@/components/ui/Modal"; @@ -20,6 +21,16 @@ import { FileText } from 'lucide-react'; import { englishToPersian } from '@/utils/numberUtils'; +import { API_GATE_WAY } from '@/constant/routes'; + +const resolveImageUrl = (imageUrl?: string): string => { + if (!imageUrl) return ''; + const trimmedUrl = imageUrl.trim(); + if (trimmedUrl.startsWith('http://') || trimmedUrl.startsWith('https://')) { + return trimmedUrl; + } + return `${API_GATE_WAY}${trimmedUrl.startsWith('/') ? '' : '/'}${trimmedUrl}`; +}; const getStatusColor = (status: OrderStatus) => { const colors = { @@ -81,7 +92,12 @@ const OrderDetailPage = () => { const { data, isLoading, error } = useOrder(id || ''); const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus(); + const { data: shippingMethods = [] } = useShippingMethods(); const order = data?.order; + + const shippingMethod = order?.shipping_method_id + ? shippingMethods.find(method => method.id === order.shipping_method_id) + : null; const handleStatusUpdate = () => { if (id) { updateStatus( @@ -99,7 +115,6 @@ const OrderDetailPage = () => { }; if (isLoading) return ; - console.log(order) if (error || !order) { return ( @@ -143,9 +158,148 @@ const OrderDetailPage = () => { -
- {/* ستون اصلی */} -
+
+ {/* اطلاعات کاربر سفارش‌دهنده و آدرس */} +
+
+
+
+ +
+ اطلاعات مشتری و آدرس +
+
+
+
+ {/* اطلاعات کاربر */} +
+

+ + اطلاعات کاربر +

+ {order?.user ? ( +
+
+
+ {order.user.avatar ? ( + {`${order.user.first_name} + ) : ( +
+ + {(order.user.first_name?.charAt(0) || '') + (order.user.last_name?.charAt(0) || '') || 'U'} + +
+ )} +
+
+

+ {(order.user.first_name || 'نامشخص') + ' ' + (order.user.last_name || '')} +

+ + {order.user.verified ? 'تأیید شده' : 'تأیید نشده'} + +
+
+
+
+ +
+

ایمیل

+

{order.user.email || 'ایمیل نامشخص'}

+
+
+ {order.user.phone_number && ( +
+ +
+

شماره تلفن

+

+ {englishToPersian(order.user.phone_number)} +

+
+
+ )} +
+
+ ) : ( +

اطلاعات کاربر در دسترس نیست

+ )} +
+ + {/* آدرس ارسال */} +
+

+ + آدرس ارسال +

+ {order?.shipping_address ? ( +
+
+
+ {order.shipping_address.name && ( +
+ نام: + {order.shipping_address.name} +
+ )} + {order.shipping_address.address && ( +
+ آدرس: + {order.shipping_address.address} +
+ )} + {(order.shipping_address.city || order.shipping_address.state) && ( +
+ شهر/استان: + + {order.shipping_address.city || 'نامشخص'}{order.shipping_address.state ? `، ${order.shipping_address.state}` : ''} + +
+ )} + {order.shipping_address.region && ( +
+ منطقه: + {order.shipping_address.region} +
+ )} + {order.shipping_address.postal_code && ( +
+ کد پستی: + {order.shipping_address.postal_code} +
+ )} + {order.shipping_address.plaque && ( +
+ پلاک/واحد: + + {order.shipping_address.plaque}{order.shipping_address.unit ? `، واحد ${order.shipping_address.unit}` : ''} + +
+ )} + {order.shipping_address.receiving_address && ( +
+ آدرس تحویل: + {order.shipping_address.receiving_address} +
+ )} +
+
+
+ ) : ( +

آدرس ارسال در دسترس نیست

+ )} +
+
+
+
+ {/* اطلاعات سفارش */}
@@ -196,6 +350,30 @@ const OrderDetailPage = () => {

{formatDate(order.estimated_delivery)}

)} + {order?.shipping_method_id !== undefined && order?.shipping_method_id !== null && ( +
+

متد ارسال

+

+ {shippingMethod + ? shippingMethod.name + : shippingMethods.length === 0 + ? `شناسه: ${order.shipping_method_id} (در حال بارگذاری...)` + : `شناسه: ${order.shipping_method_id}` + } +

+
+ )} + {order?.selected_delivery_slot && ( +
+

زمان تحویل انتخاب شده

+

+ {typeof order.selected_delivery_slot === 'object' + ? `${order.selected_delivery_slot.date || ''} ${order.selected_delivery_slot.from_hour || ''}:${order.selected_delivery_slot.to_hour || ''}` + : String(order.selected_delivery_slot) + } +

+
+ )}
{order?.notes && (
@@ -225,16 +403,32 @@ const OrderDetailPage = () => { const baseWeight = (item.weight ?? 0) as number; const weightGr = Math.round(baseWeight * 1000); const formatFa = (n: number) => new Intl.NumberFormat('fa-IR').format(n); + const imageUrl = item.image_url || item.product_image; return (
+
+ {imageUrl && ( +
+ {item.product_name { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + }} + /> +
+ )}
{item.product_name || `محصول شناسه: ${item.product_id}`}
{item.product_variant_name || `واریانت شناسه: ${item.product_variant_id}`} +
@@ -270,87 +464,6 @@ const OrderDetailPage = () => { )}
- {/* آدرس‌ها - منتقل شده به زیر محصولات */} -
-
-
-
- -
- آدرس‌ها -
-
-
-
-

آدرس ارسال

-
-

نام: {order?.shipping_address?.name || 'نام نامشخص'}

-

آدرس: {order?.shipping_address?.address || 'آدرس نامشخص'}

-

شهر: {order?.shipping_address?.city || 'شهر نامشخص'}, استان: {order?.shipping_address?.state || 'استان نامشخص'}

-

منطقه: {order?.shipping_address?.region || 'منطقه نامشخص'}

-

کد پستی: {order?.shipping_address?.postal_code || 'نامشخص'}

- {order?.shipping_address?.plaque && ( -

پلاک: {order.shipping_address.plaque}, واحد: {order.shipping_address.unit || 'ندارد'}

- )} - {order?.shipping_address?.receiving_address && ( -

آدرس تحویل: {order.shipping_address.receiving_address}

- )} -
-
-
-
-
- - {/* ستون جانبی */} -
- {/* اطلاعات کاربر سفارش‌دهنده */} -
-
-
-
- -
- کاربر سفارش‌دهنده -
-
-
- {order?.user ? ( -
-
-

نام

-

- {(order.user.first_name || 'نامشخص') + ' ' + (order.user.last_name || '')} -

-
-
- -

{order.user.email || 'ایمیل نامشخص'}

-
- {order.user.phone_number && ( -
- -

- {englishToPersian(order.user.phone_number)} -

-
- )} -
- - {order.user.verified ? 'تأیید شده' : 'تأیید نشده'} - -
-
- ) : ( -

اطلاعات کاربر در دسترس نیست

- )} -
-
- - - {/* اطلاعات پرداخت */}
@@ -362,42 +475,42 @@ const OrderDetailPage = () => { پرداخت
-
-
- جمع اقلام - {formatCurrency(order?.net_total || 0)} +
+
+ جمع اقلام + {formatCurrency(order?.net_total || 0)}
-
- مالیات - {formatCurrency(order?.vat_total || 0)} +
+ مالیات + {formatCurrency(order?.vat_total || 0)}
{order?.base_gold_price !== undefined && ( -
- قیمت پایه طلا - {formatCurrency(order.base_gold_price)} +
+ قیمت پایه طلا + {formatCurrency(order.base_gold_price)}
)} -
- هزینه ارسال - {formatCurrency(order?.shipping_total || 0)} +
+ هزینه ارسال + {formatCurrency(order?.shipping_total || 0)}
{(order?.discount_total || 0) > 0 && ( -
- تخفیف کل - -{formatCurrency(order?.discount_total || 0)} +
+ تخفیف کل + -{formatCurrency(order?.discount_total || 0)}
)}
-
- مجموع نهایی - {formatCurrency(order?.final_total || 0)} +
+ مجموع نهایی + {formatCurrency(order?.final_total || 0)}
-
- وضعیت پرداخت - + وضعیت پرداخت + @@ -405,32 +518,31 @@ const OrderDetailPage = () => {
{Array.isArray((data as any)?.payments) && (data as any)?.payments.length > 0 && ( -
- روش پرداخت - {formatPaymentType((data as any).payments[0].payment_type)} +
+ روش پرداخت + {formatPaymentType((data as any).payments[0].payment_type)}
)} {order?.invoice_id && ( -
- شماره فاکتور - {order.invoice_id} +
+ شماره فاکتور + {order.invoice_id}
)} {Array.isArray((data as any)?.payments) && (data as any)?.payments[0]?.transaction_id && ( -
- شناسه تراکنش - {(data as any).payments[0].transaction_id} +
+ شناسه تراکنش + {(data as any).payments[0].transaction_id}
)} {Array.isArray((data as any)?.payments) && (data as any)?.payments[0]?.image_urls?.length > 0 && ( -
- رسید پرداخت +
+ رسید پرداخت رسید پرداخت
)} -
diff --git a/src/pages/orders/orders-list/OrdersListPage.tsx b/src/pages/orders/orders-list/OrdersListPage.tsx index 6450923..7b28ce1 100644 --- a/src/pages/orders/orders-list/OrdersListPage.tsx +++ b/src/pages/orders/orders-list/OrdersListPage.tsx @@ -1,26 +1,28 @@ import React, { useMemo, useState } from 'react'; -import { englishToPersian } from '@/utils/numberUtils'; +import { englishToPersian, persianToEnglish, formatWithThousands, parseFormattedNumber } from '@/utils/numberUtils'; import { useNavigate } from 'react-router-dom'; import { useOrders, useOrderStats, useUpdateOrderStatus } from '../core/_hooks'; -import { Order, OrderFilters, OrderStatus } from '../core/_models'; +import { OrderFilters, OrderStatus } from '../core/_models'; import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; import { Modal } from "@/components/ui/Modal"; import { Pagination } from "@/components/ui/Pagination"; -import { PageContainer, PageTitle, SectionTitle } from "@/components/ui/Typography"; +import { PageContainer, PageTitle } from "@/components/ui/Typography"; import { Table } from "@/components/ui/Table"; import { TableColumn } from "@/types"; +import { StatsCard } from '@/components/dashboard/StatsCard'; +import DatePicker from 'react-multi-date-picker'; +import persian from 'react-date-object/calendars/persian'; +import persian_fa from 'react-date-object/locales/persian_fa'; +import DateObject from 'react-date-object'; import { ShoppingCart, - Package, DollarSign, Clock, Search, Filter, Eye, Edit3, - TrendingUp, - Calendar + TrendingUp } from 'lucide-react'; const getStatusColor = (status: OrderStatus) => { @@ -59,26 +61,145 @@ const ListSkeleton = () => ( ); +const getDefaultFilters = (): OrderFilters => ({ + page: 1, + limit: 20, + status: 'pending', + payment_status: undefined, + search: '', + user_id: undefined, + invoice_id: undefined, + discount_code: undefined, + created_from: undefined, + created_to: undefined, + updated_from: undefined, + updated_to: undefined, + min_total: undefined, + max_total: undefined, +}); + +const toIsoDate = (date?: DateObject | null) => { + if (!date) return undefined; + try { + const g = date.convert(undefined); + const yyyy = g.year.toString().padStart(4, '0'); + const mm = g.month.toString().padStart(2, '0'); + const dd = g.day.toString().padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; + } catch { + return undefined; + } +}; + +const fromIsoDate = (value?: string) => { + if (!value) return undefined; + try { + const d = new Date(value); + if (isNaN(d.getTime())) return undefined; + return new DateObject(d).convert(persian, persian_fa); + } catch { + return undefined; + } +}; + +const buildRangeValue = (from?: string, to?: string) => { + const start = fromIsoDate(from); + const end = fromIsoDate(to); + if (start && end) return [start, end]; + if (start) return [start]; + if (end) return [end]; + return []; +}; + const OrdersListPage = () => { const navigate = useNavigate(); const [statusUpdateId, setStatusUpdateId] = useState(null); const [newStatus, setNewStatus] = useState('processing'); - const [filters, setFilters] = useState({ - page: 1, - limit: 20, - order_number: '', - status: 'pending', - payment_status: undefined, - search: '', - }); + const [filters, setFilters] = useState(getDefaultFilters()); const { data: ordersData, isLoading, error } = useOrders(filters); - // Temporarily disabled stats API - // const { data: stats, isLoading: statsLoading } = useOrderStats(!isLoading); - const stats = null; - const statsLoading = false; + const { data: stats, isLoading: statsLoading, error: statsError } = useOrderStats(true); const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus(); + const handleIdFilterChange = (key: keyof OrderFilters, raw: string) => { + const converted = persianToEnglish(raw).replace(/[^\d]/g, ''); + const numeric = converted ? Number(converted) : undefined; + setFilters(prev => ({ + ...prev, + [key]: numeric, + page: 1, + })); + }; + + const handleTextFilterChange = (key: keyof OrderFilters, value: string) => { + setFilters(prev => ({ + ...prev, + [key]: value || undefined, + page: 1, + })); + }; + + const formatNumberDisplay = (val?: number) => { + if (val === undefined || val === null || Number.isNaN(val)) return ''; + return formatWithThousands(val); + }; + + const handleAmountFilterChange = (key: keyof OrderFilters, raw: string) => { + const converted = persianToEnglish(raw); + const numeric = parseFormattedNumber(converted); + setFilters(prev => ({ + ...prev, + [key]: numeric, + page: 1, + })); + }; + + const handleDateRangeChange = (startKey: keyof OrderFilters, endKey: keyof OrderFilters, range: (DateObject | null)[] | DateObject | null) => { + if (Array.isArray(range)) { + const [start, end] = range; + setFilters(prev => ({ + ...prev, + [startKey]: toIsoDate(start), + [endKey]: toIsoDate(end), + page: 1, + })); + return; + } + setFilters(prev => ({ + ...prev, + [startKey]: toIsoDate(range as DateObject | null), + [endKey]: undefined, + page: 1, + })); + }; + + const statsItems = useMemo(() => ([ + { + title: 'کل سفارشات', + value: stats?.total_orders_count ?? 0, + icon: ShoppingCart, + color: 'yellow' as const, + }, + { + title: 'مجموع فروش', + value: stats?.total_amount_of_sale ?? 0, + icon: DollarSign, + color: 'green' as const, + }, + { + title: 'سفارش‌های در انتظار', + value: stats?.total_order_pending ?? 0, + icon: Clock, + color: 'blue' as const, + }, + { + title: 'میانگین سفارش', + value: stats?.order_avg ?? 0, + icon: TrendingUp, + color: 'purple' as const, + }, + ]), [stats]); + const columns: TableColumn[] = useMemo(() => [ { key: 'order_number', label: 'شماره سفارش', sortable: true, align: 'right', render: (v: string) => `#${v}` }, @@ -171,64 +292,24 @@ const OrdersListPage = () => { - {/* آمار کلی */} -
-
-
-
- -
-
-

کل سفارشات

-

- -- -

-
-
-
- -
-
-
- -
-
-

کل فروش

-

- -- -

-
-
-
- -
-
-
- -
-
-

در انتظار

-

- -- -

-
-
-
- -
-
-
- -
-
-

میانگین سفارش

-

- -- -

-
-
-
+
+ {statsLoading ? ( + <> + {[...Array(4)].map((_, idx) => ( +
+ ))} + + ) : ( + statsItems.map((stat, index) => ( + + )) + )}
+ {statsError && ( +
+ خطا در دریافت آمار سفارشات +
+ )} {/* فیلترها */}
@@ -237,7 +318,7 @@ const OrdersListPage = () => { setFilters(prev => ({ ...prev, search: e.target.value, page: 1 }))} className="w-full pr-10 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-300" @@ -245,6 +326,42 @@ const OrdersListPage = () => {
+ + handleIdFilterChange('user_id', e.target.value)} + placeholder="مثلا 1024" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> +
+ +
+ + handleIdFilterChange('invoice_id', e.target.value)} + placeholder="invoice_id" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> +
+ +
+ + handleTextFilterChange('discount_code', e.target.value)} + placeholder="مثلا SPRING2025" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> +
+ +
+ setFilters(prev => ({ ...prev, payment_status: e.target.value as any || undefined, page: 1 }))} @@ -275,9 +393,67 @@ const OrdersListPage = () => {
+
+ + handleAmountFilterChange('min_total', e.target.value)} + placeholder="مثلا 3000000" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> +
+ +
+ + handleAmountFilterChange('max_total', e.target.value)} + placeholder="مثلا 9000000" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100" + /> +
+ +
+ + handleDateRangeChange('created_from', 'created_to', range as any)} + format="YYYY/MM/DD" + range + calendar={persian} + locale={persian_fa} + calendarPosition="bottom-center" + inputClass="w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500" + containerClassName="w-full" + editable={false} + placeholder="از تاریخ / تا تاریخ" + /> +
+ +
+ + handleDateRangeChange('updated_from', 'updated_to', range as any)} + format="YYYY/MM/DD" + range + calendar={persian} + locale={persian_fa} + calendarPosition="bottom-center" + inputClass="w-full px-3 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-500" + containerClassName="w-full" + editable={false} + placeholder="از تاریخ / تا تاریخ" + /> +
+ +
+ + + ); +}; + +export default CardFormPage; + diff --git a/src/pages/payment-card/core/_hooks.ts b/src/pages/payment-card/core/_hooks.ts new file mode 100644 index 0000000..ab37247 --- /dev/null +++ b/src/pages/payment-card/core/_hooks.ts @@ -0,0 +1,30 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { QUERY_KEYS } from "@/utils/query-key"; +import toast from "react-hot-toast"; +import { getPaymentCard, updatePaymentCard } from "./_requests"; +import { UpdatePaymentCardRequest } from "./_models"; + +export const usePaymentCard = () => { + return useQuery({ + queryKey: [QUERY_KEYS.GET_PAYMENT_CARD], + queryFn: getPaymentCard, + }); +}; + +export const useUpdatePaymentCard = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: UpdatePaymentCardRequest) => updatePaymentCard(payload), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [QUERY_KEYS.GET_PAYMENT_CARD], + }); + toast.success("اطلاعات کارت با موفقیت به‌روزرسانی شد"); + }, + onError: (error: any) => { + toast.error(error?.message || "خطا در به‌روزرسانی اطلاعات کارت"); + }, + }); +}; + diff --git a/src/pages/payment-card/core/_models.ts b/src/pages/payment-card/core/_models.ts new file mode 100644 index 0000000..9be6540 --- /dev/null +++ b/src/pages/payment-card/core/_models.ts @@ -0,0 +1,19 @@ +export interface PaymentCard { + bank_name: string; + card_number: string; + name: string; + is_active: boolean; + updated_at?: string; +} + +export interface UpdatePaymentCardRequest { + bank_name: string; + card_number: string; + name: string; + is_active: boolean; +} + +export interface UpdatePaymentCardResponse { + success?: boolean; +} + diff --git a/src/pages/payment-card/core/_requests.ts b/src/pages/payment-card/core/_requests.ts new file mode 100644 index 0000000..ca41cc9 --- /dev/null +++ b/src/pages/payment-card/core/_requests.ts @@ -0,0 +1,21 @@ +import { httpGetRequest, httpPutRequest, APIUrlGenerator } from "@/utils/baseHttpService"; +import { API_ROUTES } from "@/constant/routes"; +import { PaymentCard, UpdatePaymentCardRequest, UpdatePaymentCardResponse } from "./_models"; + +export const getPaymentCard = async (): Promise => { + const response = await httpGetRequest( + APIUrlGenerator(API_ROUTES.GET_PAYMENT_CARD) + ); + return response.data; +}; + +export const updatePaymentCard = async ( + payload: UpdatePaymentCardRequest +): Promise => { + const response = await httpPutRequest( + APIUrlGenerator(API_ROUTES.UPDATE_PAYMENT_CARD), + payload + ); + return response.data; +}; + diff --git a/src/pages/payment-ipg/core/_hooks.ts b/src/pages/payment-ipg/core/_hooks.ts new file mode 100644 index 0000000..de18574 --- /dev/null +++ b/src/pages/payment-ipg/core/_hooks.ts @@ -0,0 +1,34 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { QUERY_KEYS } from "@/utils/query-key"; +import toast from "react-hot-toast"; +import { getIPGStatus, updateIPGStatus } from "./_requests"; +import { UpdateIPGStatusRequest } from "./_models"; + +export const useIPGStatus = () => { + return useQuery({ + queryKey: [QUERY_KEYS.GET_IPG_STATUS], + queryFn: getIPGStatus, + }); +}; + +export const useUpdateIPGStatus = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload: UpdateIPGStatusRequest) => updateIPGStatus(payload), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [QUERY_KEYS.GET_IPG_STATUS], + }); + toast.success("وضعیت درگاه پرداخت با موفقیت به‌روزرسانی شد"); + }, + onError: (error: any) => { + const errorMessage = + error?.response?.data?.error === "validation failed" + ? "مقدار وارد شده معتبر نیست" + : error?.message || "خطا در به‌روزرسانی وضعیت درگاه پرداخت"; + toast.error(errorMessage); + }, + }); +}; + diff --git a/src/pages/payment-ipg/core/_models.ts b/src/pages/payment-ipg/core/_models.ts new file mode 100644 index 0000000..587a58c --- /dev/null +++ b/src/pages/payment-ipg/core/_models.ts @@ -0,0 +1,29 @@ +export type IPGType = "cafe_bazaar" | "cep" | "fadax" | "sep" | "zarinpal"; + +export interface IPGStatus { + ipg_type: IPGType; + is_active: boolean; + updated_at: string; +} + +export interface IPGStatusResponse { + statuses: IPGStatus[]; +} + +export interface UpdateIPGStatusRequest { + ipg_type: IPGType; + is_active: boolean; +} + +export interface UpdateIPGStatusResponse { + success: boolean; +} + +export const IPG_LABELS: Record = { + cafe_bazaar: "کافه‌بازار", + cep: "CEP", + fadax: "فدکس", + sep: "سامان", + zarinpal: "زرین‌پال", +}; + diff --git a/src/pages/payment-ipg/core/_requests.ts b/src/pages/payment-ipg/core/_requests.ts new file mode 100644 index 0000000..3415d0f --- /dev/null +++ b/src/pages/payment-ipg/core/_requests.ts @@ -0,0 +1,21 @@ +import { httpGetRequest, httpPutRequest, APIUrlGenerator } from "@/utils/baseHttpService"; +import { API_ROUTES } from "@/constant/routes"; +import { IPGStatusResponse, UpdateIPGStatusRequest, UpdateIPGStatusResponse } from "./_models"; + +export const getIPGStatus = async (): Promise => { + const response = await httpGetRequest( + APIUrlGenerator(API_ROUTES.GET_IPG_STATUS) + ); + return response.data; +}; + +export const updateIPGStatus = async ( + payload: UpdateIPGStatusRequest +): Promise => { + const response = await httpPutRequest( + APIUrlGenerator(API_ROUTES.UPDATE_IPG_STATUS), + payload + ); + return response.data; +}; + diff --git a/src/pages/payment-ipg/ipg-list/IPGListPage.tsx b/src/pages/payment-ipg/ipg-list/IPGListPage.tsx new file mode 100644 index 0000000..9bb04e1 --- /dev/null +++ b/src/pages/payment-ipg/ipg-list/IPGListPage.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { CreditCard, Loader2 } from 'lucide-react'; +import { PageContainer, PageTitle } from '@/components/ui/Typography'; +import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; +import { useIPGStatus, useUpdateIPGStatus } from '../core/_hooks'; +import { IPGStatus, IPG_LABELS } from '../core/_models'; + +const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('fa-IR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +const ToggleSwitch = ({ + checked, + onChange, + disabled, +}: { + checked: boolean; + onChange: (checked: boolean) => void; + disabled?: boolean; +}) => { + return ( +
-
- - -
@@ -112,6 +141,98 @@ const ShippingMethodFormPage = () => {
+
+ + +
+
+
+
+ +

بازه‌های زمانی را به ساعت ۲۴ ساعته وارد کنید

+
+ +
+
+ {form.open_hours.map((item, index) => ( +
+
+
+ + { + const value = (e.target as HTMLInputElement).value; + setForm(prev => { + const open_hours = [...prev.open_hours]; + open_hours[index] = { ...open_hours[index], from_hour: value }; + return { ...prev, open_hours }; + }); + }} + numeric + placeholder="مثلاً 9" + /> +
+
+ + { + const value = (e.target as HTMLInputElement).value; + setForm(prev => { + const open_hours = [...prev.open_hours]; + open_hours[index] = { ...open_hours[index], to_hour: value }; + return { ...prev, open_hours }; + }); + }} + numeric + placeholder="مثلاً 18" + /> +
+
+ +
+
+
+ ))} +
+
+
+ setForm(prev => ({ ...prev, addresses: values }))} + label="محدوده‌های پوشش" + placeholder="آدرس را تایپ و Enter کنید" + /> +