Compare commits
2 Commits
db7889fb7a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab9eab656 | ||
|
|
c13a4655bf |
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 шт.:",
|
||||||
|
|||||||
@@ -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:",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user