Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8177f71b3b | |||
| 333643784e | |||
| 9b2bdc36bb | |||
| d9012e524a | |||
| 1552830e76 | |||
| e4e54d6203 | |||
| 43be71afef | |||
| 7b2a86d3cd | |||
| 84aefb2a14 | |||
| 541898d48e |
+23
@@ -139,7 +139,30 @@ const queryClient = new QueryClient({
|
|||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
|
|
||||||
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// استخراج توکنها از URL در صورت وجود
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const tokenFromUrl = urlParams.get('token')
|
||||||
|
const refreshTokenFromUrl = urlParams.get('refreshToken')
|
||||||
|
|
||||||
|
if (tokenFromUrl && refreshTokenFromUrl) {
|
||||||
|
// ذخیره توکنها (جایگزین کردن توکنهای قبلی اگر وجود داشته باشند)
|
||||||
|
setToken(tokenFromUrl)
|
||||||
|
setRefreshToken(refreshTokenFromUrl)
|
||||||
|
|
||||||
|
// حذف پارامترها از URL
|
||||||
|
const newUrl = window.location.pathname
|
||||||
|
window.history.replaceState({}, '', newUrl)
|
||||||
|
|
||||||
|
// تنظیم وضعیت لاگین و هدایت به صفحه اصلی
|
||||||
|
setIsLogin('isLogin')
|
||||||
|
if (window.location.pathname === Pages.auth.login) {
|
||||||
|
window.location.href = Pages.dashboard
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const token = getToken()
|
const token = getToken()
|
||||||
if (token) {
|
if (token) {
|
||||||
setIsLogin('isLogin')
|
setIsLogin('isLogin')
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
|
|
||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
|
<div className='mt-1'>
|
||||||
<Select
|
<Select
|
||||||
key={field.name}
|
key={field.name}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
@@ -182,6 +183,7 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
items={field.options}
|
items={field.options}
|
||||||
className='min-w-[170px]'
|
className='min-w-[170px]'
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'multiselect':
|
case 'multiselect':
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const CouponBasicInfo: FC<CouponBasicInfoProps> = ({
|
|||||||
const handleGenerateCode = () => {
|
const handleGenerateCode = () => {
|
||||||
generateCouponCode(undefined, {
|
generateCouponCode(undefined, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
formik.setFieldValue('code', data.data)
|
formik.setFieldValue('code', data.data?.code)
|
||||||
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { extractErrorMessage } from '@/config/func'
|
|||||||
type CategoryFormValues = {
|
type CategoryFormValues = {
|
||||||
title: string
|
title: string
|
||||||
icon: File[]
|
icon: File[]
|
||||||
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const CategoryFood: FC = () => {
|
const CategoryFood: FC = () => {
|
||||||
@@ -47,7 +48,8 @@ const CategoryFood: FC = () => {
|
|||||||
const formik = useFormik<CategoryFormValues>({
|
const formik = useFormik<CategoryFormValues>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
title: '',
|
title: '',
|
||||||
icon: []
|
icon: [],
|
||||||
|
order: 0
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object().shape({
|
validationSchema: Yup.object().shape({
|
||||||
title: Yup.string().required('عنوان دستهبندی الزامی است')
|
title: Yup.string().required('عنوان دستهبندی الزامی است')
|
||||||
@@ -57,7 +59,8 @@ const CategoryFood: FC = () => {
|
|||||||
const categoryData: CreateCategoryType = {
|
const categoryData: CreateCategoryType = {
|
||||||
title: values.title,
|
title: values.title,
|
||||||
isActive,
|
isActive,
|
||||||
avatarUrl: avatarUrl || selectedIconUrl || editingCategory?.avatarUrl || ''
|
avatarUrl: avatarUrl || selectedIconUrl || editingCategory?.avatarUrl || '',
|
||||||
|
order: values.order
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingCategory) {
|
if (editingCategory) {
|
||||||
@@ -115,7 +118,8 @@ const CategoryFood: FC = () => {
|
|||||||
const handleEdit = (category: Category) => {
|
const handleEdit = (category: Category) => {
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
title: category.title,
|
title: category.title,
|
||||||
icon: []
|
icon: [],
|
||||||
|
order: category.order || 0
|
||||||
})
|
})
|
||||||
setIsActive(category.isActive)
|
setIsActive(category.isActive)
|
||||||
setIconFiles([])
|
setIconFiles([])
|
||||||
@@ -151,7 +155,8 @@ const CategoryFood: FC = () => {
|
|||||||
const categoryData: CreateCategoryType = {
|
const categoryData: CreateCategoryType = {
|
||||||
title: category.title,
|
title: category.title,
|
||||||
isActive: value,
|
isActive: value,
|
||||||
avatarUrl: category.avatarUrl
|
avatarUrl: category.avatarUrl,
|
||||||
|
order: category.order || 0
|
||||||
}
|
}
|
||||||
updateCategory(
|
updateCategory(
|
||||||
{ id, params: categoryData },
|
{ id, params: categoryData },
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ const CreateFood: FC = () => {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
images: [],
|
images: [],
|
||||||
isSpecialOffer: false,
|
isSpecialOffer: false,
|
||||||
dailyStock: 0
|
dailyStock: 0,
|
||||||
|
order: 0
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object().shape({
|
validationSchema: Yup.object().shape({
|
||||||
title: Yup.string().required('نام غذا الزامی است'),
|
title: Yup.string().required('نام غذا الزامی است'),
|
||||||
desc: Yup.string().required('توضیحات غذا الزامی است'),
|
|
||||||
categoryId: Yup.string().required('دستهبندی الزامی است'),
|
categoryId: Yup.string().required('دستهبندی الزامی است'),
|
||||||
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
||||||
prepareTime: Yup.number().required('زمان آمادهسازی الزامی است').min(0, 'زمان آمادهسازی باید مثبت باشد'),
|
prepareTime: Yup.number().required('زمان آمادهسازی الزامی است').min(0, 'زمان آمادهسازی باید مثبت باشد'),
|
||||||
@@ -203,6 +203,18 @@ const CreateFood: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-5'>
|
||||||
|
<Input
|
||||||
|
name='order'
|
||||||
|
label='اولویت'
|
||||||
|
placeholder='عدد'
|
||||||
|
type='number'
|
||||||
|
value={formik.values.order}
|
||||||
|
onChange={(e) => formik.setFieldValue('order', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='text-sm mb-2'>محتوای غذا</div>
|
<div className='text-sm mb-2'>محتوای غذا</div>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ const FoodsList: FC = () => {
|
|||||||
handlePageChange,
|
handlePageChange,
|
||||||
} = useFoodFilters()
|
} = useFoodFilters()
|
||||||
|
|
||||||
const { data: foodsData, isLoading } = useGetFoods(apiParams)
|
const { data: foodsData, isLoading } = useGetFoods(apiParams, filters)
|
||||||
const { mutate: deleteFood, isPending: isDeleting } = useDeleteFood()
|
const { mutate: deleteFood, isPending: isDeleting } = useDeleteFood()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const foods = foodsData?.data || []
|
const foods = foodsData?.data || []
|
||||||
const columns = getFoodTableColumns({
|
const columns = getFoodTableColumns({
|
||||||
onDelete: (id: string) => deleteFood(id),
|
onDelete: (id: string) => deleteFood(id),
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ const UpdateFood: FC = () => {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
images: [],
|
images: [],
|
||||||
isSpecialOffer: false,
|
isSpecialOffer: false,
|
||||||
dailyStock: 0
|
dailyStock: 0,
|
||||||
|
order: 0
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object().shape({
|
validationSchema: Yup.object().shape({
|
||||||
title: Yup.string().required('نام غذا الزامی است'),
|
title: Yup.string().required('نام غذا الزامی است'),
|
||||||
desc: Yup.string().required('توضیحات غذا الزامی است'),
|
|
||||||
categoryId: Yup.string().required('دستهبندی الزامی است'),
|
categoryId: Yup.string().required('دستهبندی الزامی است'),
|
||||||
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
||||||
prepareTime: Yup.number().required('زمان آمادهسازی الزامی است').min(0, 'زمان آمادهسازی باید مثبت باشد'),
|
prepareTime: Yup.number().required('زمان آمادهسازی الزامی است').min(0, 'زمان آمادهسازی باید مثبت باشد'),
|
||||||
@@ -126,7 +126,8 @@ const UpdateFood: FC = () => {
|
|||||||
isActive: food.isActive || false,
|
isActive: food.isActive || false,
|
||||||
images: food.images || [],
|
images: food.images || [],
|
||||||
isSpecialOffer: food.isSpecialOffer || false,
|
isSpecialOffer: food.isSpecialOffer || false,
|
||||||
dailyStock: food.inventory?.totalStock || 0
|
dailyStock: food.inventory?.totalStock || 0,
|
||||||
|
order: food.order || 0
|
||||||
})
|
})
|
||||||
setIsActive(food.isActive || false)
|
setIsActive(food.isActive || false)
|
||||||
setIsSpecial(food.isSpecialOffer || false)
|
setIsSpecial(food.isSpecialOffer || false)
|
||||||
|
|||||||
@@ -92,6 +92,18 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories }) =>
|
|||||||
error_text={formik.touched.desc && formik.errors.desc ? formik.errors.desc : ''}
|
error_text={formik.touched.desc && formik.errors.desc ? formik.errors.desc : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-5'>
|
||||||
|
<Input
|
||||||
|
name='order'
|
||||||
|
label='اولویت'
|
||||||
|
placeholder='عدد'
|
||||||
|
type='number'
|
||||||
|
value={formik.values.order}
|
||||||
|
onChange={(e) => formik.setFieldValue('order', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import IconSelectModal from './IconSelectModal'
|
|||||||
type CategoryFormValues = {
|
type CategoryFormValues = {
|
||||||
title: string
|
title: string
|
||||||
icon: File[]
|
icon: File[]
|
||||||
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -66,6 +67,17 @@ const CategoryForm: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 lg:mb-4">
|
||||||
|
<Input
|
||||||
|
label="اولویت"
|
||||||
|
name="order"
|
||||||
|
type="number"
|
||||||
|
value={formik.values.order}
|
||||||
|
onChange={(e) => formik.setFieldValue('order', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-4 lg:mb-6">
|
<div className="mb-4 lg:mb-6">
|
||||||
<div className="text-xs lg:text-sm mb-2 font-medium">آیکون دسته بندی</div>
|
<div className="text-xs lg:text-sm mb-2 font-medium">آیکون دسته بندی</div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import type { FieldType } from '@/components/Filters'
|
import type { FieldType } from '@/components/Filters'
|
||||||
|
import { useGetCategories } from '../hooks/useFoodData'
|
||||||
|
|
||||||
export const useFoodFiltersFields = (): FieldType[] => {
|
export const useFoodFiltersFields = (): FieldType[] => {
|
||||||
|
|
||||||
|
const { data } = useGetCategories()
|
||||||
|
|
||||||
return useMemo(() => [
|
return useMemo(() => [
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'search',
|
name: 'search',
|
||||||
placeholder: 'جستجو',
|
placeholder: 'جستجو',
|
||||||
},
|
},
|
||||||
], [])
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'categoryId',
|
||||||
|
placeholder: 'انتخاب',
|
||||||
|
options: data?.data?.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.id
|
||||||
|
}
|
||||||
|
}) || []
|
||||||
|
},
|
||||||
|
], [data?.data])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
CreateFoodType,
|
CreateFoodType,
|
||||||
GetFoodsParams,
|
GetFoodsParams,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
import type { FilterValues } from "@/components/Filters";
|
||||||
|
|
||||||
export const useCreateFood = () => {
|
export const useCreateFood = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -12,10 +13,13 @@ export const useCreateFood = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetFoods = (params?: GetFoodsParams) => {
|
export const useGetFoods = (
|
||||||
|
params?: GetFoodsParams,
|
||||||
|
filters?: FilterValues,
|
||||||
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["foods", params],
|
queryKey: ["foods", params, filters],
|
||||||
queryFn: () => api.getFoods(params),
|
queryFn: () => api.getFoods(params, filters),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
GetFoodsResponseType,
|
GetFoodsResponseType,
|
||||||
GetIconsResponseType,
|
GetIconsResponseType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
import type { FilterValues } from "@/components/Filters";
|
||||||
|
|
||||||
export const createFood = async (params: CreateFoodType) => {
|
export const createFood = async (params: CreateFoodType) => {
|
||||||
const { data } = await axios.post(`/admin/foods`, params);
|
const { data } = await axios.post(`/admin/foods`, params);
|
||||||
@@ -15,19 +16,23 @@ export const createFood = async (params: CreateFoodType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getFoods = async (
|
export const getFoods = async (
|
||||||
params?: GetFoodsParams
|
params?: GetFoodsParams,
|
||||||
|
filters?: FilterValues,
|
||||||
): Promise<GetFoodsResponseType> => {
|
): Promise<GetFoodsResponseType> => {
|
||||||
const { data } = await axios.get<GetFoodsResponseType>(`/admin/foods`, {
|
const { data } = await axios.get<GetFoodsResponseType>(`/admin/foods`, {
|
||||||
params,
|
params: {
|
||||||
|
...params,
|
||||||
|
...filters,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFoodDetails = async (
|
export const getFoodDetails = async (
|
||||||
id: string
|
id: string,
|
||||||
): Promise<GetFoodDetailsResponseType> => {
|
): Promise<GetFoodDetailsResponseType> => {
|
||||||
const { data } = await axios.get<GetFoodDetailsResponseType>(
|
const { data } = await axios.get<GetFoodDetailsResponseType>(
|
||||||
`/admin/foods/${id}`
|
`/admin/foods/${id}`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -39,7 +44,7 @@ export const updateFood = async (id: string, params: CreateFoodType) => {
|
|||||||
|
|
||||||
export const getCategories = async (): Promise<GetCategoriesResponseType> => {
|
export const getCategories = async (): Promise<GetCategoriesResponseType> => {
|
||||||
const { data } = await axios.get<GetCategoriesResponseType>(
|
const { data } = await axios.get<GetCategoriesResponseType>(
|
||||||
`/admin/categories`
|
`/admin/categories`,
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -61,7 +66,7 @@ export const deleteCategory = async (id: string) => {
|
|||||||
|
|
||||||
export const updateCategory = async (
|
export const updateCategory = async (
|
||||||
id: string,
|
id: string,
|
||||||
params: CreateCategoryType
|
params: CreateCategoryType,
|
||||||
) => {
|
) => {
|
||||||
const { data } = await axios.patch(`/admin/categories/${id}`, params);
|
const { data } = await axios.patch(`/admin/categories/${id}`, params);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type CreateFoodType = {
|
|||||||
discount: number;
|
discount: number;
|
||||||
isSpecialOffer: boolean;
|
isSpecialOffer: boolean;
|
||||||
dailyStock: number;
|
dailyStock: number;
|
||||||
|
order: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Category = {
|
export type Category = {
|
||||||
@@ -126,6 +127,7 @@ export type CreateCategoryType = {
|
|||||||
title: string;
|
title: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
order: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Icon = {
|
export type Icon = {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import PaymentInfo from './components/PaymentInfo'
|
|||||||
import OrderActions from './components/OrderActions'
|
import OrderActions from './components/OrderActions'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { printOrderReceipt } from './print/orderReceiptPrint'
|
||||||
|
|
||||||
const OrderDetails: FC = () => {
|
const OrderDetails: FC = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -197,6 +198,10 @@ const OrderDetails: FC = () => {
|
|||||||
setShowRefundModal(false)
|
setShowRefundModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePrintOrder = () => {
|
||||||
|
printOrderReceipt(order)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
<div className='flex justify-between items-center'>
|
<div className='flex justify-between items-center'>
|
||||||
@@ -228,6 +233,7 @@ const OrderDetails: FC = () => {
|
|||||||
getPaymentStatusText={getPaymentStatusText}
|
getPaymentStatusText={getPaymentStatusText}
|
||||||
/>
|
/>
|
||||||
<OrderActions
|
<OrderActions
|
||||||
|
onPrint={handlePrintOrder}
|
||||||
showVerifyCashPaymentButton={showVerifyCashPaymentButton}
|
showVerifyCashPaymentButton={showVerifyCashPaymentButton}
|
||||||
showSendToKitchenButton={showSendToKitchenButton}
|
showSendToKitchenButton={showSendToKitchenButton}
|
||||||
showCancelButton={showCancelButton}
|
showCancelButton={showCancelButton}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Button from '@/components/Button'
|
|||||||
import { type FC, useState, useEffect } from 'react'
|
import { type FC, useState, useEffect } from 'react'
|
||||||
|
|
||||||
interface OrderActionsProps {
|
interface OrderActionsProps {
|
||||||
|
onPrint: () => void
|
||||||
showVerifyCashPaymentButton: boolean
|
showVerifyCashPaymentButton: boolean
|
||||||
showSendToKitchenButton: boolean
|
showSendToKitchenButton: boolean
|
||||||
showCancelButton: boolean
|
showCancelButton: boolean
|
||||||
@@ -21,6 +22,7 @@ interface OrderActionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OrderActions: FC<OrderActionsProps> = ({
|
const OrderActions: FC<OrderActionsProps> = ({
|
||||||
|
onPrint,
|
||||||
showVerifyCashPaymentButton,
|
showVerifyCashPaymentButton,
|
||||||
showSendToKitchenButton,
|
showSendToKitchenButton,
|
||||||
showCancelButton,
|
showCancelButton,
|
||||||
@@ -71,11 +73,15 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
onCancelOrder()
|
onCancelOrder()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCanceled) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-6 flex flex-col gap-3'>
|
<div className='mt-6 flex flex-col gap-3'>
|
||||||
{showVerifyCashPaymentButton && (
|
<Button
|
||||||
|
label='چاپ'
|
||||||
|
onClick={onPrint}
|
||||||
|
className='bg-slate-700 hover:bg-slate-800'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isCanceled && showVerifyCashPaymentButton && (
|
||||||
<Button
|
<Button
|
||||||
label='تایید پرداخت'
|
label='تایید پرداخت'
|
||||||
onClick={onVerifyCashPayment}
|
onClick={onVerifyCashPayment}
|
||||||
@@ -84,7 +90,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showSendToKitchenButton && (
|
{!isCanceled && showSendToKitchenButton && (
|
||||||
<Button
|
<Button
|
||||||
label='ارسال به آشپزخانه'
|
label='ارسال به آشپزخانه'
|
||||||
onClick={handleSendToKitchen}
|
onClick={handleSendToKitchen}
|
||||||
@@ -93,7 +99,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPreparing && isCourierDelivery && (
|
{!isCanceled && isPreparing && isCourierDelivery && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به پیک'
|
label='تحویل به پیک'
|
||||||
onClick={handleDeliverToCourier}
|
onClick={handleDeliverToCourier}
|
||||||
@@ -102,7 +108,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPreparing && isDineInOrCarDelivery && (
|
{!isCanceled && isPreparing && isDineInOrCarDelivery && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به گارسون'
|
label='تحویل به گارسون'
|
||||||
onClick={handleDeliverToWaiter}
|
onClick={handleDeliverToWaiter}
|
||||||
@@ -111,7 +117,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPreparing && isCustomerPickup && (
|
{!isCanceled && isPreparing && isCustomerPickup && (
|
||||||
<Button
|
<Button
|
||||||
label='تحویل به رسپشنیست'
|
label='تحویل به رسپشنیست'
|
||||||
onClick={handleDeliverToReceptionist}
|
onClick={handleDeliverToReceptionist}
|
||||||
@@ -120,7 +126,7 @@ const OrderActions: FC<OrderActionsProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCancelButton && (
|
{!isCanceled && showCancelButton && (
|
||||||
<Button
|
<Button
|
||||||
label='لغو سفارش'
|
label='لغو سفارش'
|
||||||
onClick={handleCancelOrder}
|
onClick={handleCancelOrder}
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { formatFaNumber, formatOptionalDate, formatPrice } from "@/helpers/func";
|
||||||
|
import type { Order } from "../types/Types";
|
||||||
|
import { printHtmlDocument } from "@/shared/print/printHtmlDocument";
|
||||||
|
|
||||||
|
const baseReceiptStyles = `
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Tahoma, Arial, sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
@page {
|
||||||
|
size: 80mm auto;
|
||||||
|
margin: 4mm;
|
||||||
|
}
|
||||||
|
.receipt {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.center { text-align: center; }
|
||||||
|
.title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.sub { color: #555; font-size: 11px; }
|
||||||
|
.divider {
|
||||||
|
border-top: 1px dashed #888;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.muted { color: #666; }
|
||||||
|
.items {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.items td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.item-name { width: 46%; }
|
||||||
|
.item-qty { width: 18%; text-align: center; }
|
||||||
|
.item-unit { width: 36%; text-align: left; }
|
||||||
|
.total-row {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getCustomerName = (order: Order): string => {
|
||||||
|
const fullName = [order.user?.firstName, order.user?.lastName].filter(Boolean).join(" ").trim();
|
||||||
|
return fullName || "-";
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildOrderReceiptHtml = (order: Order): string => {
|
||||||
|
const customerAddress = order.userAddress
|
||||||
|
? `${order.userAddress.province || ""}، ${order.userAddress.city || ""}، ${order.userAddress.address || ""}، کد پستی: ${order.userAddress.postalCode || "-"}`.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const itemsHtml = order.items
|
||||||
|
.map(
|
||||||
|
(item) => `
|
||||||
|
<tr>
|
||||||
|
<td class="item-name">${item.food?.title || "-"}</td>
|
||||||
|
<td class="item-qty">${formatFaNumber(item.quantity)}</td>
|
||||||
|
<td class="item-unit">${formatPrice(item.unitPrice)}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<main class="receipt">
|
||||||
|
<section class="center">
|
||||||
|
<div class="title">${order.restaurant?.name || "رسید سفارش"}</div>
|
||||||
|
<div class="sub">سفارش شماره ${formatFaNumber(order.orderNumber)}</div>
|
||||||
|
<div class="sub">${formatOptionalDate(order.createdAt, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="row"><span class="muted">مشتری</span><span>${getCustomerName(order)}</span></div>
|
||||||
|
<div class="row"><span class="muted">موبایل</span><span>${order.user?.phone || "-"}</span></div>
|
||||||
|
<div class="row"><span class="muted">روش تحویل</span><span>${order.deliveryMethod?.description || "-"}</span></div>
|
||||||
|
<div class="row"><span class="muted">روش پرداخت</span><span>${order.paymentMethod?.description || "-"}</span></div>
|
||||||
|
${
|
||||||
|
customerAddress
|
||||||
|
? `<div class="divider"></div><div><span class="muted">آدرس مشتری</span><div>${customerAddress}</div></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<table class="items">
|
||||||
|
<tbody>
|
||||||
|
${itemsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="row"><span class="muted">جمع آیتمها</span><span>${formatPrice(order.subTotal)}</span></div>
|
||||||
|
${
|
||||||
|
order.itemsDiscount > 0
|
||||||
|
? `<div class="row"><span class="muted">تخفیف آیتمها</span><span>${formatPrice(order.itemsDiscount)}</span></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
order.couponDiscount > 0
|
||||||
|
? `<div class="row"><span class="muted">تخفیف کوپن</span><span>${formatPrice(order.couponDiscount)}</span></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="row"><span class="muted">هزینه ارسال</span><span>${formatPrice(order.deliveryFee)}</span></div>
|
||||||
|
${
|
||||||
|
order.tax > 0
|
||||||
|
? `<div class="row"><span class="muted">مالیات</span><span>${formatPrice(order.tax)}</span></div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="row total-row"><span>مبلغ کل</span><span>${formatPrice(order.total)}</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
${
|
||||||
|
order.description
|
||||||
|
? `<div class="divider"></div><section><div class="muted">توضیحات:</div><div>${order.description}</div></section>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const printOrderReceipt = (order: Order): void => {
|
||||||
|
const html = buildOrderReceiptHtml(order);
|
||||||
|
printHtmlDocument({
|
||||||
|
title: `سفارش ${order.orderNumber}`,
|
||||||
|
html,
|
||||||
|
widthMm: 80,
|
||||||
|
styles: baseReceiptStyles,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import Tabs from '@/components/Tabs'
|
import Tabs from '@/components/Tabs'
|
||||||
import { Money2, Setting4 } from 'iconsax-react'
|
import { Money2, NotificationStatus, Setting4 } from 'iconsax-react'
|
||||||
import { useState, type FC } from 'react'
|
import { useState, type FC } from 'react'
|
||||||
import { SettingTabEnum } from './enum/Enum'
|
import { SettingTabEnum } from './enum/Enum'
|
||||||
import GeneralSettings from './GeneralSettings'
|
import GeneralSettings from './GeneralSettings'
|
||||||
import ScoreSetting from './components/ScoreSetting'
|
import ScoreSetting from './components/ScoreSetting'
|
||||||
|
import NotificationsList from '../notifications/List'
|
||||||
|
|
||||||
const Setting: FC = () => {
|
const Setting: FC = () => {
|
||||||
|
|
||||||
@@ -26,6 +27,11 @@ const Setting: FC = () => {
|
|||||||
label: 'تنظیمات امتیاز',
|
label: 'تنظیمات امتیاز',
|
||||||
value: SettingTabEnum.FINANCIAL,
|
value: SettingTabEnum.FINANCIAL,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <NotificationStatus color={activeTab === SettingTabEnum.NOTIFICATION ? 'black' : '#8C90A3'} size={24} />,
|
||||||
|
label: 'تنظیمات اعلان',
|
||||||
|
value: SettingTabEnum.NOTIFICATION,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onChange={(value: string) => setActiveTab(value as SettingTabEnum)}
|
onChange={(value: string) => setActiveTab(value as SettingTabEnum)}
|
||||||
active={activeTab}
|
active={activeTab}
|
||||||
@@ -39,6 +45,9 @@ const Setting: FC = () => {
|
|||||||
{
|
{
|
||||||
activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />
|
activeTab === SettingTabEnum.FINANCIAL && <ScoreSetting />
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
activeTab === SettingTabEnum.NOTIFICATION && <NotificationsList />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export const enum SettingTabEnum {
|
|||||||
PASSWORD = "password",
|
PASSWORD = "password",
|
||||||
FINANCIAL = "financial",
|
FINANCIAL = "financial",
|
||||||
DELIVERY = "delivery",
|
DELIVERY = "delivery",
|
||||||
|
NOTIFICATION = "notification",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const Statistics: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='p-6 max-w-[1600px] mx-auto'>
|
<div className='p-6 max-w-[1600px] mx-auto'>
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6'>
|
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6'>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={<Category size={24} color='#6B7FED' />}
|
icon={<Category size={24} color='#6B7FED' />}
|
||||||
title='غذاها'
|
title='غذاها'
|
||||||
@@ -43,12 +43,6 @@ const Statistics: FC = () => {
|
|||||||
value={`${formatFaNumber(stats?.totalOrders)} سفارش`}
|
value={`${formatFaNumber(stats?.totalOrders)} سفارش`}
|
||||||
color='#FF9A62'
|
color='#FF9A62'
|
||||||
/>
|
/>
|
||||||
<StatCard
|
|
||||||
icon={<DollarCircle size={24} color='#10B981' />}
|
|
||||||
title='درآمد'
|
|
||||||
value={stats?.totalRevenue ? formatPrice(stats.totalRevenue) : '۰ تومان'}
|
|
||||||
color='#10B981'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Charts Section */}
|
{/* Charts Section */}
|
||||||
@@ -57,10 +51,18 @@ const Statistics: FC = () => {
|
|||||||
<RevenueChart />
|
<RevenueChart />
|
||||||
</div>
|
</div>
|
||||||
<div className='lg:col-span-4'>
|
<div className='lg:col-span-4'>
|
||||||
|
<StatCard
|
||||||
|
icon={<DollarCircle size={24} color='#10B981' />}
|
||||||
|
title='درآمد'
|
||||||
|
value={stats?.totalRevenue ? formatPrice(stats.totalRevenue) : '۰ تومان'}
|
||||||
|
color='#10B981'
|
||||||
|
/>
|
||||||
|
<div className='mt-5'>
|
||||||
<FoodSalesChart />
|
<FoodSalesChart />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type FC, useEffect } from 'react'
|
|||||||
import LogoImage from '../assets/images/logo.png'
|
import LogoImage from '../assets/images/logo.png'
|
||||||
import LogoSmall from '../assets/images/logo-small.png'
|
import LogoSmall from '../assets/images/logo-small.png'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { DocumentText, Home2, Logout, NotificationStatus, People, Setting2, TicketDiscount, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1, NotificationBing, SmsEdit } from 'iconsax-react'
|
import { DocumentText, Home2, Logout, People, Setting2, TicketDiscount, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1, NotificationBing, SmsEdit } from 'iconsax-react'
|
||||||
import SideBarItem from './SideBarItem'
|
import SideBarItem from './SideBarItem'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { Pages } from '../config/Pages'
|
import { Pages } from '../config/Pages'
|
||||||
@@ -222,7 +222,7 @@ const SideBar: FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{
|
{/* {
|
||||||
checkPermission(PermissionEnum.MANAGE_SETTINGS) && (
|
checkPermission(PermissionEnum.MANAGE_SETTINGS) && (
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
icon={<NotificationStatus variant={isActive('notifications') ? 'Bold' : 'Outline'} color={isActive('notifications') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
icon={<NotificationStatus variant={isActive('notifications') ? 'Bold' : 'Outline'} color={isActive('notifications') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
@@ -233,7 +233,7 @@ const SideBar: FC = () => {
|
|||||||
activeName='notifications'
|
activeName='notifications'
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
} */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { clx } from '../helpers/utils'
|
|||||||
import { Pages } from '../config/Pages'
|
import { Pages } from '../config/Pages'
|
||||||
import { useSharedStore } from './store/sharedStore'
|
import { useSharedStore } from './store/sharedStore'
|
||||||
import { removeToken, removeRefreshToken } from '../config/func'
|
import { removeToken, removeRefreshToken } from '../config/func'
|
||||||
|
import { SideBarItemHasSubMenu } from '../config/SideBarSubMenu'
|
||||||
// import { useGetAdminPermissions } from '../pages/users/hooks/useUserData'
|
// import { useGetAdminPermissions } from '../pages/users/hooks/useUserData'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -27,9 +28,12 @@ const SideBarItem: FC<Props> = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (props.name) {
|
if (props.name && SideBarItemHasSubMenu.includes(props.name)) {
|
||||||
setSubMenuName(props.name)
|
setSubMenuName(props.name)
|
||||||
setSubtMenu(true)
|
setSubtMenu(true)
|
||||||
|
} else {
|
||||||
|
setSubtMenu(false)
|
||||||
|
setSubMenuName('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if (props.activeName === '' || props.activeName && adminPermissions?.data?.permissions?.some((permission: { name: string }) => permission.name === props.activeName)) {
|
// if (props.activeName === '' || props.activeName && adminPermissions?.data?.permissions?.some((permission: { name: string }) => permission.name === props.activeName)) {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
type PrintHtmlDocumentParams = {
|
||||||
|
title: string;
|
||||||
|
html: string;
|
||||||
|
widthMm?: number;
|
||||||
|
styles?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultStyles = `
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #111;
|
||||||
|
font-family: Tahoma, Arial, sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
@page {
|
||||||
|
size: 80mm auto;
|
||||||
|
margin: 4mm;
|
||||||
|
}
|
||||||
|
.receipt {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const printHtmlDocument = ({
|
||||||
|
title,
|
||||||
|
html,
|
||||||
|
widthMm = 80,
|
||||||
|
styles,
|
||||||
|
}: PrintHtmlDocumentParams): void => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const printWindow = window.open("", "_blank");
|
||||||
|
if (!printWindow) return;
|
||||||
|
|
||||||
|
const safeTitle = title.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
const pageStyles = styles ?? defaultStyles.replace("80mm", `${widthMm}mm`);
|
||||||
|
|
||||||
|
const pageHtml = `
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>${safeTitle}</title>
|
||||||
|
<style>${pageStyles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${html}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
printWindow.document.open();
|
||||||
|
printWindow.document.write(pageHtml);
|
||||||
|
printWindow.document.close();
|
||||||
|
|
||||||
|
const handlePrint = () => {
|
||||||
|
printWindow.focus();
|
||||||
|
printWindow.print();
|
||||||
|
setTimeout(() => {
|
||||||
|
printWindow.close();
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some browsers complete loading too fast for onload assignment.
|
||||||
|
if (printWindow.document.readyState === "complete") {
|
||||||
|
setTimeout(handlePrint, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printWindow.addEventListener("load", () => {
|
||||||
|
setTimeout(handlePrint, 50);
|
||||||
|
}, { once: true });
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user