- Create OTC UI form at public/otc/index.php - Add API endpoint public/api/otc-order.php - Extract shared DB connection to includes/db.php - Add migration for OTC products (0006_phase1_otc_products.sql) - Support order_source='direct' with automatic DIR- reference generation - Include billing address capture with default values - Add payment methods cash and paypal for direct sales - Implement stock allocation and inventory management
495 lines
17 KiB
PHP
495 lines
17 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>OTC-Verkauf</title>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #f5f5f5;
|
|
color: #333;
|
|
line-height: 1.6;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
padding: 30px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 24px;
|
|
margin-bottom: 20px;
|
|
color: #2c3e50;
|
|
border-bottom: 2px solid #3498db;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 18px;
|
|
margin: 25px 0 15px 0;
|
|
color: #34495e;
|
|
padding-bottom: 5px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.section {
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 500;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
input[type="number"],
|
|
input[type="text"],
|
|
select {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 16px;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
input[type="number"]:focus,
|
|
input[type="text"]:focus,
|
|
select:focus {
|
|
outline: none;
|
|
border-color: #3498db;
|
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
|
}
|
|
|
|
.product-row {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.product-row label {
|
|
font-size: 14px;
|
|
color: #555;
|
|
}
|
|
|
|
.price-note {
|
|
font-size: 13px;
|
|
color: #666;
|
|
margin-top: 5px;
|
|
font-style: italic;
|
|
}
|
|
|
|
.price-breakdown {
|
|
background: #f8f9fa;
|
|
border: 1px solid #eee;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
margin-top: 10px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.price-line {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.price-line.total {
|
|
font-weight: bold;
|
|
border-top: 1px solid #ddd;
|
|
padding-top: 5px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.billing-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 15px;
|
|
}
|
|
|
|
.billing-grid .form-group:first-child,
|
|
.billing-grid .form-group:nth-child(2) {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.billing-grid .form-group:nth-child(3) {
|
|
grid-column: span 2;
|
|
}
|
|
|
|
.billing-grid .form-group:nth-child(4),
|
|
.billing-grid .form-group:nth-child(5),
|
|
.billing-grid .form-group:nth-child(6) {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.billing-grid .form-group:nth-child(4) {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.billing-grid .form-group:nth-child(5) {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.billing-grid .form-group:nth-child(6) {
|
|
grid-column: span 2;
|
|
}
|
|
|
|
button {
|
|
background: #3498db;
|
|
color: white;
|
|
border: none;
|
|
padding: 14px 20px;
|
|
font-size: 16px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
width: 100%;
|
|
font-weight: 500;
|
|
transition: background 0.3s;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
button:hover {
|
|
background: #2980b9;
|
|
}
|
|
|
|
button:disabled {
|
|
background: #95a5a6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.error {
|
|
color: #e74c3c;
|
|
font-size: 14px;
|
|
margin-top: 5px;
|
|
display: none;
|
|
}
|
|
|
|
.success {
|
|
background: #2ecc71;
|
|
color: white;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin-top: 20px;
|
|
display: none;
|
|
}
|
|
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.loading {
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
border: 2px solid #fff;
|
|
border-radius: 50%;
|
|
border-top-color: transparent;
|
|
animation: spin 0.8s linear infinite;
|
|
margin-right: 10px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.container {
|
|
padding: 20px;
|
|
}
|
|
|
|
.billing-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.billing-grid .form-group:nth-child(3),
|
|
.billing-grid .form-group:nth-child(6) {
|
|
grid-column: span 1;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>OTC-Verkauf</h1>
|
|
|
|
<div class="section">
|
|
<h2>PRODUKTE</h2>
|
|
|
|
<div class="product-row">
|
|
<label for="product1">PURE Shiitake Extrakt Tinktur 50ml</label>
|
|
<input type="number" id="product1" min="0" step="1" value="0" data-title="PURE Shiitake Extrakt Tinktur 50ml">
|
|
</div>
|
|
|
|
<div class="product-row">
|
|
<label for="product2">PURE Reishi Extrakt Tinktur 50ml</label>
|
|
<input type="number" id="product2" min="0" step="1" value="0" data-title="PURE Reishi Extrakt Tinktur 50ml">
|
|
</div>
|
|
|
|
<div class="product-row">
|
|
<label for="product3">PURE Lion's Mane Extrakt Tinktur 50ml</label>
|
|
<input type="number" id="product3" min="0" step="1" value="0" data-title="PURE Lion's Mane Extrakt Tinktur 50ml">
|
|
</div>
|
|
|
|
<div class="product-row">
|
|
<label for="product4">PURE Chaga Aroma Extrakt Tinktur 50ml</label>
|
|
<input type="number" id="product4" min="0" step="1" value="0" data-title="PURE Chaga Aroma Extrakt Tinktur 50ml">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="form-group">
|
|
<label for="totalPrice">Preis alle Flaschen brutto (CHF)</label>
|
|
<input type="number" id="totalPrice" min="0" step="0.01" value="0.00">
|
|
<div class="price-note">
|
|
Der Preis wird durch die Anzahl aller Flaschen geteilt und das Ergebnis ist der Preis jeder einzelnen Flasche.
|
|
</div>
|
|
</div>
|
|
|
|
<div id="priceBreakdown" class="price-breakdown hidden">
|
|
<div class="price-line">
|
|
<span>Total Flaschen:</span>
|
|
<span id="totalBottles">0</span>
|
|
</div>
|
|
<div class="price-line">
|
|
<span>Preis pro Flasche:</span>
|
|
<span id="pricePerBottle">CHF 0.00</span>
|
|
</div>
|
|
<div class="price-line total">
|
|
<span>Gesamtpreis:</span>
|
|
<span id="displayTotalPrice">CHF 0.00</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="form-group">
|
|
<label for="paymentMethod">Bezahlung</label>
|
|
<select id="paymentMethod">
|
|
<option value="twint">Twint</option>
|
|
<option value="cash">Barzahlung</option>
|
|
<option value="paypal">PayPal</option>
|
|
<option value="bank_transfer">Überweisung</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>RECHNUNGSADRESSE</h2>
|
|
<div class="billing-grid">
|
|
<div class="form-group">
|
|
<label for="firstName">Vorname</label>
|
|
<input type="text" id="firstName" value="Fabienne">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="lastName">Nachname</label>
|
|
<input type="text" id="lastName" value="Föhn">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="street">Strasse</label>
|
|
<input type="text" id="street" value="Im Hochrain">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="houseNumber">Hausnummer</label>
|
|
<input type="text" id="houseNumber" value="2">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="zip">PLZ</label>
|
|
<input type="text" id="zip" value="8102">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="city">Ort</label>
|
|
<input type="text" id="city" value="Oberengstringen">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="error" class="error"></div>
|
|
|
|
<button id="submitBtn" onclick="submitOrder()">
|
|
Verkaufen
|
|
</button>
|
|
|
|
<div id="success" class="success">
|
|
Bestellung erfolgreich erfasst! Die Bestellnummer wird automatisch generiert.
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const productInputs = [
|
|
document.getElementById('product1'),
|
|
document.getElementById('product2'),
|
|
document.getElementById('product3'),
|
|
document.getElementById('product4')
|
|
];
|
|
|
|
const totalPriceInput = document.getElementById('totalPrice');
|
|
const priceBreakdown = document.getElementById('priceBreakdown');
|
|
const totalBottlesEl = document.getElementById('totalBottles');
|
|
const pricePerBottleEl = document.getElementById('pricePerBottle');
|
|
const displayTotalPriceEl = document.getElementById('displayTotalPrice');
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const errorEl = document.getElementById('error');
|
|
const successEl = document.getElementById('success');
|
|
|
|
function updatePriceBreakdown() {
|
|
const totalQty = productInputs.reduce((sum, input) => sum + parseInt(input.value || 0), 0);
|
|
const totalPrice = parseFloat(totalPriceInput.value || 0);
|
|
|
|
if (totalQty > 0 && totalPrice > 0) {
|
|
priceBreakdown.classList.remove('hidden');
|
|
totalBottlesEl.textContent = totalQty;
|
|
const pricePerBottle = totalPrice / totalQty;
|
|
pricePerBottleEl.textContent = 'CHF ' + pricePerBottle.toFixed(2);
|
|
displayTotalPriceEl.textContent = 'CHF ' + totalPrice.toFixed(2);
|
|
} else {
|
|
priceBreakdown.classList.add('hidden');
|
|
}
|
|
|
|
validateForm();
|
|
}
|
|
|
|
function validateForm() {
|
|
const totalQty = productInputs.reduce((sum, input) => sum + parseInt(input.value || 0), 0);
|
|
const totalPrice = parseFloat(totalPriceInput.value || 0);
|
|
const paymentMethod = document.getElementById('paymentMethod').value;
|
|
|
|
const firstName = document.getElementById('firstName').value.trim();
|
|
const lastName = document.getElementById('lastName').value.trim();
|
|
const street = document.getElementById('street').value.trim();
|
|
const houseNumber = document.getElementById('houseNumber').value.trim();
|
|
const zip = document.getElementById('zip').value.trim();
|
|
const city = document.getElementById('city').value.trim();
|
|
|
|
let isValid = true;
|
|
let errorMsg = '';
|
|
|
|
if (totalQty === 0) {
|
|
isValid = false;
|
|
errorMsg = 'Mindestens ein Produkt mit Menge > 0 erforderlich.';
|
|
} else if (totalPrice <= 0) {
|
|
isValid = false;
|
|
errorMsg = 'Preis muss größer als 0 sein.';
|
|
} else if (!paymentMethod) {
|
|
isValid = false;
|
|
errorMsg = 'Zahlungsart auswählen.';
|
|
} else if (!firstName || !lastName || !street || !houseNumber || !zip || !city) {
|
|
isValid = false;
|
|
errorMsg = 'Alle Rechnungsadress-Felder ausfüllen.';
|
|
}
|
|
|
|
if (!isValid) {
|
|
errorEl.textContent = errorMsg;
|
|
errorEl.style.display = 'block';
|
|
submitBtn.disabled = true;
|
|
} else {
|
|
errorEl.style.display = 'none';
|
|
submitBtn.disabled = false;
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
productInputs.forEach(input => {
|
|
input.addEventListener('input', updatePriceBreakdown);
|
|
});
|
|
|
|
totalPriceInput.addEventListener('input', updatePriceBreakdown);
|
|
|
|
document.getElementById('paymentMethod').addEventListener('change', validateForm);
|
|
document.getElementById('firstName').addEventListener('input', validateForm);
|
|
document.getElementById('lastName').addEventListener('input', validateForm);
|
|
document.getElementById('street').addEventListener('input', validateForm);
|
|
document.getElementById('houseNumber').addEventListener('input', validateForm);
|
|
document.getElementById('zip').addEventListener('input', validateForm);
|
|
document.getElementById('city').addEventListener('input', validateForm);
|
|
|
|
async function submitOrder() {
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
const products = productInputs
|
|
.filter(input => parseInt(input.value || 0) > 0)
|
|
.map(input => ({
|
|
title: input.dataset.title,
|
|
qty: parseInt(input.value)
|
|
}));
|
|
|
|
const orderData = {
|
|
products: products,
|
|
totalPrice: parseFloat(totalPriceInput.value),
|
|
paymentMethod: document.getElementById('paymentMethod').value,
|
|
billing: {
|
|
firstName: document.getElementById('firstName').value.trim(),
|
|
lastName: document.getElementById('lastName').value.trim(),
|
|
street: document.getElementById('street').value.trim(),
|
|
houseNumber: document.getElementById('houseNumber').value.trim(),
|
|
zip: document.getElementById('zip').value.trim(),
|
|
city: document.getElementById('city').value.trim()
|
|
}
|
|
};
|
|
|
|
const originalBtnText = submitBtn.innerHTML;
|
|
submitBtn.innerHTML = '<span class="loading"></span> Wird verarbeitet...';
|
|
submitBtn.disabled = true;
|
|
errorEl.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch('/api/otc-order.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(orderData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok && result.ok) {
|
|
successEl.style.display = 'block';
|
|
productInputs.forEach(input => input.value = '0');
|
|
totalPriceInput.value = '0.00';
|
|
priceBreakdown.classList.add('hidden');
|
|
|
|
setTimeout(() => {
|
|
successEl.style.display = 'none';
|
|
}, 5000);
|
|
} else {
|
|
errorEl.textContent = result.error || 'Unbekannter Fehler';
|
|
errorEl.style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
errorEl.textContent = 'Netzwerkfehler: ' + error.message;
|
|
errorEl.style.display = 'block';
|
|
} finally {
|
|
submitBtn.innerHTML = originalBtnText;
|
|
submitBtn.disabled = false;
|
|
validateForm();
|
|
}
|
|
}
|
|
|
|
updatePriceBreakdown();
|
|
</script>
|
|
</body>
|
|
</html>
|