order report by food
This commit is contained in:
@@ -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;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type FC, useEffect, useState, type ChangeEvent, useRef } from 'react';
|
||||
import { Filter } from 'iconsax-react';
|
||||
import DatePicker from './DatePicker';
|
||||
import DateTimePicker from './DateTimePicker';
|
||||
import Input from './Input';
|
||||
import Select, { type ItemsSelectType } from './Select';
|
||||
import MultiSelect from './MultiSelect';
|
||||
@@ -15,6 +16,13 @@ export type DateFieldType = {
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export type DateTimeFieldType = {
|
||||
type: 'datetime';
|
||||
name: string;
|
||||
placeholder: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export type SelectFieldType = {
|
||||
type: 'select';
|
||||
name: string;
|
||||
@@ -38,7 +46,7 @@ export type InputFieldType = {
|
||||
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>;
|
||||
|
||||
@@ -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':
|
||||
return (
|
||||
<div className='mt-1'>
|
||||
|
||||
@@ -25,6 +25,7 @@ export const Pages = {
|
||||
list: "/orders/list",
|
||||
add: "/orders/add",
|
||||
detail: "/orders/detail/",
|
||||
foodReport: "/orders/report/by-food",
|
||||
},
|
||||
customers: {
|
||||
list: "/customers/list",
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
"add_food": "افزودن غذا",
|
||||
"categories": "دسته بندی ها",
|
||||
"orders_list": "لیست سفارشات",
|
||||
"orders_food_report": "گزارش فروش به تفکیک غذا",
|
||||
"add_order": "افزودن سفارش",
|
||||
"discount_list": "لیست تخفیف",
|
||||
"add_discount": "افزودن تخفیف",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from "@/config/axios";
|
||||
import type { GetOrdersResponse, GetOrderByIdResponse } from "../types/Types";
|
||||
import type { GetOrdersResponse, GetOrderByIdResponse, GetFoodOrderReportResponse } from "../types/Types";
|
||||
|
||||
export interface GetOrdersParams {
|
||||
page?: number;
|
||||
@@ -82,3 +82,19 @@ export const refundPayment = async (
|
||||
): Promise<void> => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -220,3 +220,17 @@ export interface Order extends RowDataType {
|
||||
|
||||
export type GetOrdersResponse = 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[]>;
|
||||
|
||||
@@ -15,6 +15,7 @@ import CustomerDetails from '@/pages/customers/Details'
|
||||
import CreateCustomer from '@/pages/customers/Create'
|
||||
import OrdersList from '@/pages/orders/List'
|
||||
import OrderDetails from '@/pages/orders/Details'
|
||||
import FoodOrderReport from '@/pages/orders/FoodOrderReport'
|
||||
import CommentsList from '@/pages/comments/List'
|
||||
import CommentsDetails from '@/pages/comments/Details'
|
||||
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.add} element={<CreateCustomer />} />
|
||||
<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.comments.list} element={<CommentsList />} />
|
||||
<Route path={Pages.comments.detail + ':id'} element={<CommentsDetails />} />
|
||||
|
||||
@@ -33,6 +33,11 @@ const OrdersSubMenu: FC = () => {
|
||||
isActive={isActive('list')}
|
||||
link={Pages.orders.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.orders_food_report')}
|
||||
isActive={isActive('report/by-food')}
|
||||
link={Pages.orders.foodReport}
|
||||
/>
|
||||
{/* <SubMenuItem
|
||||
title={t('submenu.add_order')}
|
||||
isActive={isActive('add')}
|
||||
|
||||
Reference in New Issue
Block a user