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 { 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'>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "افزودن تخفیف",
|
||||||
|
|||||||
@@ -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 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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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[]>;
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
Reference in New Issue
Block a user