added baha tassyklamak
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import styles from "./Checkout.module.scss";
|
||||
import { X } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
usePlaceOrderMutation,
|
||||
@@ -9,202 +8,145 @@ import {
|
||||
} from "../../app/api/orderApi";
|
||||
import { useGetLocationsQuery } from "../../app/api/locationApi";
|
||||
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
const useDeviceType = () => {
|
||||
const [deviceType, setDeviceType] = useState("desktop");
|
||||
|
||||
useEffect(() => {
|
||||
const userAgent = navigator.userAgent;
|
||||
if (/Mobi|Android/i.test(userAgent)) {
|
||||
setDeviceType("mobile");
|
||||
} else {
|
||||
setDeviceType("desktop");
|
||||
}
|
||||
setDeviceType(
|
||||
/Mobi|Android/i.test(navigator.userAgent) ? "mobile" : "desktop",
|
||||
);
|
||||
}, []);
|
||||
|
||||
return deviceType;
|
||||
};
|
||||
|
||||
const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceOrder }) => {
|
||||
const Checkout = ({
|
||||
cartItems,
|
||||
shippingPrice,
|
||||
productIds,
|
||||
onBackToCart,
|
||||
onPlaceOrder,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
customer_name: "",
|
||||
customer_phone: "",
|
||||
customer_phone: "+993 ",
|
||||
customer_address: "",
|
||||
deliveryAddress: "null",
|
||||
payment_type_id: "",
|
||||
notes: "",
|
||||
region: "",
|
||||
});
|
||||
|
||||
const [selectedAddress, setSelectedAddress] = useState(null);
|
||||
const [placeOrder, { isLoading: isPlacingOrder }] = usePlaceOrderMutation();
|
||||
const { data: orderTimes = {} } = useGetOrderTimesQuery();
|
||||
const [placeOrder] = usePlaceOrderMutation();
|
||||
const { data: orderPayments = [] } = useGetOrderPaymentsQuery();
|
||||
const { data: locationsData } = useGetLocationsQuery();
|
||||
const deviceType = useDeviceType();
|
||||
|
||||
// Sepetteki tüm ürünlerin fiyatı 0 mı?
|
||||
const allItemsZeroPrice = cartItems?.every((item) =>
|
||||
isPriceZero(item.product?.price_amount),
|
||||
);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
|
||||
if (name === "customer_phone") {
|
||||
// Always keep the +993 prefix
|
||||
const prefix = "+993 ";
|
||||
|
||||
// If user is trying to delete the prefix, prevent it
|
||||
if (value.length < prefix.length) {
|
||||
return; // Don't update state, keep the current value
|
||||
}
|
||||
|
||||
// Extract only the digits after the prefix
|
||||
const inputWithoutPrefix = value.substring(prefix.length).replace(/\D/g, "");
|
||||
|
||||
// Limit to 8 digits max (Turkmenistan mobile number format)
|
||||
const limitedDigits = inputWithoutPrefix.substring(0, 8);
|
||||
|
||||
// Format with space after first 2 digits
|
||||
let formattedPhone = prefix;
|
||||
if (limitedDigits.length > 0) {
|
||||
formattedPhone += limitedDigits.substring(0, 2);
|
||||
|
||||
if (limitedDigits.length > 2) {
|
||||
formattedPhone += " " + limitedDigits.substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: formattedPhone,
|
||||
}));
|
||||
if (value.length < prefix.length) return;
|
||||
|
||||
const digits = value
|
||||
.substring(prefix.length)
|
||||
.replace(/\D/g, "")
|
||||
.substring(0, 8);
|
||||
let formatted = prefix + digits.substring(0, 2);
|
||||
if (digits.length > 2) formatted += " " + digits.substring(2);
|
||||
|
||||
setFormData((prev) => ({ ...prev, [name]: formatted }));
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddressSelect = (value) => {
|
||||
setSelectedAddress(value);
|
||||
const selectedLocation = locationsData?.data?.find(
|
||||
(location) => location.name === value
|
||||
);
|
||||
|
||||
const selectedLocation = locationsData?.data?.find((l) => l.name === value);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
address: value,
|
||||
region: selectedLocation ? selectedLocation.region : "",
|
||||
region: selectedLocation?.region || "",
|
||||
}));
|
||||
};
|
||||
|
||||
// Initialize phone with prefix
|
||||
useEffect(() => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
customer_phone: "+993 "
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const formatPhoneNumber = (phoneNumber) => {
|
||||
// Remove the +993 prefix and any spaces
|
||||
return phoneNumber.replace(/^\+993\s*/, "").replace(/\s+/g, "");
|
||||
};
|
||||
|
||||
const handleClearAddress = () => {
|
||||
setSelectedAddress(null);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
address: "",
|
||||
}));
|
||||
setFormData((prev) => ({ ...prev, address: "" }));
|
||||
};
|
||||
|
||||
const handleFocus = (event) => {
|
||||
event.target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
};
|
||||
const handleFocus = (e) =>
|
||||
e.target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
|
||||
const formatPhoneNumber = (phone) =>
|
||||
phone.replace(/^\+993\s*/, "").replace(/\s+/g, "");
|
||||
|
||||
const getOrderData = () => {
|
||||
// Validation checks
|
||||
if (
|
||||
!formData.customer_name ||
|
||||
!formData.customer_phone ||
|
||||
!formData.customer_address ||
|
||||
!formData.payment_type_id
|
||||
) {
|
||||
console.error("Missing required fields");
|
||||
alert("Please fill in all required fields");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set default values for delivery
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
const defaultTimeSlot = {
|
||||
date: currentDate,
|
||||
hour: "12:00-14:00" // Default time slot
|
||||
};
|
||||
const currentDate = new Date().toISOString().split("T")[0];
|
||||
|
||||
// Prepare data in the format expected by the API
|
||||
return {
|
||||
customer_name: formData.customer_name,
|
||||
customer_phone: formatPhoneNumber(formData.customer_phone),
|
||||
customer_address: formData.customer_address,
|
||||
shipping_method: "standard", // Default to standard shipping
|
||||
shipping_method: "standard",
|
||||
payment_type_id: formData.payment_type_id,
|
||||
delivery_time: defaultTimeSlot.hour,
|
||||
delivery_at: defaultTimeSlot.date,
|
||||
delivery_time: "12:00-14:00",
|
||||
delivery_at: currentDate,
|
||||
region: formData.region || "",
|
||||
notes: formData.notes || "",
|
||||
// Add shipping price and product IDs
|
||||
shipping_price: shippingPrice,
|
||||
product_ids: productIds // Array of product IDs [1, 3, 4, etc.]
|
||||
product_ids: productIds,
|
||||
};
|
||||
};
|
||||
|
||||
// Create the place order function
|
||||
const handlePlaceOrder = async () => {
|
||||
const orderDetails = getOrderData();
|
||||
if (!orderDetails) return false;
|
||||
|
||||
try {
|
||||
const response = await placeOrder(orderDetails).unwrap();
|
||||
|
||||
console.log("Order placed successfully:", response);
|
||||
await placeOrder(orderDetails).unwrap();
|
||||
window.location.href = "/orders";
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Failed to place order:", error);
|
||||
|
||||
if (
|
||||
const isHtmlResponse =
|
||||
error.data &&
|
||||
typeof error.data === "string" &&
|
||||
error.data.includes("<!doctype html>")
|
||||
) {
|
||||
console.error(
|
||||
"Server returned HTML instead of a proper API response"
|
||||
);
|
||||
alert(
|
||||
"There was a problem with the server. Please try again later or contact support."
|
||||
);
|
||||
} else {
|
||||
alert(
|
||||
"Failed to place order. Please check your information and try again."
|
||||
);
|
||||
}
|
||||
error.data.includes("<!doctype html>");
|
||||
|
||||
alert(
|
||||
isHtmlResponse
|
||||
? "There was a problem with the server. Please try again later."
|
||||
: "Failed to place order. Please check your information and try again.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Expose the function to parent component via callback
|
||||
useEffect(() => {
|
||||
if (onPlaceOrder) {
|
||||
onPlaceOrder(handlePlaceOrder);
|
||||
}
|
||||
if (onPlaceOrder) onPlaceOrder(handlePlaceOrder);
|
||||
}, [formData, shippingPrice, productIds]);
|
||||
|
||||
return (
|
||||
<div className={styles.checkoutContainer}>
|
||||
<h2>{t("cart.basket")} ({cartItems?.length || 0})</h2>
|
||||
{/* <h2>{t("cart.basket")} ({cartItems?.length || 0})</h2> */}
|
||||
<div className={styles.formSection}>
|
||||
<div className={styles.paymentOptions}>
|
||||
<h3>{t("checkout.paymentMethod")}:</h3>
|
||||
@@ -221,15 +163,15 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
||||
<label
|
||||
htmlFor={`payment${payment.id}`}
|
||||
className={styles.customRadio}
|
||||
></label>
|
||||
/>
|
||||
<div
|
||||
className={styles.text}
|
||||
onClick={() => {
|
||||
onClick={() =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
payment_type_id: String(payment.id),
|
||||
}));
|
||||
}}
|
||||
}))
|
||||
}
|
||||
>
|
||||
<span className={styles.optionTitle}>{payment.name}</span>
|
||||
<span className={styles.optionDesc}>
|
||||
@@ -256,7 +198,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t("checkout.telephone")}*</label>
|
||||
<input
|
||||
@@ -270,7 +211,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t("checkout.moreAboutYourAddress")}*</label>
|
||||
@@ -283,7 +223,6 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t("checkout.note")}</label>
|
||||
<input
|
||||
@@ -301,22 +240,17 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
||||
<ul>
|
||||
<li>
|
||||
{t(
|
||||
"checkout.Delivery_is_carried_out_in_the_cities_of_Ashgabat_Buzmein_and_Anau"
|
||||
)}
|
||||
</li>
|
||||
{/* <li>
|
||||
{t(
|
||||
"checkout.The_minimum_order_amount_must_be_at_least_50_manat_for_orders_over_150_manat_delivery_is_free"
|
||||
)}
|
||||
</li> */}
|
||||
<li>
|
||||
{t(
|
||||
"checkout.After_you_place_an_order_on_the_website_the_operator_will_call_you_to_confirm_the_order_for_regular_customers_confirmation_is_carried_out_automatically_at_their_request"
|
||||
"checkout.Delivery_is_carried_out_in_the_cities_of_Ashgabat_Buzmein_and_Anau",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
"checkout.Payment_is_made_after_you_check_and_accept_the_order_The_amount_of_your_payment_is_indicated_on_the_delivery_persons_payment_document_Payment_is_made_in_cash_and_by_card_in_national_currency_Accepted_and_paid_goods_are_not_subject_to_return"
|
||||
"checkout.After_you_place_an_order_on_the_website_the_operator_will_call_you_to_confirm_the_order_for_regular_customers_confirmation_is_carried_out_automatically_at_their_request",
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
"checkout.Payment_is_made_after_you_check_and_accept_the_order_The_amount_of_your_payment_is_indicated_on_the_delivery_persons_payment_document_Payment_is_made_in_cash_and_by_card_in_national_currency_Accepted_and_paid_goods_are_not_subject_to_return",
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -326,4 +260,4 @@ const Checkout = ({ cartItems, shippingPrice, productIds, onBackToCart, onPlaceO
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkout;
|
||||
export default Checkout;
|
||||
|
||||
@@ -3,36 +3,32 @@ import styles from "./ProductCard.module.scss";
|
||||
import { IoMdHeartEmpty, IoMdHeart } from "react-icons/io";
|
||||
import { FaShoppingCart } from "react-icons/fa";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { debounce } from "lodash";
|
||||
import {
|
||||
useAddFavoriteMutation,
|
||||
useRemoveFavoriteMutation,
|
||||
useGetFavoritesQuery,
|
||||
} from "../../app/api/favoritesApi";
|
||||
import { useGetFavoritesQuery } from "../../app/api/favoritesApi";
|
||||
import {
|
||||
useAddToCartMutation,
|
||||
useUpdateCartItemMutation,
|
||||
useRemoveFromCartMutation,
|
||||
useGetCartQuery,
|
||||
} from "../../app/api/cartApi";
|
||||
import { Modal } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DecreaseIcon, IncreaseIcon } from "../Icons";
|
||||
import ImageCarousel from "./imageCarousel/index";
|
||||
import { useCart } from "../../app/api/useCart";
|
||||
|
||||
// Helper function to strip HTML tags and truncate text
|
||||
const truncateDescription = (htmlString, maxLength = 80) => {
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = htmlString;
|
||||
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
||||
const truncatedText =
|
||||
textContent.length > maxLength
|
||||
? textContent.substring(0, maxLength).trim() + "..."
|
||||
: textContent;
|
||||
return truncatedText;
|
||||
return textContent.length > maxLength
|
||||
? textContent.substring(0, maxLength).trim() + "..."
|
||||
: textContent;
|
||||
};
|
||||
|
||||
import { useCart } from "../../app/api/useCart";
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
const ProductCard = ({
|
||||
product,
|
||||
@@ -45,22 +41,20 @@ const ProductCard = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [stockErrorModalVisible, setStockErrorModalVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const [addFavorite] = useAddFavoriteMutation();
|
||||
const [removeFavorite] = useRemoveFavoriteMutation();
|
||||
const { data: favoriteProducts = [] } = useGetFavoritesQuery();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [localIsFavorite, setLocalIsFavorite] = useState(
|
||||
favoriteProducts.some((fav) => fav.product?.id === product.id),
|
||||
);
|
||||
// const [isHovered, setIsHovered] = useState(false);
|
||||
const truncatedDesc = truncateDescription(
|
||||
product.description,
|
||||
descriptionMaxLength,
|
||||
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||
);
|
||||
|
||||
const { getCartItem } = useCart();
|
||||
|
||||
const [addToCart] = useAddToCartMutation();
|
||||
const [updateCartItem] = useUpdateCartItemMutation();
|
||||
const [removeFromCart] = useRemoveFromCartMutation();
|
||||
@@ -69,26 +63,54 @@ const ProductCard = ({
|
||||
const [localQuantity, setLocalQuantity] = useState(0);
|
||||
const [pendingQuantity, setPendingQuantity] = useState(0);
|
||||
|
||||
// ✅ Cart item değiştiğinde local state'i güncelle
|
||||
const { name, price_amount, old_price_amount, media = [], reviews } = product;
|
||||
|
||||
const truncatedDesc = truncateDescription(product.description, descriptionMaxLength);
|
||||
|
||||
const calculatedDiscount =
|
||||
!product.discount &&
|
||||
old_price_amount &&
|
||||
price_amount &&
|
||||
old_price_amount > price_amount
|
||||
? Math.round(((old_price_amount - price_amount) / old_price_amount) * 100)
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
const qty = parseInt(
|
||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||
10,
|
||||
);
|
||||
const qty = parseInt(cartItem?.quantity || cartItem?.product_quantity || 0, 10);
|
||||
setLocalQuantity(qty);
|
||||
setPendingQuantity(qty);
|
||||
}, [cartItem]);
|
||||
|
||||
// ✅ Favorite state'i güncelle
|
||||
useEffect(() => {
|
||||
if (Array.isArray(favoriteProducts)) {
|
||||
const isFav = favoriteProducts.some(
|
||||
(fav) => fav.product?.id === product.id,
|
||||
setLocalIsFavorite(
|
||||
favoriteProducts.some((fav) => fav.product?.id === product.id)
|
||||
);
|
||||
setLocalIsFavorite(isFav);
|
||||
}
|
||||
}, [favoriteProducts, product.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const serverQty = parseInt(cartItem?.quantity || cartItem?.product_quantity || 0, 10);
|
||||
|
||||
if (pendingQuantity === serverQty || pendingQuantity <= 0) return;
|
||||
|
||||
const handler = setTimeout(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updateCartItem({ productId: product.id, quantity: pendingQuantity }).unwrap();
|
||||
} catch {
|
||||
setLocalQuantity(serverQty);
|
||||
setPendingQuantity(serverQty);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
||||
|
||||
const handleCardClick = () => navigate(`/product/${product.id}`);
|
||||
|
||||
const handleAddToCart = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -98,51 +120,17 @@ const ProductCard = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Optimistic update
|
||||
setLocalQuantity((prev) => prev + 1);
|
||||
setPendingQuantity((prev) => prev + 1);
|
||||
|
||||
try {
|
||||
await addToCart({ productId: product.id, quantity: 1 }).unwrap();
|
||||
// ✅ Başarılı - RTK Query otomatik cache'i güncelleyecek
|
||||
} catch (error) {
|
||||
console.error("Failed to add to cart:", error);
|
||||
// ✅ Hata varsa geri al
|
||||
} catch {
|
||||
setLocalQuantity((prev) => prev - 1);
|
||||
setPendingQuantity((prev) => prev - 1);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Debounced update - sadece mutation, refetch yok
|
||||
useEffect(() => {
|
||||
const serverQty = parseInt(
|
||||
cartItem?.quantity || cartItem?.product_quantity || 0,
|
||||
10,
|
||||
);
|
||||
|
||||
if (pendingQuantity === serverQty || pendingQuantity <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = setTimeout(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updateCartItem({
|
||||
productId: product.id,
|
||||
quantity: pendingQuantity,
|
||||
}).unwrap();
|
||||
} catch (error) {
|
||||
console.error("Failed to update cart item:", error);
|
||||
setLocalQuantity(serverQty);
|
||||
setPendingQuantity(serverQty);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [pendingQuantity, cartItem, product.id, updateCartItem]);
|
||||
|
||||
const handleQuantityIncrease = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -165,24 +153,17 @@ const ProductCard = ({
|
||||
if (isLoading) return;
|
||||
|
||||
if (pendingQuantity <= 1) {
|
||||
// ✅ Sıfıra düşünce direkt sil
|
||||
setPendingQuantity(0);
|
||||
setLocalQuantity(0);
|
||||
setIsLoading(true);
|
||||
|
||||
removeFromCart({ productId: product.id })
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
// ✅ Başarılı - RTK Query cache'i güncelleyecek
|
||||
})
|
||||
.catch(() => {
|
||||
// ✅ Hata varsa geri al
|
||||
setLocalQuantity(1);
|
||||
setPendingQuantity(1);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
.finally(() => setIsLoading(false));
|
||||
} else {
|
||||
setLocalQuantity((prev) => prev - 1);
|
||||
setPendingQuantity((prev) => prev - 1);
|
||||
@@ -196,45 +177,25 @@ const ProductCard = ({
|
||||
if (isLoading) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// ✅ Optimistic update
|
||||
setLocalIsFavorite(!localIsFavorite);
|
||||
setLocalIsFavorite((prev) => !prev);
|
||||
|
||||
try {
|
||||
if (localIsFavorite) {
|
||||
const result = await removeFavorite(product.id).unwrap();
|
||||
// ✅ Başarılı - RTK Query otomatik güncelleyecek
|
||||
await removeFavorite(product.id).unwrap();
|
||||
} else {
|
||||
const result = await addFavorite(product.id).unwrap();
|
||||
// ✅ Başarılı - RTK Query otomatik güncelleyecek
|
||||
await addFavorite(product.id).unwrap();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle favorite:", error);
|
||||
// ✅ Hata varsa geri al
|
||||
setLocalIsFavorite(localIsFavorite);
|
||||
} catch {
|
||||
setLocalIsFavorite((prev) => !prev); // revert
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCardClick = () => {
|
||||
navigate(`/product/${product.id}`);
|
||||
};
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const { name, price_amount, old_price_amount, media = [], reviews } = product;
|
||||
|
||||
// Hesaplanmış indirim oranı
|
||||
let calculatedDiscount = null;
|
||||
if (!product.discount && old_price_amount && price_amount && old_price_amount > price_amount) {
|
||||
calculatedDiscount = Math.round(((old_price_amount - price_amount) / old_price_amount) * 100);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={styles.productCard}
|
||||
<div
|
||||
className={styles.productCard}
|
||||
onClick={handleCardClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
@@ -242,7 +203,7 @@ const ProductCard = ({
|
||||
<div className={styles.imageContainer}>
|
||||
{(product.discount || calculatedDiscount) && (
|
||||
<span className={styles.discountBadge}>
|
||||
-{product.discount ? product.discount : calculatedDiscount}%
|
||||
-{product.discount ?? calculatedDiscount}%
|
||||
</span>
|
||||
)}
|
||||
{product.stock === 0 && (
|
||||
@@ -250,22 +211,29 @@ const ProductCard = ({
|
||||
{t("common.out_of_stock")}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ImageCarousel images={media} altText={name} isHovered={isHovered}/>
|
||||
<ImageCarousel images={media} altText={name} isHovered={isHovered} />
|
||||
</div>
|
||||
|
||||
<div className={styles.productInfo}>
|
||||
<h3 className={styles.productName}>{name}</h3>
|
||||
<p className={styles.productDescription}>{truncatedDesc}</p>
|
||||
|
||||
<div className={styles.priceContainer}>
|
||||
<div>
|
||||
<span className={styles.currentPrice}>{price_amount} m.</span>
|
||||
{old_price_amount && (
|
||||
<span className={styles.oldPrice}>{old_price_amount} m.</span>
|
||||
{isPriceZero(price_amount) ? (
|
||||
<span className={styles.currentPrice}>Bahasyny anyklamaly</span>
|
||||
) : (
|
||||
<>
|
||||
<span className={styles.currentPrice}>{price_amount} m.</span>
|
||||
{old_price_amount && (
|
||||
<span className={styles.oldPrice}>{old_price_amount} m.</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{showFavoriteButton && (
|
||||
<button
|
||||
@@ -276,6 +244,7 @@ const ProductCard = ({
|
||||
{localIsFavorite ? <IoMdHeart /> : <IoMdHeartEmpty />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showAddToCart && (
|
||||
<>
|
||||
{localQuantity > 0 ? (
|
||||
@@ -337,4 +306,4 @@ const ProductCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
export default ProductCard;
|
||||
@@ -16,10 +16,9 @@
|
||||
.cartHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
justify-content: space-between;
|
||||
background-color: #f3f4f6;
|
||||
padding-bottom: 15px;
|
||||
padding-top: 10px;
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
@@ -27,6 +26,7 @@
|
||||
@media screen and (max-width: 768px) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +226,7 @@
|
||||
@media screen and (max-width: 1023px) {
|
||||
width: 100%;
|
||||
position: static;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
@@ -2,13 +2,10 @@ import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import styles from "./CartPage.module.scss";
|
||||
import { FaTrashAlt } from "react-icons/fa";
|
||||
import Checkout from "../../components/Checkout";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Modal } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EmptyCartState from "./emptyCart";
|
||||
import {
|
||||
useGetCartQuery,
|
||||
useAddToCartMutation,
|
||||
useRemoveFromCartMutation,
|
||||
useUpdateCartItemMutation,
|
||||
useCleanCartMutation,
|
||||
@@ -17,9 +14,9 @@ import { useCart } from "../../app/api/useCart";
|
||||
import { DecreaseIcon, IncreaseIcon } from "../../components/Icons";
|
||||
import Loader from "../../components/Loader/index";
|
||||
|
||||
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||
const stripHtml = (html) => {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
return doc.body.textContent || "";
|
||||
@@ -32,11 +29,9 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||
<div className={styles.truncatedDescription}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: isExpanded
|
||||
? description
|
||||
: shouldTruncate
|
||||
? description.substring(0, maxLength) + "..."
|
||||
: description,
|
||||
__html: shouldTruncate
|
||||
? description.substring(0, maxLength) + "..."
|
||||
: description,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -44,20 +39,16 @@ const TruncatedDescription = ({ description, maxLength = 100 }) => {
|
||||
};
|
||||
|
||||
const CartPage = () => {
|
||||
const { cartData, cartItems, isLoading, isError, error } = useCart();
|
||||
const { cartData, cartItems, isLoading } = useCart();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const [checkoutStores, setCheckoutStores] = useState({});
|
||||
const [addToCart] = useAddToCartMutation();
|
||||
const [removeFromCart] = useRemoveFromCartMutation();
|
||||
const [updateCartItem] = useUpdateCartItemMutation();
|
||||
const [cleanCart] = useCleanCartMutation();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const expandedRef = useRef(null);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [emptyCartModalVisible, setEmptyCartModalVisible] = useState(false);
|
||||
const [itemToDelete, setItemToDelete] = useState(null);
|
||||
|
||||
const [localQuantities, setLocalQuantities] = useState({});
|
||||
const [pendingQuantities, setPendingQuantities] = useState({});
|
||||
const [loadingItems, setLoadingItems] = useState({});
|
||||
@@ -70,43 +61,35 @@ const CartPage = () => {
|
||||
width: 400,
|
||||
};
|
||||
|
||||
// Convert grouped data to stores array
|
||||
const stores = useMemo(() => {
|
||||
return Object.entries(cartData)
|
||||
.map(([storeSlug, items]) => {
|
||||
if (!items || !items.length) return null;
|
||||
|
||||
// Get store info from first item
|
||||
if (!items?.length) return null;
|
||||
const storeInfo = items[0]?.product?.channel?.[0];
|
||||
|
||||
return {
|
||||
id: storeInfo?.id || storeSlug,
|
||||
name: storeInfo?.name || storeSlug,
|
||||
slug: storeSlug,
|
||||
shipping_price: storeInfo?.shipping_price,
|
||||
items: items,
|
||||
items,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}, [cartData]);
|
||||
|
||||
// ✅ Initialize local quantities from cart items
|
||||
useEffect(() => {
|
||||
const newLocalQuantities = {};
|
||||
const newPendingQuantities = {};
|
||||
|
||||
const newLocal = {};
|
||||
const newPending = {};
|
||||
cartItems.forEach((item) => {
|
||||
const productId = item.product.id;
|
||||
const quantity = parseInt(item.product_quantity, 10) || 0;
|
||||
newLocalQuantities[productId] = quantity;
|
||||
newPendingQuantities[productId] = quantity;
|
||||
const id = item.product.id;
|
||||
const qty = parseInt(item.product_quantity, 10) || 0;
|
||||
newLocal[id] = qty;
|
||||
newPending[id] = qty;
|
||||
});
|
||||
|
||||
setLocalQuantities(newLocalQuantities);
|
||||
setPendingQuantities(newPendingQuantities);
|
||||
setLocalQuantities(newLocal);
|
||||
setPendingQuantities(newPending);
|
||||
}, [cartItems]);
|
||||
|
||||
// ✅ Debounced Cart Update - Her ürün için ayrı debounce
|
||||
useEffect(() => {
|
||||
const timers = {};
|
||||
|
||||
@@ -114,141 +97,94 @@ const CartPage = () => {
|
||||
const serverItem = cartItems.find(
|
||||
(item) => String(item.product.id) === String(productId),
|
||||
);
|
||||
const serverQuantity = serverItem
|
||||
const serverQty = serverItem
|
||||
? parseInt(serverItem.product_quantity, 10)
|
||||
: 0;
|
||||
const pendingQuantity = pendingQuantities[productId];
|
||||
const pendingQty = pendingQuantities[productId];
|
||||
|
||||
// Değişiklik yoksa veya 0 ise (Delete modalı tetikler) bir şey yapma
|
||||
if (
|
||||
pendingQuantity === undefined ||
|
||||
pendingQuantity === serverQuantity ||
|
||||
pendingQuantity <= 0
|
||||
) {
|
||||
pendingQty === undefined ||
|
||||
pendingQty === serverQty ||
|
||||
pendingQty <= 0
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
timers[productId] = setTimeout(async () => {
|
||||
try {
|
||||
setLoadingItems((prev) => ({ ...prev, [productId]: true }));
|
||||
await updateCartItem({
|
||||
productId,
|
||||
quantity: pendingQuantity,
|
||||
}).unwrap();
|
||||
} catch (error) {
|
||||
console.error("Failed to update cart:", error);
|
||||
// Hata durumunda rollback
|
||||
setLocalQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: serverQuantity,
|
||||
}));
|
||||
setPendingQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: serverQuantity,
|
||||
}));
|
||||
await updateCartItem({ productId, quantity: pendingQty }).unwrap();
|
||||
} catch {
|
||||
setLocalQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||||
setPendingQuantities((prev) => ({ ...prev, [productId]: serverQty }));
|
||||
} finally {
|
||||
setLoadingItems((prev) => ({ ...prev, [productId]: false }));
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Object.values(timers).forEach((timer) => clearTimeout(timer));
|
||||
};
|
||||
return () => Object.values(timers).forEach(clearTimeout);
|
||||
}, [pendingQuantities, cartItems, updateCartItem]);
|
||||
|
||||
const handleQuantityIncrease = (productId) => (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (loadingItems[productId]) return;
|
||||
|
||||
const item = cartItems.find((item) => item.product.id === productId);
|
||||
if (!item) return;
|
||||
const item = cartItems.find((i) => i.product.id === productId);
|
||||
if (!item || localQuantities[productId] >= item.product.stock) return;
|
||||
|
||||
if (localQuantities[productId] >= item.product.stock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuantity = (localQuantities[productId] || 0) + 1;
|
||||
setLocalQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: newQuantity,
|
||||
}));
|
||||
setPendingQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: newQuantity,
|
||||
}));
|
||||
const newQty = (localQuantities[productId] || 0) + 1;
|
||||
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||
};
|
||||
|
||||
const handleQuantityDecrease = (productId) => (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (loadingItems[productId]) return;
|
||||
|
||||
const currentQuantity = localQuantities[productId] || 0;
|
||||
|
||||
if (currentQuantity <= 1) {
|
||||
const currentQty = localQuantities[productId] || 0;
|
||||
if (currentQty <= 1) {
|
||||
showDeleteConfirm(productId);
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuantity = currentQuantity - 1;
|
||||
setLocalQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: newQuantity,
|
||||
}));
|
||||
setPendingQuantities((prev) => ({
|
||||
...prev,
|
||||
[productId]: newQuantity,
|
||||
}));
|
||||
const newQty = currentQty - 1;
|
||||
setLocalQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||
setPendingQuantities((prev) => ({ ...prev, [productId]: newQty }));
|
||||
};
|
||||
|
||||
const calculateStoreTotal = (storeItems) => {
|
||||
return storeItems.reduce((sum, item) => {
|
||||
const itemPrice = parseFloat(item.product.price_amount) || 0;
|
||||
const itemQuantity = parseInt(item.product_quantity, 10) || 0;
|
||||
return sum + itemPrice * itemQuantity;
|
||||
const getStoreShippingPrice = (store) =>
|
||||
store.shipping_price != null ? parseFloat(store.shipping_price) : 20;
|
||||
|
||||
// Store içinde fiyatsız ürün var mı?
|
||||
const storeHasZeroPriceItem = (storeItems) =>
|
||||
storeItems.some((item) => isPriceZero(item.product.price_amount));
|
||||
|
||||
const calculateStoreTotal = (storeItems) =>
|
||||
storeItems.reduce((sum, item) => {
|
||||
return (
|
||||
sum +
|
||||
(parseFloat(item.product.price_amount) || 0) *
|
||||
(parseInt(item.product_quantity, 10) || 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const getStoreShippingPrice = (store) => {
|
||||
return store.shipping_price !== null && store.shipping_price !== undefined
|
||||
? parseFloat(store.shipping_price)
|
||||
: 20;
|
||||
};
|
||||
|
||||
const handleCheckout = (storeId) => {
|
||||
const handleCheckout = (storeId) =>
|
||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: true }));
|
||||
};
|
||||
|
||||
const handleBackToCart = (storeId) => {
|
||||
const handleBackToCart = (storeId) =>
|
||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||
};
|
||||
|
||||
const handleOrderSubmit = async (storeId, storeItems) => {
|
||||
const handleOrderSubmit = async (storeId) => {
|
||||
if (checkoutStores[storeId] && checkoutRefs.current[storeId]) {
|
||||
const success = await checkoutRefs.current[storeId]();
|
||||
if (success) {
|
||||
setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||
}
|
||||
if (success) setCheckoutStores((prev) => ({ ...prev, [storeId]: false }));
|
||||
} else {
|
||||
handleCheckout(storeId);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (expandedRef.current && !expandedRef.current.contains(event.target)) {
|
||||
setIsExpanded(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const showDeleteConfirm = (productId) => {
|
||||
setItemToDelete(productId);
|
||||
setDeleteModalVisible(true);
|
||||
@@ -258,48 +194,41 @@ const CartPage = () => {
|
||||
if (itemToDelete) {
|
||||
try {
|
||||
await removeFromCart({ productId: itemToDelete }).unwrap();
|
||||
|
||||
setLocalQuantities((prev) => {
|
||||
const newState = { ...prev };
|
||||
delete newState[itemToDelete];
|
||||
return newState;
|
||||
const s = { ...prev };
|
||||
delete s[itemToDelete];
|
||||
return s;
|
||||
});
|
||||
setPendingQuantities((prev) => {
|
||||
const newState = { ...prev };
|
||||
delete newState[itemToDelete];
|
||||
return newState;
|
||||
const s = { ...prev };
|
||||
delete s[itemToDelete];
|
||||
return s;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to remove item:", error);
|
||||
} catch (e) {
|
||||
console.error("Failed to remove item:", e);
|
||||
}
|
||||
}
|
||||
setDeleteModalVisible(false);
|
||||
setItemToDelete(null);
|
||||
};
|
||||
|
||||
const showEmptyCartConfirm = () => {
|
||||
setEmptyCartModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEmptyCartConfirm = async () => {
|
||||
try {
|
||||
await cleanCart().unwrap();
|
||||
|
||||
setLocalQuantities({});
|
||||
setPendingQuantities({});
|
||||
setCheckoutStores({});
|
||||
} catch (error) {
|
||||
console.error("Failed to clean cart:", error);
|
||||
} catch (e) {
|
||||
console.error("Failed to clean cart:", e);
|
||||
}
|
||||
setEmptyCartModalVisible(false);
|
||||
};
|
||||
|
||||
const getTotalItemCount = () => {
|
||||
return cartItems.reduce(
|
||||
const getTotalItemCount = () =>
|
||||
cartItems.reduce(
|
||||
(sum, item) => sum + parseInt(item.product_quantity, 10),
|
||||
0,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.cartContainer}>
|
||||
@@ -339,21 +268,20 @@ const CartPage = () => {
|
||||
<h2>
|
||||
{t("cart.basket")} ({getTotalItemCount()})
|
||||
</h2>
|
||||
<div>
|
||||
<button
|
||||
className={styles.deleteBtn}
|
||||
style={{ padding: "4px 12px" }}
|
||||
onClick={showEmptyCartConfirm}
|
||||
>
|
||||
<FaTrashAlt /> {t("cart.clearCart")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={styles.deleteBtn}
|
||||
style={{ padding: "4px 12px" }}
|
||||
onClick={() => setEmptyCartModalVisible(true)}
|
||||
>
|
||||
<FaTrashAlt /> {t("cart.clearCart")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{stores.map((store) => {
|
||||
const shippingPrice = getStoreShippingPrice(store);
|
||||
const storeTotal = calculateStoreTotal(store.items);
|
||||
const totalWithShipping = storeTotal + shippingPrice;
|
||||
const hasZeroPrice = storeHasZeroPriceItem(store.items);
|
||||
|
||||
return (
|
||||
<div key={store.id} className={styles.storeSection}>
|
||||
@@ -363,8 +291,8 @@ const CartPage = () => {
|
||||
shippingPrice={shippingPrice}
|
||||
productIds={store.items.map((item) => item.product.id)}
|
||||
onBackToCart={() => handleBackToCart(store.id)}
|
||||
onPlaceOrder={(placeOrderFn) => {
|
||||
checkoutRefs.current[store.id] = placeOrderFn;
|
||||
onPlaceOrder={(fn) => {
|
||||
checkoutRefs.current[store.id] = fn;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -391,10 +319,9 @@ const CartPage = () => {
|
||||
</div>
|
||||
<div className={styles.priceQuantity}>
|
||||
<span className={styles.price}>
|
||||
{(
|
||||
parseFloat(item.product.price_amount) || 0
|
||||
).toFixed(2)}{" "}
|
||||
m.
|
||||
{isPriceZero(item.product.price_amount)
|
||||
? "Bahasyny anyklamaly"
|
||||
: `${parseFloat(item.product.price_amount).toFixed(2)} m.`}
|
||||
</span>
|
||||
<div className={styles.quantityControls}>
|
||||
<button
|
||||
@@ -441,26 +368,46 @@ const CartPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ✅ Store Summary - fiyatsız ürün varsa "Baha anyklamak" */}
|
||||
<div className={styles.storeSummary}>
|
||||
<div className={styles.cartContent}>
|
||||
<h3>
|
||||
{store.name} - {t("cart.basket")}:
|
||||
</h3>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.price")}:</span>
|
||||
<span>{storeTotal.toFixed(2)} m.</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.delivery")}:</span>
|
||||
<span>{shippingPrice.toFixed(2)} m.</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.total")}:</span>
|
||||
<span>{totalWithShipping.toFixed(2)} m.</span>
|
||||
</div>
|
||||
{hasZeroPrice ? (
|
||||
<>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.price")}:</span>
|
||||
<span>Bahasyny anyklamaly</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.delivery")}:</span>
|
||||
<span>Bahasyny anyklamaly</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.total")}:</span>
|
||||
<span>Bahasyny anyklamaly</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.price")}:</span>
|
||||
<span>{storeTotal.toFixed(2)} m.</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.delivery")}:</span>
|
||||
<span>{shippingPrice.toFixed(2)} m.</span>
|
||||
</div>
|
||||
<div className={styles.summaryRow}>
|
||||
<span>{t("cart.total")}:</span>
|
||||
<span>{totalWithShipping.toFixed(2)} m.</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleOrderSubmit(store.id, store.items)}
|
||||
onClick={() => handleOrderSubmit(store.id)}
|
||||
className={styles.checkoutBtn}
|
||||
>
|
||||
{checkoutStores[store.id]
|
||||
@@ -472,7 +419,6 @@ const CartPage = () => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mobile sticky summary */}
|
||||
{/* <div className={styles.container}>
|
||||
<div className={styles.summaryCard} ref={expandedRef}>
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -140,6 +141,7 @@
|
||||
.row {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
gap: 12px;
|
||||
@media screen and (max-width: 640px) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -157,10 +159,10 @@
|
||||
&:first-child {
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
width: 25%;
|
||||
@media screen and (max-width: 640px) {
|
||||
width: 50%;
|
||||
}
|
||||
// width: 25%;
|
||||
// @media screen and (max-width: 640px) {
|
||||
// width: 50%;
|
||||
// }
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@@ -295,3 +297,106 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pendingPriceBadgeWrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pendingPriceBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #faeeda;
|
||||
border: 0.5px solid #ef9f27;
|
||||
color: #854f0b;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pendingPriceTooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-background-primary, #ffffff);
|
||||
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||
border-radius: var(--border-radius-md, 6px);
|
||||
padding: 8px 12px;
|
||||
width: 220px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-primary, #333333);
|
||||
line-height: 1.5;
|
||||
z-index: 100;
|
||||
white-space: normal;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text-primary, #000000);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.pending-price-modal {
|
||||
.ant-modal-content {
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
@media (max-width: 767px) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
margin-bottom: 12px;
|
||||
.ant-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
@media (max-width: 767px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #555;
|
||||
margin: 0;
|
||||
@media (max-width: 767px) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: 20px;
|
||||
.ant-btn-primary {
|
||||
background-color: #888888;
|
||||
border-color: #888888;
|
||||
border-radius: 6px;
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
font-weight: 500;
|
||||
&:hover {
|
||||
background-color: #666666;
|
||||
border-color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,128 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import styles from "./OrderDetail.module.scss";
|
||||
import { Ban, CircleCheck, X } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetOrderByIdQuery } from "../../app/api/orderApi"; // Update with your correct path
|
||||
import track from "../../assets/track.jpg"; // Keep for delivery service icon
|
||||
import { useGetOrderByIdQuery } from "../../app/api/orderApi";
|
||||
import track from "../../assets/track.jpg";
|
||||
import Loader from "../../components/Loader/index";
|
||||
import { Result, Button } from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Result, Button, Modal } from "antd";
|
||||
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
const PendingPriceModal = ({ open, onClose, t }) => (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={onClose}
|
||||
onCancel={onClose}
|
||||
okText={t ? t("common.ok") : "OK"}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
centered
|
||||
title="Bahasy anyklamaly"
|
||||
className="pending-price-modal"
|
||||
width={400}
|
||||
>
|
||||
<p>
|
||||
Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik.
|
||||
Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer.
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const PendingPriceBadge = ({ t }) => {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [isMobile] = useState(() => /Mobi|Android/i.test(navigator.userAgent));
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMobile) setModalVisible(true);
|
||||
};
|
||||
|
||||
const stopPropagation = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<span onClick={stopPropagation}>
|
||||
<span
|
||||
className={styles.pendingPriceBadgeWrapper}
|
||||
onMouseEnter={() => !isMobile && setTooltipVisible(true)}
|
||||
onMouseLeave={() => setTooltipVisible(false)}
|
||||
onClick={handleClick}
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span className={styles.pendingPriceBadge}>!</span>
|
||||
|
||||
{tooltipVisible && (
|
||||
<span className={styles.pendingPriceTooltip}>
|
||||
<strong>Bahasyny anyklamaly</strong>
|
||||
Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip
|
||||
goşmaça maglumat berer.
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<PendingPriceModal
|
||||
open={modalVisible}
|
||||
onClose={() => setModalVisible(false)}
|
||||
t={t}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const OrderDetail = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams(); // Get the order ID from URL params
|
||||
const { data: orderData, isLoading, error } = useGetOrderByIdQuery(id);
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
// Format date function
|
||||
const { data: orderData, isLoading, error } = useGetOrderByIdQuery(id);
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("tk-TM", {
|
||||
return new Date(dateString).toLocaleString("tk-TM", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
// Format delivery time for display
|
||||
const formatDeliveryTime = (time, date) => {
|
||||
try {
|
||||
const deliveryDate = new Date(date);
|
||||
const formattedDate = deliveryDate.toLocaleDateString("tk-TM", {
|
||||
const formatted = new Date(date).toLocaleDateString("tk-TM", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
return `${time} (${formattedDate})`;
|
||||
} catch (e) {
|
||||
return `${time}`;
|
||||
return `${time} (${formatted})`;
|
||||
} catch {
|
||||
return time;
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate total order amount
|
||||
const calculateTotal = (orderItems) => {
|
||||
if (!orderItems || !orderItems.length) return 0;
|
||||
if (!orderItems?.length) return null;
|
||||
const hasZero = orderItems.some((item) =>
|
||||
isPriceZero(item.unit_price_amount),
|
||||
);
|
||||
if (hasZero) return null;
|
||||
return orderItems
|
||||
.reduce(
|
||||
(sum, item) => sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||
0
|
||||
0,
|
||||
)
|
||||
.toFixed(2);
|
||||
};
|
||||
|
||||
// Handle loading state
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
// Handle error state
|
||||
if (error)
|
||||
return (
|
||||
<Result
|
||||
@@ -72,10 +137,8 @@ const OrderDetail = () => {
|
||||
/>
|
||||
);
|
||||
|
||||
// Handle case where order data is not available
|
||||
if (!orderData) return <div className={styles.notFound}>Order not found</div>;
|
||||
|
||||
// Calculate total
|
||||
const totalAmount = calculateTotal(orderData.orderItems);
|
||||
|
||||
return (
|
||||
@@ -84,39 +147,10 @@ const OrderDetail = () => {
|
||||
<h1>
|
||||
{t("order.orderNumber")}: {orderData.id}
|
||||
</h1>
|
||||
<div className={styles.Buttons}>
|
||||
{/* <button className={styles.repeatButton}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M480 256c-17.67 0-32 14.31-32 32c0 52.94-43.06 96-96 96H192L192 344c0-9.469-5.578-18.06-14.23-21.94C169.1 318.3 159 319.8 151.9 326.2l-80 72C66.89 402.7 64 409.2 64 416s2.891 13.28 7.938 17.84l80 72C156.4 509.9 162.2 512 168 512c3.312 0 6.615-.6875 9.756-2.062C186.4 506.1 192 497.5 192 488L192 448h160c88.22 0 160-71.78 160-160C512 270.3 497.7 256 480 256zM160 128h159.1L320 168c0 9.469 5.578 18.06 14.23 21.94C337.4 191.3 340.7 192 343.1 192c5.812 0 11.57-2.125 16.07-6.156l80-72C445.1 109.3 448 102.8 448 95.1s-2.891-13.28-7.938-17.84l-80-72c-7.047-6.312-17.19-7.875-25.83-4.094C325.6 5.938 319.1 14.53 319.1 24L320 64H160C71.78 64 0 135.8 0 224c0 17.69 14.33 32 32 32s32-14.31 32-32C64 171.1 107.1 128 160 128z"></path>
|
||||
</svg>{" "}
|
||||
{t("order.repeatOrder")}
|
||||
</button> */}
|
||||
{/* <button className={styles.cancelButton}>
|
||||
{" "}
|
||||
<Ban />
|
||||
{t("order.dropOrder")}
|
||||
</button> */}
|
||||
</div>
|
||||
<div className={styles.Buttons} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{/* Order Status */}
|
||||
{/* <div className={styles.status}>
|
||||
<p className={styles.statusText}>
|
||||
<span className={styles.statusIcon}>
|
||||
<CircleCheck />
|
||||
</span>{" "}
|
||||
{t("order.Your_order_has_been_accepted")}
|
||||
</p>
|
||||
<span className={styles.close}>
|
||||
<X />
|
||||
</span>
|
||||
</div> */}
|
||||
|
||||
{/* Order Details */}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.rowContainer}>
|
||||
<div className={styles.row}>
|
||||
@@ -132,7 +166,7 @@ const OrderDetail = () => {
|
||||
<span>
|
||||
{formatDeliveryTime(
|
||||
orderData.delivery_time,
|
||||
orderData.delivery_at
|
||||
orderData.delivery_at,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -144,11 +178,26 @@ const OrderDetail = () => {
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<span>{t("order.sum")}:</span>
|
||||
<span className={styles.total}>{totalAmount} m.</span>
|
||||
<span className={styles.total}>
|
||||
{totalAmount === null ? (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<PendingPriceBadge t={t} />
|
||||
</span>
|
||||
) : (
|
||||
`${totalAmount} m.`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop table */}
|
||||
<div className={styles.tableContainer}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
@@ -165,9 +214,12 @@ const OrderDetail = () => {
|
||||
<tbody>
|
||||
{orderData.orderItems.map((item, index) => {
|
||||
const product = item.product;
|
||||
const itemTotal = (
|
||||
parseFloat(item.unit_price_amount) * item.quantity
|
||||
).toFixed(2);
|
||||
const zeroPriceItem = isPriceZero(item.unit_price_amount);
|
||||
const itemTotal = zeroPriceItem
|
||||
? null
|
||||
: (
|
||||
parseFloat(item.unit_price_amount) * item.quantity
|
||||
).toFixed(2);
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
@@ -181,27 +233,50 @@ const OrderDetail = () => {
|
||||
<td>{product.name}</td>
|
||||
<td>{product.brand || "-"}</td>
|
||||
<td>{product.id || "-"}</td>
|
||||
<td>{item.unit_price_amount} m.</td>
|
||||
<td>
|
||||
{zeroPriceItem ? (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<PendingPriceBadge t={t} />
|
||||
</span>
|
||||
) : (
|
||||
`${item.unit_price_amount} m.`
|
||||
)}
|
||||
</td>
|
||||
<td>{item.quantity}</td>
|
||||
<td>{itemTotal} m.</td>
|
||||
<td>
|
||||
{itemTotal === null ? (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<PendingPriceBadge t={t} />
|
||||
</span>
|
||||
) : (
|
||||
`${itemTotal} m.`
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{/* Add delivery service row if shipping method exists */}
|
||||
|
||||
{orderData.shipping_method && (
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
src={track}
|
||||
alt="Delivery Service"
|
||||
className={styles.image}
|
||||
/>
|
||||
<img src={track} alt="Delivery" className={styles.image} />
|
||||
</td>
|
||||
<td>Eltip bermek hyzmaty</td>
|
||||
<td>Beýleki</td>
|
||||
<td>DELIVERY</td>
|
||||
<td>10.00 m.</td>{" "}
|
||||
{/* You may need to get actual delivery cost from API */}
|
||||
<td>10.00 m.</td>
|
||||
<td>1</td>
|
||||
<td>10.00 m.</td>
|
||||
</tr>
|
||||
@@ -210,13 +285,15 @@ const OrderDetail = () => {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/* Mobile View */}
|
||||
|
||||
{/* Mobile cards */}
|
||||
<div className={styles.productList}>
|
||||
{orderData.orderItems.map((item, index) => {
|
||||
const product = item.product;
|
||||
const itemTotal = (
|
||||
parseFloat(item.unit_price_amount) * item.quantity
|
||||
).toFixed(2);
|
||||
const zeroPriceItem = isPriceZero(item.unit_price_amount);
|
||||
const itemTotal = zeroPriceItem
|
||||
? null
|
||||
: (parseFloat(item.unit_price_amount) * item.quantity).toFixed(2);
|
||||
|
||||
return (
|
||||
<div className={styles.card} key={index}>
|
||||
@@ -233,18 +310,30 @@ const OrderDetail = () => {
|
||||
{t("order.quantity")}: {item.quantity}
|
||||
</span>
|
||||
<span className={styles.price}>
|
||||
{item.unit_price_amount} m.
|
||||
{zeroPriceItem ? (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<PendingPriceBadge t={t} />
|
||||
</span>
|
||||
) : (
|
||||
`${item.unit_price_amount} m.`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Add delivery service card if shipping method exists */}
|
||||
{orderData.shipping_method && (
|
||||
|
||||
{/* {orderData.shipping_method && (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.imageContainer}>
|
||||
<img src={track} alt="Delivery Service" />
|
||||
<img src={track} alt="Delivery" />
|
||||
</div>
|
||||
<div className={styles.detailsMobile}>
|
||||
<h3 className={styles.title}>Beýleki</h3>
|
||||
@@ -257,7 +346,7 @@ const OrderDetail = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// SargytlarymComponent.module.scss
|
||||
.container {
|
||||
padding: 15px 24px 0 24px;
|
||||
padding: 15px 24px 24px 24px;
|
||||
max-width: 1366px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
a{
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
@@ -121,3 +121,106 @@
|
||||
font-weight: 700;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.pendingPriceBadgeWrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pendingPriceBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #faeeda;
|
||||
border: 0.5px solid #ef9f27;
|
||||
color: #854f0b;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pendingPriceTooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-background-primary, #ffffff);
|
||||
border: 0.5px solid var(--color-border-secondary, #e2e2e2);
|
||||
border-radius: var(--border-radius-md, 6px);
|
||||
padding: 8px 12px;
|
||||
width: 220px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-primary, #333333);
|
||||
line-height: 1.5;
|
||||
z-index: 100;
|
||||
white-space: normal;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text-primary, #000000);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.pending-price-modal {
|
||||
.ant-modal-content {
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
@media (max-width: 767px) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
margin-bottom: 12px;
|
||||
.ant-modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
@media (max-width: 767px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #555;
|
||||
margin: 0;
|
||||
@media (max-width: 767px) {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: 20px;
|
||||
.ant-btn-primary {
|
||||
background-color: #888888;
|
||||
border-color: #888888;
|
||||
border-radius: 6px;
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
font-weight: 500;
|
||||
&:hover {
|
||||
background-color: #666666;
|
||||
border-color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,104 @@
|
||||
// Orders.jsx
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import styles from "./Orders.module.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetOrdersQuery } from "../../app/api/orderApi"; // Update with your correct path
|
||||
import EmptyOrderState from "./emptyOrder"; // Import the EmptyOrderState component
|
||||
import { useGetOrdersQuery } from "../../app/api/orderApi";
|
||||
import EmptyOrderState from "./emptyOrder";
|
||||
import Loader from "../../components/Loader/index";
|
||||
import { Result, Button } from "antd";
|
||||
import { Result, Button, Modal } from "antd";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const isPriceZero = (price) => !price || parseFloat(price) === 0;
|
||||
|
||||
const orderHasZeroPrice = (orderItems) =>
|
||||
orderItems?.some((item) => isPriceZero(item.unit_price_amount));
|
||||
|
||||
const PendingPriceModal = ({ open, onClose, t }) => (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={onClose}
|
||||
onCancel={onClose}
|
||||
okText={t("common.ok")}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
centered
|
||||
title="Bahasy anyklamaly"
|
||||
className="pending-price-modal"
|
||||
width={400}
|
||||
>
|
||||
<p>
|
||||
Bu sargytdaky bir ýa-da birnäçe harydyň bahasy entek kesgitlenmedik.
|
||||
Operatorymyz siziň bilen habarlaşyp, goşmaça maglumat berer.
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const PendingPriceBadge = ({ t }) => {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [isMobile] = useState(() => /Mobi|Android/i.test(navigator.userAgent));
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isMobile) setModalVisible(true);
|
||||
};
|
||||
|
||||
const stopPropagation = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<span onClick={stopPropagation}>
|
||||
<span
|
||||
className={styles.pendingPriceBadgeWrapper}
|
||||
onMouseEnter={() => !isMobile && setTooltipVisible(true)}
|
||||
onMouseLeave={() => setTooltipVisible(false)}
|
||||
onClick={handleClick}
|
||||
onTouchEnd={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span className={styles.pendingPriceBadge}>!</span>
|
||||
|
||||
{tooltipVisible && (
|
||||
<span className={styles.pendingPriceTooltip}>
|
||||
<strong>Bahasyny anyklamaly</strong>
|
||||
Bu sargytdaky harydyň bahasy kesgitlenmedik. Operator size jaň edip
|
||||
goşmaça maglumat berer.
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<PendingPriceModal
|
||||
open={modalVisible}
|
||||
onClose={() => setModalVisible(false)}
|
||||
t={t}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const Orders = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: orders, isLoading, error } = useGetOrdersQuery();
|
||||
const navigate = useNavigate();
|
||||
// Function to format date - implement this or use a library like date-fns
|
||||
|
||||
const formatOrderDate = (dateString) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("tk-TM", {
|
||||
return new Date(dateString).toLocaleString("tk-TM", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) return <Loader />;
|
||||
|
||||
// Handle error state
|
||||
if (error)
|
||||
return (
|
||||
<Result
|
||||
@@ -45,16 +113,13 @@ const Orders = () => {
|
||||
/>
|
||||
);
|
||||
|
||||
// Handle empty orders - render EmptyOrderState component
|
||||
if (!orders || orders.length === 0) {
|
||||
return <EmptyOrderState />;
|
||||
}
|
||||
if (!orders || orders.length === 0) return <EmptyOrderState />;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h2 className={styles.title}>Sargytlarym</h2>
|
||||
|
||||
{/* Desktop table view */}
|
||||
{/* Desktop table */}
|
||||
<div className={styles.tableContainer}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
@@ -69,11 +134,11 @@ const Orders = () => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.map((order) => {
|
||||
// Calculate total order amount
|
||||
const hasZeroPrice = orderHasZeroPrice(order.orderItems);
|
||||
const totalAmount = order.orderItems.reduce(
|
||||
(sum, item) =>
|
||||
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -81,7 +146,19 @@ const Orders = () => {
|
||||
<td>{order.id}</td>
|
||||
<td>{formatOrderDate(order.delivery_at)}</td>
|
||||
<td style={{ color: "#888888", fontWeight: "700" }}>
|
||||
{totalAmount.toFixed(2)} m.
|
||||
{hasZeroPrice ? (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
Bahasyny anyklamaly <PendingPriceBadge t={t} />
|
||||
</span>
|
||||
) : (
|
||||
`${totalAmount.toFixed(2)} m.`
|
||||
)}
|
||||
</td>
|
||||
<td>{order.payment_type}</td>
|
||||
<td>{order.status}</td>
|
||||
@@ -99,50 +176,72 @@ const Orders = () => {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile card view */}
|
||||
{/* Mobile cards */}
|
||||
<div className={styles.Mobilecontainer}>
|
||||
{orders.map((order) => {
|
||||
const hasZeroPrice = orderHasZeroPrice(order.orderItems);
|
||||
const totalAmount = order.orderItems.reduce(
|
||||
(sum, item) =>
|
||||
sum + parseFloat(item.unit_price_amount) * item.quantity,
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<Link to={`/orderdetail/${order.id}`} key={order.id}>
|
||||
<div className={styles.orderCard}>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>
|
||||
{t("order.orderNumber")}:
|
||||
</span>
|
||||
<span className={styles.value}>{order.id}</span>
|
||||
</div>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>{t("order.orderDate")}:</span>
|
||||
<span className={styles.value}>
|
||||
{formatOrderDate(order.delivery_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>{t("order.sum")}:</span>
|
||||
<span className={styles.total}>
|
||||
{totalAmount.toFixed(2)} m.
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>
|
||||
{t("checkout.paymentMethod")}:
|
||||
</span>
|
||||
<span className={styles.value}>{order.payment_type}</span>
|
||||
</div>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>
|
||||
{t("order.orderStatus")}:
|
||||
</span>
|
||||
<span className={styles.value}>{order.status}</span>
|
||||
</div>
|
||||
<div
|
||||
key={order.id}
|
||||
className={styles.orderCard}
|
||||
onClick={(e) => {
|
||||
// Modal veya badge içerisine tıklandığında yönlendirmeyi engelle
|
||||
if (
|
||||
e.target.closest(`.${styles.pendingPriceBadgeWrapper}`) ||
|
||||
e.target.closest(".ant-modal-root") ||
|
||||
e.target.closest(".ant-modal-wrap")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
navigate(`/orderdetail/${order.id}`);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>{t("order.orderNumber")}:</span>
|
||||
<span className={styles.value}>{order.id}</span>
|
||||
</div>
|
||||
</Link>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>{t("order.orderDate")}:</span>
|
||||
<span className={styles.value}>
|
||||
{formatOrderDate(order.delivery_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>{t("order.sum")}:</span>
|
||||
<span className={styles.total}>
|
||||
{hasZeroPrice ? (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
Bahasyny anyklamaly <PendingPriceBadge t={t} />
|
||||
</span>
|
||||
) : (
|
||||
`${totalAmount.toFixed(2)} m.`
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>
|
||||
{t("checkout.paymentMethod")}:
|
||||
</span>
|
||||
<span className={styles.value}>{order.payment_type}</span>
|
||||
</div>
|
||||
<div className={styles.orderRow}>
|
||||
<span className={styles.label}>{t("order.orderStatus")}:</span>
|
||||
<span className={styles.value}>{order.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user