added shipping method
This commit is contained in:
@@ -13,9 +13,13 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import DeliveryTypeSelector from "./DeliveryTypeSelector";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { DeliveryType, PaymentType, Province } from "@/lib/types/api";
|
||||
import type {
|
||||
DeliveryType,
|
||||
PaymentType,
|
||||
Province,
|
||||
OrderDelivery,
|
||||
} from "@/lib/types/api";
|
||||
import { useState } from "react";
|
||||
|
||||
interface OrderBillingItem {
|
||||
@@ -37,7 +41,8 @@ interface OrderSummaryProps {
|
||||
billing: OrderBilling;
|
||||
};
|
||||
paymentType: PaymentType | null;
|
||||
deliveryType: DeliveryType;
|
||||
orderDeliveries: OrderDelivery[];
|
||||
selectedOrderDelivery: OrderDelivery | null;
|
||||
selectedRegion: string;
|
||||
selectedProvince: number | null;
|
||||
notes: string;
|
||||
@@ -51,7 +56,7 @@ interface OrderSummaryProps {
|
||||
onNameChange: (name: string) => void;
|
||||
onLastNameChange: (lastName: string) => void;
|
||||
onPaymentTypeChange: (type: PaymentType) => void;
|
||||
onDeliveryTypeChange: (type: DeliveryType) => void;
|
||||
onOrderDeliveryChange: (delivery: OrderDelivery) => void;
|
||||
onRegionChange: (regionCode: string) => void;
|
||||
onProvinceChange: (provinceId: number) => void;
|
||||
onNoteChange: (notes: string) => void;
|
||||
@@ -62,7 +67,8 @@ interface OrderSummaryProps {
|
||||
export default function OrderSummary({
|
||||
order,
|
||||
paymentType,
|
||||
deliveryType,
|
||||
orderDeliveries,
|
||||
selectedOrderDelivery,
|
||||
selectedRegion,
|
||||
selectedProvince,
|
||||
notes,
|
||||
@@ -76,7 +82,7 @@ export default function OrderSummary({
|
||||
onNameChange,
|
||||
onLastNameChange,
|
||||
onPaymentTypeChange,
|
||||
onDeliveryTypeChange,
|
||||
onOrderDeliveryChange,
|
||||
onRegionChange,
|
||||
onProvinceChange,
|
||||
onNoteChange,
|
||||
@@ -90,6 +96,15 @@ export default function OrderSummary({
|
||||
? regionGroups[selectedRegion] || []
|
||||
: [];
|
||||
|
||||
const filteredDeliveries = orderDeliveries.filter((delivery) => {
|
||||
if (!selectedRegion) return true;
|
||||
if (selectedRegion === "ag") {
|
||||
return delivery.name === "standart" || delivery.name === "self_pickup";
|
||||
} else {
|
||||
return delivery.name === "region";
|
||||
}
|
||||
});
|
||||
|
||||
const phoneDigits = phone.replace(/\D/g, "");
|
||||
const isPhoneValid = phoneDigits.length === 11;
|
||||
|
||||
@@ -97,6 +112,7 @@ export default function OrderSummary({
|
||||
selectedRegion &&
|
||||
selectedProvince &&
|
||||
paymentType &&
|
||||
selectedOrderDelivery &&
|
||||
isPhoneValid &&
|
||||
name.trim() !== "" &&
|
||||
lastName.trim() !== "";
|
||||
@@ -136,7 +152,7 @@ export default function OrderSummary({
|
||||
return (
|
||||
<Card className="w-full md:w-[380px] p-4 md:p-6 rounded-xl h-fit sticky top-20">
|
||||
{/* Customer Information */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold mb-3">
|
||||
{t("customer_information")}
|
||||
</h3>
|
||||
@@ -198,7 +214,7 @@ export default function OrderSummary({
|
||||
</div>
|
||||
|
||||
{/* Payment Type */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold mb-3">{t("payment_type")}</h3>
|
||||
<div className="flex gap-2">
|
||||
{paymentTypes.map((type) => (
|
||||
@@ -231,16 +247,13 @@ export default function OrderSummary({
|
||||
</div>
|
||||
|
||||
{/* Region Selection */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<Label className="text-lg font-semibold mb-3 block">
|
||||
{t("choose_region")}
|
||||
</Label>
|
||||
<RadioGroup
|
||||
value={selectedRegion}
|
||||
onValueChange={(value) => {
|
||||
onRegionChange(value);
|
||||
onProvinceChange(null as any);
|
||||
}}
|
||||
onValueChange={(value) => onRegionChange(value)}
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
{availableRegions.map((regionCode) => (
|
||||
@@ -270,7 +283,7 @@ export default function OrderSummary({
|
||||
|
||||
{/* Province Selection */}
|
||||
{selectedRegion && provincesForSelectedRegion.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<Label className="text-lg font-semibold mb-3 block">
|
||||
{t("choose_address")}
|
||||
</Label>
|
||||
@@ -299,8 +312,56 @@ export default function OrderSummary({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shipping Method */}
|
||||
{selectedRegion && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold mb-3">{t("shipping_method")}</h3>
|
||||
<div className="flex gap-2">
|
||||
{filteredDeliveries.map((delivery) => (
|
||||
<Card
|
||||
key={delivery.name}
|
||||
className={`flex-1 cursor-pointer py-4 transition-all ${
|
||||
selectedOrderDelivery?.name === delivery.name
|
||||
? "border-2 border-[#005bff] bg-blue-50"
|
||||
: showValidation && !selectedOrderDelivery
|
||||
? "border-2 border-red-500"
|
||||
: "border-2 border-gray-200"
|
||||
}`}
|
||||
onClick={() => onOrderDeliveryChange(delivery)}
|
||||
>
|
||||
<div className="flex items-center flex-col p-4">
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
selectedOrderDelivery?.name === delivery.name
|
||||
? "text-[#005bff]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{t(delivery.name)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-bold ${
|
||||
selectedOrderDelivery?.name === delivery.name
|
||||
? "text-[#005bff]"
|
||||
: "text-green-600"
|
||||
}`}
|
||||
>
|
||||
{delivery.price === 0 ? t("free") : `${delivery.price} TMT`}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{showValidation && !selectedOrderDelivery && (
|
||||
<p className="text-xs text-red-500 mt-1">{t("requiredField")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Note */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-4">
|
||||
<Label className="text-lg font-semibold mb-3 block">{t("note")}</Label>
|
||||
<Textarea
|
||||
value={notes}
|
||||
|
||||
@@ -5,15 +5,14 @@ import {
|
||||
UseQueryOptions,
|
||||
} from "@tanstack/react-query";
|
||||
import { apiClient } from "@/lib/api";
|
||||
import type { CartItem } from "@/lib/types/api";
|
||||
import type {
|
||||
CartItem,
|
||||
CartResponse,
|
||||
CreateOrderPayload,
|
||||
OrderDelivery,
|
||||
} from "@/lib/types/api";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface CartResponse {
|
||||
message: string;
|
||||
data: CartItem[];
|
||||
errorDetails?: string;
|
||||
}
|
||||
|
||||
const pendingUpdates = new Map<number, number>();
|
||||
let updateLock = false;
|
||||
|
||||
@@ -457,21 +456,24 @@ export function useUpdateCartItemQuantity() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useOrderDeliveries() {
|
||||
return useQuery({
|
||||
queryKey: ["order-deliveries"],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get<{
|
||||
message: string;
|
||||
data: OrderDelivery[];
|
||||
}>("/order-deliveries");
|
||||
return response.data.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateOrder() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: {
|
||||
customer_name?: string;
|
||||
customer_phone: number;
|
||||
customer_address: string;
|
||||
shipping_method: string;
|
||||
payment_type_id: number;
|
||||
delivery_time?: string;
|
||||
delivery_at?: string;
|
||||
region: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
mutationFn: async (payload: CreateOrderPayload) => {
|
||||
const response = await apiClient.post("/orders", payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -23,9 +23,10 @@ import {
|
||||
MapPin,
|
||||
CreditCard,
|
||||
ShoppingBag,
|
||||
Banknote,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useOrders, useCancelOrder } from "@/lib/hooks";
|
||||
import { useOrders, useCancelOrder, useOrderDeliveries } from "@/lib/hooks";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { Order } from "@/lib/types/api";
|
||||
import EmptyOrders from "./EmptyOrders";
|
||||
@@ -42,6 +43,8 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const { data: orders, isLoading, isError } = useOrders();
|
||||
const { data: orderDeliveries, isLoading: deliveriesLoading } =
|
||||
useOrderDeliveries();
|
||||
const { mutate: cancelOrder, isPending: isCancellingOrder } =
|
||||
useCancelOrder();
|
||||
|
||||
@@ -83,7 +86,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
if (
|
||||
lowerStatus.includes("ожидается") ||
|
||||
lowerStatus.includes("pending") ||
|
||||
lowerStatus.includes("garaşlama")
|
||||
lowerStatus.includes("garaşylýar")
|
||||
) {
|
||||
return (
|
||||
<Badge
|
||||
@@ -147,20 +150,89 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
|
||||
const activeOrders = useMemo(
|
||||
() => orders?.filter((o) => isActiveOrder(o.status)) || [],
|
||||
[orders, isActiveOrder]
|
||||
[orders, isActiveOrder],
|
||||
);
|
||||
const completedOrders = useMemo(
|
||||
() => orders?.filter((o) => !isActiveOrder(o.status)) || [],
|
||||
[orders, isActiveOrder]
|
||||
[orders, isActiveOrder],
|
||||
);
|
||||
|
||||
const calculateTotal = useCallback((order: Order) => {
|
||||
return order.orderItems.reduce((sum, item) => {
|
||||
return sum + parseFloat(item.unit_price_amount) * item.quantity;
|
||||
}, 0);
|
||||
}, []);
|
||||
const getShippingPrice = useCallback(
|
||||
(order: Order) => {
|
||||
if (order.shipping_price !== undefined && order.shipping_price !== null) {
|
||||
return Number(order.shipping_price);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
if (!orderDeliveries || orderDeliveries.length === 0) return 0;
|
||||
|
||||
const methodFromOrder = order.shipping_method.toLowerCase();
|
||||
|
||||
// Find delivery method by matching internal name, translated name, or common keywords
|
||||
const delivery = orderDeliveries.find((d) => {
|
||||
const internalName = d.name.toLowerCase();
|
||||
const translatedName = t(internalName).toLowerCase(); // d.name should be used for translation
|
||||
|
||||
// Direct match
|
||||
if (
|
||||
internalName === methodFromOrder ||
|
||||
translatedName === methodFromOrder
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keyword based matching for "region"
|
||||
if (
|
||||
(internalName === "region" || internalName === "çapar") &&
|
||||
(methodFromOrder.includes("welaýat") ||
|
||||
methodFromOrder.includes("region") ||
|
||||
methodFromOrder.includes("регион") ||
|
||||
methodFromOrder.includes("çapar") ||
|
||||
methodFromOrder.includes("welayat"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keyword based matching for "self_pickup"
|
||||
if (
|
||||
internalName === "self_pickup" &&
|
||||
(methodFromOrder.includes("özüm") ||
|
||||
methodFromOrder.includes("özüň") ||
|
||||
methodFromOrder.includes("pickup") ||
|
||||
methodFromOrder.includes("самовывоз"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keyword based matching for "standart"
|
||||
if (
|
||||
internalName === "standart" &&
|
||||
(methodFromOrder.includes("standart") ||
|
||||
methodFromOrder.includes("standard"))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return delivery ? Number(delivery.price) : 0;
|
||||
},
|
||||
[orderDeliveries, t],
|
||||
);
|
||||
|
||||
const calculateTotal = useCallback(
|
||||
(order: Order) => {
|
||||
const itemsTotal = order.orderItems.reduce((sum, item) => {
|
||||
return sum + parseFloat(item.unit_price_amount) * item.quantity;
|
||||
}, 0);
|
||||
|
||||
const shippingPrice = getShippingPrice(order);
|
||||
return itemsTotal + shippingPrice;
|
||||
},
|
||||
[getShippingPrice],
|
||||
);
|
||||
|
||||
if (isLoading || deliveriesLoading) {
|
||||
return (
|
||||
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6">
|
||||
@@ -247,8 +319,10 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
isCancelling={isCancellingOrder}
|
||||
getStatusBadge={getStatusBadge}
|
||||
calculateTotal={calculateTotal}
|
||||
getShippingPrice={getShippingPrice}
|
||||
showCancelButton
|
||||
t={t}
|
||||
orderDeliveries={orderDeliveries || []}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -270,12 +344,13 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
order={order}
|
||||
isExpanded={expandedOrders.has(order.id)}
|
||||
onToggle={() => toggleOrderExpand(order.id)}
|
||||
onCancel={handleCancelOrder}
|
||||
isCancelling={isCancellingOrder}
|
||||
getStatusBadge={getStatusBadge}
|
||||
calculateTotal={calculateTotal}
|
||||
getShippingPrice={getShippingPrice}
|
||||
showCancelButton={false}
|
||||
t={t}
|
||||
orderDeliveries={orderDeliveries || []}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -319,12 +394,14 @@ interface CompactOrderCardProps {
|
||||
order: Order;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
onCancel: (order: Order) => void;
|
||||
onCancel?: (order: Order) => void;
|
||||
isCancelling: boolean;
|
||||
getStatusBadge: (status: string) => React.ReactNode;
|
||||
calculateTotal: (order: Order) => number;
|
||||
getShippingPrice: (order: Order) => number;
|
||||
showCancelButton: boolean;
|
||||
t: any;
|
||||
orderDeliveries: any[];
|
||||
}
|
||||
|
||||
function CompactOrderCard({
|
||||
@@ -335,12 +412,19 @@ function CompactOrderCard({
|
||||
isCancelling,
|
||||
getStatusBadge,
|
||||
calculateTotal,
|
||||
getShippingPrice,
|
||||
showCancelButton,
|
||||
t,
|
||||
orderDeliveries,
|
||||
}: CompactOrderCardProps) {
|
||||
const total = useMemo(() => calculateTotal(order), [calculateTotal, order]);
|
||||
const itemCount = order.orderItems.length;
|
||||
|
||||
const shippingPrice = useMemo(
|
||||
() => getShippingPrice(order),
|
||||
[order, getShippingPrice],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-md">
|
||||
{/* Compact Header - Always Visible */}
|
||||
@@ -430,6 +514,20 @@ function CompactOrderCard({
|
||||
<p className="text-sm text-gray-900">{order.shipping_method}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 border-t md:border-t-0 pt-2 md:pt-0">
|
||||
<Banknote className="h-5 w-5 text-orange-500 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
{t("shipping_price")}
|
||||
</p>
|
||||
<p className="text-sm font-bold text-green-600">
|
||||
{shippingPrice === 0
|
||||
? t("free")
|
||||
: `${shippingPrice.toFixed(2)} TMT`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products List */}
|
||||
@@ -476,7 +574,22 @@ function CompactOrderCard({
|
||||
|
||||
{/* Footer with Total and Actions */}
|
||||
<div className="border-t p-4 bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between text-sm text-gray-600">
|
||||
<span>{t("products")}:</span>
|
||||
<span>{(total - shippingPrice).toFixed(2)} TMT</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-gray-600">
|
||||
<span>{t("shipping_method")}:</span>
|
||||
<span>
|
||||
{shippingPrice === 0
|
||||
? t("free")
|
||||
: `${shippingPrice.toFixed(2)} TMT`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-3 pt-2 border-t">
|
||||
<span className="text-base font-semibold text-gray-700">
|
||||
{t("total_price")}:
|
||||
</span>
|
||||
@@ -490,7 +603,7 @@ function CompactOrderCard({
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCancel(order);
|
||||
onCancel?.(order);
|
||||
}}
|
||||
disabled={isCancelling}
|
||||
className="w-full cursor-pointer"
|
||||
|
||||
Reference in New Issue
Block a user