From 2479f5d1da570f059a8dfe948d6413608a56da65 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 26 Nov 2025 12:05:31 +0330 Subject: [PATCH] customers list --- src/helpers/func.ts | 28 ++ .../admin/components/AdminTableColumns.tsx | 3 +- src/pages/comments/List.tsx | 13 +- src/pages/customers/List.tsx | 277 ++++++++---------- src/pages/customers/hooks/useUsersData.ts | 9 + src/pages/customers/service/UsersService.ts | 7 + src/pages/customers/types/Types.ts | 32 ++ .../roles/components/RoleTableColumns.tsx | 3 +- .../components/ScheduleTableColumns.tsx | 3 +- .../components/ShipmentMethodTableColumns.tsx | 5 +- 10 files changed, 207 insertions(+), 173 deletions(-) create mode 100644 src/pages/customers/hooks/useUsersData.ts create mode 100644 src/pages/customers/service/UsersService.ts create mode 100644 src/pages/customers/types/Types.ts diff --git a/src/helpers/func.ts b/src/helpers/func.ts index c3f4700..8d890a8 100644 --- a/src/helpers/func.ts +++ b/src/helpers/func.ts @@ -25,3 +25,31 @@ export const formatDateShort = (dateString: string): string => { return dateString; } }; + +export const formatOptionalDate = ( + value?: string | null, + options?: Intl.DateTimeFormatOptions +): string => { + if (!value) { + return "-"; + } + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return "-"; + } + return new Intl.DateTimeFormat( + "fa-IR", + options ?? { + year: "numeric", + month: "2-digit", + day: "2-digit", + } + ).format(date); +}; + +export const formatFaNumber = (value?: number | null): string => { + if (value === undefined || value === null || Number.isNaN(value)) { + return "0"; + } + return value.toLocaleString("fa-IR"); +}; diff --git a/src/pages/admin/components/AdminTableColumns.tsx b/src/pages/admin/components/AdminTableColumns.tsx index 81b9676..096edf4 100644 --- a/src/pages/admin/components/AdminTableColumns.tsx +++ b/src/pages/admin/components/AdminTableColumns.tsx @@ -4,6 +4,7 @@ import TrashWithConfrim from '@/components/TrashWithConfrim' import { Eye } from 'iconsax-react' import { Link } from 'react-router-dom' import { Pages } from '@/config/Pages' +import { formatOptionalDate } from '@/helpers/func' interface GetAdminTableColumnsParams { onDelete?: (id: string) => void @@ -44,7 +45,7 @@ export const getAdminTableColumns = ({ onDelete, isDeleting }: GetAdminTableColu key: 'createdAt', title: 'تاریخ ایجاد', render: (item: Admin) => { - return new Date(item.createdAt).toLocaleDateString('fa-IR') + return formatOptionalDate(item.createdAt) } }, { diff --git a/src/pages/comments/List.tsx b/src/pages/comments/List.tsx index abf5197..8358e57 100644 --- a/src/pages/comments/List.tsx +++ b/src/pages/comments/List.tsx @@ -7,7 +7,7 @@ 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' -import moment from 'moment-jalaali' +import { formatOptionalDate } from '@/helpers/func' interface CommentData extends RowDataType { id: number @@ -49,15 +49,6 @@ const CommentsList: FC = () => { ] } - const formatDate = (dateString: string): string => { - try { - const jalaliDate = moment(dateString, 'YYYY-MM-DD').format('jYYYY/jMM/jDD') - return `${jalaliDate}` - } catch { - return dateString - } - } - const handleDisplayToggle = (item: CommentData, value: boolean) => { console.log('تغییر وضعیت نمایش کامنت:', item.id, value) } @@ -87,7 +78,7 @@ const CommentsList: FC = () => { key: 'date', title: 'تاریخ', render: (item: CommentData) => { - return {formatDate(item.date)} + return {formatOptionalDate(item.date)} }, }, { diff --git a/src/pages/customers/List.tsx b/src/pages/customers/List.tsx index fb96277..8927d12 100644 --- a/src/pages/customers/List.tsx +++ b/src/pages/customers/List.tsx @@ -1,205 +1,168 @@ import Button from '@/components/Button' import Table from '@/components/Table' -import Filters from '@/components/Filters' +import Filters, { type FilterValues, type FieldType } from '@/components/Filters' import { Add, Eye } from 'iconsax-react' -import { type FC, useState } from 'react' -import type { RowDataType, ActionType } from '@/components/types/TableTypes' -import type { FilterValues } from '@/components/Filters' +import { type FC, useState, useMemo, useCallback } from 'react' import { Pages } from '@/config/Pages' import { Link } from 'react-router-dom' +import { useGetUsers } from './hooks/useUsersData' +import type { Customer, CustomerListRow } from '@/pages/customers/types/Types' +import { formatOptionalDate, formatFaNumber } from '@/helpers/func' -interface CustomerData extends RowDataType { - id: number - name: string - phone: string - email: string - customerCode: string - ordersCount: number - ticketsCount: number +const mapCustomerToRow = (customer: Customer): CustomerListRow => { + const fullName = [customer.firstName, customer.lastName].filter(Boolean).join(' ').trim() + return { + id: customer.id, + fullName: fullName || 'بدون نام', + phone: customer.phone || '-', + birthDate: formatOptionalDate(customer.birthDate), + marriageDate: formatOptionalDate(customer.marriageDate), + wallet: formatFaNumber(customer.wallet), + points: formatFaNumber(customer.points), + status: customer.isActive ? 'فعال' : 'غیرفعال', + } } const CustomersList: FC = () => { + const { data: usersData, isLoading } = useGetUsers() + const [filters, setFilters] = useState({ search: '', status: 'all' }) const [currentPage, setCurrentPage] = useState(1) - const totalPages = 10 + const limit = 10 - const handlePageChange = (page: number) => { - setCurrentPage(page) - } - - const handleFiltersChange = (newFilters: FilterValues) => { - // اینجا می‌توانید API call جدید برای فیلتر کردن داده‌ها بزنید - console.log('فیلترها:', newFilters) - } - - const handleRowActions = (item: CustomerData): ActionType[] => { - return [ - { - label: 'ویرایش', - onClick: () => { - console.log('ویرایش مشتری:', item.id) - }, - }, - { - label: 'حذف', - onClick: () => { - console.log('حذف مشتری:', item.id) - }, - }, - ] - } - - const columns = [ + const filterFields = useMemo(() => [ { - key: 'name', - title: 'نام', + type: 'input', + name: 'search', + placeholder: 'نام یا شماره تماس', + }, + { + type: 'select', + name: 'status', + placeholder: 'انتخاب وضعیت', + defaultValue: 'all', + options: [ + { label: 'همه وضعیت‌ها', value: 'all' }, + { label: 'فعال', value: 'active' }, + { label: 'غیرفعال', value: 'inactive' }, + ], + }, + ], []) + + const customers = useMemo(() => usersData?.data ?? [], [usersData]) + + const filteredCustomers = useMemo(() => { + const searchValue = (filters.search || '').toLowerCase().trim() + const statusFilter = filters.status || 'all' + + return customers.filter((customer) => { + const fullName = [customer.firstName, customer.lastName].filter(Boolean).join(' ').toLowerCase().trim() + const matchesSearch = + searchValue === '' || + fullName.includes(searchValue) || + (customer.phone || '').includes(searchValue) + + const matchesStatus = + statusFilter === 'all' || + (statusFilter === 'active' && customer.isActive) || + (statusFilter === 'inactive' && !customer.isActive) + + return matchesSearch && matchesStatus + }) + }, [customers, filters]) + + const totalPages = Math.max(1, Math.ceil(filteredCustomers.length / limit) || 1) + + const paginatedCustomers = useMemo(() => { + const startIndex = (currentPage - 1) * limit + return filteredCustomers.slice(startIndex, startIndex + limit) + }, [filteredCustomers, currentPage, limit]) + + const tableData = useMemo(() => paginatedCustomers.map(mapCustomerToRow), [paginatedCustomers]) + + const columns = useMemo(() => [ + { + key: 'fullName', + title: 'نام و نام خانوادگی', }, { key: 'phone', title: 'شماره تماس', }, { - key: 'email', - title: 'ایمیل', + key: 'birthDate', + title: 'تاریخ تولد', }, { - key: 'customerCode', - title: 'کد مشتری', + key: 'marriageDate', + title: 'تاریخ ازدواج', }, { - key: 'ordersCount', - title: 'سفارشات', - render: (item: CustomerData) => { - return ( - - ) - }, + key: 'wallet', + title: 'کیف پول (تومان)', }, { - key: 'ticketsCount', - title: 'تیکت‌ها', - render: (item: CustomerData) => { - return ( - - ) - }, + key: 'points', + title: 'امتیاز', + }, + { + key: 'status', + title: 'وضعیت', + render: (item: CustomerListRow) => ( + + {item.status} + + ), }, { key: 'details', - title: 'جزییات', - render: (item: CustomerData) => { - return ( - - - - ) - }, + title: 'جزئیات', + render: (item: CustomerListRow) => ( + + + + ), }, - ] + ], []) - const data: CustomerData[] = [ - { - id: 1, - name: 'مهرداد مظفری', - phone: '۰۹۱۲۹۲۸۳۳۹۵', - email: 'info@example.com', - customerCode: '۱۲۲', - ordersCount: 4, - ticketsCount: 4, - }, - { - id: 2, - name: 'مهرداد مظفری', - phone: '۰۹۱۲۹۲۸۳۳۹۵', - email: 'info@example.com', - customerCode: '۱۲۲', - ordersCount: 4, - ticketsCount: 4, - }, - { - id: 3, - name: 'مهرداد مظفری', - phone: '۰۹۱۲۹۲۸۳۳۹۵', - email: 'info@example.com', - customerCode: '۱۲۲', - ordersCount: 4, - ticketsCount: 4, - }, - { - id: 4, - name: 'مهرداد مظفری', - phone: '۰۹۱۲۹۲۸۳۳۹۵', - email: 'info@example.com', - customerCode: '۱۲۲', - ordersCount: 4, - ticketsCount: 4, - }, - { - id: 5, - name: 'مهرداد مظفری', - phone: '۰۹۱۲۹۲۸۳۳۹۵', - email: 'info@example.com', - customerCode: '۱۲۲', - ordersCount: 4, - ticketsCount: 4, - }, - { - id: 6, - name: 'مهرداد مظفری', - phone: '۰۹۱۲۹۲۸۳۳۹۵', - email: 'info@example.com', - customerCode: '۱۲۲', - ordersCount: 4, - ticketsCount: 4, - }, - ] + const handlePageChange = (page: number) => { + setCurrentPage(page) + } + + const handleFiltersChange = useCallback((newFilters: FilterValues) => { + setFilters(newFilters) + setCurrentPage(1) + }, []) return (

لیست مشتریان

- + + +
{ + return useQuery({ + queryKey: ["users"], + queryFn: api.getUsers, + }); +}; diff --git a/src/pages/customers/service/UsersService.ts b/src/pages/customers/service/UsersService.ts new file mode 100644 index 0000000..de42b17 --- /dev/null +++ b/src/pages/customers/service/UsersService.ts @@ -0,0 +1,7 @@ +import axios from "@/config/axios"; +import type { GetCustomersResponse } from "@/pages/customers/types/Types"; + +export const getUsers = async () => { + const { data } = await axios.get(`/admin/users`); + return data; +}; diff --git a/src/pages/customers/types/Types.ts b/src/pages/customers/types/Types.ts new file mode 100644 index 0000000..040f893 --- /dev/null +++ b/src/pages/customers/types/Types.ts @@ -0,0 +1,32 @@ +import type { IResponse } from "@/types/response.types"; +import type { RowDataType } from "@/components/types/TableTypes"; + +export interface Customer { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + restaurant: string; + firstName: string; + lastName: string; + phone: string; + birthDate: string; + marriageDate: string; + referrer: string | null; + isActive: boolean; + gender: boolean; + wallet: number; + points: number; +} + +export type GetCustomersResponse = IResponse; + +export type CustomerListRow = RowDataType & { + fullName: string; + phone: string; + birthDate: string; + marriageDate: string; + wallet: string; + points: string; + status: string; +}; diff --git a/src/pages/roles/components/RoleTableColumns.tsx b/src/pages/roles/components/RoleTableColumns.tsx index 17ed69e..8a7b08e 100644 --- a/src/pages/roles/components/RoleTableColumns.tsx +++ b/src/pages/roles/components/RoleTableColumns.tsx @@ -4,6 +4,7 @@ import TrashWithConfrim from '@/components/TrashWithConfrim' import { Eye } from 'iconsax-react' import { Link } from 'react-router-dom' import { Pages } from '@/config/Pages' +import { formatOptionalDate } from '@/helpers/func' interface GetRoleTableColumnsParams { onDelete?: (id: string) => void @@ -27,7 +28,7 @@ export const getRoleTableColumns = ({ onDelete, isDeleting }: GetRoleTableColumn key: 'createdAt', title: 'تاریخ ایجاد', render: (item: RoleType) => { - return new Date(item.createdAt).toLocaleDateString('fa-IR') + return formatOptionalDate(item.createdAt) } }, { diff --git a/src/pages/schedule/components/ScheduleTableColumns.tsx b/src/pages/schedule/components/ScheduleTableColumns.tsx index f67ac9b..b6604de 100644 --- a/src/pages/schedule/components/ScheduleTableColumns.tsx +++ b/src/pages/schedule/components/ScheduleTableColumns.tsx @@ -6,6 +6,7 @@ import { Eye } from 'iconsax-react' import { Link } from 'react-router-dom' import { Pages } from '@/config/Pages' import type { TFunction } from 'i18next' +import { formatOptionalDate } from '@/helpers/func' interface GetScheduleTableColumnsParams { onDelete?: (id: string) => void @@ -43,7 +44,7 @@ export const getScheduleTableColumns = ({ onDelete, isDeleting, t }: GetSchedule key: 'createdAt', title: 'تاریخ ایجاد', render: (item: Schedule) => { - return new Date(item.createdAt).toLocaleDateString('fa-IR') + return formatOptionalDate(item.createdAt) } }, { diff --git a/src/pages/shipmentMethod/components/ShipmentMethodTableColumns.tsx b/src/pages/shipmentMethod/components/ShipmentMethodTableColumns.tsx index 88daebe..c446b98 100644 --- a/src/pages/shipmentMethod/components/ShipmentMethodTableColumns.tsx +++ b/src/pages/shipmentMethod/components/ShipmentMethodTableColumns.tsx @@ -5,6 +5,7 @@ import { Eye } from 'iconsax-react' import { Link } from 'react-router-dom' import { Pages } from '@/config/Pages' import Status from '@/components/Status' +import { formatFaNumber } from '@/helpers/func' interface GetShipmentMethodTableColumnsParams { onDelete?: (id: string) => void @@ -31,14 +32,14 @@ export const getShipmentMethodTableColumns = ({ onDelete, isDeleting }: GetShipm key: 'price', title: 'قیمت', render: (item: RestaurantShipmentMethod) => { - return item.price ? `${item.price.toLocaleString()} تومان` : 'رایگان' + return item.price ? `${formatFaNumber(item.price)} تومان` : 'رایگان' } }, { key: 'minOrderPrice', title: 'حداقل قیمت سفارش', render: (item: RestaurantShipmentMethod) => { - return item.minOrderPrice ? `${item.minOrderPrice.toLocaleString()} تومان` : '-' + return item.minOrderPrice ? `${formatFaNumber(item.minOrderPrice)} تومان` : '-' } }, {