Genesis
This commit is contained in:
138
src/components/profile/AddressForm.js
Normal file
138
src/components/profile/AddressForm.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import React from "react";
|
||||
import { Box, TextField, Typography } from "@mui/material";
|
||||
|
||||
const AddressForm = ({ title, address, onChange, errors, namePrefix }) => {
|
||||
// Helper function to determine if a required field should show error styling
|
||||
const getRequiredFieldError = (fieldName, value) => {
|
||||
const isEmpty = !value || value.trim() === "";
|
||||
return isEmpty;
|
||||
};
|
||||
|
||||
// Helper function to get label styling for required fields
|
||||
const getRequiredFieldLabelSx = (fieldName, value) => {
|
||||
const showError = getRequiredFieldError(fieldName, value);
|
||||
return showError
|
||||
? {
|
||||
"&.MuiInputLabel-shrink": {
|
||||
color: "#d32f2f", // Material-UI error color
|
||||
},
|
||||
}
|
||||
: {};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
|
||||
gap: 2,
|
||||
mt: 3,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Vorname"
|
||||
name="firstName"
|
||||
value={address.firstName}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
error={!!errors[`${namePrefix}FirstName`]}
|
||||
helperText={errors[`${namePrefix}FirstName`]}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
sx: getRequiredFieldLabelSx("firstName", address.firstName),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Nachname"
|
||||
name="lastName"
|
||||
value={address.lastName}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
error={!!errors[`${namePrefix}LastName`]}
|
||||
helperText={errors[`${namePrefix}LastName`]}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
sx: getRequiredFieldLabelSx("lastName", address.lastName),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Adresszusatz"
|
||||
name="addressAddition"
|
||||
value={address.addressAddition || ""}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
label="Straße"
|
||||
name="street"
|
||||
value={address.street}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
error={!!errors[`${namePrefix}Street`]}
|
||||
helperText={errors[`${namePrefix}Street`]}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
sx: getRequiredFieldLabelSx("street", address.street),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Hausnummer"
|
||||
name="houseNumber"
|
||||
value={address.houseNumber}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
error={!!errors[`${namePrefix}HouseNumber`]}
|
||||
helperText={errors[`${namePrefix}HouseNumber`]}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
sx: getRequiredFieldLabelSx("houseNumber", address.houseNumber),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="PLZ"
|
||||
name="postalCode"
|
||||
value={address.postalCode}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
error={!!errors[`${namePrefix}PostalCode`]}
|
||||
helperText={errors[`${namePrefix}PostalCode`]}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
sx: getRequiredFieldLabelSx("postalCode", address.postalCode),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Stadt"
|
||||
name="city"
|
||||
value={address.city}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
error={!!errors[`${namePrefix}City`]}
|
||||
helperText={errors[`${namePrefix}City`]}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
sx: getRequiredFieldLabelSx("city", address.city),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Land"
|
||||
name="country"
|
||||
value={address.country}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
disabled
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressForm;
|
||||
510
src/components/profile/CartTab.js
Normal file
510
src/components/profile/CartTab.js
Normal file
@@ -0,0 +1,510 @@
|
||||
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";
|
||||
|
||||
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.connected) {
|
||||
this.context.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": "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 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: 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}
|
||||
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'
|
||||
}
|
||||
}}
|
||||
>
|
||||
← 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 CartTab;
|
||||
185
src/components/profile/CheckoutForm.js
Normal file
185
src/components/profile/CheckoutForm.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { Component } from "react";
|
||||
import { Box, Typography, TextField, Checkbox, FormControlLabel, Button } from "@mui/material";
|
||||
import AddressForm from "./AddressForm.js";
|
||||
import DeliveryMethodSelector from "./DeliveryMethodSelector.js";
|
||||
import PaymentMethodSelector from "./PaymentMethodSelector.js";
|
||||
import OrderSummary from "./OrderSummary.js";
|
||||
|
||||
class CheckoutForm extends Component {
|
||||
render() {
|
||||
const {
|
||||
paymentMethod,
|
||||
invoiceAddress,
|
||||
deliveryAddress,
|
||||
useSameAddress,
|
||||
saveAddressForFuture,
|
||||
addressFormErrors,
|
||||
termsAccepted,
|
||||
note,
|
||||
deliveryMethod,
|
||||
hasStecklinge,
|
||||
isPickupOnly,
|
||||
deliveryCost,
|
||||
cartItems,
|
||||
displayError,
|
||||
isCompletingOrder,
|
||||
preSubmitError,
|
||||
onInvoiceAddressChange,
|
||||
onDeliveryAddressChange,
|
||||
onUseSameAddressChange,
|
||||
onSaveAddressForFutureChange,
|
||||
onTermsAcceptedChange,
|
||||
onNoteChange,
|
||||
onDeliveryMethodChange,
|
||||
onPaymentMethodChange,
|
||||
onCompleteOrder,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{paymentMethod !== "cash" && (
|
||||
<>
|
||||
<AddressForm
|
||||
title="Rechnungsadresse"
|
||||
address={invoiceAddress}
|
||||
onChange={onInvoiceAddressChange}
|
||||
errors={addressFormErrors}
|
||||
namePrefix="invoice"
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={saveAddressForFuture}
|
||||
onChange={onSaveAddressForFutureChange}
|
||||
sx={{ '& .MuiSvgIcon-root': { fontSize: 28 } }}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
Für zukünftige Bestellungen speichern
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasStecklinge && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ mb: 2, fontWeight: "bold", color: "#2e7d32" }}
|
||||
>
|
||||
Für welchen Termin ist die Abholung der Stecklinge
|
||||
gewünscht?
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Anmerkung"
|
||||
name="note"
|
||||
value={note}
|
||||
onChange={onNoteChange}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
sx={{ mb: 2 }}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
<DeliveryMethodSelector
|
||||
deliveryMethod={deliveryMethod}
|
||||
onChange={onDeliveryMethodChange}
|
||||
isPickupOnly={isPickupOnly || hasStecklinge}
|
||||
/>
|
||||
|
||||
{(deliveryMethod === "DHL" || deliveryMethod === "DPD") && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={useSameAddress}
|
||||
onChange={onUseSameAddressChange}
|
||||
sx={{ '& .MuiSvgIcon-root': { fontSize: 28 } }}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body1">
|
||||
Lieferadresse ist identisch mit Rechnungsadresse
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
{!useSameAddress && (
|
||||
<AddressForm
|
||||
title="Lieferadresse"
|
||||
address={deliveryAddress}
|
||||
onChange={onDeliveryAddressChange}
|
||||
errors={addressFormErrors}
|
||||
namePrefix="delivery"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<PaymentMethodSelector
|
||||
paymentMethod={paymentMethod}
|
||||
onChange={onPaymentMethodChange}
|
||||
deliveryMethod={deliveryMethod}
|
||||
onDeliveryMethodChange={onDeliveryMethodChange}
|
||||
cartItems={cartItems}
|
||||
deliveryCost={deliveryCost}
|
||||
/>
|
||||
|
||||
<OrderSummary deliveryCost={deliveryCost} cartItems={cartItems} />
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={termsAccepted}
|
||||
onChange={onTermsAcceptedChange}
|
||||
sx={{
|
||||
'& .MuiSvgIcon-root': { fontSize: 28 },
|
||||
alignSelf: 'flex-start',
|
||||
mt: -0.5
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
Ich habe die AGBs, die Datenschutzerklärung und die
|
||||
Bestimmungen zum Widerrufsrecht gelesen
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mb: 3, mt: 2 }}
|
||||
/>
|
||||
|
||||
{/* @note Reserve space for error message to prevent layout shift */}
|
||||
<Box sx={{ minHeight: '24px', mb: 2, textAlign: "center" }}>
|
||||
{displayError && (
|
||||
<Typography color="error" sx={{ lineHeight: '24px' }}>
|
||||
{displayError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{ bgcolor: "#2e7d32", "&:hover": { bgcolor: "#1b5e20" } }}
|
||||
onClick={onCompleteOrder}
|
||||
disabled={isCompletingOrder || !!preSubmitError}
|
||||
>
|
||||
{isCompletingOrder
|
||||
? "Bestellung wird verarbeitet..."
|
||||
: "Bestellung abschließen"}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckoutForm;
|
||||
150
src/components/profile/CheckoutValidation.js
Normal file
150
src/components/profile/CheckoutValidation.js
Normal file
@@ -0,0 +1,150 @@
|
||||
class CheckoutValidation {
|
||||
static validateAddressForm(state) {
|
||||
const {
|
||||
invoiceAddress,
|
||||
deliveryAddress,
|
||||
useSameAddress,
|
||||
deliveryMethod,
|
||||
paymentMethod,
|
||||
} = state;
|
||||
const errors = {};
|
||||
|
||||
// Validate invoice address (skip if payment method is "cash")
|
||||
if (paymentMethod !== "cash") {
|
||||
if (!invoiceAddress.firstName)
|
||||
errors.invoiceFirstName = "Vorname erforderlich";
|
||||
if (!invoiceAddress.lastName)
|
||||
errors.invoiceLastName = "Nachname erforderlich";
|
||||
if (!invoiceAddress.street) errors.invoiceStreet = "Straße erforderlich";
|
||||
if (!invoiceAddress.houseNumber)
|
||||
errors.invoiceHouseNumber = "Hausnummer erforderlich";
|
||||
if (!invoiceAddress.postalCode)
|
||||
errors.invoicePostalCode = "PLZ erforderlich";
|
||||
if (!invoiceAddress.city) errors.invoiceCity = "Stadt erforderlich";
|
||||
}
|
||||
|
||||
// Validate delivery address for shipping methods that require it
|
||||
if (
|
||||
!useSameAddress &&
|
||||
(deliveryMethod === "DHL" || deliveryMethod === "DPD")
|
||||
) {
|
||||
if (!deliveryAddress.firstName)
|
||||
errors.deliveryFirstName = "Vorname erforderlich";
|
||||
if (!deliveryAddress.lastName)
|
||||
errors.deliveryLastName = "Nachname erforderlich";
|
||||
if (!deliveryAddress.street)
|
||||
errors.deliveryStreet = "Straße erforderlich";
|
||||
if (!deliveryAddress.houseNumber)
|
||||
errors.deliveryHouseNumber = "Hausnummer erforderlich";
|
||||
if (!deliveryAddress.postalCode)
|
||||
errors.deliveryPostalCode = "PLZ erforderlich";
|
||||
if (!deliveryAddress.city) errors.deliveryCity = "Stadt erforderlich";
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
static getValidationErrorMessage(state, isAddressOnly = false) {
|
||||
const { termsAccepted } = state;
|
||||
|
||||
const addressErrors = this.validateAddressForm(state);
|
||||
|
||||
if (isAddressOnly) {
|
||||
return addressErrors;
|
||||
}
|
||||
|
||||
if (Object.keys(addressErrors).length > 0) {
|
||||
return "Bitte überprüfen Sie Ihre Eingaben in den Adressfeldern.";
|
||||
}
|
||||
|
||||
// Validate terms acceptance
|
||||
if (!termsAccepted) {
|
||||
return "Bitte akzeptieren Sie die AGBs, Datenschutzerklärung und Widerrufsrecht, um fortzufahren.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static getOptimalPaymentMethod(deliveryMethod, cartItems = [], deliveryCost = 0) {
|
||||
// Calculate total amount
|
||||
const subtotal = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
|
||||
const totalAmount = subtotal + deliveryCost;
|
||||
|
||||
// If total is 0, only cash is allowed
|
||||
if (totalAmount === 0) {
|
||||
return "cash";
|
||||
}
|
||||
|
||||
// If total is less than 0.50€, stripe is not available
|
||||
if (totalAmount < 0.50) {
|
||||
return "wire";
|
||||
}
|
||||
|
||||
// Prefer stripe when available and meets minimum amount
|
||||
if (deliveryMethod === "DHL" || deliveryMethod === "DPD" || deliveryMethod === "Abholung") {
|
||||
return "stripe";
|
||||
}
|
||||
|
||||
// Fall back to wire transfer
|
||||
return "wire";
|
||||
}
|
||||
|
||||
static validatePaymentMethodForDelivery(deliveryMethod, paymentMethod, cartItems = [], deliveryCost = 0) {
|
||||
let newPaymentMethod = paymentMethod;
|
||||
|
||||
// Calculate total amount for minimum validation
|
||||
const subtotal = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
|
||||
const totalAmount = subtotal + deliveryCost;
|
||||
|
||||
// Reset payment method if it's no longer valid
|
||||
if (deliveryMethod !== "DHL" && paymentMethod === "onDelivery") {
|
||||
newPaymentMethod = "wire";
|
||||
}
|
||||
|
||||
// Allow stripe for DHL, DPD, and Abholung delivery methods, but check minimum amount
|
||||
if (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung" && paymentMethod === "stripe") {
|
||||
newPaymentMethod = "wire";
|
||||
}
|
||||
|
||||
// Check minimum amount for stripe payments
|
||||
if (paymentMethod === "stripe" && totalAmount < 0.50) {
|
||||
newPaymentMethod = "wire";
|
||||
}
|
||||
|
||||
if (deliveryMethod !== "Abholung" && paymentMethod === "cash") {
|
||||
newPaymentMethod = "wire";
|
||||
}
|
||||
|
||||
return newPaymentMethod;
|
||||
}
|
||||
|
||||
static shouldForcePickupDelivery(cartItems) {
|
||||
const isPickupOnly = cartItems.some(
|
||||
(item) => item.versandklasse === "nur Abholung"
|
||||
);
|
||||
const hasStecklinge = cartItems.some(
|
||||
(item) =>
|
||||
item.id &&
|
||||
typeof item.id === "string" &&
|
||||
item.id.endsWith("steckling")
|
||||
);
|
||||
|
||||
return isPickupOnly || hasStecklinge;
|
||||
}
|
||||
|
||||
static getCartItemFlags(cartItems) {
|
||||
const isPickupOnly = cartItems.some(
|
||||
(item) => item.versandklasse === "nur Abholung"
|
||||
);
|
||||
const hasStecklinge = cartItems.some(
|
||||
(item) =>
|
||||
item.id &&
|
||||
typeof item.id === "string" &&
|
||||
item.id.endsWith("steckling")
|
||||
);
|
||||
|
||||
return { isPickupOnly, hasStecklinge };
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckoutValidation;
|
||||
122
src/components/profile/DeliveryMethodSelector.js
Normal file
122
src/components/profile/DeliveryMethodSelector.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
|
||||
const DeliveryMethodSelector = ({ deliveryMethod, onChange, isPickupOnly }) => {
|
||||
const deliveryOptions = [
|
||||
{
|
||||
id: 'DHL',
|
||||
name: 'DHL',
|
||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
|
||||
price: '6,99 €',
|
||||
disabled: isPickupOnly
|
||||
},
|
||||
{
|
||||
id: 'DPD',
|
||||
name: 'DPD',
|
||||
description: isPickupOnly ? "nicht auswählbar weil ein oder mehrere Artikel nur abgeholt werden können" : 'Standardversand',
|
||||
price: '4,90 €',
|
||||
disabled: isPickupOnly
|
||||
},
|
||||
{
|
||||
id: 'Sperrgut',
|
||||
name: 'Sperrgut',
|
||||
description: 'Für große und schwere Artikel',
|
||||
price: '28,99 €',
|
||||
disabled: true,
|
||||
isCheckbox: true
|
||||
},
|
||||
{
|
||||
id: 'Abholung',
|
||||
name: 'Abholung in der Filiale',
|
||||
description: '',
|
||||
price: ''
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Versandart wählen
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{deliveryOptions.map((option, index) => (
|
||||
<Box
|
||||
key={option.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mb: index < deliveryOptions.length - 1 ? 1 : 0,
|
||||
p: 1,
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: 1,
|
||||
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
||||
backgroundColor: option.disabled ? '#f5f5f5' : 'transparent',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': !option.disabled ? {
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderColor: '#2e7d32',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
} : {},
|
||||
...(deliveryMethod === option.id && !option.disabled && {
|
||||
backgroundColor: '#e8f5e8',
|
||||
borderColor: '#2e7d32'
|
||||
})
|
||||
}}
|
||||
onClick={!option.disabled && !option.isCheckbox ? () => onChange({ target: { value: option.id } }) : undefined}
|
||||
>
|
||||
{option.isCheckbox ? (
|
||||
<Checkbox
|
||||
id={option.id}
|
||||
disabled={option.disabled}
|
||||
checked={false}
|
||||
sx={{ color: 'rgba(0, 0, 0, 0.54)' }}
|
||||
/>
|
||||
) : (
|
||||
<Radio
|
||||
id={option.id}
|
||||
name="deliveryMethod"
|
||||
value={option.id}
|
||||
checked={deliveryMethod === option.id}
|
||||
onChange={onChange}
|
||||
disabled={option.disabled}
|
||||
sx={{ cursor: option.disabled ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ ml: 2, flexGrow: 1 }}>
|
||||
<label
|
||||
htmlFor={option.id}
|
||||
style={{
|
||||
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
||||
color: option.disabled ? 'rgba(0, 0, 0, 0.54)' : 'inherit'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" sx={{ color: 'inherit' }}>
|
||||
{option.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ color: 'inherit' }}
|
||||
>
|
||||
{option.description}
|
||||
</Typography>
|
||||
</label>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: option.disabled ? 'rgba(0, 0, 0, 0.54)' : 'inherit' }}
|
||||
>
|
||||
{option.price}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryMethodSelector;
|
||||
171
src/components/profile/OrderDetailsDialog.js
Normal file
171
src/components/profile/OrderDetailsDialog.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
|
||||
const OrderDetailsDialog = ({ open, onClose, order }) => {
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currencyFormatter = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
|
||||
|
||||
const handleCancelOrder = () => {
|
||||
// Implement order cancellation logic here
|
||||
console.log(`Cancel order: ${order.orderId}`);
|
||||
onClose(); // Close the dialog after action
|
||||
};
|
||||
|
||||
const subtotal = order.items.reduce((acc, item) => acc + item.price * item.quantity_ordered, 0);
|
||||
const total = subtotal + order.delivery_cost;
|
||||
|
||||
// Calculate VAT breakdown similar to CartDropdown
|
||||
const vatCalculations = order.items.reduce((acc, item) => {
|
||||
const totalItemPrice = item.price * item.quantity_ordered;
|
||||
const netPrice = totalItemPrice / (1 + item.vat / 100);
|
||||
const vatAmount = totalItemPrice - netPrice;
|
||||
|
||||
acc.totalGross += totalItemPrice;
|
||||
acc.totalNet += netPrice;
|
||||
|
||||
if (item.vat === 7) {
|
||||
acc.vat7 += vatAmount;
|
||||
} else if (item.vat === 19) {
|
||||
acc.vat19 += vatAmount;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, { totalGross: 0, totalNet: 0, vat7: 0, vat19: 0 });
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Bestelldetails: {order.orderId}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6">Lieferadresse</Typography>
|
||||
<Typography>{order.shipping_address_name}</Typography>
|
||||
<Typography>{order.shipping_address_street} {order.shipping_address_house_number}</Typography>
|
||||
<Typography>{order.shipping_address_postal_code} {order.shipping_address_city}</Typography>
|
||||
<Typography>{order.shipping_address_country}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6">Rechnungsadresse</Typography>
|
||||
<Typography>{order.invoice_address_name}</Typography>
|
||||
<Typography>{order.invoice_address_street} {order.invoice_address_house_number}</Typography>
|
||||
<Typography>{order.invoice_address_postal_code} {order.invoice_address_city}</Typography>
|
||||
<Typography>{order.invoice_address_country}</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Order Details Section */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>Bestelldetails</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 4 }}>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">Lieferart:</Typography>
|
||||
<Typography variant="body1">{order.deliveryMethod || order.delivery_method || 'Nicht angegeben'}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary">Zahlungsart:</Typography>
|
||||
<Typography variant="body1">{order.paymentMethod || order.payment_method || 'Nicht angegeben'}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" gutterBottom>Bestellte Artikel</Typography>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Artikel</TableCell>
|
||||
<TableCell align="right">Menge</TableCell>
|
||||
<TableCell align="right">Preis</TableCell>
|
||||
<TableCell align="right">Gesamt</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{order.items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell align="right">{item.quantity_ordered}</TableCell>
|
||||
<TableCell align="right">{currencyFormatter.format(item.price)}</TableCell>
|
||||
<TableCell align="right">{currencyFormatter.format(item.price * item.quantity_ordered)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">Gesamtnettopreis</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">{currencyFormatter.format(vatCalculations.totalNet)}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{vatCalculations.vat7 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">7% Mehrwertsteuer</TableCell>
|
||||
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat7)}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{vatCalculations.vat19 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">19% Mehrwertsteuer</TableCell>
|
||||
<TableCell align="right">{currencyFormatter.format(vatCalculations.vat19)}</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">Zwischensumme</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">{currencyFormatter.format(subtotal)}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">Lieferkosten</TableCell>
|
||||
<TableCell align="right">{currencyFormatter.format(order.delivery_cost)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} />
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">Gesamtsumme</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">{currencyFormatter.format(total)}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{order.status === 'new' && (
|
||||
<Button onClick={handleCancelOrder} color="error">
|
||||
Bestellung stornieren
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onClose}>Schließen</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderDetailsDialog;
|
||||
315
src/components/profile/OrderProcessingService.js
Normal file
315
src/components/profile/OrderProcessingService.js
Normal file
@@ -0,0 +1,315 @@
|
||||
import { isUserLoggedIn } from "../LoginComponent.js";
|
||||
|
||||
class OrderProcessingService {
|
||||
constructor(getContext, setState) {
|
||||
this.getContext = getContext;
|
||||
this.setState = setState;
|
||||
this.verifyTokenHandler = null;
|
||||
this.verifyTokenTimeout = null;
|
||||
this.socketHandler = null;
|
||||
this.paymentCompletionData = null;
|
||||
}
|
||||
|
||||
// Clean up all event listeners and timeouts
|
||||
cleanup() {
|
||||
if (this.verifyTokenHandler) {
|
||||
window.removeEventListener('cart', this.verifyTokenHandler);
|
||||
this.verifyTokenHandler = null;
|
||||
}
|
||||
if (this.verifyTokenTimeout) {
|
||||
clearTimeout(this.verifyTokenTimeout);
|
||||
this.verifyTokenTimeout = null;
|
||||
}
|
||||
if (this.socketHandler) {
|
||||
window.removeEventListener('cart', this.socketHandler);
|
||||
this.socketHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle payment completion from parent component
|
||||
handlePaymentCompletion(paymentCompletion, onClearPaymentCompletion) {
|
||||
// Store payment completion data before clearing
|
||||
this.paymentCompletionData = { ...paymentCompletion };
|
||||
|
||||
// Clear payment completion data to prevent duplicates
|
||||
if (onClearPaymentCompletion) {
|
||||
onClearPaymentCompletion();
|
||||
}
|
||||
|
||||
// Show payment confirmation immediately but wait for verifyToken to complete
|
||||
this.setState({
|
||||
showPaymentConfirmation: true,
|
||||
cartItems: [] // Clear UI cart immediately
|
||||
});
|
||||
|
||||
// Wait for verifyToken to complete and populate window.cart, then process order
|
||||
this.waitForVerifyTokenAndProcessOrder();
|
||||
}
|
||||
|
||||
waitForVerifyTokenAndProcessOrder() {
|
||||
// Check if window.cart is already populated (verifyToken already completed)
|
||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
||||
this.processStripeOrderWithCart(window.cart);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for cart event which is dispatched after verifyToken completes
|
||||
this.verifyTokenHandler = () => {
|
||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
||||
this.processStripeOrderWithCart([...window.cart]); // Copy the cart
|
||||
|
||||
// Clear window.cart after copying
|
||||
window.cart = [];
|
||||
window.dispatchEvent(new CustomEvent("cart"));
|
||||
} else {
|
||||
this.setState({
|
||||
completionError: "Cart is empty. Please add items to your cart before placing an order."
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up listener
|
||||
if (this.verifyTokenHandler) {
|
||||
window.removeEventListener('cart', this.verifyTokenHandler);
|
||||
this.verifyTokenHandler = null;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('cart', this.verifyTokenHandler);
|
||||
|
||||
// Set up a timeout as fallback (in case verifyToken fails)
|
||||
this.verifyTokenTimeout = setTimeout(() => {
|
||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
||||
this.processStripeOrderWithCart([...window.cart]);
|
||||
window.cart = [];
|
||||
window.dispatchEvent(new CustomEvent("cart"));
|
||||
} else {
|
||||
this.setState({
|
||||
completionError: "Unable to load cart data. Please refresh the page and try again."
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up
|
||||
if (this.verifyTokenHandler) {
|
||||
window.removeEventListener('cart', this.verifyTokenHandler);
|
||||
this.verifyTokenHandler = null;
|
||||
}
|
||||
}, 5000); // 5 second timeout
|
||||
}
|
||||
|
||||
processStripeOrderWithCart(cartItems) {
|
||||
// Clear timeout if it exists
|
||||
if (this.verifyTokenTimeout) {
|
||||
clearTimeout(this.verifyTokenTimeout);
|
||||
this.verifyTokenTimeout = null;
|
||||
}
|
||||
|
||||
// Store cart items in state and process order
|
||||
this.setState({
|
||||
originalCartItems: cartItems
|
||||
}, () => {
|
||||
this.processStripeOrder();
|
||||
});
|
||||
}
|
||||
|
||||
processStripeOrder() {
|
||||
// If no original cart items, don't process
|
||||
if (!this.getState().originalCartItems || this.getState().originalCartItems.length === 0) {
|
||||
this.setState({ completionError: "Cart is empty. Please add items to your cart before placing an order." });
|
||||
return;
|
||||
}
|
||||
|
||||
// If socket is ready, process immediately
|
||||
const context = this.getContext();
|
||||
if (context && context.connected) {
|
||||
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
||||
if (isAuthenticated) {
|
||||
this.sendStripeOrder();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for socket to be ready
|
||||
this.socketHandler = () => {
|
||||
const context = this.getContext();
|
||||
if (context && context.connected) {
|
||||
const { isLoggedIn: isAuthenticated } = isUserLoggedIn();
|
||||
const state = this.getState();
|
||||
|
||||
if (isAuthenticated && state.showPaymentConfirmation && !state.isCompletingOrder) {
|
||||
this.sendStripeOrder();
|
||||
}
|
||||
}
|
||||
// Clean up
|
||||
if (this.socketHandler) {
|
||||
window.removeEventListener('cart', this.socketHandler);
|
||||
this.socketHandler = null;
|
||||
}
|
||||
};
|
||||
window.addEventListener('cart', this.socketHandler);
|
||||
}
|
||||
|
||||
sendStripeOrder() {
|
||||
const state = this.getState();
|
||||
|
||||
// Don't process if already processing or completed
|
||||
if (state.isCompletingOrder || state.orderCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isCompletingOrder: true, completionError: null });
|
||||
|
||||
const {
|
||||
deliveryMethod,
|
||||
invoiceAddress,
|
||||
deliveryAddress,
|
||||
useSameAddress,
|
||||
originalCartItems,
|
||||
note,
|
||||
saveAddressForFuture,
|
||||
} = state;
|
||||
|
||||
const deliveryCost = this.getDeliveryCost();
|
||||
|
||||
const orderData = {
|
||||
items: originalCartItems,
|
||||
invoiceAddress,
|
||||
deliveryAddress: useSameAddress ? invoiceAddress : deliveryAddress,
|
||||
deliveryMethod,
|
||||
paymentMethod: "stripe",
|
||||
deliveryCost,
|
||||
note,
|
||||
domain: window.location.origin,
|
||||
stripeData: this.paymentCompletionData ? {
|
||||
paymentIntent: this.paymentCompletionData.paymentIntent,
|
||||
paymentIntentClientSecret: this.paymentCompletionData.paymentIntentClientSecret,
|
||||
redirectStatus: this.paymentCompletionData.redirectStatus,
|
||||
} : null,
|
||||
saveAddressForFuture,
|
||||
};
|
||||
|
||||
// Emit stripe order to backend via socket.io
|
||||
const context = this.getContext();
|
||||
context.emit("issueStripeOrder", orderData, (response) => {
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
orderCompleted: true,
|
||||
completionError: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: response.error || "Failed to complete order. Please try again.",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process regular (non-Stripe) orders
|
||||
processRegularOrder(orderData) {
|
||||
const context = this.getContext();
|
||||
if (context) {
|
||||
context.emit("issueOrder", orderData, (response) => {
|
||||
if (response.success) {
|
||||
// Clear the cart
|
||||
window.cart = [];
|
||||
window.dispatchEvent(new CustomEvent("cart"));
|
||||
|
||||
// Reset state and navigate to orders tab
|
||||
this.setState({
|
||||
isCheckingOut: false,
|
||||
cartItems: [],
|
||||
isCompletingOrder: false,
|
||||
completionError: null,
|
||||
});
|
||||
|
||||
// Call success callback if provided
|
||||
if (this.onOrderSuccess) {
|
||||
this.onOrderSuccess();
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: response.error || "Failed to complete order. Please try again.",
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error("Socket context not available");
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: "Cannot connect to server. Please try again later.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create Stripe payment intent
|
||||
createStripeIntent(totalAmount, loadStripeComponent) {
|
||||
const context = this.getContext();
|
||||
if (context) {
|
||||
context.emit(
|
||||
"createStripeIntent",
|
||||
{ amount: totalAmount },
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
loadStripeComponent(response.client_secret);
|
||||
} else {
|
||||
console.error("Error:", response.error);
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: response.error || "Failed to create Stripe payment intent. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.error("Socket context not available");
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: "Cannot connect to server. Please try again later.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate delivery cost
|
||||
getDeliveryCost() {
|
||||
const { deliveryMethod, paymentMethod } = this.getState();
|
||||
let cost = 0;
|
||||
|
||||
switch (deliveryMethod) {
|
||||
case "DHL":
|
||||
cost = 6.99;
|
||||
break;
|
||||
case "DPD":
|
||||
cost = 4.9;
|
||||
break;
|
||||
case "Sperrgut":
|
||||
cost = 28.99;
|
||||
break;
|
||||
case "Abholung":
|
||||
cost = 0;
|
||||
break;
|
||||
default:
|
||||
cost = 6.99;
|
||||
}
|
||||
|
||||
// Add onDelivery surcharge if selected
|
||||
if (paymentMethod === "onDelivery") {
|
||||
cost += 8.99;
|
||||
}
|
||||
|
||||
return cost;
|
||||
}
|
||||
|
||||
// Helper method to get current state (to be overridden by component)
|
||||
getState() {
|
||||
throw new Error("getState method must be implemented by the component");
|
||||
}
|
||||
|
||||
// Set callback for order success
|
||||
setOrderSuccessCallback(callback) {
|
||||
this.onOrderSuccess = callback;
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderProcessingService;
|
||||
106
src/components/profile/OrderSummary.js
Normal file
106
src/components/profile/OrderSummary.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
|
||||
const OrderSummary = ({ deliveryCost, cartItems = [] }) => {
|
||||
const currencyFormatter = new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
});
|
||||
|
||||
// Calculate VAT breakdown for cart items (similar to CartDropdown)
|
||||
const cartVatCalculations = cartItems.reduce((acc, item) => {
|
||||
const totalItemPrice = item.price * item.quantity;
|
||||
const netPrice = totalItemPrice / (1 + item.vat / 100);
|
||||
const vatAmount = totalItemPrice - netPrice;
|
||||
|
||||
acc.totalGross += totalItemPrice;
|
||||
acc.totalNet += netPrice;
|
||||
|
||||
if (item.vat === 7) {
|
||||
acc.vat7 += vatAmount;
|
||||
} else if (item.vat === 19) {
|
||||
acc.vat19 += vatAmount;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, { totalGross: 0, totalNet: 0, vat7: 0, vat19: 0 });
|
||||
|
||||
// Calculate shipping VAT (19% VAT for shipping costs)
|
||||
const shippingNetPrice = deliveryCost / (1 + 19 / 100);
|
||||
const shippingVat = deliveryCost - shippingNetPrice;
|
||||
|
||||
// Combine totals - add shipping VAT to the 19% VAT total
|
||||
const totalVat7 = cartVatCalculations.vat7;
|
||||
const totalVat19 = cartVatCalculations.vat19 + shippingVat;
|
||||
const totalGross = cartVatCalculations.totalGross + deliveryCost;
|
||||
|
||||
return (
|
||||
<Box sx={{ my: 3, p: 2, bgcolor: '#f5f5f5', borderRadius: 1 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Bestellübersicht
|
||||
</Typography>
|
||||
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Waren (netto):</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(cartVatCalculations.totalNet)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{deliveryCost > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>Versandkosten (netto):</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(shippingNetPrice)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{totalVat7 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>7% Mehrwertsteuer:</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(totalVat7)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{totalVat19 > 0 && (
|
||||
<TableRow>
|
||||
<TableCell>19% Mehrwertsteuer (inkl. Versand):</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(totalVat19)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Gesamtsumme Waren:</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||
{currencyFormatter.format(cartVatCalculations.totalGross)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{deliveryCost > 0 && (
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>Versandkosten:</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
|
||||
{currencyFormatter.format(deliveryCost)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow sx={{ borderTop: '1px solid #e0e0e0' }}>
|
||||
<TableCell sx={{ fontWeight: 'bold', pt: 2 }}>Gesamtsumme:</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 'bold', pt: 2 }}>
|
||||
{currencyFormatter.format(totalGross)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderSummary;
|
||||
246
src/components/profile/OrdersTab.js
Normal file
246
src/components/profile/OrdersTab.js
Normal file
@@ -0,0 +1,246 @@
|
||||
import React, { useState, useEffect, useContext, useCallback } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Alert,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import SocketContext from "../../contexts/SocketContext.js";
|
||||
import OrderDetailsDialog from "./OrderDetailsDialog.js";
|
||||
|
||||
// Constants
|
||||
const statusTranslations = {
|
||||
new: "in Bearbeitung",
|
||||
pending: "Neu",
|
||||
processing: "in Bearbeitung",
|
||||
cancelled: "Storniert",
|
||||
shipped: "Verschickt",
|
||||
delivered: "Geliefert",
|
||||
};
|
||||
|
||||
const statusEmojis = {
|
||||
"in Bearbeitung": "⚙️",
|
||||
pending: "⏳",
|
||||
processing: "🔄",
|
||||
cancelled: "❌",
|
||||
Verschickt: "🚚",
|
||||
Geliefert: "✅",
|
||||
Storniert: "❌",
|
||||
Retoure: "↩️",
|
||||
"Teil Retoure": "↪️",
|
||||
"Teil geliefert": "⚡",
|
||||
};
|
||||
|
||||
const statusColors = {
|
||||
"in Bearbeitung": "#ed6c02", // orange
|
||||
pending: "#ff9800", // orange for pending
|
||||
processing: "#2196f3", // blue for processing
|
||||
cancelled: "#d32f2f", // red for cancelled
|
||||
Verschickt: "#2e7d32", // green
|
||||
Geliefert: "#2e7d32", // green
|
||||
Storniert: "#d32f2f", // red
|
||||
Retoure: "#9c27b0", // purple
|
||||
"Teil Retoure": "#9c27b0", // purple
|
||||
"Teil geliefert": "#009688", // teal
|
||||
};
|
||||
|
||||
const currencyFormatter = new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
});
|
||||
|
||||
// Orders Tab Content Component
|
||||
const OrdersTab = ({ orderIdFromHash }) => {
|
||||
const [orders, setOrders] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
|
||||
|
||||
const socket = useContext(SocketContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleViewDetails = useCallback(
|
||||
(orderId) => {
|
||||
const orderToView = orders.find((order) => order.orderId === orderId);
|
||||
if (orderToView) {
|
||||
setSelectedOrder(orderToView);
|
||||
setIsDetailsDialogOpen(true);
|
||||
}
|
||||
},
|
||||
[orders]
|
||||
);
|
||||
|
||||
const fetchOrders = useCallback(() => {
|
||||
if (socket && socket.connected) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
socket.emit("getOrders", (response) => {
|
||||
if (response.success) {
|
||||
setOrders(response.orders);
|
||||
} else {
|
||||
setError(response.error || "Failed to fetch orders.");
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
// Socket not connected yet, but don't show error immediately on first load
|
||||
console.log("Socket not connected yet, waiting for connection to fetch orders");
|
||||
setLoading(false); // Stop loading when socket is not connected
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, [fetchOrders]);
|
||||
|
||||
// Monitor socket connection changes
|
||||
useEffect(() => {
|
||||
if (socket && socket.connected && orders.length === 0) {
|
||||
// Socket just connected and we don't have orders yet, fetch them
|
||||
fetchOrders();
|
||||
}
|
||||
}, [socket, socket?.connected, orders.length, fetchOrders]);
|
||||
|
||||
useEffect(() => {
|
||||
if (orderIdFromHash && orders.length > 0) {
|
||||
handleViewDetails(orderIdFromHash);
|
||||
}
|
||||
}, [orderIdFromHash, orders, handleViewDetails]);
|
||||
|
||||
const getStatusDisplay = (status) => {
|
||||
return statusTranslations[status] || status;
|
||||
};
|
||||
|
||||
const getStatusEmoji = (status) => {
|
||||
return statusEmojis[status] || "❓";
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
return statusColors[status] || "#757575"; // default gray
|
||||
};
|
||||
|
||||
const handleCloseDetailsDialog = () => {
|
||||
setIsDetailsDialogOpen(false);
|
||||
setSelectedOrder(null);
|
||||
navigate("/profile", { replace: true });
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ p: 3, display: "flex", justifyContent: "center" }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Alert severity="error">{error}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
{orders.length > 0 ? (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Bestellnummer</TableCell>
|
||||
<TableCell>Datum</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Artikel</TableCell>
|
||||
<TableCell align="right">Summe</TableCell>
|
||||
<TableCell align="center">Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{orders.map((order) => {
|
||||
const displayStatus = getStatusDisplay(order.status);
|
||||
const subtotal = order.items.reduce(
|
||||
(acc, item) => acc + item.price * item.quantity_ordered,
|
||||
0
|
||||
);
|
||||
const total = subtotal + order.delivery_cost;
|
||||
return (
|
||||
<TableRow key={order.orderId} hover>
|
||||
<TableCell>{order.orderId}</TableCell>
|
||||
<TableCell>
|
||||
{new Date(order.created_at).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
color: getStatusColor(displayStatus),
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: "1.2rem" }}>
|
||||
{getStatusEmoji(displayStatus)}
|
||||
</span>
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
sx={{ fontWeight: "medium" }}
|
||||
>
|
||||
{displayStatus}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{order.items.reduce(
|
||||
(acc, item) => acc + item.quantity_ordered,
|
||||
0
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{currencyFormatter.format(total)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Tooltip title="Details anzeigen">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={() => handleViewDetails(order.orderId)}
|
||||
>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
Sie haben noch keine Bestellungen aufgegeben.
|
||||
</Alert>
|
||||
)}
|
||||
<OrderDetailsDialog
|
||||
open={isDetailsDialogOpen}
|
||||
onClose={handleCloseDetailsDialog}
|
||||
order={selectedOrder}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersTab;
|
||||
97
src/components/profile/PaymentConfirmationDialog.js
Normal file
97
src/components/profile/PaymentConfirmationDialog.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { Component } from "react";
|
||||
import { Box, Typography, Button } from "@mui/material";
|
||||
|
||||
class PaymentConfirmationDialog extends Component {
|
||||
render() {
|
||||
const {
|
||||
paymentCompletionData,
|
||||
isCompletingOrder,
|
||||
completionError,
|
||||
orderCompleted,
|
||||
onContinueShopping,
|
||||
onViewOrders,
|
||||
} = this.props;
|
||||
|
||||
if (!paymentCompletionData) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
mb: 3,
|
||||
p: 3,
|
||||
border: '2px solid',
|
||||
borderColor: paymentCompletionData.isSuccessful ? '#2e7d32' : '#d32f2f',
|
||||
borderRadius: 2,
|
||||
bgcolor: paymentCompletionData.isSuccessful ? '#e8f5e8' : '#ffebee'
|
||||
}}>
|
||||
<Typography variant="h5" sx={{
|
||||
mb: 2,
|
||||
color: paymentCompletionData.isSuccessful ? '#2e7d32' : '#d32f2f',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{paymentCompletionData.isSuccessful ? 'Zahlung erfolgreich!' : 'Zahlung fehlgeschlagen'}
|
||||
</Typography>
|
||||
|
||||
{paymentCompletionData.isSuccessful ? (
|
||||
<>
|
||||
{orderCompleted ? (
|
||||
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
|
||||
🎉 Ihre Bestellung wurde erfolgreich abgeschlossen! Sie können jetzt Ihre Bestellungen einsehen.
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body1" sx={{ mt: 2, color: '#2e7d32' }}>
|
||||
Ihre Zahlung wurde erfolgreich verarbeitet. Die Bestellung wird automatisch abgeschlossen.
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="body1" sx={{ mt: 2, color: '#d32f2f' }}>
|
||||
Ihre Zahlung konnte nicht verarbeitet werden. Bitte versuchen Sie es erneut oder wählen Sie eine andere Zahlungsmethode.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{isCompletingOrder && (
|
||||
<Typography variant="body2" sx={{ mt: 2, color: '#2e7d32', p: 2, bgcolor: '#e8f5e8', borderRadius: 1 }}>
|
||||
Bestellung wird abgeschlossen...
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{completionError && (
|
||||
<Typography variant="body2" sx={{ mt: 2, color: '#d32f2f', p: 2, bgcolor: '#ffcdd2', borderRadius: 1 }}>
|
||||
{completionError}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{orderCompleted && (
|
||||
<Box sx={{ mt: 3, display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
onClick={onContinueShopping}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
color: '#2e7d32',
|
||||
borderColor: '#2e7d32',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(46, 125, 50, 0.04)',
|
||||
borderColor: '#1b5e20'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Weiter einkaufen
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onViewOrders}
|
||||
variant="contained"
|
||||
sx={{
|
||||
bgcolor: '#2e7d32',
|
||||
'&:hover': { bgcolor: '#1b5e20' }
|
||||
}}
|
||||
>
|
||||
Zu meinen Bestellungen
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PaymentConfirmationDialog;
|
||||
178
src/components/profile/PaymentMethodSelector.js
Normal file
178
src/components/profile/PaymentMethodSelector.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import { Box, Typography, Radio } from "@mui/material";
|
||||
|
||||
const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeliveryMethodChange, cartItems = [], deliveryCost = 0 }) => {
|
||||
|
||||
// Calculate total amount
|
||||
const subtotal = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
|
||||
const totalAmount = subtotal + deliveryCost;
|
||||
|
||||
// Handle payment method changes with automatic delivery method adjustment
|
||||
const handlePaymentMethodChange = useCallback((event) => {
|
||||
const selectedPaymentMethod = event.target.value;
|
||||
|
||||
// If "Zahlung in der Filiale" is selected, force delivery method to "Abholung"
|
||||
if (selectedPaymentMethod === "cash" && deliveryMethod !== "Abholung") {
|
||||
if (onDeliveryMethodChange) {
|
||||
onDeliveryMethodChange({ target: { value: "Abholung" } });
|
||||
}
|
||||
}
|
||||
|
||||
onChange(event);
|
||||
}, [deliveryMethod, onDeliveryMethodChange, onChange]);
|
||||
|
||||
// Handle delivery method changes - auto-switch to stripe when DHL/DPD is selected
|
||||
useEffect(() => {
|
||||
if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") {
|
||||
handlePaymentMethodChange({ target: { value: "stripe" } });
|
||||
}
|
||||
}, [deliveryMethod, paymentMethod, handlePaymentMethodChange]);
|
||||
|
||||
// Auto-switch to cash when total amount is 0
|
||||
useEffect(() => {
|
||||
if (totalAmount === 0 && paymentMethod !== "cash") {
|
||||
handlePaymentMethodChange({ target: { value: "cash" } });
|
||||
}
|
||||
}, [totalAmount, paymentMethod, handlePaymentMethodChange]);
|
||||
|
||||
const paymentOptions = [
|
||||
{
|
||||
id: "wire",
|
||||
name: "Überweisung",
|
||||
description: "Bezahlen Sie per Banküberweisung",
|
||||
disabled: totalAmount === 0,
|
||||
},
|
||||
{
|
||||
id: "stripe",
|
||||
name: "Karte oder Sofortüberweisung",
|
||||
description: totalAmount < 0.50 && totalAmount > 0
|
||||
? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)"
|
||||
: "Bezahlen Sie per Karte oder Sofortüberweisung",
|
||||
disabled: totalAmount < 0.50 || (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung"),
|
||||
icons: [
|
||||
"/assets/images/giropay.png",
|
||||
"/assets/images/maestro.png",
|
||||
"/assets/images/mastercard.png",
|
||||
"/assets/images/visa_electron.png",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "onDelivery",
|
||||
name: "Nachnahme",
|
||||
description: "Bezahlen Sie bei Lieferung (8,99 € Aufschlag)",
|
||||
disabled: totalAmount === 0 || deliveryMethod !== "DHL",
|
||||
icons: ["/assets/images/cash.png"],
|
||||
},
|
||||
{
|
||||
id: "cash",
|
||||
name: "Zahlung in der Filiale",
|
||||
description: "Bei Abholung bezahlen",
|
||||
disabled: false, // Always enabled
|
||||
icons: ["/assets/images/cash.png"],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Zahlungsart wählen
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{paymentOptions.map((option, index) => (
|
||||
<Box
|
||||
key={option.id}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mb: index < paymentOptions.length - 1 ? 1 : 0,
|
||||
p: 1,
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: 1,
|
||||
cursor: option.disabled ? "not-allowed" : "pointer",
|
||||
opacity: option.disabled ? 0.6 : 1,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
"&:hover": !option.disabled
|
||||
? {
|
||||
backgroundColor: "#f5f5f5",
|
||||
borderColor: "#2e7d32",
|
||||
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||
}
|
||||
: {},
|
||||
...(paymentMethod === option.id &&
|
||||
!option.disabled && {
|
||||
backgroundColor: "#e8f5e8",
|
||||
borderColor: "#2e7d32",
|
||||
}),
|
||||
}}
|
||||
onClick={
|
||||
!option.disabled
|
||||
? () => handlePaymentMethodChange({ target: { value: option.id } })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Radio
|
||||
id={option.id}
|
||||
name="paymentMethod"
|
||||
value={option.id}
|
||||
checked={paymentMethod === option.id}
|
||||
onChange={handlePaymentMethodChange}
|
||||
disabled={option.disabled}
|
||||
sx={{ cursor: option.disabled ? "not-allowed" : "pointer" }}
|
||||
/>
|
||||
<Box sx={{ ml: 2, flex: 1 }}>
|
||||
<label
|
||||
htmlFor={option.id}
|
||||
style={{
|
||||
cursor: option.disabled ? "not-allowed" : "pointer",
|
||||
color: option.disabled ? "#999" : "inherit",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ color: option.disabled ? "#999" : "inherit" }}
|
||||
>
|
||||
{option.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ color: option.disabled ? "#ccc" : "text.secondary" }}
|
||||
>
|
||||
{option.description}
|
||||
</Typography>
|
||||
</label>
|
||||
</Box>
|
||||
{option.icons && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: 1,
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
ml: 2
|
||||
}}
|
||||
>
|
||||
{option.icons.map((iconPath, iconIndex) => (
|
||||
<img
|
||||
key={iconIndex}
|
||||
src={iconPath}
|
||||
alt={`Payment method ${iconIndex + 1}`}
|
||||
style={{
|
||||
height: "24px",
|
||||
width: "auto",
|
||||
opacity: option.disabled ? 0.5 : 1,
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMethodSelector;
|
||||
426
src/components/profile/SettingsTab.js
Normal file
426
src/components/profile/SettingsTab.js
Normal file
@@ -0,0 +1,426 @@
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
IconButton,
|
||||
Snackbar
|
||||
} from '@mui/material';
|
||||
import { ContentCopy } from '@mui/icons-material';
|
||||
|
||||
class SettingsTab extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
password: '',
|
||||
newEmail: '',
|
||||
passwordError: '',
|
||||
passwordSuccess: '',
|
||||
emailError: '',
|
||||
emailSuccess: '',
|
||||
loading: false,
|
||||
// API Key management state
|
||||
hasApiKey: false,
|
||||
apiKey: '',
|
||||
apiKeyDisplay: '',
|
||||
apiKeyError: '',
|
||||
apiKeySuccess: '',
|
||||
loadingApiKey: false,
|
||||
copySnackbarOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Load user data
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
try {
|
||||
const user = JSON.parse(storedUser);
|
||||
this.setState({ newEmail: user.email || '' });
|
||||
|
||||
// Check if user has an API key
|
||||
this.props.socket.emit('isApiKey', (response) => {
|
||||
if (response.success && response.hasApiKey) {
|
||||
this.setState({
|
||||
hasApiKey: true,
|
||||
apiKeyDisplay: '************'
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading user data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdatePassword = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset states
|
||||
this.setState({
|
||||
passwordError: '',
|
||||
passwordSuccess: ''
|
||||
});
|
||||
|
||||
// Validation
|
||||
if (!this.state.currentPassword || !this.state.newPassword || !this.state.confirmPassword) {
|
||||
this.setState({ passwordError: 'Bitte füllen Sie alle Felder aus' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.newPassword !== this.state.confirmPassword) {
|
||||
this.setState({ passwordError: 'Die neuen Passwörter stimmen nicht überein' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.newPassword.length < 8) {
|
||||
this.setState({ passwordError: 'Das neue Passwort muss mindestens 8 Zeichen lang sein' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
// Call socket.io endpoint to update password
|
||||
this.props.socket.emit('updatePassword',
|
||||
{ oldPassword: this.state.currentPassword, newPassword: this.state.newPassword },
|
||||
(response) => {
|
||||
this.setState({ loading: false });
|
||||
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
passwordSuccess: 'Passwort erfolgreich aktualisiert',
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
passwordError: response.message || 'Fehler beim Aktualisieren des Passworts'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleUpdateEmail = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Reset states
|
||||
this.setState({
|
||||
emailError: '',
|
||||
emailSuccess: ''
|
||||
});
|
||||
|
||||
// Validation
|
||||
if (!this.state.password || !this.state.newEmail) {
|
||||
this.setState({ emailError: 'Bitte füllen Sie alle Felder aus' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.state.newEmail)) {
|
||||
this.setState({ emailError: 'Bitte geben Sie eine gültige E-Mail-Adresse ein' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
// Call socket.io endpoint to update email
|
||||
this.props.socket.emit('updateEmail',
|
||||
{ password: this.state.password, email: this.state.newEmail },
|
||||
(response) => {
|
||||
this.setState({ loading: false });
|
||||
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
emailSuccess: 'E-Mail-Adresse erfolgreich aktualisiert',
|
||||
password: ''
|
||||
});
|
||||
|
||||
// Update user in sessionStorage
|
||||
try {
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const user = JSON.parse(storedUser);
|
||||
user.email = this.state.newEmail;
|
||||
sessionStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user in sessionStorage:', error);
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
emailError: response.message || 'Fehler beim Aktualisieren der E-Mail-Adresse'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleGenerateApiKey = () => {
|
||||
this.setState({
|
||||
apiKeyError: '',
|
||||
apiKeySuccess: '',
|
||||
loadingApiKey: true
|
||||
});
|
||||
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
if (!storedUser) {
|
||||
this.setState({
|
||||
apiKeyError: 'Benutzer nicht gefunden',
|
||||
loadingApiKey: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = JSON.parse(storedUser);
|
||||
|
||||
this.props.socket.emit('createApiKey', user.id, (response) => {
|
||||
this.setState({ loadingApiKey: false });
|
||||
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
hasApiKey: true,
|
||||
apiKey: response.apiKey,
|
||||
apiKeyDisplay: response.apiKey,
|
||||
apiKeySuccess: response.message || 'API-Schlüssel erfolgreich generiert'
|
||||
});
|
||||
|
||||
// After 10 seconds, hide the actual key and show asterisks
|
||||
setTimeout(() => {
|
||||
this.setState({ apiKeyDisplay: '************' });
|
||||
}, 10000);
|
||||
} else {
|
||||
this.setState({
|
||||
apiKeyError: response.message || 'Fehler beim Generieren des API-Schlüssels'
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating API key:', error);
|
||||
this.setState({
|
||||
apiKeyError: 'Fehler beim Generieren des API-Schlüssels',
|
||||
loadingApiKey: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleCopyToClipboard = () => {
|
||||
navigator.clipboard.writeText(this.state.apiKey).then(() => {
|
||||
this.setState({ copySnackbarOpen: true });
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy to clipboard:', err);
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = this.state.apiKey;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
this.setState({ copySnackbarOpen: true });
|
||||
});
|
||||
};
|
||||
|
||||
handleCloseSnackbar = () => {
|
||||
this.setState({ copySnackbarOpen: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Paper sx={{ p: 3}}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
Passwort ändern
|
||||
</Typography>
|
||||
|
||||
{this.state.passwordError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.passwordError}</Alert>}
|
||||
{this.state.passwordSuccess && <Alert severity="success" sx={{ mb: 2 }}>{this.state.passwordSuccess}</Alert>}
|
||||
|
||||
<Box component="form" onSubmit={this.handleUpdatePassword}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Aktuelles Passwort"
|
||||
type="password"
|
||||
fullWidth
|
||||
value={this.state.currentPassword}
|
||||
onChange={(e) => this.setState({ currentPassword: e.target.value })}
|
||||
disabled={this.state.loading}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Neues Passwort"
|
||||
type="password"
|
||||
fullWidth
|
||||
value={this.state.newPassword}
|
||||
onChange={(e) => this.setState({ newPassword: e.target.value })}
|
||||
disabled={this.state.loading}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Neues Passwort bestätigen"
|
||||
type="password"
|
||||
fullWidth
|
||||
value={this.state.confirmPassword}
|
||||
onChange={(e) => this.setState({ confirmPassword: e.target.value })}
|
||||
disabled={this.state.loading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
||||
disabled={this.state.loading}
|
||||
>
|
||||
{this.state.loading ? <CircularProgress size={24} /> : 'Passwort aktualisieren'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Divider sx={{ my: 4 }} />
|
||||
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
E-Mail-Adresse ändern
|
||||
</Typography>
|
||||
|
||||
{this.state.emailError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.emailError}</Alert>}
|
||||
{this.state.emailSuccess && <Alert severity="success" sx={{ mb: 2 }}>{this.state.emailSuccess}</Alert>}
|
||||
|
||||
<Box component="form" onSubmit={this.handleUpdateEmail}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Passwort"
|
||||
type="password"
|
||||
fullWidth
|
||||
value={this.state.password}
|
||||
onChange={(e) => this.setState({ password: e.target.value })}
|
||||
disabled={this.state.loading}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Neue E-Mail-Adresse"
|
||||
type="email"
|
||||
fullWidth
|
||||
value={this.state.newEmail}
|
||||
onChange={(e) => this.setState({ newEmail: e.target.value })}
|
||||
disabled={this.state.loading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 2, bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
||||
disabled={this.state.loading}
|
||||
>
|
||||
{this.state.loading ? <CircularProgress size={24} /> : 'E-Mail aktualisieren'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Divider sx={{ my: 4 }} />
|
||||
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="subtitle1" fontWeight="bold" gutterBottom>
|
||||
API-Schlüssel
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Verwenden Sie Ihren API-Schlüssel für die Integration mit externen Anwendungen.
|
||||
</Typography>
|
||||
|
||||
{this.state.apiKeyError && <Alert severity="error" sx={{ mb: 2 }}>{this.state.apiKeyError}</Alert>}
|
||||
{this.state.apiKeySuccess && (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
{this.state.apiKeySuccess}
|
||||
{this.state.apiKey && this.state.apiKeyDisplay !== '************' && (
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Speichern Sie diesen Schlüssel sicher. Er wird aus Sicherheitsgründen in 10 Sekunden ausgeblendet.
|
||||
</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
API-Dokumentation: {' '}
|
||||
<a
|
||||
href={`${window.location.protocol}//${window.location.host}/api/`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#2e7d32' }}
|
||||
>
|
||||
{`${window.location.protocol}//${window.location.host}/api/`}
|
||||
</a>
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mt: 2 }}>
|
||||
<TextField
|
||||
label="API-Schlüssel"
|
||||
value={this.state.apiKeyDisplay}
|
||||
disabled
|
||||
fullWidth
|
||||
sx={{
|
||||
'& .MuiInputBase-input.Mui-disabled': {
|
||||
WebkitTextFillColor: this.state.apiKeyDisplay === '************' ? '#666' : '#000',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{this.state.apiKeyDisplay !== '************' && this.state.apiKey && (
|
||||
<IconButton
|
||||
onClick={this.handleCopyToClipboard}
|
||||
sx={{
|
||||
color: '#2e7d32',
|
||||
'&:hover': { bgcolor: 'rgba(46, 125, 50, 0.1)' }
|
||||
}}
|
||||
title="In Zwischenablage kopieren"
|
||||
>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={this.handleGenerateApiKey}
|
||||
disabled={this.state.loadingApiKey}
|
||||
sx={{
|
||||
minWidth: 120,
|
||||
bgcolor: '#2e7d32',
|
||||
'&:hover': { bgcolor: '#1b5e20' }
|
||||
}}
|
||||
>
|
||||
{this.state.loadingApiKey ? (
|
||||
<CircularProgress size={24} />
|
||||
) : (
|
||||
this.state.hasApiKey ? 'Regenerieren' : 'Generieren'
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Snackbar
|
||||
open={this.state.copySnackbarOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={this.handleCloseSnackbar}
|
||||
message="API-Schlüssel in Zwischenablage kopiert"
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsTab;
|
||||
Reference in New Issue
Block a user