Compare commits

..

10 Commits

Author SHA1 Message Date
hamid zarghami 8177f71b3b print for kitchen
deploy to danak / build_and_deploy (push) Has been cancelled
2026-04-30 10:53:44 +03:30
hamid zarghami 333643784e generate code 2026-02-10 09:21:03 +03:30
hamid zarghami 9b2bdc36bb optional description 2026-02-07 12:42:29 +03:30
hamid zarghami d9012e524a filter category id 2026-02-07 12:39:45 +03:30
hamid zarghami 1552830e76 income box 2026-02-07 12:16:02 +03:30
hamid zarghami e4e54d6203 lag sidebar 2026-01-05 11:32:39 +03:30
hamid zarghami 43be71afef add order in category 2026-01-05 11:14:36 +03:30
hamid zarghami 7b2a86d3cd add order in food 2026-01-05 11:11:40 +03:30
hamid zarghami 84aefb2a14 direct login 2026-01-05 10:41:08 +03:30
hamid zarghami 541898d48e setting notification 2026-01-05 10:14:10 +03:30
22 changed files with 410 additions and 51 deletions
+23
View File
@@ -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')
+10 -8
View File
@@ -174,14 +174,16 @@ const Filters: FC<FiltersProps> = ({
case 'select': case 'select':
return ( return (
<Select <div className='mt-1'>
key={field.name} <Select
placeholder={field.placeholder} key={field.name}
onChange={(e) => handleChange(field.name, e.target.value)} placeholder={field.placeholder}
defaultValue={currentValue as string || field.defaultValue || ''} onChange={(e) => handleChange(field.name, e.target.value)}
items={field.options} defaultValue={currentValue as string || field.defaultValue || ''}
className='min-w-[170px]' items={field.options}
/> 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) => {
+9 -4
View File
@@ -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 },
+14 -2
View File
@@ -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'>
+3 -1
View File
@@ -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),
+4 -3
View File
@@ -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])
} }
+7 -3
View File
@@ -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),
}); });
}; };
+11 -6
View File
@@ -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;
+2
View File
@@ -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 = {
+6
View File
@@ -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}
+14 -8
View File
@@ -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}
+160
View File
@@ -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,
});
};
+10 -1
View File
@@ -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>
) )
+1
View File
@@ -3,4 +3,5 @@ export const enum SettingTabEnum {
PASSWORD = "password", PASSWORD = "password",
FINANCIAL = "financial", FINANCIAL = "financial",
DELIVERY = "delivery", DELIVERY = "delivery",
NOTIFICATION = "notification",
} }
+10 -8
View File
@@ -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,7 +51,15 @@ const Statistics: FC = () => {
<RevenueChart /> <RevenueChart />
</div> </div>
<div className='lg:col-span-4'> <div className='lg:col-span-4'>
<FoodSalesChart /> <StatCard
icon={<DollarCircle size={24} color='#10B981' />}
title='درآمد'
value={stats?.totalRevenue ? formatPrice(stats.totalRevenue) : '۰ تومان'}
color='#10B981'
/>
<div className='mt-5'>
<FoodSalesChart />
</div>
</div> </div>
</div> </div>
</div> </div>
+3 -3
View File
@@ -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>
)} )}
+5 -1
View File
@@ -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)) {
+77
View File
@@ -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, "&lt;").replace(/>/g, "&gt;");
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 });
};