279 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Telvero Sales Panel</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs" defer></script>
<style>[x-cloak] { display: none !important; } .custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }</style>
</head>
<body class="bg-slate-100 min-h-screen font-sans" x-data="salesApp()">
<template x-if="!isLoggedIn">
<div class="fixed inset-0 bg-slate-900 flex items-center justify-center p-4 z-50">
<div class="bg-white p-10 rounded-[2.5rem] shadow-2xl w-full max-w-md text-center border-t-8 border-blue-600">
<h2 class="text-3xl font-black mb-8 italic tracking-tighter text-slate-800">TELVERO <span class="text-blue-600">LOGIN</span></h2>
<div class="space-y-4">
<input type="text" x-model="loginForm.username" placeholder="Gebruikersnaam" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50">
<input type="password" x-model="loginForm.password" @keyup.enter="doLogin()" placeholder="Wachtwoord" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50">
<button @click="doLogin()" class="w-full bg-blue-600 text-white p-5 rounded-2xl font-black shadow-lg hover:bg-blue-700 transition uppercase tracking-widest text-sm">Inloggen</button>
</div>
</div>
</div>
</template>
<div x-show="isLoggedIn" x-cloak class="max-w-[1440px] mx-auto p-6">
<header class="flex justify-between items-center mb-8 bg-white p-6 rounded-3xl shadow-sm border-b-4 border-blue-600">
<h1 class="text-2xl font-black italic tracking-tighter">TELVERO <span class="text-blue-600">PANEL</span></h1>
<div class="flex items-center gap-6 text-sm font-bold text-slate-400">
<span x-text="'Agent: ' + currentUser"></span>
<button @click="doLogout()" class="text-red-500 underline uppercase text-xs font-black">Uitloggen</button>
</div>
</header>
<div class="grid grid-cols-12 gap-8">
<div class="col-span-12 lg:col-span-4 bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200">
<div class="mb-8 p-6 bg-blue-50 rounded-2xl border-2 border-blue-100 shadow-inner">
<label class="block text-[10px] font-black text-blue-600 uppercase tracking-widest mb-3 italic text-center">Mediacode</label>
<select x-model="meta.mediacode" class="w-full border-2 border-white p-4 rounded-xl font-bold text-blue-800 shadow-sm outline-none focus:border-blue-300">
<option value="">-- KIES MEDIACODE --</option>
<option value="Telvero - Net5">Telvero - Net5</option>
<option value="Telvero - SBS9">Telvero - SBS9</option>
</select>
</div>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-3">
<input type="text" x-model="form.initials" @blur="formatInitials()" placeholder="Voorletters" class="border p-3 rounded-xl w-full bg-slate-50">
<input type="text" x-model="form.lastname" @blur="formatLastname()" placeholder="Achternaam" class="border p-3 rounded-xl w-full bg-slate-50">
</div>
<div class="grid grid-cols-3 gap-2">
<input type="text" x-model="form.postcode" placeholder="Postcode" class="border p-3 rounded-xl w-full uppercase font-mono">
<input type="text" x-model="form.houseno" @blur="lookupAddress()" placeholder="Nr." class="border p-3 rounded-xl w-full">
<input type="text" x-model="form.suffix" placeholder="Toev." class="border p-3 rounded-xl w-full">
</div>
<input type="text" x-model="form.street" placeholder="Straat" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs" readonly>
<input type="text" x-model="form.city" placeholder="Stad" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs" readonly>
<input type="tel" x-model="form.phone" placeholder="Telefoon (06...)" class="border p-3 rounded-xl w-full focus:border-blue-500 outline-none">
<input type="email" x-model="form.email" placeholder="E-mail (Verplicht)" class="border-2 border-amber-300 p-3 rounded-xl w-full outline-none focus:border-amber-500 shadow-sm">
</div>
</div>
<div class="col-span-12 lg:col-span-5 bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200">
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest border-b pb-2 text-center italic">Product Selecteren</h2>
<select x-model="selectedProductId" @change="selectProduct()" class="w-full border-2 border-slate-100 p-5 rounded-2xl font-black text-slate-700 mb-6 bg-slate-50 outline-none focus:border-blue-500 shadow-sm">
<option value="">-- Kies Hoofdproduct --</option>
<template x-for="p in products" :key="p.id">
<option :value="p.id" x-text="p.name"></option>
</template>
</select>
<div x-show="variations.length > 0" x-cloak class="mb-8 p-6 bg-blue-50 rounded-3xl border border-blue-100 shadow-inner text-center">
<select x-model="selectedVariationId" @change="selectVariation()" class="w-full border-2 border-white p-4 rounded-2xl font-bold bg-white text-slate-700 shadow-sm outline-none">
<option value="">-- Kies Optie --</option>
<template x-for="v in variations" :key="v.id">
<option :value="v.id" x-text="getVarName(v) + ' (€' + v.price + ')'"></option>
</template>
</select>
</div>
<div class="mt-8 border-t pt-6 text-center">
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest italic">Extra Product Toevoegen</h2>
<div class="flex gap-2">
<select x-model="extraProductId" class="flex-1 border-2 border-slate-100 p-3 rounded-xl font-bold text-xs bg-slate-50 outline-none focus:border-green-500">
<option value="">-- Kies extra product --</option>
<template x-for="p in products" :key="'extra-'+p.id">
<option :value="p.id" x-text="p.name + ' (€' + p.price + ')'"></option>
</template>
</select>
<button @click="addExtraItem()" class="bg-green-600 text-white px-6 py-3 rounded-xl font-black text-[10px] uppercase shadow-md hover:bg-green-700 transition active:scale-95">Toevoegen</button>
</div>
</div>
<div x-show="upsellOptions.length > 0" x-cloak class="mt-8 space-y-3">
<p class="text-[10px] font-black text-red-500 uppercase tracking-widest italic px-2 text-center">Upsell Suggesties</p>
<template x-for="u in upsellOptions" :key="u.id">
<div class="flex items-center justify-between p-4 border rounded-2xl bg-slate-50 hover:bg-white transition-all shadow-sm">
<span class="text-xs font-bold text-slate-700" x-text="u.name + ' (€' + u.price + ')'"></span>
<button @click="toggleUpsell(u)" :class="isInCart(u.id) ? 'bg-red-500' : 'bg-green-600'" class="text-white px-6 py-2 rounded-xl text-[10px] font-black uppercase shadow-md" x-text="isInCart(u.id) ? 'Verwijder' : 'Voeg toe'"></button>
</div>
</template>
</div>
</div>
<div class="col-span-12 lg:col-span-3">
<div class="bg-slate-900 text-white p-8 rounded-[2.5rem] shadow-2xl sticky top-6 border border-slate-800">
<h2 class="font-bold mb-6 border-b border-slate-800 pb-2 text-[10px] uppercase text-slate-500 tracking-widest italic text-center">Overzicht</h2>
<div class="space-y-4 mb-6 min-h-[80px] max-h-[250px] overflow-y-auto pr-2 custom-scrollbar">
<template x-for="(item, index) in cart" :key="index">
<div class="flex justify-between items-center group">
<div class="flex flex-col flex-1">
<span x-text="item.name" class="text-[11px] font-medium leading-tight text-slate-300"></span>
<span x-text="'€' + item.price" class="text-[11px] font-black text-blue-400"></span>
</div>
<button @click="removeFromCart(index)" class="text-slate-600 hover:text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</div>
</template>
</div>
<div class="mb-6 pt-4 border-t border-slate-800 text-center">
<label class="text-[10px] text-slate-500 uppercase font-black mb-2 block italic tracking-widest">Verzendkosten (€)</label>
<input type="number" x-model="shipping" step="0.01" min="0" class="w-full bg-slate-800 border border-slate-700 rounded-xl p-2 text-center text-sm font-bold text-blue-400 outline-none focus:border-blue-500">
</div>
<div class="mb-8 pt-4 border-t border-slate-800 text-center">
<p class="text-[10px] text-slate-500 uppercase font-black mb-3 italic tracking-widest">Betaalwijze</p>
<div class="space-y-2 max-h-[180px] overflow-y-auto pr-2 custom-scrollbar">
<template x-for="method in filteredPaymentMethods" :key="method.id">
<button @click="payment_method = method.id"
:class="payment_method === method.id ? 'bg-blue-600 border-blue-400 ring-2 ring-blue-500/50' : 'bg-slate-800 border-slate-700 opacity-60'"
class="w-full text-left p-3 rounded-2xl border flex items-center gap-3 transition-all duration-200">
<img :src="method.image" class="w-7 h-7 rounded bg-white p-1">
<span class="text-[10px] font-black uppercase text-white tracking-tighter" x-text="method.title"></span>
</button>
</template>
</div>
</div>
<div class="flex justify-between items-center mb-8 pt-4 border-t border-slate-800 text-right">
<span class="text-3xl font-black text-green-400" x-text="'€' + total"></span>
</div>
<button @click="submitOrder()"
:disabled="submitting || !form.email || !meta.mediacode || cart.length === 0"
class="w-full bg-blue-600 hover:bg-blue-500 p-6 rounded-2xl font-black text-lg shadow-xl disabled:opacity-20 uppercase tracking-tighter transition active:scale-95 shadow-blue-500/20">
<span x-text="submitting ? 'PROCESSING...' : 'ORDER BEVESTIGEN'"></span>
</button>
</div>
</div>
</div>
</div>
<script>
function salesApp() {
return {
isLoggedIn: false, currentUser: '', loginForm: { username: '', password: '' },
products: [], paymentMethods: [], upsellOptions: [], cart: [], activeProduct: null,
selectedProductId: '', selectedVariationId: '', variations: [], payment_method: '',
extraProductId: '', shipping: '9.95',
submitting: false,
form: { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' },
meta: { mediacode: '' },
async doLogin() {
const res = await fetch('api.php?action=login', { method: 'POST', body: JSON.stringify(this.loginForm) });
if(res.ok) {
const data = await res.json();
this.isLoggedIn = true; this.currentUser = data.user;
await this.initData();
} else { alert("Login mislukt"); }
},
async initData() {
const [pRes, mRes] = await Promise.all([
fetch('api.php?action=get_products'),
fetch('api.php?action=get_payment_methods')
]);
this.products = await pRes.json();
let methods = await mRes.json();
this.paymentMethods = methods.map(m => {
let iconKey = m.id.replace('mollie_wc_gateway_', '');
if (m.id.includes('riverty')) iconKey = 'riverty';
if (m.id.includes('klarna')) iconKey = 'klarna';
if (m.id.includes('in3')) iconKey = 'in3';
return { ...m, image: `https://www.mollie.com/external/icons/payment-methods/${iconKey}.svg` };
});
this.payment_method = 'mollie_wc_gateway_ideal';
},
get filteredPaymentMethods() {
return this.paymentMethods.filter(method => {
const isTermin = method.id.includes('in3') || method.id.includes('klarna') || method.id.includes('riverty');
if (isTermin && parseFloat(this.total) < 100) {
if (this.payment_method === method.id) this.payment_method = 'mollie_wc_gateway_ideal';
return false;
}
return true;
});
},
selectProduct() {
const p = this.products.find(x => x.id == this.selectedProductId);
if(!p) return;
this.activeProduct = p; this.variations = p.variation_details || [];
this.cart = []; this.selectedVariationId = '';
if (p.type !== 'variable') {
this.cart.push({ id: parseInt(p.id), name: p.name, price: p.price });
this.loadUpsells(p);
}
},
selectVariation() {
const v = this.variations.find(x => x.id == this.selectedVariationId);
if(!v) return;
this.cart = [{ id: parseInt(this.activeProduct.id), variation_id: parseInt(v.id), name: this.activeProduct.name + ' - ' + this.getVarName(v), price: v.price }];
this.loadUpsells(this.activeProduct);
},
addExtraItem() {
const p = this.products.find(x => x.id == this.extraProductId);
if(!p) return;
this.cart.push({ id: parseInt(p.id), name: p.name, price: p.price });
this.extraProductId = '';
},
loadUpsells(product) {
this.upsellOptions = [];
if (product.upsell_ids && product.upsell_ids.length > 0) {
const idsToFind = product.upsell_ids.map(id => parseInt(id));
this.upsellOptions = this.products.filter(p => idsToFind.includes(parseInt(p.id)));
}
},
removeFromCart(index) { this.cart.splice(index, 1); },
getVarName(v) { return v.attributes.map(a => a.option).join(' '); },
toggleUpsell(u) {
const idx = this.cart.findIndex(i => parseInt(i.id) === parseInt(u.id));
if(idx > -1) { this.cart.splice(idx, 1); }
else { this.cart.push({ id: parseInt(u.id), name: u.name, price: u.price }); }
},
isInCart(id) { return this.cart.some(i => parseInt(i.id) === parseInt(id)); },
get total() {
const itemsTotal = this.cart.reduce((sum, item) => sum + parseFloat(item.price), 0);
return (itemsTotal + (parseFloat(this.shipping) || 0)).toFixed(2);
},
formatInitials() { let v = this.form.initials.replace(/[^a-z]/gi, '').toUpperCase(); this.form.initials = v.split('').join('.') + (v ? '.' : ''); },
formatLastname() { this.form.lastname = this.form.lastname.charAt(0).toUpperCase() + this.form.lastname.slice(1); },
async submitOrder() {
this.submitting = true;
const payload = {
payment_method: this.payment_method, mediacode_internal: this.meta.mediacode,
shipping_total: this.shipping,
billing: { first_name: this.form.initials, last_name: this.form.lastname, address_1: (this.form.street + ' ' + this.form.houseno + ' ' + (this.form.suffix || '')).trim(), city: this.form.city, postcode: this.form.postcode, country: 'NL', email: this.form.email, phone: this.form.phone },
line_items: this.cart.map(i => ({ product_id: i.id, variation_id: i.variation_id || 0, quantity: 1 }))
};
try {
const res = await fetch('api.php?action=create_order', { method: 'POST', body: JSON.stringify(payload) });
const result = await res.json();
if(result.payment_url) { alert("Succes!"); this.cart = []; this.selectedProductId = ''; this.shipping = '9.95'; this.form = { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' }; }
else { alert("Fout: " + result.error); }
} catch(e) { alert("Systeemfout"); }
this.submitting = false;
},
async doLogout() { await fetch('api.php?action=logout'); location.reload(); },
async lookupAddress() {
if (this.form.postcode.length >= 6 && this.form.houseno) {
const res = await fetch(`api.php?action=postcode_check&postcode=${this.form.postcode}&number=${this.form.houseno}`);
const data = await res.json();
if (data.street) { this.form.street = data.street.toUpperCase(); this.form.city = data.city.toUpperCase(); }
}
}
}
}
</script>
</body>
</html>