<!doctype html>
<html lang="el">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Φόρμα Παραγγελίας</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<header class="topbar">
<div class="wrap topbar-inner">
<div class="brand">
<div class="brand-mark">VIOMES</div>
</div>
</div>
</header>
<section class="hero">
<div class="hero-bg" aria-hidden="true"></div>
<div class="wrap hero-inner">
<h1 class="hero-title">Παραγγελία</h1>
<p class="hero-sub">
Επιλέξτε προϊόντα από τον κατάλογο και δηλώστε τεμάχια. Η παραγγελία
θα υποβληθεί για έγκριση.
</p>
</div>
</section>
<main class="wrap page">
<div class="page-head">
<div class="quickbar">
<div class="quickbar-title">Γρήγορη παραγγελία</div>
<div class="quickbar-row">
<input
id="quickCode"
class="text quick"
type="text"
placeholder="Κωδικός"
autocomplete="off"
/>
<input
id="quickQty"
class="text quick qty"
type="number"
min="1"
step="1"
inputmode="numeric"
placeholder="Τεμάχια"
/>
<div class="quickbar-hint">Enter για προσθήκη</div>
</div>
<div class="small quickbar-msg" id="quickMsg"></div>
</div>
<div class="toolbar">
<input
id="q"
type="search"
placeholder="Αναζήτηση με κωδικό/περιγραφή/χρώμα…"
/>
<button class="btn ghost" id="reloadBtn" type="button">
Ανανέωση
</button>
<span class="pill" id="countPill">0 προϊόντα</span>
</div>
</div>
<div class="grid">
<section class="panel">
<div class="catalog-table-wrap" id="catalog"></div>
<div class="status" id="catalogStatus">Φόρτωση…</div>
</section>
<aside class="panel">
<div class="panel-head">
<h3>Στοιχεία & Παραγγελία</h3>
</div>
<div class="actions">
<div class="customerName">
<label for="customerName" class="small">Όνομα πελάτη</label>
<input
id="customerName"
class="text"
type="text"
placeholder="π.χ. ABC Market"
/>
</div>
<div class="customerEmail">
<label for="customerEmail" class="small mt">Email</label>
<input
id="customerEmail"
class="text"
type="email"
placeholder="π.χ. Αυτή η διεύθυνση Email προστατεύεται από τους αυτοματισμούς αποστολέων ανεπιθύμητων μηνυμάτων. Χρειάζεται να ενεργοποιήσετε τη JavaScript για να μπορέσετε να τη δείτε."
/>
</div>
<div class="cart" id="cart"></div>
<label class="small mt">Σχόλια</label>
<textarea
id="notes"
placeholder="π.χ. Παράδοση Δευτέρα…"
></textarea>
<div class="buttons">
<button class="btn ghost" id="clearBtn" type="button">
Καθαρισμός
</button>
<button id="submitBtn" type="button" class="btn primary">
Υποβολή
</button>
</div>
<div class="small" id="submitStatus"></div>
</div>
</aside>
</div>
<footer class="foot">
<div class="foot-inner">
<span>© VIOMES</span>
<span class="dot">•</span>
<span>B2B Order Form (demo)</span>
</div>
</footer>
</main>
<script src="/order_form.js"></script>
</body>
</html>
// ====== Ρυθμίσεις ======
const token = new URLSearchParams(location.search).get("t") || "demo-token";
// ====== Mock Data (μέχρι να συνδέσουμε Entersoft) ======
const MOCK = [
{
code: "P-1001",
title: "Δοχείο 1L",
desc: "Πλαστικό δοχείο τροφίμων, BPA free.",
img: "https://via.placeholder.com/300x300?text=Packshot",
},
{
code: "P-1002",
title: "Κουτί Αποθήκευσης 5L",
desc: "Κουτί με καπάκι, διάφανο.",
img: "https://via.placeholder.com/300x300?text=Packshot",
},
{
code: "P-1003",
title: "Καλάθι Μπάνιου",
desc: "Καλάθι με τρύπες, ανθεκτικό.",
img: "https://via.placeholder.com/300x300?text=Packshot",
},
];
const API_BASE = "http://127.0.0.1:8000";
let catalog = [];
const cart = new Map(); // code -> {code,title,qty}
const els = {
q: document.getElementById("q"),
catalog: document.getElementById("catalog"),
cart: document.getElementById("cart"),
countPill: document.getElementById("countPill"),
catalogStatus: document.getElementById("catalogStatus"),
notes: document.getElementById("notes"),
clearBtn: document.getElementById("clearBtn"),
submitBtn: document.getElementById("submitBtn"),
submitStatus: document.getElementById("submitStatus"),
reloadBtn: document.getElementById("reloadBtn"),
quickCode: document.getElementById("quickCode"),
quickQty: document.getElementById("quickQty"),
quickMsg: document.getElementById("quickMsg"),
};
function findProductByCode(code) {
const needle = (code || "").trim().toLowerCase();
if (!needle) return null;
// ακριβές match πρώτα
let p = catalog.find((x) => (x.code || "").toLowerCase() === needle);
if (p) return p;
// αν γράψει π.χ. "1002" ή μέρος του κωδικού, δοκιμάζουμε startsWith
p = catalog.find((x) => (x.code || "").toLowerCase().startsWith(needle));
return p || null;
}
function setQuickMsg(text, isError) {
if (!els.quickMsg) return;
els.quickMsg.textContent = text || "";
els.quickMsg.style.color = isError ? "#b00020" : ""; // κόκκινο μόνο σε error
}
function quickAdd() {
if (!catalog.length) {
setQuickMsg("Ο κατάλογος δεν έχει φορτώσει ακόμα.", true);
return;
}
const code = els.quickCode?.value?.trim() || "";
const qty = parseInt(els.quickQty?.value, 10);
if (!code) {
setQuickMsg("Γράψε κωδικό προϊόντος.", true);
return;
}
if (!Number.isFinite(qty) || qty <= 0) {
setQuickMsg("Βάλε τεμάχια (>= 1).", true);
return;
}
const p = findProductByCode(code);
if (!p) {
setQuickMsg(`Δεν βρέθηκε προϊόν με κωδικό: ${code}`, true);
return;
}
// ====== VALIDATION: ποσότητα πρέπει να είναι πολλαπλάσιο των τεμ./συσκ. ======
const ppp = parseInt(p.pieces_per_package, 10) || 1;
if (qty % ppp !== 0) {
setQuickMsg(
`Λάθος ποσότητα. Το προϊόν ${p.code} έχει ${ppp} τεμ./συσκ. (βάλε πολλαπλάσιο).`,
true,
);
return;
}
addToCart(p, qty);
setQuickMsg(`Προστέθηκε: ${p.code} (${qty} τεμ.)`, false);
// καθάρισμα και focus για γρήγορη επόμενη γραμμή
if (els.quickCode) els.quickCode.value = "";
if (els.quickQty) els.quickQty.value = "";
els.quickCode?.focus();
}
function escapeHtml(s) {
return String(s).replace(
/[&<>"']/g,
(m) =>
({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[m],
);
}
function productTitle(p) {
const t = (p.title || "").trim();
if (t) return t;
const color = (p.color || "").trim();
return color ? `${p.code} • ${color}` : `${p.code}`;
}
function renderCatalog(items) {
const rows = items
.map(
(p) => `
<tr data-id="${p.id}">
<td class="td-code">${escapeHtml(p.code)}</td>
<td class="td-desc">
<div style="font-weight:600;">
${escapeHtml(p.description || "")}
</div>
</td>
<td class="td-pack">
<img class="packshot"
src="/${escapeHtml(p.image_url || "https://via.placeholder.com/300x300?text=Packshot")}"
alt="${escapeHtml(p.code)}"
loading="lazy" />
</td>
<td class="td-vol">${escapeHtml(String(p.volume_liters ?? ""))} L</td>
<td class="td-bundle"><span class="bundle-pill">${p.pieces_per_package} τεμ.</span></td>
<td class="td-qty">
<div class="qty-inline">
<input
type="number"
min="${p.pieces_per_package}"
step="${p.pieces_per_package}"
inputmode="numeric"
data-ppp="${p.pieces_per_package}"
/>
<button type="button" class="btn ghost addBtn">Προσθήκη</button>
</div>
</td>
</tr>
`,
)
.join("");
els.catalog.innerHTML = `
<table class="catalog-table">
<thead>
<tr>
<th>ΚΩΔ.</th>
<th>ΠΕΡΙΓΡΑΦΗ</th>
<th>ΕΙΔΟΣ</th>
<th>ΟΓΚΟΣ</th>
<th>ΣΥΣΚ.</th>
<th>ΤΕΜΑΧΙΑ</th>
</tr>
</thead>
<tbody>
${
rows ||
`<tr><td colspan="6" style="padding:14px; color:#6b6b6b;">Δεν βρέθηκαν προϊόντα.</td></tr>`
}
</tbody>
</table>
`;
// Bind add buttons per row
els.catalog.querySelectorAll("tr[data-id]").forEach((tr) => {
const id = parseInt(tr.getAttribute("data-id"), 10);
const input = tr.querySelector("input");
const btn = tr.querySelector(".addBtn");
btn.addEventListener("click", () => {
const qty = parseInt(input.value, 10);
const p = catalog.find((x) => x.id === id);
const ppp =
parseInt(input.getAttribute("data-ppp"), 10) ||
(p?.pieces_per_package ?? 1);
if (!Number.isFinite(qty) || qty <= 0) {
input.setCustomValidity("Βάλε ποσότητα.");
input.reportValidity();
return;
}
if (qty % ppp !== 0) {
input.setCustomValidity(
`Πρέπει να είναι πολλαπλάσιο των ${ppp} (τεμ./συσκ.).`,
);
input.reportValidity();
return;
}
input.setCustomValidity("");
addToCart(p, qty);
input.value = "";
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
btn.click();
}
});
});
els.countPill.textContent = `${items.length} προϊόντα`;
}
function renderCart() {
if (cart.size === 0) {
els.cart.innerHTML = `<div class="small">Δεν έχετε επιλέξει προϊόντα.</div>`;
return;
}
els.cart.innerHTML = Array.from(cart.values())
.map(
(it) => `
<div class="cartItem">
<div class="cartLeft">
<div class="cartTitle">${escapeHtml(it.title || it.code)}</div>
<div class="cartCode">${escapeHtml(it.code)}</div>
</div>
<div style="display:flex; gap:10px; align-items:center;">
<div class="cartQty">${it.qty} τεμ.</div>
<button type="button" class="secondary" data-remove="${escapeHtml(it.code)}">X</button>
</div>
</div>
`,
)
.join("");
els.cart.querySelectorAll("button[data-remove]").forEach((b) => {
b.addEventListener("click", () => {
cart.delete(b.getAttribute("data-remove"));
renderCart();
});
});
}
function addToCart(p, qty) {
const existing = cart.get(p.code);
cart.set(p.code, {
code: p.code,
title: productTitle(p),
qty: (existing ? existing.qty : 0) + qty,
});
renderCart();
}
function filterCatalog() {
const q = els.q.value.trim().toLowerCase();
if (!q) return renderCatalog(catalog);
const filtered = catalog.filter(
(p) =>
(p.code || "").toLowerCase().includes(q) ||
(p.description || "").toLowerCase().includes(q) ||
(p.color || "").toLowerCase().includes(q),
);
renderCatalog(filtered);
}
// Enter = προσθήκη στο καλάθι από τη Γρήγορη Παραγγελία
[els.quickCode, els.quickQty].forEach((inp) => {
if (!inp) return;
inp.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
quickAdd();
}
});
});
async function loadCatalog() {
els.catalogStatus.textContent = "Φόρτωση…";
try {
const res = await fetch(`${API_BASE}/api/catalog`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
catalog = data;
renderCatalog(catalog);
els.catalogStatus.textContent = "Έτοιμο.";
} catch (e) {
console.error("Catalog load/render error:", e);
els.catalogStatus.textContent = `Σφάλμα: ${e?.message || e}`;
}
}
async function submitOrder() {
els.submitStatus.textContent = "";
if (cart.size === 0) {
els.submitStatus.textContent = "Βάλε τουλάχιστον 1 προϊόν στην παραγγελία.";
return;
}
const payload = {
token,
notes: els.notes.value.trim(),
lines: Array.from(cart.values()).map((x) => ({
itemCode: x.code,
qty: x.qty,
})),
};
els.submitBtn.disabled = true;
els.submitStatus.textContent = "Αποστολή…";
try {
// TODO: αργότερα:
// const res = await fetch(`${API_BASE}/api/order`, {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(payload),
// });
// if (!res.ok) throw new Error("Submit failed");
// const out = await res.json();
await new Promise((r) => setTimeout(r, 600)); // mock
const out = { ok: true, requestId: "REQ-000123" };
els.submitStatus.textContent = `Η παραγγελία υποβλήθηκε για έγκριση (ID: ${out.requestId}).`;
cart.clear();
renderCart();
els.notes.value = "";
} catch {
els.submitStatus.textContent = "Αποτυχία υποβολής. Δοκίμασε ξανά.";
} finally {
els.submitBtn.disabled = false;
}
}
els.q.addEventListener("input", filterCatalog);
els.clearBtn.addEventListener("click", () => {
cart.clear();
renderCart();
els.notes.value = "";
});
els.submitBtn.addEventListener("click", submitOrder);
els.reloadBtn.addEventListener("click", loadCatalog);
// Init
loadCatalog();
renderCart();
/* =========================
VIOMES B2B Order Form
styles.css (refreshed)
- wider layout (less empty sides)
- table fits all columns (no horizontal scroll)
- clearer column widths & responsive behavior
========================= */
:root {
--bg: #f6f7fb;
--card: #ffffff;
--text: #3d3d3d;
--muted: #6b7280;
--line: #e5e7eb;
--primary: #2a69b8;
--primary-hover: #215a9e;
--primary-active: #1b4a82;
--focus: rgba(42, 105, 184, 0.25);
--shadow: 0 12px 34px rgba(0, 0, 0, 0.07);
--shadow-sm: 0 6px 18px rgba(0, 0, 0, 0.08);
--radius: 14px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
Segoe UI,
Roboto,
Arial;
background: var(--bg);
color: var(--text);
}
/* ---------- Layout width (use more screen) ---------- */
.wrap {
/* Use more horizontal space on large screens */
width: min(1520px, calc(100% - 56px));
margin: 0 auto;
}
@media (max-width: 700px) {
.wrap {
width: calc(100% - 24px);
}
}
/* ---------- Header ---------- */
header {
padding: 18px 16px;
background: #fff;
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 2;
}
/* ---------- Page spacing ---------- */
main {
padding: 18px 0 26px 0;
}
.page-head {
padding-bottom: 1em;
}
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 12px;
align-items: center;
}
input[type="search"] {
flex: 1;
min-width: 320px;
padding: 11px 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: #fff;
}
/* Generic inputs */
input[type="text"],
input[type="email"],
input[type="number"],
textarea {
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: #fff;
}
textarea {
width: 100%;
min-height: 90px;
resize: vertical;
margin-top: 6px;
}
/* ---------- Buttons ---------- */
button,
.btn {
padding: 10px 12px;
border: 1px solid var(--primary);
border-radius: 12px;
background: var(--primary);
color: #fff;
cursor: pointer;
transition:
transform 0.08s ease,
box-shadow 0.15s ease,
background-color 0.15s ease,
border-color 0.15s ease;
}
button:hover,
.btn:hover {
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
background: var(--primary-hover);
border-color: var(--primary-hover);
}
button:active,
.btn:active {
transform: translateY(0);
box-shadow: none;
background: var(--primary-active);
border-color: var(--primary-active);
}
button:disabled,
.btn:disabled {
opacity: 0.55;
cursor: not-allowed;
box-shadow: none;
transform: none;
}
/* Secondary / ghost */
button.secondary,
.btn.ghost {
background: #fff;
color: #111827;
border-color: #cfd5df;
}
button.secondary:hover,
.btn.ghost:hover {
background: #f7f9fc;
border-color: #bfc7d4;
}
/* ---------- Focus states ---------- */
input[type="search"],
input[type="text"],
input[type="email"],
input[type="number"],
textarea {
outline: none;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease,
background-color 0.15s ease;
}
input[type="search"]:hover,
input[type="text"]:hover,
input[type="email"]:hover,
input[type="number"]:hover,
textarea:hover {
border-color: #cfd5df;
}
input[type="search"]:focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="number"]:focus,
textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px var(--focus);
}
/* ---------- Grid ---------- */
/* Use more space for catalog; keep order panel comfortable */
.grid {
display: grid;
grid-template-columns: minmax(780px, 1fr) 420px;
gap: 18px;
align-items: start;
}
@media (max-width: 1100px) {
.grid {
grid-template-columns: 1fr;
}
}
/* ---------- Panels ---------- */
.panel {
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
text-align: center;
box-shadow: var(--shadow);
}
/* If you have a panel head */
.panel h2,
.panel h3 {
margin: 0;
padding: 14px 14px;
border-bottom: 1px solid var(--line);
font-size: 16px;
}
/* ---------- Pills / small text ---------- */
.small {
font-size: 12px;
color: var(--muted);
}
.pill {
display: inline-block;
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12px;
color: var(--muted);
background: #fff;
align-self: center;
}
.status {
padding: 10px 14px;
border-top: 1px solid var(--line);
font-size: 13px;
color: var(--muted);
}
.panel .status {
border-top: 0;
}
/* ---------- Quickbar ---------- */
.quickbar {
margin: 12px 0 14px 0;
background: #fafafa;
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 12px 14px;
}
.quickbar-title {
font-size: 13px;
font-weight: 700;
margin-bottom: 8px;
}
.quickbar-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.text.quick {
margin-top: 0;
}
.text.quick.qty {
width: 150px;
}
.quickbar-hint {
font-size: 12px;
color: var(--muted);
padding: 8px 10px;
border: 1px dashed var(--line);
border-radius: 999px;
background: #fff;
}
.quickbar-msg {
display: inline-block;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--line);
background: #fff;
margin-top: 8px;
}
.quickbar-msg:empty {
display: none;
}
/* ---------- Cart ---------- */
.cart {
padding: 14px;
}
.cartItem {
display: flex;
justify-content: space-between;
gap: 10px;
padding: 10px 0;
border-bottom: 1px dashed var(--line);
}
.cartItem:last-child {
border-bottom: none;
}
.cartLeft {
min-width: 0;
text-align: left;
}
.cartTitle {
font-size: 13px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cartCode {
font-size: 12px;
color: var(--muted);
}
.cartQty {
font-size: 13px;
}
.cartItem button.secondary {
border-radius: 12px;
}
.cartItem button.secondary:hover {
border-color: #ffb4b4;
background: #fff2f2;
color: #9a1b1b;
}
/* Actions panel */
.actions {
padding: 14px;
text-align: left;
}
.buttons {
display: flex;
gap: 10px;
margin-top: 12px;
}
/* =========================
Catalog Table (fit all columns)
========================= */
.catalog-table-wrap {
padding: 0;
overflow: hidden; /* no horizontal scroll */
}
/* Table itself */
.catalog-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed; /* key for fitting */
background: #fff;
}
/* Header */
.catalog-table thead th {
position: sticky;
top: 0;
z-index: 5;
border-bottom: 1px solid var(--line);
color: #0f172a;
background: #dbe7f7;
font-size: 12px;
letter-spacing: 0.4px;
text-transform: uppercase;
padding: 10px 8px;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.03);
}
.catalog-table tbody td {
border-bottom: 1px solid var(--line);
padding: 10px 8px;
vertical-align: middle;
font-size: 13px;
}
.catalog-table tbody tr:nth-child(even) {
background: #fafafa;
}
.catalog-table tbody tr {
transition: background-color 0.12s ease;
}
.catalog-table tbody tr:hover {
background: #eef4ff;
}
/* Column widths: tuned to fit without overflow */
.td-code {
width: 92px;
font-weight: 800;
white-space: nowrap;
}
.td-pack {
width: 82px;
text-align: center;
}
.td-vol {
width: 20px;
text-align: center;
font-weight: 800;
color: #1f2937;
white-space: nowrap;
}
.td-bundle {
width: 110px;
text-align: center;
white-space: nowrap;
font-weight: 800;
}
.td-qty {
width: 250px; /* enough for input+button */
}
/* Description takes the remaining space */
.td-desc {
text-align: left;
min-width: 0;
}
.td-desc > div {
line-height: 1.25;
white-space: normal;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; /* show more text */
}
/* Packshot */
.packshot {
width: 56px;
height: 56px;
border: 1px solid var(--line);
border-radius: 10px;
background: #fff;
object-fit: contain;
transition:
transform 0.12s ease,
box-shadow 0.12s ease;
}
.catalog-table tbody tr:hover .packshot {
transform: scale(1.03);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
/* Bundle pill */
.bundle-pill {
display: inline-block;
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: #fff;
}
/* Qty controls (fit within td-qty) */
.qty-inline {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: nowrap;
}
.qty-inline input {
width: 92px;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: 12px;
}
.qty-inline button {
padding: 9px 10px;
border-radius: 12px;
font-size: 12px;
}
/* Make sure the blue "ghost" add button matches your scheme */
.qty-inline .btn.ghost {
border-color: var(--primary);
}
/* ---------- Responsive tweaks ---------- */
@media (max-width: 980px) {
.catalog-table-wrap {
overflow: auto;
} /* allow scroll only on small screens */
.catalog-table {
min-width: 860px;
} /* keep structure; scroll on mobile */
}