Files
erp_naurua/public/otc/index.php
Mathias Gläser d52b6953ed Add OTC sales UI with direct sales support
- 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
2026-04-06 19:55:24 +02:00

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>