order report by food

This commit is contained in:
2026-06-18 21:51:39 +03:30
parent 3ba1290e56
commit e89a74035b
13 changed files with 282 additions and 2 deletions
+70
View File
@@ -0,0 +1,70 @@
import { useEffect, useState, type FC } from 'react';
import DatePicker from './DatePicker';
import TimePicker from './TimePicker';
type Props = {
onChange: (value: string) => void;
defaultValue?: string;
placeholder?: string;
};
const parseDateTime = (value?: string) => {
if (!value) {
return { date: '', time: '' };
}
const [datePart, timePart] = value.split('T');
const time = timePart?.slice(0, 5) || '';
return {
date: datePart || '',
time,
};
};
const DateTimePicker: FC<Props> = ({ onChange, defaultValue, placeholder }) => {
const [date, setDate] = useState('');
const [time, setTime] = useState('');
useEffect(() => {
const parsed = parseDateTime(defaultValue);
setDate(parsed.date);
setTime(parsed.time);
}, [defaultValue]);
const emitChange = (nextDate: string, nextTime: string) => {
if (nextDate && nextTime) {
onChange(`${nextDate}T${nextTime}:00`);
return;
}
onChange('');
};
const handleDateChange = (nextDate: string) => {
setDate(nextDate);
emitChange(nextDate, time);
};
const handleTimeChange = (nextTime: string) => {
setTime(nextTime);
emitChange(date, nextTime);
};
return (
<div className="flex flex-col sm:flex-row gap-2 min-w-[280px]">
<DatePicker
placeholder={placeholder || 'تاریخ'}
onChange={handleDateChange}
defaulValue={date}
/>
<TimePicker
placeholder="زمان"
onChange={handleTimeChange}
defaultValue={time}
/>
</div>
);
};
export default DateTimePicker;
+19 -1
View File
@@ -1,6 +1,7 @@
import { type FC, useEffect, useState, type ChangeEvent, useRef } from 'react'; import { type FC, useEffect, useState, type ChangeEvent, useRef } from 'react';
import { Filter } from 'iconsax-react'; import { Filter } from 'iconsax-react';
import DatePicker from './DatePicker'; import DatePicker from './DatePicker';
import DateTimePicker from './DateTimePicker';
import Input from './Input'; import Input from './Input';
import Select, { type ItemsSelectType } from './Select'; import Select, { type ItemsSelectType } from './Select';
import MultiSelect from './MultiSelect'; import MultiSelect from './MultiSelect';
@@ -15,6 +16,13 @@ export type DateFieldType = {
defaultValue?: string; defaultValue?: string;
}; };
export type DateTimeFieldType = {
type: 'datetime';
name: string;
placeholder: string;
defaultValue?: string;
};
export type SelectFieldType = { export type SelectFieldType = {
type: 'select'; type: 'select';
name: string; name: string;
@@ -38,7 +46,7 @@ export type InputFieldType = {
defaultValue?: string; defaultValue?: string;
}; };
export type FieldType = DateFieldType | SelectFieldType | MultiSelectFieldType | InputFieldType; export type FieldType = DateFieldType | DateTimeFieldType | SelectFieldType | MultiSelectFieldType | InputFieldType;
export type FilterValues = Record<string, string | string[] | null>; export type FilterValues = Record<string, string | string[] | null>;
@@ -210,6 +218,16 @@ const Filters: FC<FiltersProps> = ({
/> />
); );
case 'datetime':
return (
<DateTimePicker
key={field.name}
placeholder={field.placeholder}
onChange={(value) => handleChange(field.name, value || null)}
defaultValue={(currentValue as string) || field.defaultValue || ''}
/>
);
case 'select': case 'select':
return ( return (
<div className='mt-1'> <div className='mt-1'>
+1
View File
@@ -25,6 +25,7 @@ export const Pages = {
list: "/orders/list", list: "/orders/list",
add: "/orders/add", add: "/orders/add",
detail: "/orders/detail/", detail: "/orders/detail/",
foodReport: "/orders/report/by-food",
}, },
customers: { customers: {
list: "/customers/list", list: "/customers/list",
+1
View File
@@ -97,6 +97,7 @@
"add_food": "افزودن غذا", "add_food": "افزودن غذا",
"categories": "دسته بندی ها", "categories": "دسته بندی ها",
"orders_list": "لیست سفارشات", "orders_list": "لیست سفارشات",
"orders_food_report": "گزارش فروش به تفکیک غذا",
"add_order": "افزودن سفارش", "add_order": "افزودن سفارش",
"discount_list": "لیست تخفیف", "discount_list": "لیست تخفیف",
"add_discount": "افزودن تخفیف", "add_discount": "افزودن تخفیف",
+51
View File
@@ -0,0 +1,51 @@
import { type FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Table from '@/components/Table';
import Filters from '@/components/Filters';
import { useGetFoodOrderReport } from './hooks/useFoodOrderReportData';
import { useFoodOrderReportFilters } from './hooks/useFoodOrderReportFilters';
import { getFoodOrderReportTableColumns } from './components/FoodOrderReportTableColumns';
import { useFoodOrderReportFiltersFields } from './components/FoodOrderReportFiltersFields';
import type { FoodOrderReportItem } from './types/Types';
import type { RowDataType } from '@/components/types/TableTypes';
const FoodOrderReport: FC = () => {
const { t } = useTranslation('global');
const { filters, apiParams, handleFiltersChange } = useFoodOrderReportFilters();
const { data: reportData, isLoading } = useGetFoodOrderReport(apiParams);
const columns = getFoodOrderReportTableColumns();
const filterFields = useFoodOrderReportFiltersFields();
const reportItems = useMemo(() => {
return (reportData?.data || []).map((item) => ({
...item,
id: item.food.id,
}));
}, [reportData?.data]);
return (
<div className='mt-5'>
<div className='flex justify-between items-center'>
<h1 className='text-lg font-light'>{t('submenu.orders_food_report')}</h1>
</div>
<div className='mt-8'>
<Filters
fields={filterFields}
onChange={handleFiltersChange}
initialValues={filters}
showClearButton
/>
</div>
<Table<FoodOrderReportItem & RowDataType>
columns={columns}
data={reportItems as (FoodOrderReportItem & RowDataType)[]}
isloading={isLoading}
/>
</div>
);
};
export default FoodOrderReport;
@@ -0,0 +1,27 @@
import { useMemo } from 'react';
import type { FieldType } from '@/components/Filters';
export const useFoodOrderReportFiltersFields = (): FieldType[] => {
return useMemo(() => [
{
type: 'datetime',
name: 'from',
placeholder: 'از تاریخ',
},
{
type: 'datetime',
name: 'to',
placeholder: 'تا تاریخ',
},
{
type: 'select',
name: 'sortBy',
placeholder: 'مرتب‌سازی',
defaultValue: 'count',
options: [
{ label: 'تعداد سفارش', value: 'count' },
{ label: 'قیمت', value: 'price' },
],
},
], []);
};
@@ -0,0 +1,28 @@
import type { ColumnType } from '@/components/types/TableTypes';
import type { FoodOrderReportItem } from '../types/Types';
import { formatFaNumber, formatPrice } from '@/helpers/func';
export const getFoodOrderReportTableColumns = (): ColumnType<FoodOrderReportItem>[] => {
return [
{
key: 'foodTitle',
title: 'غذا',
render: (item: FoodOrderReportItem) => item.food.title || '-',
},
{
key: 'count',
title: 'تعداد',
render: (item: FoodOrderReportItem) => formatFaNumber(item.count),
},
{
key: 'price',
title: 'قیمت',
render: (item: FoodOrderReportItem) => formatPrice(item.price),
},
{
key: 'totalPrice',
title: 'جمع کل',
render: (item: FoodOrderReportItem) => formatPrice(item.totalPrice),
},
];
};
@@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import * as api from '../service/OrderService';
import type { GetFoodOrderReportParams } from '../service/OrderService';
export const useGetFoodOrderReport = (params?: GetFoodOrderReportParams) => {
return useQuery({
queryKey: ['food-order-report', params],
queryFn: () => api.getFoodOrderReport(params),
});
};
@@ -0,0 +1,37 @@
import { useState, useMemo } from 'react';
import type { FilterValues } from '@/components/Filters';
import type { GetFoodOrderReportParams } from '../service/OrderService';
export const useFoodOrderReportFilters = () => {
const [filters, setFilters] = useState<FilterValues>({
sortBy: 'count',
});
const apiParams = useMemo(() => {
const params: GetFoodOrderReportParams = {};
if (filters.from) {
params.from = filters.from as string;
}
if (filters.to) {
params.to = filters.to as string;
}
if (filters.sortBy) {
params.sortBy = filters.sortBy as GetFoodOrderReportParams['sortBy'];
}
return params;
}, [filters]);
const handleFiltersChange = (newFilters: FilterValues) => {
setFilters(newFilters);
};
return {
filters,
apiParams,
handleFiltersChange,
};
};
+17 -1
View File
@@ -1,5 +1,5 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import type { GetOrdersResponse, GetOrderByIdResponse } from "../types/Types"; import type { GetOrdersResponse, GetOrderByIdResponse, GetFoodOrderReportResponse } from "../types/Types";
export interface GetOrdersParams { export interface GetOrdersParams {
page?: number; page?: number;
@@ -82,3 +82,19 @@ export const refundPayment = async (
): Promise<void> => { ): Promise<void> => {
await axios.post(`/admin/orders/${orderId}/refund`, params); await axios.post(`/admin/orders/${orderId}/refund`, params);
}; };
export interface GetFoodOrderReportParams {
from?: string;
to?: string;
sortBy?: 'count' | 'price';
}
export const getFoodOrderReport = async (
params?: GetFoodOrderReportParams
) => {
const { data } = await axios.get<GetFoodOrderReportResponse>(
'/admin/orders/report/by-food',
{ params }
);
return data;
};
+14
View File
@@ -220,3 +220,17 @@ export interface Order extends RowDataType {
export type GetOrdersResponse = IResponse<Order[]>; export type GetOrdersResponse = IResponse<Order[]>;
export type GetOrderByIdResponse = IResponse<Order>; export type GetOrderByIdResponse = IResponse<Order>;
export interface FoodOrderReportFood {
id: string;
title: string;
}
export interface FoodOrderReportItem extends RowDataType {
food: FoodOrderReportFood;
count: number;
price: number;
totalPrice: number;
}
export type GetFoodOrderReportResponse = IResponse<FoodOrderReportItem[]>;
+2
View File
@@ -15,6 +15,7 @@ import CustomerDetails from '@/pages/customers/Details'
import CreateCustomer from '@/pages/customers/Create' import CreateCustomer from '@/pages/customers/Create'
import OrdersList from '@/pages/orders/List' import OrdersList from '@/pages/orders/List'
import OrderDetails from '@/pages/orders/Details' import OrderDetails from '@/pages/orders/Details'
import FoodOrderReport from '@/pages/orders/FoodOrderReport'
import CommentsList from '@/pages/comments/List' import CommentsList from '@/pages/comments/List'
import CommentsDetails from '@/pages/comments/Details' import CommentsDetails from '@/pages/comments/Details'
import ScheduleList from '@/pages/schedule/List' import ScheduleList from '@/pages/schedule/List'
@@ -71,6 +72,7 @@ const MainRouter: FC = () => {
<Route path={Pages.customers.detail + ':id'} element={<CustomerDetails />} /> <Route path={Pages.customers.detail + ':id'} element={<CustomerDetails />} />
<Route path={Pages.customers.add} element={<CreateCustomer />} /> <Route path={Pages.customers.add} element={<CreateCustomer />} />
<Route path={Pages.orders.list} element={<OrdersList />} /> <Route path={Pages.orders.list} element={<OrdersList />} />
<Route path={Pages.orders.foodReport} element={<FoodOrderReport />} />
<Route path={Pages.orders.detail + ':id'} element={<OrderDetails />} /> <Route path={Pages.orders.detail + ':id'} element={<OrderDetails />} />
<Route path={Pages.comments.list} element={<CommentsList />} /> <Route path={Pages.comments.list} element={<CommentsList />} />
<Route path={Pages.comments.detail + ':id'} element={<CommentsDetails />} /> <Route path={Pages.comments.detail + ':id'} element={<CommentsDetails />} />
+5
View File
@@ -33,6 +33,11 @@ const OrdersSubMenu: FC = () => {
isActive={isActive('list')} isActive={isActive('list')}
link={Pages.orders.list} link={Pages.orders.list}
/> />
<SubMenuItem
title={t('submenu.orders_food_report')}
isActive={isActive('report/by-food')}
link={Pages.orders.foodReport}
/>
{/* <SubMenuItem {/* <SubMenuItem
title={t('submenu.add_order')} title={t('submenu.add_order')}
isActive={isActive('add')} isActive={isActive('add')}