diff --git a/src/helpers/func.ts b/src/helpers/func.ts index 8d890a8..d32a934 100644 --- a/src/helpers/func.ts +++ b/src/helpers/func.ts @@ -53,3 +53,11 @@ export const formatFaNumber = (value?: number | null): string => { } return value.toLocaleString("fa-IR"); }; + +export const formatPrice = (price: number): string => { + return new Intl.NumberFormat("fa-IR", { + style: "currency", + currency: "IRR", + minimumFractionDigits: 0, + }).format(price); +}; diff --git a/src/langs/fa.json b/src/langs/fa.json index e508db9..746d037 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -840,5 +840,18 @@ "payment_methods_list": "لیست روش های پرداخت", "payment_methods_add": "افزودن روش پرداخت", "payment_methods_update": "ویرایش روش پرداخت" + }, + "order_status": { + "pending": "در انتظار تایید", + "confirmed": "تایید شده", + "preparing": "در حال آماده سازی", + "rejectedByRestaurant": "رد شده توسط رستوران", + "cancelledByUser": "لغو شده توسط کاربر", + "cancelledBySystem": "لغو شده توسط سیستم", + "readyForCustomerPickup": "آماده برای دریافت مشتری", + "readyForDineIn": "آماده برای سرو در محل", + "readyForDeliveryCar": "آماده برای تحویل با ماشین", + "readyForDeliveryCourier": "آماده برای تحویل با پیک", + "delivered": "تحویل داده شده" } } diff --git a/src/pages/orders/List.tsx b/src/pages/orders/List.tsx index 551c473..3d26af7 100644 --- a/src/pages/orders/List.tsx +++ b/src/pages/orders/List.tsx @@ -1,231 +1,52 @@ -import { type FC, useState } from 'react' -import Button from '@/components/Button' +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' import Table from '@/components/Table' import Filters from '@/components/Filters' -import { Add, Eye } from 'iconsax-react' -import type { RowDataType, ActionType } from '@/components/types/TableTypes' -import type { FilterValues } from '@/components/Filters' -import { Link } from 'react-router-dom' -import { Pages } from '@/config/Pages' - -interface OrderData extends RowDataType { - id: number - orderNumber: string - customerName: string - orderAmount: string - orderTime: string - deliveryType: string - paymentType: string - status: string -} +import { useGetOrders } from './hooks/useGetOrders' +import { useOrderFilters } from './hooks/useOrderFilters' +import { getOrderTableColumns } from './components/OrderTableColumns' +import { useOrderFiltersFields } from './components/OrderFiltersFields' +import type { Order } from './types/Types' +import type { RowDataType } from '@/components/types/TableTypes' const OrdersList: FC = () => { - const [currentPage, setCurrentPage] = useState(1) - const totalPages = 2 + const { t } = useTranslation('global') + const { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + } = useOrderFilters() - const handlePageChange = (page: number) => { - setCurrentPage(page) - } + const { data: ordersData, isLoading } = useGetOrders(apiParams) - const handleFiltersChange = (newFilters: FilterValues) => { - console.log('فیلترها:', newFilters) - } + const orders = ordersData?.data || [] + const columns = getOrderTableColumns({ t }) + const filterFields = useOrderFiltersFields() - const handleRowActions = (item: OrderData): ActionType[] => { - return [ - { - label: 'ویرایش', - onClick: () => { - console.log('ویرایش سفارش:', item.id) - }, - }, - { - label: 'حذف', - onClick: () => { - console.log('حذف سفارش:', item.id) - }, - }, - ] - } - - const getStatusColor = (status: string) => { - switch (status) { - case 'در حال آماده سازی': - return 'text-orange-500' - case 'تایید نشده': - return 'text-red-500' - case 'تحویل داده شده': - return 'text-green-500' - default: - return '' - } - } - - const columns = [ - { - key: 'orderNumber', - title: 'شماره سفارش', - }, - { - key: 'customerName', - title: 'نام مشتری', - }, - { - key: 'orderAmount', - title: 'مبلغ سفارش', - }, - { - key: 'orderTime', - title: 'زمان سفارش', - }, - { - key: 'deliveryType', - title: 'نوع تحویل', - }, - { - key: 'paymentType', - title: 'نوع پرداخت', - }, - { - key: 'status', - title: 'وضعیت', - render: (item: OrderData) => { - return ( - - {item.status} - - ) - }, - }, - { - key: 'details', - title: 'جزییات', - render: (item: OrderData) => { - return ( - - - - ) - }, - }, - ] - - const data: OrderData[] = [ - { - id: 1, - orderNumber: '12345', - customerName: 'مهرداد مظفری', - orderAmount: '۴۲۰۰۰۰ تومان', - orderTime: '۱۴۰۳/۱۱/۲۵ ۱۳:۰۰:۰۰', - deliveryType: 'تحویل با پیک', - paymentType: 'پرداخت آنلاین', - status: 'در حال آماده سازی', - }, - { - id: 2, - orderNumber: '12345', - customerName: 'مهرداد مظفری', - orderAmount: '۴۲۰۰۰۰ تومان', - orderTime: '۱۴۰۳/۱۱/۲۵ ۱۳:۰۰:۰۰', - deliveryType: 'تحویل با پیک', - paymentType: 'پرداخت آنلاین', - status: 'در انتظار تایید', - }, - { - id: 3, - orderNumber: '12345', - customerName: 'مهرداد مظفری', - orderAmount: '۴۲۰۰۰۰ تومان', - orderTime: '۱۴۰۳/۱۱/۲۵ ۱۳:۰۰:۰۰', - deliveryType: 'تحویل با پیک', - paymentType: 'پرداخت آنلاین', - status: 'تایید نشده', - }, - { - id: 4, - orderNumber: '12345', - customerName: 'مهرداد مظفری', - orderAmount: '۴۲۰۰۰۰ تومان', - orderTime: '۱۴۰۳/۱۱/۲۵ ۱۳:۰۰:۰۰', - deliveryType: 'تحویل با پیک', - paymentType: 'پرداخت با کیف پول', - status: 'تحویل داده شده', - }, - { - id: 5, - orderNumber: '12345', - customerName: 'مهرداد مظفری', - orderAmount: '۴۲۰۰۰۰ تومان', - orderTime: '۱۴۰۳/۱۱/۲۵ ۱۳:۰۰:۰۰', - deliveryType: 'سرو در رستوران', - paymentType: 'پرداخت در محل', - status: 'تحویل داده شده', - }, - { - id: 6, - orderNumber: '12345', - customerName: 'مهرداد مظفری', - orderAmount: '۴۲۰۰۰۰ تومان', - orderTime: '۱۴۰۳/۱۱/۲۵ ۱۳:۰۰:۰۰', - deliveryType: 'تحویل در ماشین', - paymentType: 'پرداخت آنلاین', - status: 'تحویل داده شده', - }, - ] + const totalPages = Math.ceil(orders.length / limit) || 1 return (

