546 lines
18 KiB
JavaScript
546 lines
18 KiB
JavaScript
import React, { Component } from "react";
|
|
import { Box, Typography, Button } from "@mui/material";
|
|
import CartDropdown from "../CartDropdown.js";
|
|
import CheckoutForm from "./CheckoutForm.js";
|
|
import PaymentConfirmationDialog from "./PaymentConfirmationDialog.js";
|
|
import OrderProcessingService from "./OrderProcessingService.js";
|
|
import CheckoutValidation from "./CheckoutValidation.js";
|
|
import SocketContext from "../../contexts/SocketContext.js";
|
|
import { withI18n } from "../../i18n/index.js";
|
|
|
|
class CartTab extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
const initialCartItems = Array.isArray(window.cart) ? window.cart : [];
|
|
const initialDeliveryMethod = CheckoutValidation.shouldForcePickupDelivery(initialCartItems) ? "Abholung" : "DHL";
|
|
const optimalPaymentMethod = CheckoutValidation.getOptimalPaymentMethod(initialDeliveryMethod, initialCartItems, 0);
|
|
|
|
this.state = {
|
|
isCheckingOut: false,
|
|
cartItems: initialCartItems,
|
|
deliveryMethod: initialDeliveryMethod,
|
|
paymentMethod: optimalPaymentMethod,
|
|
invoiceAddress: {
|
|
firstName: "",
|
|
lastName: "",
|
|
addressAddition: "",
|
|
street: "",
|
|
houseNumber: "",
|
|
postalCode: "",
|
|
city: "",
|
|
country: "Deutschland",
|
|
},
|
|
deliveryAddress: {
|
|
firstName: "",
|
|
lastName: "",
|
|
addressAddition: "",
|
|
street: "",
|
|
houseNumber: "",
|
|
postalCode: "",
|
|
city: "",
|
|
country: "Deutschland",
|
|
},
|
|
useSameAddress: true,
|
|
saveAddressForFuture: true,
|
|
addressFormErrors: {},
|
|
termsAccepted: false,
|
|
isCompletingOrder: false,
|
|
completionError: null,
|
|
note: "",
|
|
stripeClientSecret: null,
|
|
showStripePayment: false,
|
|
StripeComponent: null,
|
|
isLoadingStripe: false,
|
|
showPaymentConfirmation: false,
|
|
orderCompleted: false,
|
|
originalCartItems: []
|
|
};
|
|
|
|
// Initialize order processing service
|
|
this.orderService = new OrderProcessingService(
|
|
() => this.context,
|
|
this.setState.bind(this)
|
|
);
|
|
this.orderService.getState = () => this.state;
|
|
this.orderService.setOrderSuccessCallback(this.props.onOrderSuccess);
|
|
}
|
|
|
|
// @note Add method to fetch and apply order template prefill data
|
|
fetchOrderTemplate = () => {
|
|
if (this.context && this.context.socket && this.context.socket.connected) {
|
|
this.context.socket.emit('getOrderTemplate', (response) => {
|
|
if (response.success && response.orderTemplate) {
|
|
const template = response.orderTemplate;
|
|
|
|
// Map the template fields to our state structure
|
|
const invoiceAddress = {
|
|
firstName: template.invoice_address_name ? template.invoice_address_name.split(' ')[0] || "" : "",
|
|
lastName: template.invoice_address_name ? template.invoice_address_name.split(' ').slice(1).join(' ') || "" : "",
|
|
addressAddition: template.invoice_address_line2 || "",
|
|
street: template.invoice_address_street || "",
|
|
houseNumber: template.invoice_address_house_number || "",
|
|
postalCode: template.invoice_address_postal_code || "",
|
|
city: template.invoice_address_city || "",
|
|
country: template.invoice_address_country || "Deutschland",
|
|
};
|
|
|
|
const deliveryAddress = {
|
|
firstName: template.shipping_address_name ? template.shipping_address_name.split(' ')[0] || "" : "",
|
|
lastName: template.shipping_address_name ? template.shipping_address_name.split(' ').slice(1).join(' ') || "" : "",
|
|
addressAddition: template.shipping_address_line2 || "",
|
|
street: template.shipping_address_street || "",
|
|
houseNumber: template.shipping_address_house_number || "",
|
|
postalCode: template.shipping_address_postal_code || "",
|
|
city: template.shipping_address_city || "",
|
|
country: template.shipping_address_country || "Deutschland",
|
|
};
|
|
|
|
// Get current cart state to check constraints
|
|
const currentCartItems = Array.isArray(window.cart) ? window.cart : [];
|
|
const { isPickupOnly, hasStecklinge } = CheckoutValidation.getCartItemFlags(currentCartItems);
|
|
|
|
// Determine delivery method - respect cart constraints
|
|
let prefillDeliveryMethod = template.delivery_method || "DHL";
|
|
if (isPickupOnly || hasStecklinge) {
|
|
prefillDeliveryMethod = "Abholung";
|
|
}
|
|
|
|
// Map delivery method values if needed
|
|
const deliveryMethodMap = {
|
|
"standard": "DHL",
|
|
"express": "DPD",
|
|
"pickup": "Abholung"
|
|
};
|
|
prefillDeliveryMethod = deliveryMethodMap[prefillDeliveryMethod] || prefillDeliveryMethod;
|
|
|
|
// Determine payment method - respect constraints
|
|
let prefillPaymentMethod = template.payment_method || "wire";
|
|
const paymentMethodMap = {
|
|
"credit_card": "mollie",/*stripe*/
|
|
"bank_transfer": "wire",
|
|
"cash_on_delivery": "onDelivery",
|
|
"cash": "cash"
|
|
};
|
|
prefillPaymentMethod = paymentMethodMap[prefillPaymentMethod] || prefillPaymentMethod;
|
|
|
|
// Validate payment method against delivery method constraints
|
|
prefillPaymentMethod = CheckoutValidation.validatePaymentMethodForDelivery(
|
|
prefillDeliveryMethod,
|
|
prefillPaymentMethod,
|
|
currentCartItems,
|
|
0 // Use 0 for delivery cost during prefill
|
|
);
|
|
|
|
// Apply prefill data to state
|
|
this.setState({
|
|
invoiceAddress,
|
|
deliveryAddress,
|
|
deliveryMethod: prefillDeliveryMethod,
|
|
paymentMethod: prefillPaymentMethod,
|
|
saveAddressForFuture: template.save_address_for_future === 1,
|
|
useSameAddress: true // Default to same address, user can change if needed
|
|
});
|
|
|
|
console.log("Order template applied successfully");
|
|
} else {
|
|
console.log("No order template available or failed to fetch");
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
componentDidMount() {
|
|
// Handle payment completion if detected
|
|
if (this.props.paymentCompletion) {
|
|
this.orderService.handlePaymentCompletion(
|
|
this.props.paymentCompletion,
|
|
this.props.onClearPaymentCompletion
|
|
);
|
|
}
|
|
|
|
// @note Fetch order template for prefill when component mounts
|
|
this.fetchOrderTemplate();
|
|
|
|
this.cart = () => {
|
|
// @note Don't update cart if we're showing payment confirmation - keep it empty
|
|
if (this.state.showPaymentConfirmation) {
|
|
return;
|
|
}
|
|
|
|
const cartItems = Array.isArray(window.cart) ? window.cart : [];
|
|
const shouldForcePickup = CheckoutValidation.shouldForcePickupDelivery(cartItems);
|
|
|
|
const newDeliveryMethod = shouldForcePickup ? "Abholung" : this.state.deliveryMethod;
|
|
const deliveryCost = this.orderService.getDeliveryCost();
|
|
|
|
// Get optimal payment method for the current state
|
|
const optimalPaymentMethod = CheckoutValidation.getOptimalPaymentMethod(
|
|
newDeliveryMethod,
|
|
cartItems,
|
|
deliveryCost
|
|
);
|
|
|
|
// Use optimal payment method if current one is invalid, otherwise keep current
|
|
const validatedPaymentMethod = CheckoutValidation.validatePaymentMethodForDelivery(
|
|
newDeliveryMethod,
|
|
this.state.paymentMethod,
|
|
cartItems,
|
|
deliveryCost
|
|
);
|
|
|
|
const newPaymentMethod = validatedPaymentMethod !== this.state.paymentMethod
|
|
? optimalPaymentMethod
|
|
: this.state.paymentMethod;
|
|
|
|
this.setState({
|
|
cartItems,
|
|
deliveryMethod: newDeliveryMethod,
|
|
paymentMethod: newPaymentMethod,
|
|
});
|
|
};
|
|
|
|
window.addEventListener("cart", this.cart);
|
|
this.cart(); // Initial check
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener("cart", this.cart);
|
|
this.orderService.cleanup();
|
|
}
|
|
|
|
handleCheckout = () => {
|
|
this.setState({ isCheckingOut: true });
|
|
};
|
|
|
|
handleContinueShopping = () => {
|
|
this.setState({ isCheckingOut: false });
|
|
};
|
|
|
|
handleDeliveryMethodChange = (event) => {
|
|
const newDeliveryMethod = event.target.value;
|
|
const deliveryCost = this.orderService.getDeliveryCost();
|
|
|
|
// Get optimal payment method for the new delivery method
|
|
const optimalPaymentMethod = CheckoutValidation.getOptimalPaymentMethod(
|
|
newDeliveryMethod,
|
|
this.state.cartItems,
|
|
deliveryCost
|
|
);
|
|
|
|
// Use optimal payment method if current one becomes invalid, otherwise keep current
|
|
const validatedPaymentMethod = CheckoutValidation.validatePaymentMethodForDelivery(
|
|
newDeliveryMethod,
|
|
this.state.paymentMethod,
|
|
this.state.cartItems,
|
|
deliveryCost
|
|
);
|
|
|
|
const newPaymentMethod = validatedPaymentMethod !== this.state.paymentMethod
|
|
? optimalPaymentMethod
|
|
: this.state.paymentMethod;
|
|
|
|
this.setState({
|
|
deliveryMethod: newDeliveryMethod,
|
|
paymentMethod: newPaymentMethod,
|
|
});
|
|
};
|
|
|
|
handlePaymentMethodChange = (event) => {
|
|
this.setState({ paymentMethod: event.target.value });
|
|
};
|
|
|
|
handleInvoiceAddressChange = (e) => {
|
|
const { name, value } = e.target;
|
|
this.setState((prevState) => ({
|
|
invoiceAddress: {
|
|
...prevState.invoiceAddress,
|
|
[name]: value,
|
|
},
|
|
}));
|
|
};
|
|
|
|
handleDeliveryAddressChange = (e) => {
|
|
const { name, value } = e.target;
|
|
this.setState((prevState) => ({
|
|
deliveryAddress: {
|
|
...prevState.deliveryAddress,
|
|
[name]: value,
|
|
},
|
|
}));
|
|
};
|
|
|
|
handleUseSameAddressChange = (e) => {
|
|
const useSameAddress = e.target.checked;
|
|
this.setState({
|
|
useSameAddress,
|
|
deliveryAddress: useSameAddress
|
|
? this.state.invoiceAddress
|
|
: this.state.deliveryAddress,
|
|
});
|
|
};
|
|
|
|
handleTermsAcceptedChange = (e) => {
|
|
this.setState({ termsAccepted: e.target.checked });
|
|
};
|
|
|
|
handleNoteChange = (e) => {
|
|
this.setState({ note: e.target.value });
|
|
};
|
|
|
|
handleSaveAddressForFutureChange = (e) => {
|
|
this.setState({ saveAddressForFuture: e.target.checked });
|
|
};
|
|
|
|
validateAddressForm = () => {
|
|
const errors = CheckoutValidation.validateAddressForm(this.state);
|
|
this.setState({ addressFormErrors: errors });
|
|
return Object.keys(errors).length === 0;
|
|
};
|
|
|
|
loadStripeComponent = async (clientSecret) => {
|
|
this.setState({ isLoadingStripe: true });
|
|
|
|
try {
|
|
const { default: Stripe } = await import("../Stripe.js");
|
|
this.setState({
|
|
StripeComponent: Stripe,
|
|
stripeClientSecret: clientSecret,
|
|
showStripePayment: true,
|
|
isCompletingOrder: false,
|
|
isLoadingStripe: false,
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to load Stripe component:", error);
|
|
this.setState({
|
|
isCompletingOrder: false,
|
|
isLoadingStripe: false,
|
|
completionError: "Failed to load payment component. Please try again.",
|
|
});
|
|
}
|
|
};
|
|
|
|
handleCompleteOrder = () => {
|
|
this.setState({ completionError: null }); // Clear previous errors
|
|
|
|
const validationError = CheckoutValidation.getValidationErrorMessage(this.state);
|
|
if (validationError) {
|
|
this.setState({ completionError: validationError });
|
|
this.validateAddressForm(); // To show field-specific errors
|
|
return;
|
|
}
|
|
|
|
this.setState({ isCompletingOrder: true });
|
|
|
|
const {
|
|
deliveryMethod,
|
|
paymentMethod,
|
|
invoiceAddress,
|
|
deliveryAddress,
|
|
useSameAddress,
|
|
cartItems,
|
|
note,
|
|
saveAddressForFuture,
|
|
} = this.state;
|
|
|
|
const deliveryCost = this.orderService.getDeliveryCost();
|
|
|
|
// Handle Stripe payment differently
|
|
if (paymentMethod === "stripe") {
|
|
// Store the cart items used for Stripe payment in sessionStorage for later reference
|
|
try {
|
|
sessionStorage.setItem('stripePaymentCart', JSON.stringify(cartItems));
|
|
} catch (error) {
|
|
console.error("Failed to store Stripe payment cart:", error);
|
|
}
|
|
|
|
// Calculate total amount for Stripe
|
|
const subtotal = cartItems.reduce(
|
|
(total, item) => total + item.price * item.quantity,
|
|
0
|
|
);
|
|
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
|
|
|
|
this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent);
|
|
return;
|
|
}
|
|
// Handle molllie payment differently
|
|
if (paymentMethod === "mollie") {
|
|
// Store the cart items used for mollie payment in sessionStorage for later reference
|
|
try {
|
|
sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems));
|
|
} catch (error) {
|
|
console.error("Failed to store mollie payment cart:", error);
|
|
}
|
|
|
|
// Calculate total amount for mollie
|
|
const subtotal = cartItems.reduce(
|
|
(total, item) => total + item.price * item.quantity,
|
|
0
|
|
);
|
|
const totalAmount = Math.round((subtotal + deliveryCost) * 100) / 100;
|
|
|
|
// Prepare complete order data for Mollie intent creation
|
|
const mollieOrderData = {
|
|
amount: totalAmount,
|
|
items: cartItems,
|
|
invoiceAddress,
|
|
deliveryAddress: useSameAddress ? invoiceAddress : deliveryAddress,
|
|
deliveryMethod,
|
|
paymentMethod: "mollie",
|
|
deliveryCost,
|
|
note,
|
|
domain: window.location.origin,
|
|
saveAddressForFuture,
|
|
};
|
|
|
|
this.orderService.createMollieIntent(mollieOrderData);
|
|
|
|
return;
|
|
}
|
|
|
|
// Handle regular orders
|
|
const orderData = {
|
|
items: cartItems,
|
|
invoiceAddress,
|
|
deliveryAddress: useSameAddress ? invoiceAddress : deliveryAddress,
|
|
deliveryMethod,
|
|
paymentMethod,
|
|
deliveryCost,
|
|
note,
|
|
domain: window.location.origin,
|
|
saveAddressForFuture,
|
|
};
|
|
|
|
this.orderService.processRegularOrder(orderData);
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
cartItems,
|
|
deliveryMethod,
|
|
paymentMethod,
|
|
invoiceAddress,
|
|
deliveryAddress,
|
|
useSameAddress,
|
|
saveAddressForFuture,
|
|
addressFormErrors,
|
|
termsAccepted,
|
|
isCompletingOrder,
|
|
completionError,
|
|
note,
|
|
stripeClientSecret,
|
|
showStripePayment,
|
|
StripeComponent,
|
|
isLoadingStripe,
|
|
showPaymentConfirmation,
|
|
orderCompleted,
|
|
} = this.state;
|
|
|
|
const deliveryCost = this.orderService.getDeliveryCost();
|
|
const { isPickupOnly, hasStecklinge } = CheckoutValidation.getCartItemFlags(cartItems);
|
|
|
|
const preSubmitError = CheckoutValidation.getValidationErrorMessage(this.state);
|
|
const displayError = completionError || preSubmitError;
|
|
|
|
return (
|
|
<Box sx={{ p: { xs: 1, sm: 3 } }}>
|
|
{/* Payment Confirmation */}
|
|
{showPaymentConfirmation && (
|
|
<PaymentConfirmationDialog
|
|
paymentCompletionData={this.orderService.paymentCompletionData}
|
|
isCompletingOrder={isCompletingOrder}
|
|
completionError={completionError}
|
|
orderCompleted={orderCompleted}
|
|
onContinueShopping={() => {
|
|
this.setState({ showPaymentConfirmation: false });
|
|
}}
|
|
onViewOrders={() => {
|
|
if (this.props.onOrderSuccess) {
|
|
this.props.onOrderSuccess();
|
|
}
|
|
this.setState({ showPaymentConfirmation: false });
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* @note Hide CartDropdown when showing payment confirmation */}
|
|
{!showPaymentConfirmation && (
|
|
<CartDropdown
|
|
cartItems={cartItems}
|
|
socket={this.context.socket}
|
|
showDetailedSummary={showStripePayment}
|
|
deliveryMethod={deliveryMethod}
|
|
deliveryCost={deliveryCost}
|
|
/>
|
|
)}
|
|
|
|
{cartItems.length > 0 && (
|
|
<Box sx={{ mt: 3 }}>
|
|
{isLoadingStripe ? (
|
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
<Typography variant="body1">
|
|
Zahlungskomponente wird geladen...
|
|
</Typography>
|
|
</Box>
|
|
) : showStripePayment && StripeComponent ? (
|
|
<>
|
|
<Box sx={{ mb: 2 }}>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => this.setState({ showStripePayment: false, stripeClientSecret: null })}
|
|
sx={{
|
|
color: '#2e7d32',
|
|
borderColor: '#2e7d32',
|
|
'&:hover': {
|
|
backgroundColor: 'rgba(46, 125, 50, 0.04)',
|
|
borderColor: '#1b5e20'
|
|
}
|
|
}}
|
|
>
|
|
{this.props.t ? this.props.t('cart.backToOrder') : '← Zurück zur Bestellung'}
|
|
</Button>
|
|
</Box>
|
|
<StripeComponent clientSecret={stripeClientSecret} />
|
|
</>
|
|
) : (
|
|
<CheckoutForm
|
|
paymentMethod={paymentMethod}
|
|
invoiceAddress={invoiceAddress}
|
|
deliveryAddress={deliveryAddress}
|
|
useSameAddress={useSameAddress}
|
|
saveAddressForFuture={saveAddressForFuture}
|
|
addressFormErrors={addressFormErrors}
|
|
termsAccepted={termsAccepted}
|
|
note={note}
|
|
deliveryMethod={deliveryMethod}
|
|
hasStecklinge={hasStecklinge}
|
|
isPickupOnly={isPickupOnly}
|
|
deliveryCost={deliveryCost}
|
|
cartItems={cartItems}
|
|
displayError={displayError}
|
|
isCompletingOrder={isCompletingOrder}
|
|
preSubmitError={preSubmitError}
|
|
onInvoiceAddressChange={this.handleInvoiceAddressChange}
|
|
onDeliveryAddressChange={this.handleDeliveryAddressChange}
|
|
onUseSameAddressChange={this.handleUseSameAddressChange}
|
|
onSaveAddressForFutureChange={this.handleSaveAddressForFutureChange}
|
|
onTermsAcceptedChange={this.handleTermsAcceptedChange}
|
|
onNoteChange={this.handleNoteChange}
|
|
onDeliveryMethodChange={this.handleDeliveryMethodChange}
|
|
onPaymentMethodChange={this.handlePaymentMethodChange}
|
|
onCompleteOrder={this.handleCompleteOrder}
|
|
/>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
// Set static contextType to access the socket
|
|
CartTab.contextType = SocketContext;
|
|
|
|
export default withI18n()(CartTab);
|