Compare commits

...

2 Commits

Author SHA1 Message Date
@jcarymuhammedow
2ab9eab656 Merge branch 'main' of https://git.webulgam.com/nurmuhammet/postshop-frontend 2026-03-02 17:46:45 +05:00
@jcarymuhammedow
c13a4655bf added shipping method 2026-03-02 17:46:18 +05:00
7 changed files with 282 additions and 71 deletions

View File

@@ -11,18 +11,18 @@ import {
useCreateOrder, useCreateOrder,
useRegions, useRegions,
usePaymentTypes, usePaymentTypes,
useOrderDeliveries,
} from "@/lib/hooks"; } from "@/lib/hooks";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { DeliveryType, PaymentType } from "@/lib/types/api"; import type { PaymentType, OrderDelivery } from "@/lib/types/api";
import EmptyCart from "@/features/cart/components/EmptyCart"; import EmptyCart from "@/features/cart/components/EmptyCart";
import ErrorPage from "@/components/ErrorPage"; import ErrorPage from "@/components/ErrorPage";
export default function CartPage() { export default function CartPage() {
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const [paymentType, setPaymentType] = useState<PaymentType | null>(null); const [paymentType, setPaymentType] = useState<PaymentType | null>(null);
const [deliveryType, setDeliveryType] = const [selectedOrderDelivery, setSelectedOrderDelivery] = useState<OrderDelivery | null>(null);
useState<DeliveryType>("SELECTED_DELIVERY");
const [selectedRegion, setSelectedRegion] = useState<string>(""); const [selectedRegion, setSelectedRegion] = useState<string>("");
const [selectedProvince, setSelectedProvince] = useState<number | null>(null); const [selectedProvince, setSelectedProvince] = useState<number | null>(null);
const [notes, setNote] = useState<string>(""); const [notes, setNote] = useState<string>("");
@@ -36,15 +36,22 @@ export default function CartPage() {
const { data: provinces = [], isLoading: provincesLoading } = useRegions(); const { data: provinces = [], isLoading: provincesLoading } = useRegions();
const { data: paymentTypes = [], isLoading: paymentTypesLoading } = const { data: paymentTypes = [], isLoading: paymentTypesLoading } =
usePaymentTypes(); usePaymentTypes();
const { data: orderDeliveries = [], isLoading: deliveriesLoading } = useOrderDeliveries();
const { mutate: createOrder, isPending: isCreatingOrder } = useCreateOrder(); const { mutate: createOrder, isPending: isCreatingOrder } = useCreateOrder();
const cartItems = cartResponse?.data || []; const cartItems = cartResponse?.data || [];
const isLoading = cartLoading || provincesLoading || paymentTypesLoading; const isLoading = cartLoading || provincesLoading || paymentTypesLoading || deliveriesLoading;
useEffect(() => { useEffect(() => {
setIsClient(true); setIsClient(true);
}, []); }, []);
const handleRegionChange = (region: string) => {
setSelectedRegion(region);
setSelectedProvince(null);
setSelectedOrderDelivery(null);
};
const regionGroups = useMemo(() => { const regionGroups = useMemo(() => {
return provinces.reduce((acc, province) => { return provinces.reduce((acc, province) => {
if (!acc[province.region]) { if (!acc[province.region]) {
@@ -77,17 +84,17 @@ export default function CartPage() {
}, [cartItems]); }, [cartItems]);
const totalAmount = useMemo(() => { const totalAmount = useMemo(() => {
return cartItems.reduce((sum, item) => { const productsTotal = cartItems.reduce((sum, item) => {
const price = parseFloat(item.product.price_amount || "0"); const price = parseFloat(item.product.price_amount || "0");
return sum + price * item.product_quantity; return sum + price * item.product_quantity;
}, 0); }, 0);
return productsTotal;
}, [cartItems]); }, [cartItems]);
const handleDeliveryTypeChange = (type: DeliveryType) => { const finalTotal = useMemo(() => {
setDeliveryType(type); const shippingPrice = selectedOrderDelivery?.price || 0;
setSelectedProvince(null); return totalAmount + shippingPrice;
}; }, [totalAmount, selectedOrderDelivery]);
const formatPhoneForBackend = (phoneNumber: string): string => { const formatPhoneForBackend = (phoneNumber: string): string => {
@@ -95,7 +102,7 @@ export default function CartPage() {
}; };
const handleCompleteOrder = () => { const handleCompleteOrder = () => {
if (!selectedRegion || !selectedProvince || !paymentType || !phone || !name) { if (!selectedRegion || !selectedProvince || !paymentType || !phone || !name || !selectedOrderDelivery) {
console.warn("Missing required fields for order"); console.warn("Missing required fields for order");
return; return;
} }
@@ -112,9 +119,10 @@ export default function CartPage() {
createOrder( createOrder(
{ {
customer_name: `${name} ${lastName}`.trim(), customer_name: `${name} ${lastName}`.trim(),
customer_phone: parseInt(phoneDigits, 10), customer_phone: phoneDigits,
customer_address: selectedProvinceData.name, customer_address: selectedProvinceData.name,
shipping_method: "standart", shipping_method: selectedOrderDelivery.name,
shipping_price: selectedOrderDelivery.price,
payment_type_id: paymentType.id, payment_type_id: paymentType.id,
region: selectedRegion, region: selectedRegion,
notes: notes || undefined, notes: notes || undefined,
@@ -226,15 +234,25 @@ export default function CartPage() {
title: t("products"), title: t("products"),
value: `${totalAmount.toFixed(2)} TMT`, value: `${totalAmount.toFixed(2)} TMT`,
}, },
...(selectedOrderDelivery
? [
{
title: t("shipping_method"),
value: `${selectedOrderDelivery.price.toFixed(2)} TMT`,
},
]
: []),
], ],
footer: { footer: {
title: t("total_price"), title: t("total_price"),
value: `${totalAmount.toFixed(2)} TMT`, value: `${finalTotal.toFixed(2)} TMT`,
}, },
}, },
}} }}
paymentType={paymentType} paymentType={paymentType}
deliveryType={deliveryType} orderDeliveries={orderDeliveries}
selectedOrderDelivery={selectedOrderDelivery}
onOrderDeliveryChange={setSelectedOrderDelivery}
selectedRegion={selectedRegion} selectedRegion={selectedRegion}
selectedProvince={selectedProvince} selectedProvince={selectedProvince}
notes={notes} notes={notes}
@@ -248,8 +266,7 @@ export default function CartPage() {
onNameChange={setName} onNameChange={setName}
onLastNameChange={setLastName} onLastNameChange={setLastName}
onPaymentTypeChange={setPaymentType} onPaymentTypeChange={setPaymentType}
onDeliveryTypeChange={handleDeliveryTypeChange} onRegionChange={handleRegionChange}
onRegionChange={setSelectedRegion}
onProvinceChange={setSelectedProvince} onProvinceChange={setSelectedProvince}
onNoteChange={setNote} onNoteChange={setNote}
onCompleteOrder={handleCompleteOrder} onCompleteOrder={handleCompleteOrder}

View File

@@ -13,9 +13,13 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import DeliveryTypeSelector from "./DeliveryTypeSelector";
import { useTranslations } from "next-intl"; 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"; import { useState } from "react";
interface OrderBillingItem { interface OrderBillingItem {
@@ -37,7 +41,8 @@ interface OrderSummaryProps {
billing: OrderBilling; billing: OrderBilling;
}; };
paymentType: PaymentType | null; paymentType: PaymentType | null;
deliveryType: DeliveryType; orderDeliveries: OrderDelivery[];
selectedOrderDelivery: OrderDelivery | null;
selectedRegion: string; selectedRegion: string;
selectedProvince: number | null; selectedProvince: number | null;
notes: string; notes: string;
@@ -51,7 +56,7 @@ interface OrderSummaryProps {
onNameChange: (name: string) => void; onNameChange: (name: string) => void;
onLastNameChange: (lastName: string) => void; onLastNameChange: (lastName: string) => void;
onPaymentTypeChange: (type: PaymentType) => void; onPaymentTypeChange: (type: PaymentType) => void;
onDeliveryTypeChange: (type: DeliveryType) => void; onOrderDeliveryChange: (delivery: OrderDelivery) => void;
onRegionChange: (regionCode: string) => void; onRegionChange: (regionCode: string) => void;
onProvinceChange: (provinceId: number) => void; onProvinceChange: (provinceId: number) => void;
onNoteChange: (notes: string) => void; onNoteChange: (notes: string) => void;
@@ -62,7 +67,8 @@ interface OrderSummaryProps {
export default function OrderSummary({ export default function OrderSummary({
order, order,
paymentType, paymentType,
deliveryType, orderDeliveries,
selectedOrderDelivery,
selectedRegion, selectedRegion,
selectedProvince, selectedProvince,
notes, notes,
@@ -76,7 +82,7 @@ export default function OrderSummary({
onNameChange, onNameChange,
onLastNameChange, onLastNameChange,
onPaymentTypeChange, onPaymentTypeChange,
onDeliveryTypeChange, onOrderDeliveryChange,
onRegionChange, onRegionChange,
onProvinceChange, onProvinceChange,
onNoteChange, onNoteChange,
@@ -90,6 +96,15 @@ export default function OrderSummary({
? regionGroups[selectedRegion] || [] ? 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 phoneDigits = phone.replace(/\D/g, "");
const isPhoneValid = phoneDigits.length === 11; const isPhoneValid = phoneDigits.length === 11;
@@ -97,6 +112,7 @@ export default function OrderSummary({
selectedRegion && selectedRegion &&
selectedProvince && selectedProvince &&
paymentType && paymentType &&
selectedOrderDelivery &&
isPhoneValid && isPhoneValid &&
name.trim() !== "" && name.trim() !== "" &&
lastName.trim() !== ""; lastName.trim() !== "";
@@ -136,7 +152,7 @@ export default function OrderSummary({
return ( return (
<Card className="w-full md:w-[380px] p-4 md:p-6 rounded-xl h-fit sticky top-20"> <Card className="w-full md:w-[380px] p-4 md:p-6 rounded-xl h-fit sticky top-20">
{/* Customer Information */} {/* Customer Information */}
<div className="mb-6"> <div className="mb-4">
<h3 className="text-lg font-semibold mb-3"> <h3 className="text-lg font-semibold mb-3">
{t("customer_information")} {t("customer_information")}
</h3> </h3>
@@ -198,7 +214,7 @@ export default function OrderSummary({
</div> </div>
{/* Payment Type */} {/* Payment Type */}
<div className="mb-6"> <div className="mb-4">
<h3 className="text-lg font-semibold mb-3">{t("payment_type")}</h3> <h3 className="text-lg font-semibold mb-3">{t("payment_type")}</h3>
<div className="flex gap-2"> <div className="flex gap-2">
{paymentTypes.map((type) => ( {paymentTypes.map((type) => (
@@ -231,16 +247,13 @@ export default function OrderSummary({
</div> </div>
{/* Region Selection */} {/* Region Selection */}
<div className="mb-6"> <div className="mb-4">
<Label className="text-lg font-semibold mb-3 block"> <Label className="text-lg font-semibold mb-3 block">
{t("choose_region")} {t("choose_region")}
</Label> </Label>
<RadioGroup <RadioGroup
value={selectedRegion} value={selectedRegion}
onValueChange={(value) => { onValueChange={(value) => onRegionChange(value)}
onRegionChange(value);
onProvinceChange(null as any);
}}
className="flex flex-wrap gap-4" className="flex flex-wrap gap-4"
> >
{availableRegions.map((regionCode) => ( {availableRegions.map((regionCode) => (
@@ -270,7 +283,7 @@ export default function OrderSummary({
{/* Province Selection */} {/* Province Selection */}
{selectedRegion && provincesForSelectedRegion.length > 0 && ( {selectedRegion && provincesForSelectedRegion.length > 0 && (
<div className="mb-6"> <div className="mb-4">
<Label className="text-lg font-semibold mb-3 block"> <Label className="text-lg font-semibold mb-3 block">
{t("choose_address")} {t("choose_address")}
</Label> </Label>
@@ -299,8 +312,56 @@ export default function OrderSummary({
</div> </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 */} {/* Note */}
<div className="mb-6"> <div className="mb-4">
<Label className="text-lg font-semibold mb-3 block">{t("note")}</Label> <Label className="text-lg font-semibold mb-3 block">{t("note")}</Label>
<Textarea <Textarea
value={notes} value={notes}

View File

@@ -5,15 +5,14 @@ import {
UseQueryOptions, UseQueryOptions,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { apiClient } from "@/lib/api"; 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"; import { useEffect } from "react";
interface CartResponse {
message: string;
data: CartItem[];
errorDetails?: string;
}
const pendingUpdates = new Map<number, number>(); const pendingUpdates = new Map<number, number>();
let updateLock = false; 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() { export function useCreateOrder() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (payload: { mutationFn: async (payload: CreateOrderPayload) => {
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;
}) => {
const response = await apiClient.post("/orders", payload); const response = await apiClient.post("/orders", payload);
return response.data; return response.data;
}, },

View File

@@ -23,9 +23,10 @@ import {
MapPin, MapPin,
CreditCard, CreditCard,
ShoppingBag, ShoppingBag,
Banknote,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useOrders, useCancelOrder } from "@/lib/hooks"; import { useOrders, useCancelOrder, useOrderDeliveries } from "@/lib/hooks";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import type { Order } from "@/lib/types/api"; import type { Order } from "@/lib/types/api";
import EmptyOrders from "./EmptyOrders"; import EmptyOrders from "./EmptyOrders";
@@ -42,6 +43,8 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
const t = useTranslations(); const t = useTranslations();
const { data: orders, isLoading, isError } = useOrders(); const { data: orders, isLoading, isError } = useOrders();
const { data: orderDeliveries, isLoading: deliveriesLoading } =
useOrderDeliveries();
const { mutate: cancelOrder, isPending: isCancellingOrder } = const { mutate: cancelOrder, isPending: isCancellingOrder } =
useCancelOrder(); useCancelOrder();
@@ -83,7 +86,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
if ( if (
lowerStatus.includes("ожидается") || lowerStatus.includes("ожидается") ||
lowerStatus.includes("pending") || lowerStatus.includes("pending") ||
lowerStatus.includes("garaşlama") lowerStatus.includes("garaşylýar")
) { ) {
return ( return (
<Badge <Badge
@@ -147,20 +150,89 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
const activeOrders = useMemo( const activeOrders = useMemo(
() => orders?.filter((o) => isActiveOrder(o.status)) || [], () => orders?.filter((o) => isActiveOrder(o.status)) || [],
[orders, isActiveOrder] [orders, isActiveOrder],
); );
const completedOrders = useMemo( const completedOrders = useMemo(
() => orders?.filter((o) => !isActiveOrder(o.status)) || [], () => orders?.filter((o) => !isActiveOrder(o.status)) || [],
[orders, isActiveOrder] [orders, isActiveOrder],
); );
const calculateTotal = useCallback((order: Order) => { const getShippingPrice = useCallback(
return order.orderItems.reduce((sum, item) => { (order: Order) => {
return sum + parseFloat(item.unit_price_amount) * item.quantity; if (order.shipping_price !== undefined && order.shipping_price !== null) {
}, 0); 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 ( return (
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen"> <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"> <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} isCancelling={isCancellingOrder}
getStatusBadge={getStatusBadge} getStatusBadge={getStatusBadge}
calculateTotal={calculateTotal} calculateTotal={calculateTotal}
getShippingPrice={getShippingPrice}
showCancelButton showCancelButton
t={t} t={t}
orderDeliveries={orderDeliveries || []}
/> />
))} ))}
</div> </div>
@@ -270,12 +344,13 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
order={order} order={order}
isExpanded={expandedOrders.has(order.id)} isExpanded={expandedOrders.has(order.id)}
onToggle={() => toggleOrderExpand(order.id)} onToggle={() => toggleOrderExpand(order.id)}
onCancel={handleCancelOrder}
isCancelling={isCancellingOrder} isCancelling={isCancellingOrder}
getStatusBadge={getStatusBadge} getStatusBadge={getStatusBadge}
calculateTotal={calculateTotal} calculateTotal={calculateTotal}
getShippingPrice={getShippingPrice}
showCancelButton={false} showCancelButton={false}
t={t} t={t}
orderDeliveries={orderDeliveries || []}
/> />
))} ))}
</div> </div>
@@ -319,12 +394,14 @@ interface CompactOrderCardProps {
order: Order; order: Order;
isExpanded: boolean; isExpanded: boolean;
onToggle: () => void; onToggle: () => void;
onCancel: (order: Order) => void; onCancel?: (order: Order) => void;
isCancelling: boolean; isCancelling: boolean;
getStatusBadge: (status: string) => React.ReactNode; getStatusBadge: (status: string) => React.ReactNode;
calculateTotal: (order: Order) => number; calculateTotal: (order: Order) => number;
getShippingPrice: (order: Order) => number;
showCancelButton: boolean; showCancelButton: boolean;
t: any; t: any;
orderDeliveries: any[];
} }
function CompactOrderCard({ function CompactOrderCard({
@@ -335,12 +412,19 @@ function CompactOrderCard({
isCancelling, isCancelling,
getStatusBadge, getStatusBadge,
calculateTotal, calculateTotal,
getShippingPrice,
showCancelButton, showCancelButton,
t, t,
orderDeliveries,
}: CompactOrderCardProps) { }: CompactOrderCardProps) {
const total = useMemo(() => calculateTotal(order), [calculateTotal, order]); const total = useMemo(() => calculateTotal(order), [calculateTotal, order]);
const itemCount = order.orderItems.length; const itemCount = order.orderItems.length;
const shippingPrice = useMemo(
() => getShippingPrice(order),
[order, getShippingPrice],
);
return ( return (
<Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-md"> <Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-md">
{/* Compact Header - Always Visible */} {/* Compact Header - Always Visible */}
@@ -430,6 +514,20 @@ function CompactOrderCard({
<p className="text-sm text-gray-900">{order.shipping_method}</p> <p className="text-sm text-gray-900">{order.shipping_method}</p>
</div> </div>
</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> </div>
{/* Products List */} {/* Products List */}
@@ -476,7 +574,22 @@ function CompactOrderCard({
{/* Footer with Total and Actions */} {/* Footer with Total and Actions */}
<div className="border-t p-4 bg-gray-50"> <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"> <span className="text-base font-semibold text-gray-700">
{t("total_price")}: {t("total_price")}:
</span> </span>
@@ -490,7 +603,7 @@ function CompactOrderCard({
variant="destructive" variant="destructive"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onCancel(order); onCancel?.(order);
}} }}
disabled={isCancelling} disabled={isCancelling}
className="w-full cursor-pointer" className="w-full cursor-pointer"

View File

@@ -23,6 +23,7 @@
"category": "Категория", "category": "Категория",
"checkout": "Оформить заказ", "checkout": "Оформить заказ",
"price_label": "Цена:", "price_label": "Цена:",
"shipping_price": "Цена доставки",
"extra_price": "Доп. цена:", "extra_price": "Доп. цена:",
"discount": "Скидка:", "discount": "Скидка:",
"total_price": "Общая цена:", "total_price": "Общая цена:",
@@ -46,6 +47,10 @@
"delivery_type": "Тип доставки", "delivery_type": "Тип доставки",
"delivery": "Доставка", "delivery": "Доставка",
"pickup": "Самовывоз", "pickup": "Самовывоз",
"standart": "Стандартная",
"self_pickup": "Самовывоз",
"region": "Çapar ugrat",
"free": "Бесплатно",
"payment_type": "Тип оплаты", "payment_type": "Тип оплаты",
"cash": "Наличные", "cash": "Наличные",
"card": "Карта", "card": "Карта",
@@ -82,7 +87,7 @@
"become_seller": "Стать продавцом", "become_seller": "Стать продавцом",
"choose_region": "Выберите регион", "choose_region": "Выберите регион",
"choose_or_enter_address": "Выберите или введите свой адрес", "choose_or_enter_address": "Выберите или введите свой адрес",
"note": "Заметка", "note": "Укажите подробнее свой адрес",
"seller_application_form": "Форма подачи заявления на открытие магазина", "seller_application_form": "Форма подачи заявления на открытие магазина",
"phone": "Телефон", "phone": "Телефон",
"unit_price": "Цена за 1 шт.:", "unit_price": "Цена за 1 шт.:",

View File

@@ -1,7 +1,7 @@
{ {
"common": { "common": {
"categories": "Bölümler", "categories": "Bölümler",
"products": "Azyk harytlary", "products": "Harytlar",
"catalog": "Katalog", "catalog": "Katalog",
"search": "Haryt gözleg", "search": "Haryt gözleg",
"orders": "Sargytlar", "orders": "Sargytlar",
@@ -22,6 +22,7 @@
"category": "Bölümler", "category": "Bölümler",
"checkout": "Sargyt et", "checkout": "Sargyt et",
"price_label": "Baha:", "price_label": "Baha:",
"shipping_price": "Eltip berme bahasy",
"extra_price": "Goşmaça baha:", "extra_price": "Goşmaça baha:",
"discount": "Arzanladyş:", "discount": "Arzanladyş:",
"total_price": "Jemi baha:", "total_price": "Jemi baha:",
@@ -45,10 +46,14 @@
"delivery_type": "Elip bermek görnüşi", "delivery_type": "Elip bermek görnüşi",
"delivery": "Eltip bermek", "delivery": "Eltip bermek",
"pickup": "Özüň baryp al", "pickup": "Özüň baryp al",
"standart": "Standart",
"self_pickup": "Özüm baryp aljak",
"region": "Çapar ugrat",
"free": "Mugt",
"payment_type": "Töleg görnüşi", "payment_type": "Töleg görnüşi",
"cash": "Nagt", "cash": "Nagt",
"card": "Kartdan tölemek", "card": "Kartdan tölemek",
"choose_address": "Adres saýla", "choose_address": "Etrap saýla",
"brands": "Brendler", "brands": "Brendler",
"color": "Reňk", "color": "Reňk",
"price": "Baha", "price": "Baha",
@@ -78,11 +83,11 @@
"add_to_cart": "Sebede goş", "add_to_cart": "Sebede goş",
"go_to_cart": "Sebede geçmek", "go_to_cart": "Sebede geçmek",
"products": "Azyk harytlary", "products": "Harytlar",
"become_seller": "Satyjy bolmak", "become_seller": "Satyjy bolmak",
"choose_region": "Etrap saýlaň", "choose_region": "Welaýat saýlaň",
"choose_or_enter_address": "Salgyňyzy saýlaň ýa-da ýazyň", "choose_or_enter_address": "Salgyňyzy saýlaň ýa-da ýazyň",
"note": "Bellik", "note": "Adresiňiz barada giňişleýin ýazyň",
"seller_application_form": "Dükan açmak üçin arza görnüşi", "seller_application_form": "Dükan açmak üçin arza görnüşi",
"phone": "Telefon", "phone": "Telefon",
"unit_price": "1 san bahasy:", "unit_price": "1 san bahasy:",

View File

@@ -153,7 +153,8 @@ export interface CartItem {
} }
export interface CartResponse { export interface CartResponse {
message?: string; message: string;
errorDetails?: string;
data: CartItem[]; data: CartItem[];
count?: number; count?: number;
total?: number; total?: number;
@@ -200,6 +201,7 @@ export interface Order {
id: number; id: number;
status: string; status: string;
shipping_method: string; shipping_method: string;
shipping_price?: number;
notes: string | null; notes: string | null;
customer_name: string; customer_name: string;
customer_phone: string; customer_phone: string;
@@ -243,6 +245,7 @@ export interface CreateOrderPayload {
customer_phone?: string; customer_phone?: string;
customer_address: string; customer_address: string;
shipping_method: string; shipping_method: string;
shipping_price: number;
payment_type_id: number; payment_type_id: number;
delivery_time?: string; delivery_time?: string;
delivery_at?: string; delivery_at?: string;
@@ -396,6 +399,11 @@ export interface ShippingMethod {
code: string; code: string;
} }
export interface OrderDelivery {
name: string;
price: number;
}
// Generic API Error Response // Generic API Error Response
export interface ApiError { export interface ApiError {
message: string; message: string;