لیست سفارشات

-
- columns={columns} - data={data} - rowActions={handleRowActions} + data={orders as (Order & RowDataType)[]} + isloading={isLoading} pagination={{ currentPage, totalPages, diff --git a/src/pages/orders/components/OrderFiltersFields.tsx b/src/pages/orders/components/OrderFiltersFields.tsx new file mode 100644 index 0000000..cb4494c --- /dev/null +++ b/src/pages/orders/components/OrderFiltersFields.tsx @@ -0,0 +1,39 @@ +import { useMemo } from 'react' +import type { FieldType } from '@/components/Filters' + +export const useOrderFiltersFields = (): FieldType[] => { + return useMemo(() => [ + { + type: 'input', + name: 'search', + placeholder: 'جستجو (شماره سفارش یا نام مشتری)', + }, + { + type: 'select', + name: 'status', + placeholder: 'وضعیت', + defaultValue: '', + options: [ + { label: 'همه', value: '' }, + { label: 'در انتظار تایید', value: 'PENDING' }, + { label: 'در حال آماده سازی', value: 'PREPARING' }, + { label: 'آماده تحویل', value: 'READY' }, + { label: 'تحویل داده شده', value: 'DELIVERED' }, + { label: 'لغو شده', value: 'CANCELLED' }, + { label: 'تایید نشده', value: 'REJECTED' }, + ], + }, + { + type: 'select', + name: 'deliveryType', + placeholder: 'نوع تحویل', + defaultValue: '', + options: [ + { label: 'همه', value: '' }, + { label: 'تحویل با پیک', value: 'DELIVERY' }, + { label: 'سرو در رستوران', value: 'DINE_IN' }, + { label: 'تحویل در ماشین', value: 'PICKUP' }, + ], + }, + ], []) +} diff --git a/src/pages/orders/components/OrderTableColumns.tsx b/src/pages/orders/components/OrderTableColumns.tsx new file mode 100644 index 0000000..02ca3c1 --- /dev/null +++ b/src/pages/orders/components/OrderTableColumns.tsx @@ -0,0 +1,109 @@ +import { Eye } from 'iconsax-react' +import type { ColumnType } from '@/components/types/TableTypes' +import type { Order } from '../types/Types' +import { formatPrice, formatOptionalDate } from '@/helpers/func' +import Status from '@/components/Status' +import { Link } from 'react-router-dom' +import { Pages } from '@/config/Pages' +import { formatFaNumber } from '@/helpers/func' +import type { TFunction } from 'i18next' + +interface GetOrderTableColumnsParams { + onDelete?: (id: string) => void + isDeleting?: boolean + t: TFunction +} + +const getStatusVariant = (status: string): 'success' | 'error' | 'warning' | 'info' | 'pending' => { + const variantMap: Record = { + 'pending': 'pending', + 'confirmed': 'info', + 'preparing': 'warning', + 'rejectedByRestaurant': 'error', + 'cancelledByUser': 'error', + 'cancelledBySystem': 'error', + 'readyForCustomerPickup': 'info', + 'readyForDineIn': 'info', + 'readyForDeliveryCar': 'info', + 'readyForDeliveryCourier': 'info', + 'delivered': 'success', + } + return variantMap[status] || 'pending' +} + +export const getOrderTableColumns = ({ t }: GetOrderTableColumnsParams): ColumnType[] => { + return [ + { + key: 'orderNumber', + title: 'شماره سفارش', + render: (item: Order) => { + return formatFaNumber(item.orderNumber) + } + }, + { + key: 'customerName', + title: 'نام مشتری', + render: (item: Order) => { + const name = [item.user.firstName, item.user.lastName].filter(Boolean).join(' ') + return name || '-' + } + }, + { + key: 'total', + title: 'مبلغ سفارش', + render: (item: Order) => { + return formatPrice(item.total) + } + }, + { + key: 'createdAt', + title: 'زمان سفارش', + render: (item: Order) => { + return formatOptionalDate(item.createdAt, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } + }, + { + key: 'deliveryMethod', + title: 'نوع تحویل', + render: (item: Order) => { + return item.deliveryMethod?.title || '-' + } + }, + { + key: 'paymentMethod', + title: 'نوع پرداخت', + render: (item: Order) => { + return item.paymentMethod?.title || '-' + } + }, + { + key: 'status', + title: 'وضعیت', + render: (item: Order) => { + return ( + + ) + } + }, + { + key: 'details', + title: '', + render: (item: Order) => { + return ( + + + + ) + } + }, + ] +} diff --git a/src/pages/orders/enum/Enum.ts b/src/pages/orders/enum/Enum.ts new file mode 100644 index 0000000..fb9347e --- /dev/null +++ b/src/pages/orders/enum/Enum.ts @@ -0,0 +1,17 @@ +export const enum OrderStatus { + Pending = "pending", + + Confirmed = "confirmed", + Preparing = "preparing", + + RejectedByRestaurant = "rejectedByRestaurant", + CancelledByUser = "cancelledByUser", + CancelledBySystem = "cancelledBySystem", + + ReadyForCustomerPickup = "readyForCustomerPickup", + ReadyForDineIn = "readyForDineIn", + ReadyForDeliveryCar = "readyForDeliveryCar", + ReadyForDeliveryCourier = "readyForDeliveryCourier", + + Delivered = "delivered", +} diff --git a/src/pages/orders/hooks/useGetOrders.ts b/src/pages/orders/hooks/useGetOrders.ts new file mode 100644 index 0000000..931a4f6 --- /dev/null +++ b/src/pages/orders/hooks/useGetOrders.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import * as api from "../service/OrderService"; +import type { GetOrdersParams } from "../service/OrderService"; + +export const useGetOrders = (params?: GetOrdersParams) => { + return useQuery({ + queryKey: ["orders", params], + queryFn: () => api.getOrders(params), + }); +}; diff --git a/src/pages/orders/hooks/useOrderFilters.ts b/src/pages/orders/hooks/useOrderFilters.ts new file mode 100644 index 0000000..3d99ed0 --- /dev/null +++ b/src/pages/orders/hooks/useOrderFilters.ts @@ -0,0 +1,50 @@ +import { useState, useMemo } from "react"; +import type { FilterValues } from "@/components/Filters"; + +const DEFAULT_PAGE = 1; +const DEFAULT_LIMIT = 10; + +export const useOrderFilters = () => { + const [filters, setFilters] = useState({}); + const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE); + const [limit] = useState(DEFAULT_LIMIT); + + const apiParams = useMemo(() => { + const params: Record = { + page: currentPage, + limit: limit, + }; + + if (filters.search) { + params.search = filters.search as string; + } + + if (filters.status) { + params.status = filters.status as string; + } + + if (filters.deliveryType) { + params.deliveryType = filters.deliveryType as string; + } + + return params; + }, [filters, currentPage, limit]); + + const handleFiltersChange = (newFilters: FilterValues) => { + setFilters(newFilters); + setCurrentPage(DEFAULT_PAGE); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + return { + filters, + currentPage, + apiParams, + handleFiltersChange, + handlePageChange, + limit, + }; +}; diff --git a/src/pages/orders/service/OrderService.ts b/src/pages/orders/service/OrderService.ts new file mode 100644 index 0000000..1b5c829 --- /dev/null +++ b/src/pages/orders/service/OrderService.ts @@ -0,0 +1,19 @@ +import axios from "@/config/axios"; +import type { GetOrdersResponse } from "../types/Types"; + +export interface GetOrdersParams { + page?: number; + limit?: number; + search?: string; + status?: string; + deliveryType?: string; +} + +export const getOrders = async ( + params?: GetOrdersParams +): Promise => { + const { data } = await axios.get("/admin/orders", { + params, + }); + return data; +}; diff --git a/src/pages/orders/types/Types.ts b/src/pages/orders/types/Types.ts new file mode 100644 index 0000000..bd73843 --- /dev/null +++ b/src/pages/orders/types/Types.ts @@ -0,0 +1,171 @@ +import type { IResponse } from "@/types/response.types"; +import type { RowDataType } from "@/components/types/TableTypes"; + +export interface OrderUser { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + firstName: string; + lastName: string; + birthDate: string; + marriageDate: string; + referrer: string | null; + isActive: boolean; + gender: boolean; + wallet: number; + points: number; + phone: string; +} + +export interface ServiceArea { + type: string; + coordinates: number[][][]; +} + +export interface OrderRestaurant { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + name: string; + slug: string; + logo: string | null; + address: string | null; + menuColor: string | null; + latitude: number | null; + longitude: number | null; + serviceArea: ServiceArea; + isActive: boolean; + establishedYear: number | null; + phoneNumber: string | null; + phone: string; + instagram: string | null; + telegram: string | null; + whatsapp: string | null; + description: string | null; + seoTitle: string | null; + seoDescription: string | null; + tagNames: string | null; + images: string[] | null; + vat: number; + domain: string; +} + +export interface OrderDeliveryMethod { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + title: string; + method: string; + restaurant: string; + deliveryFee: number; + minOrderPrice: number; + description: string; + enabled: boolean; + order: number; +} + +export interface OrderAddress { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + user: string; + title: string; + address: string; + city: string; + province: string; + postalCode: string; + latitude: number; + longitude: number; + phone: string; + isDefault: boolean; +} + +export interface OrderPaymentMethod { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + restaurant: string; + title: string; + method: string; + gateway: string | null; + description: string; + enabled: boolean; + order: number; + merchantId: string | null; +} + +export interface OrderFood { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + restaurant: string; + category: string; + title: string; + desc: string; + content: string | null; + price: number; + points: number | null; + order: number | null; + prepareTime: number | null; + sat: boolean; + sun: boolean; + mon: boolean; + breakfast: boolean; + noon: boolean; + dinner: boolean; + stock: number; + stockDefault: number; + isActive: boolean; + images: string[]; + inPlaceServe: boolean; + pickupServe: boolean; + rate: number; + discount: number; + isSpecialOffer: boolean; +} + +export interface OrderItem { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + order: string; + food: OrderFood; + quantity: number; + unitPrice: number; + discount: number; + totalPrice: number; +} + +export interface Order extends RowDataType { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + user: OrderUser; + restaurant: OrderRestaurant; + deliveryMethod: OrderDeliveryMethod; + address: OrderAddress; + paymentMethod: OrderPaymentMethod; + orderNumber: number; + couponDiscount: number; + couponDetail: unknown | null; + itemsDiscount: number; + totalDiscount: number; + subTotal: number; + tax: number; + deliveryFee: number; + total: number; + totalItems: number; + status: string; + paymentStatus: string; + items: OrderItem[]; +} + +export type GetOrdersResponse = IResponse;