Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc27bce1b2 | |||
| a269266897 | |||
| 1300491a2d | |||
| 8b6f586636 | |||
| 1cf0069db3 | |||
| 50cb3ecc65 | |||
| d2418da54a |
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
|
|||||||
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
|
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
|
||||||
import { useReceiptStore } from "@/zustand/receiptStore";
|
import { useReceiptStore } from "@/zustand/receiptStore";
|
||||||
import {
|
import {
|
||||||
|
useBulkCart,
|
||||||
useClearCart,
|
useClearCart,
|
||||||
useDecrementCart,
|
useDecrementCart,
|
||||||
useGetCartItems,
|
useGetCartItems,
|
||||||
@@ -19,6 +20,7 @@ export const useCart = () => {
|
|||||||
clear,
|
clear,
|
||||||
increment,
|
increment,
|
||||||
decrement,
|
decrement,
|
||||||
|
setQuantity,
|
||||||
items,
|
items,
|
||||||
slug,
|
slug,
|
||||||
setSlug,
|
setSlug,
|
||||||
@@ -32,6 +34,10 @@ export const useCart = () => {
|
|||||||
isPending: isDecrementPending,
|
isPending: isDecrementPending,
|
||||||
} = useDecrementCart();
|
} = useDecrementCart();
|
||||||
const { mutate: mutateClearCart } = useClearCart();
|
const { mutate: mutateClearCart } = useClearCart();
|
||||||
|
const {
|
||||||
|
mutate: mutateBulkCart,
|
||||||
|
isPending: isBulkPending,
|
||||||
|
} = useBulkCart();
|
||||||
const { data: cartItems } = useGetCartItems(isSuccess);
|
const { data: cartItems } = useGetCartItems(isSuccess);
|
||||||
|
|
||||||
const addToCart = (id: string | number) => {
|
const addToCart = (id: string | number) => {
|
||||||
@@ -62,6 +68,24 @@ export const useCart = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setCartQuantity = (id: string | number, quantity: number) => {
|
||||||
|
if (isSuccess) {
|
||||||
|
mutateBulkCart(
|
||||||
|
{ items: [{ variantId: String(id), quantity }] },
|
||||||
|
{
|
||||||
|
onError: (error) => {
|
||||||
|
toast(extractErrorMessage(error), "error");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (currentSlug && slug !== currentSlug) {
|
||||||
|
setSlug(currentSlug);
|
||||||
|
}
|
||||||
|
setQuantity(id, quantity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const clearCart = useCallback(() => {
|
const clearCart = useCallback(() => {
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
mutateClearCart(undefined, {
|
mutateClearCart(undefined, {
|
||||||
@@ -92,7 +116,7 @@ export const useCart = () => {
|
|||||||
}
|
}
|
||||||
}, [isSuccess, cartItems?.data?.items, items]);
|
}, [isSuccess, cartItems?.data?.items, items]);
|
||||||
|
|
||||||
const isCartMutating = isIncrementPending || isDecrementPending;
|
const isCartMutating = isIncrementPending || isDecrementPending || isBulkPending;
|
||||||
|
|
||||||
// برای کاربران لاگین شده، slug از API میآید (در cartItems.data.shopId)
|
// برای کاربران لاگین شده، slug از API میآید (در cartItems.data.shopId)
|
||||||
// برای کاربران لاگین نشده، slug از receiptStore میآید
|
// برای کاربران لاگین نشده، slug از receiptStore میآید
|
||||||
@@ -111,6 +135,7 @@ export const useCart = () => {
|
|||||||
clear,
|
clear,
|
||||||
addToCart,
|
addToCart,
|
||||||
removeFromCart,
|
removeFromCart,
|
||||||
|
setCartQuantity,
|
||||||
clearCart,
|
clearCart,
|
||||||
isCartMutating,
|
isCartMutating,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { useGetCartItems, useSaveAllMethod } from '@/app/[name]/(Dialogs)/cart/h
|
|||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useGetShipmentMethod } from '../../hooks/useOrderData';
|
import { useGetShipmentMethod } from '../../hooks/useOrderData';
|
||||||
|
import { useGetProducts } from '@/app/[name]/(Main)/hooks/useMenuData';
|
||||||
|
import { isVariablePricing } from '@/app/[name]/(Main)/types/Types';
|
||||||
import CartItemsList from '@/app/[name]/(Dialogs)/cart/components/CartItemsList';
|
import CartItemsList from '@/app/[name]/(Dialogs)/cart/components/CartItemsList';
|
||||||
import { useCartStore } from '@/app/[name]/(Dialogs)/cart/store/Store';
|
import { useCartStore } from '@/app/[name]/(Dialogs)/cart/store/Store';
|
||||||
|
|
||||||
@@ -33,8 +35,22 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
|||||||
const name = params.name as string;
|
const name = params.name as string;
|
||||||
const { isSuccess } = useGetProfile();
|
const { isSuccess } = useGetProfile();
|
||||||
const { data: cartData } = useGetCartItems(isSuccess);
|
const { data: cartData } = useGetCartItems(isSuccess);
|
||||||
|
const { data: productsResponse } = useGetProducts();
|
||||||
const { data: shipmentMethod } = useGetShipmentMethod();
|
const { data: shipmentMethod } = useGetShipmentMethod();
|
||||||
|
|
||||||
|
const hasVariablePricingItem = useMemo(() => {
|
||||||
|
const cartItems = cartData?.data?.items;
|
||||||
|
const products = productsResponse?.data;
|
||||||
|
if (!cartItems?.length || !products?.length) return false;
|
||||||
|
|
||||||
|
return cartItems.some((item) => {
|
||||||
|
const product = products.find((p) =>
|
||||||
|
p.variants?.some((v) => String(v.id) === String(item.variantId))
|
||||||
|
);
|
||||||
|
return product != null && isVariablePricing(product);
|
||||||
|
});
|
||||||
|
}, [cartData?.data?.items, productsResponse?.data]);
|
||||||
|
|
||||||
const selectedDeliveryMethod = useMemo(() => {
|
const selectedDeliveryMethod = useMemo(() => {
|
||||||
if (!shipmentMethod?.data || !deliveryType) return null;
|
if (!shipmentMethod?.data || !deliveryType) return null;
|
||||||
return shipmentMethod.data.find((item) => item.id === deliveryType);
|
return shipmentMethod.data.find((item) => item.id === deliveryType);
|
||||||
@@ -132,8 +148,8 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
clear();
|
clear();
|
||||||
resetCart();
|
resetCart();
|
||||||
if (data?.data?.paymentUrl) {
|
if (!hasVariablePricingItem && data?.data?.paymentUrl) {
|
||||||
window.location.href = data?.data?.paymentUrl;
|
window.location.href = data.data.paymentUrl;
|
||||||
toast('در حال ریدایرکت به صفحه پرداخت...', 'success');
|
toast('در حال ریدایرکت به صفحه پرداخت...', 'success');
|
||||||
} else {
|
} else {
|
||||||
window.location.href = `/${name}/order/track/${data?.data?.order?.id}`;
|
window.location.href = `/${name}/order/track/${data?.data?.order?.id}`;
|
||||||
@@ -193,7 +209,7 @@ export const SummarySection = ({ cartModal, onToggleCartModal, deliveryType }: S
|
|||||||
<div className='text-sm mt-2 font-semibold'>{formatPrice(total)} تومان</div>
|
<div className='text-sm mt-2 font-semibold'>{formatPrice(total)} تومان</div>
|
||||||
</div>
|
</div>
|
||||||
<Button pending={isCreateOrderPending || isSaveAllMethodPending} onClick={handleSubmit} className='px-10 w-fit dark:bg-white dark:text-black! dark:hover:bg-gray-100'>
|
<Button pending={isCreateOrderPending || isSaveAllMethodPending} onClick={handleSubmit} className='px-10 w-fit dark:bg-white dark:text-black! dark:hover:bg-gray-100'>
|
||||||
{t("ButtonSubmit")}
|
{hasVariablePricingItem ? t("ButtonRegister") : t("ButtonSubmit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
PaymentMethodsResponse,
|
PaymentMethodsResponse,
|
||||||
CreateOrderResponse,
|
CreateOrderResponse,
|
||||||
OrderDetailResponse,
|
OrderDetailResponse,
|
||||||
|
PayOrderResponse,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => {
|
export const getShipmentMethod = async (): Promise<ShipmentMethodsResponse> => {
|
||||||
@@ -81,7 +82,7 @@ export const cancelOrder = async (id: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const confirmOrder = async (id: string) => {
|
export const confirmOrder = async (id: string): Promise<PayOrderResponse> => {
|
||||||
const { data } = await api.get(`/public/payments/pay-order/${id}`);
|
const { data } = await api.get<PayOrderResponse>(`/public/payments/pay-order/${id}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BaseResponse } from "@/app/[name]/(Main)/types/Types";
|
import { BaseResponse, Product } from "@/app/[name]/(Main)/types/Types";
|
||||||
|
|
||||||
export interface ShipmentMethod {
|
export interface ShipmentMethod {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -272,6 +272,12 @@ export interface CreateOrderData {
|
|||||||
|
|
||||||
export type CreateOrderResponse = BaseResponse<CreateOrderData>;
|
export type CreateOrderResponse = BaseResponse<CreateOrderData>;
|
||||||
|
|
||||||
|
export interface PayOrderData {
|
||||||
|
paymentUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayOrderResponse = BaseResponse<PayOrderData>;
|
||||||
|
|
||||||
export interface OrderDetailUser {
|
export interface OrderDetailUser {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -361,28 +367,15 @@ export interface OrderDetailPaymentMethod {
|
|||||||
merchantId: string | null;
|
merchantId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderDetailFood {
|
export interface OrderDetailVariant {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
restaurant: string;
|
value: string;
|
||||||
category: string;
|
|
||||||
title: string;
|
|
||||||
desc: string;
|
|
||||||
content: string[];
|
|
||||||
price: number;
|
price: number;
|
||||||
order: number | null;
|
stock: number | null;
|
||||||
prepareTime: number | null;
|
product: Product;
|
||||||
weekDays: number[];
|
|
||||||
mealTypes: string[];
|
|
||||||
isActive: boolean;
|
|
||||||
images: string[];
|
|
||||||
inPlaceServe: boolean;
|
|
||||||
pickupServe: boolean;
|
|
||||||
score: number;
|
|
||||||
discount: number;
|
|
||||||
isSpecialOffer: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderDetailItem {
|
export interface OrderDetailItem {
|
||||||
@@ -391,11 +384,12 @@ export interface OrderDetailItem {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
order: string;
|
order: string;
|
||||||
food: OrderDetailFood;
|
variant: OrderDetailVariant;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
discount: number;
|
discount: number;
|
||||||
totalPrice: number;
|
totalPrice: number;
|
||||||
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderDetailHistory {
|
export interface OrderDetailHistory {
|
||||||
@@ -454,6 +448,7 @@ export interface OrderDetail {
|
|||||||
tableNumber: string | null;
|
tableNumber: string | null;
|
||||||
status:
|
status:
|
||||||
| "pendingPayment"
|
| "pendingPayment"
|
||||||
|
| "needConfirmation"
|
||||||
| "new"
|
| "new"
|
||||||
| "preparing"
|
| "preparing"
|
||||||
| "ready"
|
| "ready"
|
||||||
|
|||||||
@@ -1,32 +1,58 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import type { Product, ProductCategory } from "@/app/[name]/(Main)/types/Types";
|
||||||
import React from 'react';
|
import { isVariablePricing } from "@/app/[name]/(Main)/types/Types";
|
||||||
import Button from '@/components/button/PrimaryButton';
|
import Button from "@/components/button/PrimaryButton";
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import MenuItem from "@/components/listview/MenuItem";
|
||||||
import { Clock } from 'iconsax-react';
|
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
||||||
import clsx from 'clsx';
|
import { toast } from "@/components/Toast";
|
||||||
import useToggle from '@/hooks/helpers/useToggle';
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import Prompt from '@/components/utils/Prompt';
|
import Prompt from "@/components/utils/Prompt";
|
||||||
import { useCancelOrder, useGetOrderDetail } from '../../checkout/hooks/useOrderData';
|
import useToggle from "@/hooks/helpers/useToggle";
|
||||||
import ordersTranslations from '@/locales/fa/orders.json';
|
import { extractErrorMessage } from "@/lib/func";
|
||||||
import { toast } from '@/components/Toast';
|
import ordersTranslations from "@/locales/fa/orders.json";
|
||||||
import { extractErrorMessage } from '@/lib/func';
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import clsx from "clsx";
|
||||||
|
import { Clock } from "iconsax-react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import React from "react";
|
||||||
|
import { useCancelOrder, useConfirmOrder, useGetOrderDetail } from "../../checkout/hooks/useOrderData";
|
||||||
|
import type { OrderDetailItem } from "../../checkout/types/Types";
|
||||||
|
|
||||||
|
const orderItemToProduct = (item: OrderDetailItem): Product => {
|
||||||
|
const { product, ...variant } = item.variant;
|
||||||
|
const categoryId = typeof product.category === "string" ? product.category : (product.category?.id ?? "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
category: (typeof product.category === "object" && product.category ? product.category : { id: categoryId }) as ProductCategory,
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: variant.id,
|
||||||
|
createdAt: variant.createdAt,
|
||||||
|
updatedAt: variant.updatedAt,
|
||||||
|
deletedAt: variant.deletedAt,
|
||||||
|
product: product.id,
|
||||||
|
value: variant.value,
|
||||||
|
price: item.unitPrice,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Product;
|
||||||
|
};
|
||||||
|
|
||||||
const getDeliveryMethodTitle = (method: string) => {
|
const getDeliveryMethodTitle = (method: string) => {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'deliveryCourier':
|
case "deliveryCourier":
|
||||||
return 'ارسال توسط پیک';
|
return "ارسال توسط پیک";
|
||||||
case 'deliveryCar':
|
case "deliveryCar":
|
||||||
return 'تحویل به خودرو';
|
return "تحویل به خودرو";
|
||||||
case 'pickup':
|
case "pickup":
|
||||||
return 'تحویل حضوری';
|
return "تحویل حضوری";
|
||||||
case 'dineIn':
|
case "dineIn":
|
||||||
return 'سرو در فروشگاه';
|
return "سرو در فروشگاه";
|
||||||
default:
|
default:
|
||||||
return 'روش ارسال';
|
return "روش ارسال";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const getStatusPercentage = (status: string) => {
|
// const getStatusPercentage = (status: string) => {
|
||||||
@@ -53,17 +79,17 @@ const getDeliveryMethodTitle = (method: string) => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
const getStatusText = (status: string): string => {
|
const getStatusText = (status: string): string => {
|
||||||
const statusKey = status as keyof typeof ordersTranslations.status;
|
const statusKey = status as keyof typeof ordersTranslations.status;
|
||||||
return ordersTranslations.status[statusKey] || 'نامشخص';
|
return ordersTranslations.status[statusKey] || "نامشخص";
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string): string => {
|
const formatDate = (dateString: string): string => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString('fa-IR', {
|
return date.toLocaleDateString("fa-IR", {
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: 'long',
|
month: "long",
|
||||||
day: 'numeric'
|
day: "numeric",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const formatTime = (dateString: string): string => {
|
// const formatTime = (dateString: string): string => {
|
||||||
@@ -75,150 +101,213 @@ const formatDate = (dateString: string): string => {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
function OrderTrackingPage() {
|
function OrderTrackingPage() {
|
||||||
const { id, name } = useParams();
|
const { id, name } = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { state: cancelModal, toggle: toggleCancelModal } = useToggle();
|
const { state: cancelModal, toggle: toggleCancelModal } = useToggle();
|
||||||
const { data: orderDetail, isLoading } = useGetOrderDetail(id as string);
|
const { data: orderDetail, isLoading } = useGetOrderDetail(id as string);
|
||||||
const { mutate: cancelOrder } = useCancelOrder();
|
const { mutate: cancelOrder } = useCancelOrder();
|
||||||
|
const { mutate: payOrder, isPending: isPaying } = useConfirmOrder();
|
||||||
|
|
||||||
const onCancelOrder = (e: React.MouseEvent | null) => {
|
const orderStatus = orderDetail?.data?.status;
|
||||||
if (!e || !id) return;
|
const showPayButton = orderStatus === "pendingPayment" || orderStatus === "needConfirmation";
|
||||||
|
const isPayDisabled = orderStatus === "needConfirmation";
|
||||||
|
|
||||||
cancelOrder(id as string, {
|
const onPayOrder = () => {
|
||||||
onSuccess: () => {
|
if (!id || isPayDisabled) return;
|
||||||
toggleCancelModal();
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['order-detail', id] });
|
|
||||||
toast('سفارش با موفقیت لغو شد', 'success');
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast(extractErrorMessage(error), 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
payOrder(id as string, {
|
||||||
<div className="fixed inset-0 bg-container flex flex-col">
|
onSuccess: (data) => {
|
||||||
<div className='flex-1 overflow-y-auto px-4 py-6'>
|
const paymentUrl = data?.data?.paymentUrl;
|
||||||
<div className='max-w-2xl mx-auto flex flex-col gap-6'>
|
if (paymentUrl) {
|
||||||
{isLoading ? (
|
toast("در حال ریدایرکت به صفحه پرداخت...", "success");
|
||||||
<>
|
window.location.href = paymentUrl;
|
||||||
<Skeleton className='h-6 w-48 mx-auto' />
|
} else {
|
||||||
<div className='flex flex-col gap-4'>
|
queryClient.invalidateQueries({ queryKey: ["order-detail", id] });
|
||||||
<Skeleton className='h-5 w-full' />
|
toast("پرداخت با موفقیت انجام شد", "success");
|
||||||
<Skeleton className='h-5 w-full' />
|
}
|
||||||
<Skeleton className='h-20 w-full' />
|
},
|
||||||
<Skeleton className='h-5 w-full' />
|
onError: (error) => {
|
||||||
</div>
|
toast(extractErrorMessage(error), "error");
|
||||||
</>
|
},
|
||||||
) : orderDetail?.data ? (
|
});
|
||||||
<>
|
};
|
||||||
<h1 className='text-lg font-semibold text-center'>
|
|
||||||
{getDeliveryMethodTitle(orderDetail.data.deliveryMethod.method)}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className='flex flex-col'>
|
const onCancelOrder = (e: React.MouseEvent | null) => {
|
||||||
<div className='flex justify-between items-center h-16 border-b border-border'>
|
if (!e || !id) return;
|
||||||
<span className='text-sm text-muted-foreground flex items-center gap-2'>
|
|
||||||
<Clock className='stroke-current' size={16} />
|
|
||||||
زمان سفارش
|
|
||||||
</span>
|
|
||||||
<div className='flex items-end gap-0.5'>
|
|
||||||
<span className='text-sm font-semibold'>{formatDate(orderDetail.data.createdAt)}</span>
|
|
||||||
{/* <span className='text-xs text-muted-foreground'>{formatTime(orderDetail.data.createdAt)}</span> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-between items-center h-16 border-b border-border'>
|
|
||||||
<span className='text-sm text-muted-foreground'>نوع ارسال</span>
|
|
||||||
<span className='text-sm font-semibold'>{orderDetail.data?.deliveryMethod?.description}</span>
|
|
||||||
</div>
|
|
||||||
{orderDetail.data.carAddress && (
|
|
||||||
<div className='flex justify-between items-center h-16 border-b border-border'>
|
|
||||||
<div className='text-sm text-muted-foreground mb-1'>اطلاعات خودرو</div>
|
|
||||||
<div className='text-sm font-semibold'>
|
|
||||||
{orderDetail.data.carAddress.carModel} {orderDetail.data.carAddress.carColor} - پلاک: {orderDetail.data.carAddress.plateNumber}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='flex justify-between items-center h-16 border-b border-border'>
|
|
||||||
<span className='text-sm text-muted-foreground'>شماره سفارش</span>
|
|
||||||
<span className='text-sm font-semibold'>#{orderDetail.data.orderNumber}</span>
|
|
||||||
</div>
|
|
||||||
<div className='flex justify-between items-center h-16 border-b border-border'>
|
|
||||||
<span className='text-sm text-muted-foreground'>وضعیت سفارش</span>
|
|
||||||
<span className='text-sm font-semibold'>{getStatusText(orderDetail.data.status)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{orderDetail.data.tableNumber && (
|
cancelOrder(id as string, {
|
||||||
<div className='flex justify-between items-center h-16 border-b border-border'>
|
onSuccess: () => {
|
||||||
<span className='text-sm text-muted-foreground'>شماره میز</span>
|
toggleCancelModal();
|
||||||
<span className='text-sm font-semibold'>#{orderDetail.data.tableNumber}</span>
|
queryClient.invalidateQueries({ queryKey: ["order-detail", id] });
|
||||||
</div>
|
toast("سفارش با موفقیت لغو شد", "success");
|
||||||
)}
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast(extractErrorMessage(error), "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
{orderDetail.data.userAddress && (
|
return (
|
||||||
<div className='py-3 border-b border-border'>
|
<div className="fixed inset-0 bg-container flex flex-col">
|
||||||
<div className='text-sm text-muted-foreground mb-1'>آدرس تحویل</div>
|
<div className="flex-1 overflow-y-auto px-4 py-6">
|
||||||
<div className='text-sm'>{orderDetail.data.userAddress.address}</div>
|
<div className="max-w-2xl mx-auto flex flex-col gap-6">
|
||||||
</div>
|
{isLoading ? (
|
||||||
)}
|
<>
|
||||||
|
<Skeleton className="h-6 w-48 mx-auto" />
|
||||||
<div className='flex justify-between items-center py-4 border-t border-border'>
|
<div className="flex flex-col gap-4">
|
||||||
<span className='text-sm font-medium'>مجموع سفارش</span>
|
<Skeleton className="h-5 w-full" />
|
||||||
<span className='text-sm font-bold '>
|
<Skeleton className="h-5 w-full" />
|
||||||
{orderDetail.data.total.toLocaleString('fa-IR')} تومان
|
<Skeleton className="h-20 w-full" />
|
||||||
</span>
|
<Skeleton className="h-5 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
) : orderDetail?.data ? (
|
||||||
) : (
|
<>
|
||||||
<div className='text-center text-muted-foreground py-12'>
|
{orderDetail.data.status === "needConfirmation" && (
|
||||||
اطلاعات سفارش یافت نشد
|
<div className="rounded-2xl bg-red-50 px-4 py-3 text-center">
|
||||||
</div>
|
<p className="text-sm text-red-800 leading-6">لطفاً پس از دریافت پیامک، حداکثر تا ۲ ساعت نسبت به پرداخت اقدام کنید؛ در غیر این صورت سفارش بهصورت خودکار لغو خواهد شد.</p>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className='p-4 border-t border-border bg-container'>
|
<h1 className="text-lg font-semibold text-center">{getDeliveryMethodTitle(orderDetail.data.deliveryMethod.method)}</h1>
|
||||||
<div className='max-w-2xl mx-auto grid grid-cols-2 gap-4'>
|
|
||||||
<Button
|
<div className="flex flex-col">
|
||||||
className='dark:bg-white dark:text-black! dark:hover:bg-gray-100'
|
<div className="flex justify-between items-center h-16 border-b border-border">
|
||||||
onClick={() => router.push(`/${name}`)}
|
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||||
type='button'>
|
<Clock className="stroke-current" size={16} />
|
||||||
بازگشت به منو
|
زمان سفارش
|
||||||
</Button>
|
</span>
|
||||||
<Button
|
<div className="flex items-end gap-0.5">
|
||||||
type='submit'
|
<span className="text-sm font-semibold">{formatDate(orderDetail.data.createdAt)}</span>
|
||||||
onClick={toggleCancelModal}
|
{/* <span className='text-xs text-muted-foreground'>{formatTime(orderDetail.data.createdAt)}</span> */}
|
||||||
className='bg-disabled! text-foreground!'
|
</div>
|
||||||
disabled={
|
</div>
|
||||||
orderDetail?.data?.status === 'cancelled' ||
|
<div className="flex justify-between items-center h-16 border-b border-border">
|
||||||
orderDetail?.data?.status === 'delivered' ||
|
<span className="text-sm text-muted-foreground">نوع ارسال</span>
|
||||||
orderDetail?.data?.status === 'delivering'
|
<span className="text-sm font-semibold">{orderDetail.data?.deliveryMethod?.description}</span>
|
||||||
}
|
</div>
|
||||||
>
|
{orderDetail.data.carAddress && (
|
||||||
لغو سفارش
|
<div className="flex justify-between items-center h-16 border-b border-border">
|
||||||
</Button>
|
<div className="text-sm text-muted-foreground mb-1">اطلاعات خودرو</div>
|
||||||
|
<div className="text-sm font-semibold">
|
||||||
|
{orderDetail.data.carAddress.carModel} {orderDetail.data.carAddress.carColor} - پلاک: {orderDetail.data.carAddress.plateNumber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between items-center h-16 border-b border-border">
|
||||||
|
<span className="text-sm text-muted-foreground">شماره سفارش</span>
|
||||||
|
<span className="text-sm font-semibold">#{orderDetail.data.orderNumber}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center h-16 border-b border-border">
|
||||||
|
<span className="text-sm text-muted-foreground">وضعیت سفارش</span>
|
||||||
|
<span className="text-sm font-semibold">{getStatusText(orderDetail.data.status)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={clsx(
|
{orderDetail.data.tableNumber && (
|
||||||
'fixed inset-0 z-1001',
|
<div className="flex justify-between items-center h-16 border-b border-border">
|
||||||
!cancelModal && 'pointer-events-none'
|
<span className="text-sm text-muted-foreground">شماره میز</span>
|
||||||
)}>
|
<span className="text-sm font-semibold">#{orderDetail.data.tableNumber}</span>
|
||||||
<Prompt
|
</div>
|
||||||
title={'لغو سفارش'}
|
)}
|
||||||
description={'آیا از درخواست خود اطمینان دارید؟'}
|
|
||||||
textConfirm={'بله'}
|
{orderDetail.data.userAddress && (
|
||||||
textCancel={'منصرف شدم'}
|
<div className="py-3 border-b border-border">
|
||||||
onConfirm={onCancelOrder}
|
<div className="text-sm text-muted-foreground mb-1">آدرس تحویل</div>
|
||||||
visible={cancelModal}
|
<div className="text-sm">{orderDetail.data.userAddress.address}</div>
|
||||||
onClick={toggleCancelModal}
|
</div>
|
||||||
onCancel={toggleCancelModal}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
{orderDetail.data.items?.length > 0 && (
|
||||||
|
<div className="py-4 mt-7 border-b border-border">
|
||||||
|
<div className="text-sm font-medium mb-4">اقلام سفارش</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{orderDetail.data.items.map((item) => {
|
||||||
|
const product = orderItemToProduct(item);
|
||||||
|
const needsConfirmation = isVariablePricing(product) || product.needAdminAcceptance;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItemRenderer key={item.id}>
|
||||||
|
<MenuItem
|
||||||
|
product={product}
|
||||||
|
variantId={item.variant.id}
|
||||||
|
variantValue={item.variant.value}
|
||||||
|
readOnly
|
||||||
|
displayQuantity={item.quantity}
|
||||||
|
badge={
|
||||||
|
needsConfirmation ? (
|
||||||
|
item.status === "confirmed" ? (
|
||||||
|
<span className="inline-block rounded-lg bg-green-50 dark:bg-green-950/40 px-3 py-1.5 text-xs text-green-600 dark:text-green-400">
|
||||||
|
{ordersTranslations.itemStatus.confirmed}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-block rounded-lg bg-orange-50 dark:bg-orange-950/40 px-3 py-1.5 text-xs text-orange-600 dark:text-orange-400">
|
||||||
|
{ordersTranslations.itemStatus.needConfirmation}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuItemRenderer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center py-4 border-t border-border">
|
||||||
|
<span className="text-sm font-medium">مجموع سفارش</span>
|
||||||
|
<span className="text-sm font-bold ">{orderDetail.data.total.toLocaleString("fa-IR")} تومان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground py-12">اطلاعات سفارش یافت نشد</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4 border-t border-border bg-container">
|
||||||
|
<div className="max-w-2xl mx-auto grid grid-cols-2 gap-4">
|
||||||
|
<Button className="bg-disabled! text-foreground!" onClick={() => router.push(`/${name}`)} type="button">
|
||||||
|
بازگشت به منو
|
||||||
|
</Button>
|
||||||
|
{showPayButton ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={onPayOrder}
|
||||||
|
disabled={isPayDisabled}
|
||||||
|
pending={isPaying}
|
||||||
|
>
|
||||||
|
پرداخت
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={toggleCancelModal}
|
||||||
|
className="bg-disabled! text-foreground!"
|
||||||
|
disabled={orderDetail?.data?.status === "cancelled" || orderDetail?.data?.status === "delivered" || orderDetail?.data?.status === "delivering"}
|
||||||
|
>
|
||||||
|
لغو سفارش
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx("fixed inset-0 z-1001", !cancelModal && "pointer-events-none")}>
|
||||||
|
<Prompt
|
||||||
|
title={"لغو سفارش"}
|
||||||
|
description={"آیا از درخواست خود اطمینان دارید؟"}
|
||||||
|
textConfirm={"بله"}
|
||||||
|
textCancel={"منصرف شدم"}
|
||||||
|
onConfirm={onCancelOrder}
|
||||||
|
visible={cancelModal}
|
||||||
|
onClick={toggleCancelModal}
|
||||||
|
onCancel={toggleCancelModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrderTrackingPage;
|
export default OrderTrackingPage;
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { CartQuantityControl } from '@/components/CartQuantityControl'
|
import { CartQuantityControl } from '@/components/CartQuantityControl'
|
||||||
|
import { VariableWeightSlider } from '@/components/product/VariableWeightSlider'
|
||||||
|
import PlusIcon from '@/components/icons/PlusIcon'
|
||||||
import { ef } from '@/lib/helpers/utfNumbers'
|
import { ef } from '@/lib/helpers/utfNumbers'
|
||||||
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
import { useCart } from '@/app/[name]/(Dialogs)/cart/hook/useCart'
|
||||||
import { getPrimaryVariantId } from '@/app/[name]/(Main)/types/Types'
|
import {
|
||||||
|
getPrimaryVariantId,
|
||||||
|
getPurchaseUnitLabel,
|
||||||
|
getVariableQuantityRange,
|
||||||
|
isVariablePricing,
|
||||||
|
} from '@/app/[name]/(Main)/types/Types'
|
||||||
import { ArrowLeft, Cup, Heart, TruckTick } from 'iconsax-react'
|
import { ArrowLeft, Cup, Heart, TruckTick } from 'iconsax-react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useGetProduct, useToggleFavorite } from './hooks/useProductData'
|
import { useGetProduct, useToggleFavorite } from './hooks/useProductData'
|
||||||
import { useGetAbout } from '../about/hooks/useAboutData'
|
import { useGetAbout } from '../about/hooks/useAboutData'
|
||||||
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
import { useGetProfile } from '../../(Profile)/profile/hooks/userProfileData'
|
||||||
@@ -23,9 +30,13 @@ function ProductPage({ }: Props) {
|
|||||||
const { isSuccess } = useGetProfile();
|
const { isSuccess } = useGetProfile();
|
||||||
const { data: product, isLoading } = useGetProduct(id as string);
|
const { data: product, isLoading } = useGetProduct(id as string);
|
||||||
const { data: about } = useGetAbout();
|
const { data: about } = useGetAbout();
|
||||||
const { items, addToCart, removeFromCart, isCartMutating } = useCart();
|
const { items, addToCart, removeFromCart, setCartQuantity, isCartMutating } = useCart();
|
||||||
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
const [isFavorite, setIsFavorite] = useState<boolean>(false);
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
||||||
|
const [selectedWeight, setSelectedWeight] = useState<number>(1);
|
||||||
|
const isUserInteractingRef = useRef(false);
|
||||||
|
const cartSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const pendingCartWeightRef = useRef<number | null>(null);
|
||||||
const { mutate: toggleFavorite } = useToggleFavorite();
|
const { mutate: toggleFavorite } = useToggleFavorite();
|
||||||
|
|
||||||
const productId = product?.data?.id || id as string;
|
const productId = product?.data?.id || id as string;
|
||||||
@@ -48,6 +59,62 @@ function ProductPage({ }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddVariableToCart = () => {
|
||||||
|
if (!cartId) return;
|
||||||
|
if (cartSyncTimerRef.current) {
|
||||||
|
clearTimeout(cartSyncTimerRef.current);
|
||||||
|
cartSyncTimerRef.current = null;
|
||||||
|
}
|
||||||
|
pendingCartWeightRef.current = null;
|
||||||
|
isUserInteractingRef.current = false;
|
||||||
|
setCartQuantity(cartId, selectedWeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCartSyncTimer = useCallback(() => {
|
||||||
|
if (cartSyncTimerRef.current) {
|
||||||
|
clearTimeout(cartSyncTimerRef.current);
|
||||||
|
cartSyncTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleVariableWeightChange = useCallback((weight: number) => {
|
||||||
|
isUserInteractingRef.current = true;
|
||||||
|
setSelectedWeight(weight);
|
||||||
|
clearCartSyncTimer();
|
||||||
|
pendingCartWeightRef.current = null;
|
||||||
|
}, [clearCartSyncTimer]);
|
||||||
|
|
||||||
|
const handleVariableWeightCommit = useCallback((weight: number) => {
|
||||||
|
if (!isSuccess || !cartId || quantity <= 0) {
|
||||||
|
isUserInteractingRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCartWeightRef.current = weight;
|
||||||
|
clearCartSyncTimer();
|
||||||
|
|
||||||
|
cartSyncTimerRef.current = setTimeout(() => {
|
||||||
|
cartSyncTimerRef.current = null;
|
||||||
|
const pendingWeight = pendingCartWeightRef.current;
|
||||||
|
pendingCartWeightRef.current = null;
|
||||||
|
|
||||||
|
if (pendingWeight == null || !cartId) {
|
||||||
|
isUserInteractingRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentItem = items?.[cartId] ?? items?.[String(cartId)];
|
||||||
|
const currentQty = currentItem && typeof currentItem === 'object' && 'quantity' in currentItem
|
||||||
|
? currentItem.quantity
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (Math.abs(pendingWeight - currentQty) > 0.0001) {
|
||||||
|
setCartQuantity(cartId, pendingWeight);
|
||||||
|
}
|
||||||
|
isUserInteractingRef.current = false;
|
||||||
|
}, 1000);
|
||||||
|
}, [isSuccess, cartId, quantity, clearCartSyncTimer, items, setCartQuantity]);
|
||||||
|
|
||||||
const handleRemoveFromCart = () => {
|
const handleRemoveFromCart = () => {
|
||||||
if (cartId) {
|
if (cartId) {
|
||||||
removeFromCart(cartId);
|
removeFromCart(cartId);
|
||||||
@@ -95,6 +162,21 @@ function ProductPage({ }: Props) {
|
|||||||
setSelectedVariantId((prev) => prev ?? variants[0].id);
|
setSelectedVariantId((prev) => prev ?? variants[0].id);
|
||||||
}, [product?.data]);
|
}, [product?.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!product?.data || !isVariablePricing(product.data) || !cartId) return;
|
||||||
|
if (isUserInteractingRef.current) return;
|
||||||
|
|
||||||
|
const { min } = getVariableQuantityRange(product.data);
|
||||||
|
const targetWeight = quantity > 0 ? quantity : min;
|
||||||
|
setSelectedWeight((prev) => (
|
||||||
|
Math.abs(prev - targetWeight) < 0.0001 ? prev : targetWeight
|
||||||
|
));
|
||||||
|
}, [product?.data, cartId, quantity]);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
clearCartSyncTimer();
|
||||||
|
}, [clearCartSyncTimer]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center min-h-[400px]'>
|
<div className='flex items-center justify-center min-h-[400px]'>
|
||||||
@@ -131,6 +213,12 @@ function ProductPage({ }: Props) {
|
|||||||
const price = hasVariants
|
const price = hasVariants
|
||||||
? (selectedVariant?.price ?? 0)
|
? (selectedVariant?.price ?? 0)
|
||||||
: (productData.price ?? selectedVariant?.price ?? 0);
|
: (productData.price ?? selectedVariant?.price ?? 0);
|
||||||
|
const isVariable = isVariablePricing(productData);
|
||||||
|
const weightRange = isVariable ? getVariableQuantityRange(productData) : null;
|
||||||
|
const unitPriceLabel = isVariable
|
||||||
|
? getPurchaseUnitLabel(productData.purchaseUnit)
|
||||||
|
: '';
|
||||||
|
const calculatedPrice = isVariable ? Math.round(price * selectedWeight) : price;
|
||||||
// const prepareTime = productData.prepareTime || 0;
|
// const prepareTime = productData.prepareTime || 0;
|
||||||
const content = Array.isArray(productData.content)
|
const content = Array.isArray(productData.content)
|
||||||
? productData.content.join('، ')
|
? productData.content.join('، ')
|
||||||
@@ -147,7 +235,7 @@ function ProductPage({ }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-[285px] lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10'>
|
<div className={`lg:absolute lg:top-1/2 not-lg:max-w-lg not-lg:place-self-center lg:-translate-y-1/2 xl:right-[285px] lg:left-5 lg:right-4 lg:bottom-4 not-lg:w-full not-md:-translate-y-2 pt-6 flex flex-col lg:grid grid-cols-2 bottom-10`}>
|
||||||
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
<div className="relative w-full h-full not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
||||||
<Image
|
<Image
|
||||||
className='w-full object-cover bg-[#F2F2F9] h-full'
|
className='w-full object-cover bg-[#F2F2F9] h-full'
|
||||||
@@ -252,16 +340,58 @@ function ProductPage({ }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='mt-12 flex justify-between items-center'>
|
{isVariable && weightRange ? (
|
||||||
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
|
<div className='mt-8'>
|
||||||
<CartQuantityControl
|
<p className='text-sm font-bold'>
|
||||||
quantity={quantity}
|
قیمت هر {unitPriceLabel}{' '}
|
||||||
onAdd={handleAddToCart}
|
<span dir='ltr'>T {ef(price.toLocaleString('en-US'))}</span>
|
||||||
onRemove={handleRemoveFromCart}
|
</p>
|
||||||
isMutating={isCartMutating}
|
|
||||||
addDisabled={!cartId}
|
<VariableWeightSlider
|
||||||
/>
|
className='mt-10 mb-8'
|
||||||
</div>
|
value={selectedWeight}
|
||||||
|
min={weightRange.min}
|
||||||
|
max={weightRange.max}
|
||||||
|
step={weightRange.pitch}
|
||||||
|
purchaseUnit={productData.purchaseUnit}
|
||||||
|
onChange={handleVariableWeightChange}
|
||||||
|
onCommit={handleVariableWeightCommit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<div
|
||||||
|
dir='ltr'
|
||||||
|
className='flex-1 rounded-xl bg-[#EAECF0] dark:bg-neutral-700 px-4 py-3.5 text-sm font-semibold text-center'
|
||||||
|
>
|
||||||
|
{ef(calculatedPrice.toLocaleString('en-US'))} T
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={handleAddVariableToCart}
|
||||||
|
disabled={isCartMutating || !cartId}
|
||||||
|
className='flex-1 inline-flex items-center justify-center gap-2 rounded-xl bg-black text-white px-4 py-3.5 text-sm font-semibold disabled:opacity-60 disabled:cursor-not-allowed'
|
||||||
|
>
|
||||||
|
{isCartMutating ? (
|
||||||
|
<span className="size-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<PlusIcon className='text-white' />
|
||||||
|
)}
|
||||||
|
<span>{quantity > 0 ? 'بروزرسانی' : 'افزودن'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='mt-12 flex justify-between items-center'>
|
||||||
|
<span dir='ltr'>{ef(price.toLocaleString('en-US'))} T</span>
|
||||||
|
<CartQuantityControl
|
||||||
|
quantity={quantity}
|
||||||
|
onAdd={handleAddToCart}
|
||||||
|
onRemove={handleRemoveFromCart}
|
||||||
|
isMutating={isCartMutating}
|
||||||
|
addDisabled={!cartId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import VerticalScrollView from "@/components/listview/VerticalScrollView";
|
|
||||||
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
|
||||||
import type { MenuItemViewMode } from "@/components/listview/MenuItem";
|
import type { MenuItemViewMode } from "@/components/listview/MenuItem";
|
||||||
|
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
||||||
|
import VerticalScrollView from "@/components/listview/VerticalScrollView";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
interface MenuSkeletonProps {
|
interface MenuSkeletonProps {
|
||||||
viewMode?: MenuItemViewMode;
|
viewMode?: MenuItemViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuSkeleton = ({ viewMode = 'list' }: MenuSkeletonProps) => {
|
const MenuSkeleton = ({ viewMode = "list" }: MenuSkeletonProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 items-center pt-8 mb-8">
|
<div className="flex flex-col gap-4 items-center pt-8 mb-8">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -31,7 +31,7 @@ const MenuSkeleton = ({ viewMode = 'list' }: MenuSkeletonProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VerticalScrollView className="mt-5! overflow-y-auto h-full">
|
<VerticalScrollView className="mt-5! overflow-y-auto h-full">
|
||||||
{viewMode === 'grid' ? (
|
{viewMode === "grid" ? (
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{[...Array(6)].map((_, index) => (
|
{[...Array(6)].map((_, index) => (
|
||||||
<MenuItemRenderer key={index} variant="grid">
|
<MenuItemRenderer key={index} variant="grid">
|
||||||
@@ -52,20 +52,18 @@ const MenuSkeleton = ({ viewMode = 'list' }: MenuSkeletonProps) => {
|
|||||||
) : (
|
) : (
|
||||||
[...Array(6)].map((_, index) => (
|
[...Array(6)].map((_, index) => (
|
||||||
<MenuItemRenderer key={index} variant="list">
|
<MenuItemRenderer key={index} variant="list">
|
||||||
<div className="flex gap-4 w-full h-full items-center">
|
<div className="flex gap-4 w-full min-h-28 items-stretch">
|
||||||
<Skeleton className="min-w-28 w-28 h-28 rounded-xl" />
|
<Skeleton className="min-w-28 w-28 h-28 rounded-xl shrink-0 self-center" />
|
||||||
<div className="w-full inline-flex flex-col justify-between">
|
<div className="flex-1 flex flex-col justify-between min-w-0">
|
||||||
<div>
|
<div>
|
||||||
<Skeleton className="h-5 w-32 rounded-md mb-2" />
|
<Skeleton className="h-5 w-32 rounded-md mb-2" />
|
||||||
<Skeleton className="h-4 w-full rounded-md mb-1" />
|
<Skeleton className="h-4 w-full rounded-md mb-1" />
|
||||||
<Skeleton className="h-4 w-3/4 rounded-md" />
|
<Skeleton className="h-4 w-3/4 rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-flex mt-2 gap-2 justify-between w-full items-center">
|
<Skeleton className="h-4 w-16 rounded-md mt-2" />
|
||||||
<div className="w-full flex flex-col gap-1">
|
</div>
|
||||||
<Skeleton className="h-4 w-16 rounded-md" />
|
<div className="flex flex-col justify-end shrink-0">
|
||||||
</div>
|
<Skeleton className="h-8 w-20 rounded-full" />
|
||||||
<Skeleton className="max-w-[115px] w-full h-8 rounded-md" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MenuItemRenderer>
|
</MenuItemRenderer>
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ export interface ProductVariant {
|
|||||||
price: number;
|
price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PurchaseUnit {
|
||||||
|
NUMBER = "number",
|
||||||
|
KILOGRAM = "kilogram",
|
||||||
|
GRAM = "gram",
|
||||||
|
CENTIMETER = "centimeter",
|
||||||
|
METER = "meter",
|
||||||
|
MILLILITER = "milliliter",
|
||||||
|
LITER = "liter",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingType = "fixed" | "variable";
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -65,6 +77,12 @@ export interface Product {
|
|||||||
isSpecialOffer: boolean;
|
isSpecialOffer: boolean;
|
||||||
price: number | null;
|
price: number | null;
|
||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
|
pricingType?: PricingType;
|
||||||
|
purchaseUnit?: PurchaseUnit | string;
|
||||||
|
purchasePitch?: string;
|
||||||
|
minPurchaseQuantity?: string;
|
||||||
|
maxPurchaseQuantity?: string;
|
||||||
|
needAdminAcceptance?: boolean;
|
||||||
/** Backward compatibility / other APIs */
|
/** Backward compatibility / other APIs */
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -99,6 +117,127 @@ export function getProductEffectivePrice(product: Product): number {
|
|||||||
return first?.price ?? 0;
|
return first?.price ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isVariablePricing(product: Product): boolean {
|
||||||
|
return product.pricingType === "variable";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPurchaseUnitLabel(unit: string | undefined): string {
|
||||||
|
switch (unit) {
|
||||||
|
case PurchaseUnit.NUMBER:
|
||||||
|
return "عدد";
|
||||||
|
case PurchaseUnit.KILOGRAM:
|
||||||
|
return "کیلوگرم";
|
||||||
|
case PurchaseUnit.GRAM:
|
||||||
|
return "گرم";
|
||||||
|
case PurchaseUnit.CENTIMETER:
|
||||||
|
return "سانتیمتر";
|
||||||
|
case PurchaseUnit.METER:
|
||||||
|
return "متر";
|
||||||
|
case PurchaseUnit.MILLILITER:
|
||||||
|
return "میلیلیتر";
|
||||||
|
case PurchaseUnit.LITER:
|
||||||
|
return "لیتر";
|
||||||
|
default:
|
||||||
|
return unit ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** برچسب کوتاه برای نمایش روی اسلایدر */
|
||||||
|
export function getPurchaseUnitShortLabel(unit: string | undefined): string {
|
||||||
|
switch (unit) {
|
||||||
|
case PurchaseUnit.KILOGRAM:
|
||||||
|
return "kg";
|
||||||
|
case PurchaseUnit.GRAM:
|
||||||
|
return "gr";
|
||||||
|
case PurchaseUnit.CENTIMETER:
|
||||||
|
return "cm";
|
||||||
|
case PurchaseUnit.METER:
|
||||||
|
return "m";
|
||||||
|
case PurchaseUnit.MILLILITER:
|
||||||
|
return "ml";
|
||||||
|
case PurchaseUnit.LITER:
|
||||||
|
return "L";
|
||||||
|
case PurchaseUnit.NUMBER:
|
||||||
|
return "";
|
||||||
|
default:
|
||||||
|
return unit ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPurchaseQuantity(value: number, unit: string | undefined): string {
|
||||||
|
const short = getPurchaseUnitShortLabel(unit);
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.KILOGRAM) {
|
||||||
|
const grams = Math.round(value * 1000);
|
||||||
|
if (grams < 1000) {
|
||||||
|
return `${grams} gr`;
|
||||||
|
}
|
||||||
|
if (Number.isInteger(value)) {
|
||||||
|
return `${value} kg`;
|
||||||
|
}
|
||||||
|
return `${parseFloat(value.toFixed(3))} kg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.GRAM) {
|
||||||
|
return `${Math.round(value)} gr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.MILLILITER) {
|
||||||
|
if (value >= 1000) {
|
||||||
|
const liters = value / 1000;
|
||||||
|
return Number.isInteger(liters) ? `${liters} L` : `${parseFloat(liters.toFixed(2))} L`;
|
||||||
|
}
|
||||||
|
return `${Math.round(value)} ml`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.LITER) {
|
||||||
|
if (value < 1) {
|
||||||
|
return `${Math.round(value * 1000)} ml`;
|
||||||
|
}
|
||||||
|
return Number.isInteger(value) ? `${value} L` : `${parseFloat(value.toFixed(2))} L`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.CENTIMETER) {
|
||||||
|
if (value >= 100) {
|
||||||
|
const meters = value / 100;
|
||||||
|
return Number.isInteger(meters) ? `${meters} m` : `${parseFloat(meters.toFixed(2))} m`;
|
||||||
|
}
|
||||||
|
return `${Math.round(value)} cm`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.METER) {
|
||||||
|
if (value < 1) {
|
||||||
|
return `${Math.round(value * 100)} cm`;
|
||||||
|
}
|
||||||
|
return Number.isInteger(value) ? `${value} m` : `${parseFloat(value.toFixed(2))} m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === PurchaseUnit.NUMBER) {
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return short ? `${value} ${short}` : `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePurchaseQuantity(value: string | undefined, fallback: number): number {
|
||||||
|
const parsed = Number.parseFloat(value ?? "");
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVariableQuantityRange(product: Product) {
|
||||||
|
const pitch = parsePurchaseQuantity(product.purchasePitch, 0.05);
|
||||||
|
const min = parsePurchaseQuantity(product.minPurchaseQuantity, 1);
|
||||||
|
const max = parsePurchaseQuantity(product.maxPurchaseQuantity, min);
|
||||||
|
return {
|
||||||
|
min,
|
||||||
|
max: Math.max(max, min),
|
||||||
|
pitch: pitch > 0 ? pitch : 0.05,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use getVariableQuantityRange */
|
||||||
|
export const getVariableWeightRange = getVariableQuantityRange;
|
||||||
|
|
||||||
export interface NotificationsCountData {
|
export interface NotificationsCountData {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import { memo, useMemo } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { CartQuantityControl } from "@/components/CartQuantityControl";
|
|
||||||
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
import { useCart } from "@/app/[name]/(Dialogs)/cart/hook/useCart";
|
||||||
import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types";
|
import type { Product, ProductImage } from "@/app/[name]/(Main)/types/Types";
|
||||||
import {
|
import { formatPurchaseQuantity, getPrimaryVariantId, getProductEffectivePrice, getPurchaseUnitLabel, hasVariants, isVariablePricing } from "@/app/[name]/(Main)/types/Types";
|
||||||
hasVariants,
|
import { CartQuantityControl } from "@/components/CartQuantityControl";
|
||||||
getPrimaryVariantId,
|
import { ArrowLeft } from "iconsax-react";
|
||||||
getProductEffectivePrice,
|
import Link from "next/link";
|
||||||
} from "@/app/[name]/(Main)/types/Types";
|
import { useParams } from "next/navigation";
|
||||||
|
import { memo, useMemo, type ReactNode } from "react";
|
||||||
|
|
||||||
export type MenuItemViewMode = 'list' | 'grid';
|
export type MenuItemViewMode = "list" | "grid";
|
||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
product: Product;
|
product: Product;
|
||||||
@@ -23,19 +20,24 @@ interface MenuItemProps {
|
|||||||
variantValue?: string | null;
|
variantValue?: string | null;
|
||||||
/** حالت نمایش: list = یک ستونه (پیشفرض)، grid = دو ستونه */
|
/** حالت نمایش: list = یک ستونه (پیشفرض)، grid = دو ستونه */
|
||||||
viewMode?: MenuItemViewMode;
|
viewMode?: MenuItemViewMode;
|
||||||
|
/** فقط نمایش اطلاعات بدون دکمههای افزودن/جزئیات */
|
||||||
|
readOnly?: boolean;
|
||||||
|
/** تعداد انتخابشده — در حالت readOnly نمایش داده میشود */
|
||||||
|
displayQuantity?: number;
|
||||||
|
/** برچسب اختیاری بالای آیتم */
|
||||||
|
badge?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValueProp, viewMode = 'list' }: MenuItemProps) => {
|
const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValueProp, viewMode = "list", readOnly = false, displayQuantity, badge }: MenuItemProps) => {
|
||||||
const { items, addToCart, removeFromCart, isCartMutating } = useCart();
|
const { items, addToCart, removeFromCart, isCartMutating } = useCart();
|
||||||
const params = useParams<{ name: string }>();
|
const params = useParams<{ name: string }>();
|
||||||
const name = params?.name || "";
|
const name = params?.name || "";
|
||||||
const variantId = variantIdProp ?? getPrimaryVariantId(product);
|
const variantId = variantIdProp ?? getPrimaryVariantId(product);
|
||||||
const selectedVariant = variantId
|
const selectedVariant = variantId ? product.variants?.find((v) => v.id === variantId) : undefined;
|
||||||
? product.variants?.find((v) => v.id === variantId)
|
|
||||||
: undefined;
|
|
||||||
const withVariants = hasVariants(product);
|
const withVariants = hasVariants(product);
|
||||||
/** در لیست منو اگر تنوع داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
|
const withVariablePricing = isVariablePricing(product);
|
||||||
const showDetailsInsteadOfAdd = withVariants && variantIdProp == null;
|
/** در لیست منو اگر تنوع یا قیمت متغیر داشت دکمه جزئیات؛ در سبد همیشه افزودن/کم کردن */
|
||||||
|
const showDetailsInsteadOfAdd = (withVariants || withVariablePricing) && variantIdProp == null;
|
||||||
|
|
||||||
const quantity = useMemo(() => {
|
const quantity = useMemo(() => {
|
||||||
if (!variantId) return 0;
|
if (!variantId) return 0;
|
||||||
@@ -58,10 +60,7 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
return fallbackImage;
|
return fallbackImage;
|
||||||
}, [product.image, product.images]);
|
}, [product.image, product.images]);
|
||||||
|
|
||||||
const productName = useMemo(
|
const productName = useMemo(() => product.name || product.title || "بدون نام", [product.name, product.title]);
|
||||||
() => product.name || product.title || 'بدون نام',
|
|
||||||
[product.name, product.title]
|
|
||||||
);
|
|
||||||
|
|
||||||
/** فقط در سبد خرید تنوع نمایش داده شود */
|
/** فقط در سبد خرید تنوع نمایش داده شود */
|
||||||
const isInCart = variantIdProp != null;
|
const isInCart = variantIdProp != null;
|
||||||
@@ -69,25 +68,22 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
|
|
||||||
const productContent = useMemo(() => {
|
const productContent = useMemo(() => {
|
||||||
const content = product.content;
|
const content = product.content;
|
||||||
if (!content) return '';
|
if (!content) return "";
|
||||||
|
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return content.filter(item => item && typeof item === 'string').join('، ');
|
return content.filter((item) => item && typeof item === "string").join("، ");
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return "";
|
||||||
}, [product.content]);
|
}, [product.content]);
|
||||||
|
|
||||||
const productDescription = useMemo(() => {
|
const productDescription = useMemo(() => {
|
||||||
const desc = product.description || product.desc;
|
const desc = product.description || product.desc;
|
||||||
if (!desc || typeof desc !== 'string') return '';
|
if (!desc || typeof desc !== "string") return "";
|
||||||
return desc.replace(/,/g, '،');
|
return desc.replace(/,/g, "،");
|
||||||
}, [product.description, product.desc]);
|
}, [product.description, product.desc]);
|
||||||
|
|
||||||
const effectivePrice =
|
const effectivePrice = selectedVariant != null ? selectedVariant.price : getProductEffectivePrice(product);
|
||||||
selectedVariant != null
|
|
||||||
? selectedVariant.price
|
|
||||||
: getProductEffectivePrice(product);
|
|
||||||
const finalPrice = useMemo(() => {
|
const finalPrice = useMemo(() => {
|
||||||
const discount = product.discount || 0;
|
const discount = product.discount || 0;
|
||||||
if (discount > 0) {
|
if (discount > 0) {
|
||||||
@@ -96,21 +92,25 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
return effectivePrice;
|
return effectivePrice;
|
||||||
}, [effectivePrice, product.discount]);
|
}, [effectivePrice, product.discount]);
|
||||||
|
|
||||||
const formattedPrice = useMemo(
|
const formattedPrice = useMemo(() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"), [finalPrice]);
|
||||||
() => (finalPrice ? finalPrice.toLocaleString("fa-IR") : "0"),
|
|
||||||
[finalPrice]
|
|
||||||
);
|
|
||||||
|
|
||||||
const formattedOriginalPrice = useMemo(
|
const variablePriceLabel = useMemo(() => {
|
||||||
() =>
|
if (!withVariablePricing) return null;
|
||||||
effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0",
|
const unitLabel = getPurchaseUnitLabel(product.purchaseUnit);
|
||||||
[effectivePrice]
|
return `قیمت هر ${unitLabel} T ${formattedPrice}`;
|
||||||
);
|
}, [withVariablePricing, product.purchaseUnit, formattedPrice]);
|
||||||
|
|
||||||
const hasDiscount = useMemo(
|
const formattedOriginalPrice = useMemo(() => (effectivePrice ? effectivePrice.toLocaleString("fa-IR") : "0"), [effectivePrice]);
|
||||||
() => (product.discount || 0) > 0,
|
|
||||||
[product.discount]
|
const hasDiscount = useMemo(() => (product.discount || 0) > 0, [product.discount]);
|
||||||
);
|
|
||||||
|
const quantityLabel = useMemo(() => {
|
||||||
|
if (displayQuantity == null) return null;
|
||||||
|
if (withVariablePricing) {
|
||||||
|
return formatPurchaseQuantity(displayQuantity, product.purchaseUnit);
|
||||||
|
}
|
||||||
|
return `${displayQuantity.toLocaleString("fa-IR")}×`;
|
||||||
|
}, [displayQuantity, withVariablePricing, product.purchaseUnit]);
|
||||||
|
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = () => {
|
||||||
if (variantId) addToCart(variantId);
|
if (variantId) addToCart(variantId);
|
||||||
@@ -121,60 +121,38 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
};
|
};
|
||||||
|
|
||||||
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
|
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
|
||||||
|
const actionButtonClass = "bg-background active:drop-shadow-xs rounded-md h-8 inline-flex p-1 justify-center items-center gap-2 relative overflow-hidden";
|
||||||
|
const actionButtonLabelClass = "text-sm2 pt-0.5 font-normal text-foreground";
|
||||||
|
|
||||||
if (viewMode === 'grid') {
|
if (viewMode === "grid") {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full">
|
<div className="flex flex-col w-full h-full">
|
||||||
<Link href={productDetailUrl} className="cursor-pointer rounded-xl aspect-square overflow-hidden flex items-center justify-center max-h-[100px]">
|
<Link href={productDetailUrl} className="cursor-pointer rounded-xl aspect-square overflow-hidden flex items-center justify-center max-h-[100px]">
|
||||||
<img
|
<img className="rounded-xl max-w-full max-h-full object-center" src={resolvedImage} alt={productName} />
|
||||||
className="rounded-xl max-w-full max-h-full object-center"
|
|
||||||
src={resolvedImage}
|
|
||||||
alt={productName}
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={productDetailUrl} className="cursor-pointer min-w-0 mt-4 flex items-center justify-between gap-2">
|
<Link href={productDetailUrl} className="cursor-pointer min-w-0 mt-4 flex items-center justify-between gap-2">
|
||||||
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word line-clamp-2 min-w-0">
|
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word line-clamp-2 min-w-0">{productName}</div>
|
||||||
{productName}
|
{variantValueDisplay && <span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">{variantValueDisplay}</span>}
|
||||||
</div>
|
|
||||||
{variantValueDisplay && (
|
|
||||||
<span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">
|
|
||||||
{variantValueDisplay}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-col gap-2 mt-1">
|
<div className="flex flex-col gap-2 mt-1">
|
||||||
<div className="flex flex-col gap-1 min-h-10 justify-center" dir="ltr">
|
<div className="flex flex-col gap-1 min-h-10 justify-center" dir="ltr">
|
||||||
{hasDiscount ? (
|
{variablePriceLabel ? (
|
||||||
|
<span className="text-sm text-right">{variablePriceLabel}</span>
|
||||||
|
) : hasDiscount ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs text-disabled-text line-through">
|
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
|
||||||
{formattedOriginalPrice} T
|
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium text-primary dark:text-foreground">
|
|
||||||
{formattedPrice} T
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm text-right">
|
<span className="text-sm text-right">{formattedPrice} T</span>
|
||||||
{formattedPrice} T
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showDetailsInsteadOfAdd ? (
|
{showDetailsInsteadOfAdd ? (
|
||||||
<Link
|
<Link href={productDetailUrl} className={`${actionButtonClass} w-full`}>
|
||||||
href={productDetailUrl}
|
<span className={actionButtonLabelClass}>جزئیات</span>
|
||||||
className="bg-background active:drop-shadow-xs w-full rounded-md h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
|
|
||||||
>
|
|
||||||
جزئیات
|
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<CartQuantityControl
|
<CartQuantityControl quantity={quantity} onAdd={handleAddToCart} onRemove={handleRemoveFromCart} isMutating={isCartMutating} fullWidth />
|
||||||
quantity={quantity}
|
|
||||||
onAdd={handleAddToCart}
|
|
||||||
onRemove={handleRemoveFromCart}
|
|
||||||
isMutating={isCartMutating}
|
|
||||||
fullWidth
|
|
||||||
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,71 +160,63 @@ const MenuItem = ({ product, variantId: variantIdProp, variantValue: variantValu
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 w-full h-full items-center">
|
<>
|
||||||
<Link href={productDetailUrl} className="cursor-pointer">
|
{badge && (
|
||||||
<img
|
<div className="absolute top-3 left-3 z-10">
|
||||||
className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 aspect-square object-cover"
|
{badge}
|
||||||
src={resolvedImage}
|
</div>
|
||||||
height={112}
|
)}
|
||||||
width={112}
|
<div className="flex gap-4 w-full min-h-28 items-stretch">
|
||||||
alt={productName}
|
<Link href={productDetailUrl} className="cursor-pointer shrink-0 self-center">
|
||||||
/>
|
<img className="rounded-xl bg-[#F2F2F9] min-w-28 w-28 aspect-square object-cover" src={resolvedImage} height={112} width={112} alt={productName} />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="w-full inline-flex flex-col justify-between min-w-0">
|
<div className="flex-1 flex flex-col justify-between min-w-0">
|
||||||
<Link href={productDetailUrl} className="cursor-pointer min-w-0">
|
<Link href={productDetailUrl} className="cursor-pointer min-w-0">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center justify-between gap-2 min-w-0">
|
<div className="flex items-center justify-between gap-2 min-w-0">
|
||||||
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word min-w-0">
|
<div className="text-sm2 font-bold text-black dark:text-white wrap-break-word min-w-0">{productName}</div>
|
||||||
{productName}
|
{variantValueDisplay && <span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">{variantValueDisplay}</span>}
|
||||||
</div>
|
</div>
|
||||||
{variantValueDisplay && (
|
{(productContent || productDescription) && (
|
||||||
<span className="bg-primary text-primary-foreground text-[10px] px-1 py-0.5 rounded-md shrink-0">
|
<div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-4 wrap-break-word overflow-hidden">{productContent || productDescription}</div>
|
||||||
{variantValueDisplay}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(productContent || productDescription) && (
|
</Link>
|
||||||
<div className="text-[#7F7F7F] line-clamp-2 text-xs leading-5 font-normal mt-2 wrap-break-word overflow-hidden">
|
<div className="flex flex-col gap-1 mt-2" dir="ltr">
|
||||||
{productContent || productDescription}
|
{variablePriceLabel ? (
|
||||||
|
<span className="text-sm text-right">{variablePriceLabel}</span>
|
||||||
|
) : hasDiscount ? (
|
||||||
|
<>
|
||||||
|
<span className="text-xs text-disabled-text line-through">{formattedOriginalPrice} T</span>
|
||||||
|
<span className="text-sm font-medium text-primary dark:text-foreground">{formattedPrice} T</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-right">{formattedPrice} T</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{readOnly && quantityLabel ? (
|
||||||
|
<div className="flex flex-col justify-end shrink-0">
|
||||||
|
<span className="text-sm font-semibold text-foreground">{quantityLabel}</span>
|
||||||
|
</div>
|
||||||
|
) : !readOnly ? (
|
||||||
|
<div className="flex flex-col justify-end shrink-0">
|
||||||
|
{showDetailsInsteadOfAdd ? (
|
||||||
|
<div className="w-[115px]">
|
||||||
|
<Link href={productDetailUrl} className={`${actionButtonClass} w-full`}>
|
||||||
|
<span className={actionButtonLabelClass}>جزئیات</span>
|
||||||
|
<ArrowLeft size={16} className="stroke-foreground" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-[115px]">
|
||||||
|
<CartQuantityControl quantity={quantity} onAdd={handleAddToCart} onRemove={handleRemoveFromCart} isMutating={isCartMutating} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
) : null}
|
||||||
<div className="flex flex-col mt-2 gap-2 w-full">
|
|
||||||
<div className="flex flex-col gap-1 min-h-10 justify-center" dir="ltr">
|
|
||||||
{hasDiscount ? (
|
|
||||||
<>
|
|
||||||
<span className="text-xs text-disabled-text line-through">
|
|
||||||
{formattedOriginalPrice} T
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium text-primary dark:text-foreground">
|
|
||||||
{formattedPrice} T
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-right">
|
|
||||||
{formattedPrice} T
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{showDetailsInsteadOfAdd ? (
|
|
||||||
<Link
|
|
||||||
href={productDetailUrl}
|
|
||||||
className="bg-background active:drop-shadow-xs w-full rounded-md h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
|
|
||||||
>
|
|
||||||
جزئیات
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<CartQuantityControl
|
|
||||||
quantity={quantity}
|
|
||||||
onAdd={handleAddToCart}
|
|
||||||
onRemove={handleRemoveFromCart}
|
|
||||||
isMutating={isCartMutating}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function MenuItemRendererComponent({ children, variant = 'list', ...rest }: Prop
|
|||||||
className={`${rest.className} box-shadow-normal transition-all duration-200 ease-out w-full bg-container rounded-3xl ${
|
className={`${rest.className} box-shadow-normal transition-all duration-200 ease-out w-full bg-container rounded-3xl ${
|
||||||
isGrid
|
isGrid
|
||||||
? 'p-3 flex flex-col'
|
? 'p-3 flex flex-col'
|
||||||
: 'py-4 pb-4! px-4 flex items-center justify-between gap-4'
|
: 'relative py-4 pb-4! px-4 flex items-stretch overflow-visible'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { formatPurchaseQuantity } from '@/app/[name]/(Main)/types/Types'
|
||||||
|
import { ef } from '@/lib/helpers/utfNumbers'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
interface VariableWeightSliderProps {
|
||||||
|
value: number
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
step: number
|
||||||
|
purchaseUnit?: string
|
||||||
|
onChange: (value: number) => void
|
||||||
|
onCommit?: (value: number) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableWeightSlider({
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
purchaseUnit,
|
||||||
|
onChange,
|
||||||
|
onCommit,
|
||||||
|
className,
|
||||||
|
}: VariableWeightSliderProps) {
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [tooltipLeft, setTooltipLeft] = useState(0)
|
||||||
|
|
||||||
|
const updateTooltipPosition = useCallback(() => {
|
||||||
|
const root = rootRef.current
|
||||||
|
if (!root) return
|
||||||
|
const thumb = root.querySelector<HTMLElement>('[data-slot="slider-thumb"]')
|
||||||
|
if (!thumb) return
|
||||||
|
const rootRect = root.getBoundingClientRect()
|
||||||
|
const thumbRect = thumb.getBoundingClientRect()
|
||||||
|
setTooltipLeft(thumbRect.left - rootRect.left + thumbRect.width / 2)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTooltipPosition()
|
||||||
|
window.addEventListener('resize', updateTooltipPosition)
|
||||||
|
return () => window.removeEventListener('resize', updateTooltipPosition)
|
||||||
|
}, [value, min, max, updateTooltipPosition])
|
||||||
|
|
||||||
|
const quantityLabel = ef(formatPurchaseQuantity(value, purchaseUnit))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className={cn('relative w-full', className)}>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute -top-10 z-10 -translate-x-1/2 whitespace-nowrap rounded-lg bg-[#EAECF0] px-3.5 py-1.5 text-xs font-semibold text-foreground"
|
||||||
|
style={{ left: tooltipLeft }}
|
||||||
|
>
|
||||||
|
{quantityLabel}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
value={[value]}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
onValueChange={(values) => onChange(values[0] ?? min)}
|
||||||
|
onValueCommit={(values) => onCommit?.(values[0] ?? min)}
|
||||||
|
className="relative flex w-full touch-none select-none items-center py-3"
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className="relative h-[3px] w-full grow overflow-hidden rounded-full bg-[#EAECF0]"
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className="absolute h-full bg-black"
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
className="block size-5 shrink-0 cursor-grab rounded-full bg-black shadow-none outline-none active:cursor-grabbing focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com";
|
// export const API_BASE_URL = "https://dmenuplus-api.dev.danakcorp.com";
|
||||||
export const API_BASE_URL = "https://dkala-api.danakcorp.com";
|
export const API_BASE_URL = "https://dkala-api.danakcorp.com";
|
||||||
// export const API_BASE_URL = "http://10.191.241.88:4000";
|
// export const API_BASE_URL = "http://192.168.99.131:4000";
|
||||||
export const TOKEN_NAME = "dkala-t";
|
export const TOKEN_NAME = "dkala-t";
|
||||||
export const REFRESH_TOKEN_NAME = "dkala-rt";
|
export const REFRESH_TOKEN_NAME = "dkala-rt";
|
||||||
|
|||||||
@@ -19,8 +19,13 @@
|
|||||||
"Online": "پرداخت آنلاین",
|
"Online": "پرداخت آنلاین",
|
||||||
"Wallet": "پرداخت با کیف پول"
|
"Wallet": "پرداخت با کیف پول"
|
||||||
},
|
},
|
||||||
|
"itemStatus": {
|
||||||
|
"needConfirmation": "در انتظار تایید قیمت نهایی",
|
||||||
|
"confirmed": "تایید شده"
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"new": "سفارش جدید",
|
"new": "سفارش جدید",
|
||||||
|
"needConfirmation": "نیاز به تایید فروشگاه",
|
||||||
"pendingPayment": "در انتظار پرداخت",
|
"pendingPayment": "در انتظار پرداخت",
|
||||||
"paid": "پرداخت شده",
|
"paid": "پرداخت شده",
|
||||||
"confirmed": "تایید شده",
|
"confirmed": "تایید شده",
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
"Title": "اقلام سفارش"
|
"Title": "اقلام سفارش"
|
||||||
},
|
},
|
||||||
"ButtonSubmit": "پرداخت",
|
"ButtonSubmit": "پرداخت",
|
||||||
|
"ButtonRegister": "ثبت",
|
||||||
"PayableAmountLabel": "مبلغ قابل پرداخت"
|
"PayableAmountLabel": "مبلغ قابل پرداخت"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type ReceiptStore = {
|
|||||||
setSlug: (slug: string) => void;
|
setSlug: (slug: string) => void;
|
||||||
increment: (id: string | number) => void;
|
increment: (id: string | number) => void;
|
||||||
decrement: (id: string | number) => void;
|
decrement: (id: string | number) => void;
|
||||||
|
setQuantity: (id: string | number, quantity: number) => void;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +46,13 @@ export const useReceiptStore = create<ReceiptStore>()(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
setQuantity: (id, quantity) =>
|
||||||
|
set((state) => ({
|
||||||
|
items: {
|
||||||
|
...state.items,
|
||||||
|
[id]: { quantity },
|
||||||
|
},
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "receipt-storage",
|
name: "receipt-storage",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user