636 lines
25 KiB
JavaScript
636 lines
25 KiB
JavaScript
// Offer → Excel frontend
|
||
(() => {
|
||
const $ = (id) => document.getElementById(id);
|
||
const sections = {
|
||
upload: $("s-upload"),
|
||
processing: $("s-processing"),
|
||
result: $("s-result"),
|
||
};
|
||
const show = (name) => {
|
||
for (const [k, el] of Object.entries(sections)) el.classList.toggle("hidden", k !== name);
|
||
};
|
||
|
||
let state = { jobId: null, data: null };
|
||
|
||
// ── Upload ──
|
||
const fileInput = $("file-input");
|
||
const dropZone = $("drop-zone");
|
||
$("browse-btn").addEventListener("click", () => fileInput.click());
|
||
fileInput.addEventListener("change", (e) => e.target.files[0] && upload(e.target.files[0]));
|
||
["dragenter", "dragover"].forEach((ev) =>
|
||
dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.add("drag-over"); }));
|
||
["dragleave", "drop"].forEach((ev) =>
|
||
dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.remove("drag-over"); }));
|
||
dropZone.addEventListener("drop", (e) => e.dataTransfer.files[0] && upload(e.dataTransfer.files[0]));
|
||
|
||
// Close any open combobox menu when clicking outside it (bound once).
|
||
document.addEventListener("click", (e) => {
|
||
document.querySelectorAll(".combo-menu").forEach((m) => {
|
||
if (!m.closest(".combo-wrap").contains(e.target)) m.classList.add("hidden");
|
||
});
|
||
});
|
||
|
||
async function upload(file) {
|
||
show("processing");
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append("file", file);
|
||
const r = await fetch("api/upload", { method: "POST", body: fd });
|
||
if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
|
||
const json = await r.json();
|
||
state.jobId = json.job_id;
|
||
state.data = json.data || {};
|
||
await initOrder();
|
||
renderForm();
|
||
show("result");
|
||
} catch (err) {
|
||
alert("Chyba: " + err.message);
|
||
show("upload");
|
||
}
|
||
}
|
||
|
||
// ── Render editable form ──
|
||
function nestedField(parent, key, label, value, opts = {}) {
|
||
const isEmpty = value === null || value === undefined || value === "";
|
||
const cls = "inv-input" + (isEmpty ? " is-empty" : "");
|
||
return `
|
||
<div class="inv-field ${opts.wide ? "inv-field-wide" : ""}">
|
||
<label class="inv-label">${escapeHtml(label)}</label>
|
||
<input class="${cls}" data-parent="${parent}" data-key="${key}"
|
||
type="text" value="${escapeHtmlAttr(isEmpty ? "" : value)}"
|
||
placeholder="${escapeHtmlAttr(opts.placeholder || "—")}">
|
||
</div>`;
|
||
}
|
||
|
||
function renderForm() {
|
||
const d = state.data;
|
||
const sup = d.supplier || {};
|
||
const items = d.line_items || [];
|
||
|
||
const warn = (d.is_offer === false)
|
||
? `<div class="offer-warn">⚠️ Tento dokument zřejmě není dodavatelská
|
||
nabídka. Data mohou být neúplná — zkontrolujte je, nebo nahrajte
|
||
jiný soubor.</div>`
|
||
: "";
|
||
|
||
const html = `
|
||
${warn}
|
||
<!-- Dodavatel -->
|
||
<div class="inv-card">
|
||
<div class="inv-card-title">Dodavatel</div>
|
||
<div class="inv-grid">
|
||
${nestedField("supplier", "name", "Jméno", sup.name, {wide: true})}
|
||
${nestedField("supplier", "ico", "IČO", sup.ico)}
|
||
${nestedField("supplier", "dic", "DIČ", sup.dic)}
|
||
${nestedField("supplier", "email", "Kontaktní email", sup.email)}
|
||
${nestedField("supplier", "address", "Adresa", sup.address, {wide: true})}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Položky -->
|
||
<div class="inv-card">
|
||
<div class="inv-card-title">Položky (${items.length})</div>
|
||
<div class="inv-table-wrap">
|
||
<table class="inv-table" id="items-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="min-width:220px">Název zboží</th>
|
||
<th style="width:150px">ID zboží dodavatele</th>
|
||
<th style="width:90px">Množství</th>
|
||
<th style="width:80px">Jednotka</th>
|
||
<th style="width:120px">Jednotková cena</th>
|
||
<th style="width:120px">Cena celkem</th>
|
||
<th class="inv-row-actions"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="items-body">
|
||
${items.map((it, i) => renderItemRow(it, i)).join("")}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="inv-table-foot">
|
||
<button class="btn-add-row" type="button" id="add-item-btn">+ Přidat položku</button>
|
||
</div>
|
||
</div>
|
||
<div id="order-card-wrap">${orderCardHtml()}</div>
|
||
`;
|
||
$("offer-form").innerHTML = html;
|
||
bindFieldHandlers();
|
||
bindOrderHandlers();
|
||
initDatePickers();
|
||
}
|
||
|
||
function rerenderOrderCard() {
|
||
const wrap = $("order-card-wrap");
|
||
if (!wrap) return;
|
||
wrap.innerHTML = orderCardHtml();
|
||
bindOrderHandlers();
|
||
initDatePickers();
|
||
}
|
||
|
||
function initDatePickers() {
|
||
if (typeof flatpickr === "undefined") return;
|
||
if (flatpickr.l10ns && flatpickr.l10ns.cs) flatpickr.localize(flatpickr.l10ns.cs);
|
||
document.querySelectorAll(".fp-date").forEach((el) => {
|
||
if (el._flatpickr) return;
|
||
flatpickr(el, {
|
||
dateFormat: "d.m.Y", // matches the PDF / store format
|
||
allowInput: true,
|
||
disableMobile: true,
|
||
onChange: () => {
|
||
state.order[el.dataset.order] = el.value;
|
||
el.classList.toggle("is-empty", el.value === "");
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderItemRow(item, i) {
|
||
return `
|
||
<tr data-row="${i}">
|
||
<td><input data-field="name" type="text" value="${escapeHtmlAttr(item.name || "")}"></td>
|
||
<td><input data-field="supplier_id" type="text" value="${escapeHtmlAttr(item.supplier_id || "")}"></td>
|
||
<td class="num"><input data-field="quantity" type="text" value="${fmtNum(item.quantity)}"></td>
|
||
<td><input data-field="unit" type="text" value="${escapeHtmlAttr(item.unit || "")}"></td>
|
||
<td class="num"><input data-field="unit_price" type="text" value="${fmtNum(item.unit_price)}"></td>
|
||
<td class="num"><input data-field="total_price" type="text" value="${fmtNum(item.total_price)}"></td>
|
||
<td class="inv-row-actions"><button class="inv-row-delete" type="button" data-action="delete-item" title="Smazat">×</button></td>
|
||
</tr>`;
|
||
}
|
||
|
||
function bindFieldHandlers() {
|
||
$("offer-form").querySelectorAll("input[data-key]").forEach((el) => {
|
||
el.addEventListener("input", () => {
|
||
const key = el.dataset.key;
|
||
const parent = el.dataset.parent;
|
||
if (parent) {
|
||
if (!state.data[parent]) state.data[parent] = {};
|
||
state.data[parent][key] = el.value;
|
||
} else {
|
||
state.data[key] = el.value;
|
||
}
|
||
el.classList.toggle("is-empty", el.value === "");
|
||
});
|
||
});
|
||
|
||
const itemsBody = $("items-body");
|
||
if (itemsBody) {
|
||
itemsBody.addEventListener("input", (e) => {
|
||
const inp = e.target.closest("input");
|
||
if (!inp) return;
|
||
const i = +inp.closest("tr").dataset.row;
|
||
const field = inp.dataset.field;
|
||
if (!state.data.line_items) state.data.line_items = [];
|
||
if (!state.data.line_items[i]) state.data.line_items[i] = {};
|
||
const numFields = ["quantity", "unit_price", "total_price"];
|
||
state.data.line_items[i][field] =
|
||
numFields.includes(field) ? parseNum(inp.value) : inp.value;
|
||
});
|
||
itemsBody.addEventListener("click", (e) => {
|
||
const btn = e.target.closest('[data-action="delete-item"]');
|
||
if (!btn) return;
|
||
const i = +btn.closest("tr").dataset.row;
|
||
state.data.line_items.splice(i, 1);
|
||
renderForm();
|
||
});
|
||
}
|
||
const addItem = $("add-item-btn");
|
||
if (addItem) {
|
||
addItem.addEventListener("click", () => {
|
||
if (!state.data.line_items) state.data.line_items = [];
|
||
state.data.line_items.push({name: "", supplier_id: "", quantity: null,
|
||
unit: "", unit_price: null, total_price: null});
|
||
renderForm();
|
||
});
|
||
}
|
||
}
|
||
|
||
function parseNum(s) {
|
||
if (s === "" || s == null) return null;
|
||
const n = parseFloat(String(s).replace(",", ".").replace(/\s/g, ""));
|
||
return Number.isFinite(n) ? n : null;
|
||
}
|
||
function fmtNum(v) {
|
||
if (v === null || v === undefined || v === "") return "";
|
||
return String(v);
|
||
}
|
||
|
||
// ── Export ──
|
||
$("export-btn").addEventListener("click", async () => {
|
||
if (!state.jobId) return;
|
||
await fetch(`api/save/${state.jobId}`, {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({data: state.data}),
|
||
}).catch(() => {});
|
||
window.location.href = `api/export/${state.jobId}`;
|
||
});
|
||
|
||
$("restart-btn").addEventListener("click", () => {
|
||
state = { jobId: null, data: null };
|
||
fileInput.value = "";
|
||
$("offer-form").innerHTML = "";
|
||
show("upload");
|
||
});
|
||
|
||
// ── Objednávka (order form) ──────────────────────────────────
|
||
function sumTotals() {
|
||
return (state.data.line_items || []).reduce(
|
||
(a, it) => a + (Number(it.total_price) || 0), 0);
|
||
}
|
||
|
||
async function initOrder() {
|
||
const d = state.data;
|
||
const offerNo = d.offer_number || "";
|
||
state.order = {
|
||
objednavka_number: "",
|
||
kod_zakazky: "",
|
||
nazev_zakazky: "",
|
||
dodavatel: d.supplier || {},
|
||
text: "Objednáváme u vás zboží dle cenové nabídky " + offerNo,
|
||
prilohy: offerNo,
|
||
termin_dodani_text: "Co nejdříve",
|
||
misto_dodani: "",
|
||
kontaktni_osoba: "",
|
||
splatnost: "dle VNP nebo VSP",
|
||
zaruka: "",
|
||
poznamka: "",
|
||
vystavil: "",
|
||
popis: "",
|
||
zpusob_platby: "Bankovním převodem",
|
||
zpusob_odberu: "-",
|
||
zpusob_dopravy: "-",
|
||
forma_objednavky: "mailem",
|
||
termin_dodani: "",
|
||
fakturujte: "s DPH",
|
||
};
|
||
state.projects = [];
|
||
state.orderHistory = {};
|
||
try {
|
||
const r = await fetch("api/projects");
|
||
if (r.ok) {
|
||
const j = await r.json();
|
||
state.projects = j.projects || [];
|
||
if (j.next_order_number) state.order.objednavka_number = j.next_order_number;
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
// Body fields. `hist` = remembered per project, shown with a history
|
||
// quick-pick (most-used first).
|
||
const ORDER_FIELDS = [
|
||
["text", "Text", true, false],
|
||
["termin_dodani_text", "Termín dodání (text)", false, false],
|
||
["misto_dodani", "Místo dodání", true, true],
|
||
["kontaktni_osoba", "Kontaktní osoba v místě dodání", true, true],
|
||
["splatnost", "Splatnost", false, true],
|
||
["zaruka", "Záruka", false, true],
|
||
["poznamka", "Poznámka", false, false],
|
||
["prilohy", "Přílohy", false, false],
|
||
["vystavil", "Vystavil", false, true],
|
||
["popis", "Popis", false, true],
|
||
];
|
||
|
||
function fieldHtml(key, label, wide, hist) {
|
||
return hist ? historyField(key, label, wide) : textField(key, label, wide);
|
||
}
|
||
|
||
// ── date helpers (PDF/store use DD.MM.YYYY, <input type=date> uses ISO) ──
|
||
function toDateInput(s) {
|
||
s = (s == null ? "" : String(s)).trim();
|
||
let m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||
if (m) return `${m[3]}-${m[2].padStart(2,"0")}-${m[1].padStart(2,"0")}`;
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
|
||
return "";
|
||
}
|
||
function fromDateInput(s) {
|
||
const m = (s || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
return m ? `${+m[3]}.${+m[2]}.${m[1]}` : (s || "");
|
||
}
|
||
|
||
// Combobox: type freely OR pick a previously-used value from a themed
|
||
// dropdown attached to the field; each option can be deleted from history.
|
||
function comboMenuHtml(key, filter) {
|
||
const hist = (state.orderHistory && state.orderHistory[key]) || [];
|
||
const f = (filter || "").toLowerCase();
|
||
const items = hist.filter((h) => !f || String(h.value).toLowerCase().includes(f));
|
||
if (!items.length) return '<div class="combo-empty">Žádné dříve použité</div>';
|
||
return items.map((h) => `
|
||
<div class="combo-opt" data-val="${escapeHtmlAttr(h.value)}">
|
||
<span class="combo-opt-text">${escapeHtml(h.value)}
|
||
<span class="combo-opt-count">${h.count}×</span></span>
|
||
<button class="combo-del" type="button" title="Smazat z historie">×</button>
|
||
</div>`).join("");
|
||
}
|
||
|
||
function historyField(key, label, wide) {
|
||
const v = state.order[key];
|
||
const cur = v == null ? "" : String(v);
|
||
const hasHist = ((state.orderHistory && state.orderHistory[key]) || []).length > 0;
|
||
return `
|
||
<div class="inv-field ${wide ? "inv-field-wide" : ""}">
|
||
<label class="inv-label">${escapeHtml(label)}</label>
|
||
<div class="combo-wrap" data-combo="${key}">
|
||
<input class="inv-input combo-input${cur === "" ? " is-empty" : ""}" data-order="${key}"
|
||
type="text" autocomplete="off" value="${escapeHtmlAttr(cur)}">
|
||
${hasHist ? '<button class="combo-toggle" type="button" tabindex="-1" aria-label="Dříve použité">⌄</button>' : ""}
|
||
<div class="combo-menu hidden">${comboMenuHtml(key)}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// Dodací a platební podmínky — selectable presets (editable: type or pick).
|
||
const TERMS_FIELDS = [
|
||
["zpusob_platby", "Způsob platby", ["Bankovním převodem", "Hotově", "Kartou", "Zálohově", "Dobírka"]],
|
||
["forma_objednavky", "Forma objednávky", ["mailem", "telefonicky", "osobně", "písemně"]],
|
||
["zpusob_odberu", "Způsob odběru", ["-", "Osobní odběr", "Dodání na stavbu", "Dopravcem"]],
|
||
["termin_dodani", "Termín dodání", []],
|
||
["zpusob_dopravy", "Způsob dopravy", ["-", "Dodavatel", "Vlastní", "Dopravce"]],
|
||
["fakturujte", "Fakturujte", ["s DPH", "bez DPH"]],
|
||
];
|
||
|
||
function textField(key, label, wide) {
|
||
const v = state.order[key];
|
||
const isEmpty = v === null || v === undefined || v === "";
|
||
return `
|
||
<div class="inv-field ${wide ? "inv-field-wide" : ""}">
|
||
<label class="inv-label">${escapeHtml(label)}</label>
|
||
<input class="inv-input${isEmpty ? " is-empty" : ""}" data-order="${key}"
|
||
type="text" value="${escapeHtmlAttr(isEmpty ? "" : v)}">
|
||
</div>`;
|
||
}
|
||
|
||
function comboField(key, label, options) {
|
||
const v = state.order[key];
|
||
const cur = v == null ? "" : String(v);
|
||
// Termín dodání → flatpickr date picker (value stays DD.MM.YYYY).
|
||
if (key === "termin_dodani") {
|
||
return `
|
||
<div class="inv-field">
|
||
<label class="inv-label">${escapeHtml(label)}</label>
|
||
<input class="inv-input fp-date${cur === "" ? " is-empty" : ""}" data-order="${key}"
|
||
type="text" placeholder="dd.mm.rrrr" value="${escapeHtmlAttr(cur)}">
|
||
</div>`;
|
||
}
|
||
// Real dropdown: history (most-used first) + presets + any custom value.
|
||
const histVals = ((state.orderHistory && state.orderHistory[key]) || [])
|
||
.map((h) => h.value);
|
||
const vals = [];
|
||
for (const x of [...histVals, ...(options || []), cur]) {
|
||
if (x && !vals.includes(x)) vals.push(x);
|
||
}
|
||
const opts = ['<option value=""></option>']
|
||
.concat(vals.map((o) =>
|
||
`<option value="${escapeHtmlAttr(o)}"${o === cur ? " selected" : ""}>${escapeHtml(o)}</option>`))
|
||
.join("");
|
||
return `
|
||
<div class="inv-field">
|
||
<label class="inv-label">${escapeHtml(label)}</label>
|
||
<select class="inv-input" data-order="${key}">${opts}</select>
|
||
</div>`;
|
||
}
|
||
|
||
function projectOptions() {
|
||
const cur = state.order.kod_zakazky || "";
|
||
const opts = ['<option value="">— vyberte zakázku —</option>'];
|
||
for (const p of state.projects || []) {
|
||
const sel = p.kod === cur ? " selected" : "";
|
||
const label = p.nazev ? `${p.kod} — ${p.nazev}` : p.kod;
|
||
opts.push(`<option value="${escapeHtmlAttr(p.kod)}"${sel}>${escapeHtml(label)}</option>`);
|
||
}
|
||
return opts.join("");
|
||
}
|
||
|
||
function orderCardHtml() {
|
||
return `
|
||
<div class="inv-card">
|
||
<div class="inv-card-title">Objednávka (vyplní se do PDF)</div>
|
||
<p class="order-hint">Zakázku vyberte z nabídky (nebo přidejte přes
|
||
<strong>Spravovat zakázky</strong>). Pole jako <strong>Místo dodání</strong>,
|
||
kontaktní osoba či Vystavil se pamatují podle zakázky. Číslo objednávky
|
||
se předvyplní jako poslední + 1. Cena celkem z položek:
|
||
${fmtCzk(sumTotals())}.</p>
|
||
<div class="inv-grid">
|
||
${textField("objednavka_number", "Číslo objednávky", false)}
|
||
<div class="inv-field">
|
||
<label class="inv-label">Kód zakázky / NS</label>
|
||
<div class="proj-row">
|
||
<select class="inv-input" id="proj-select">${projectOptions()}</select>
|
||
<button class="btn btn-secondary btn-sm" id="proj-manage" type="button">Spravovat</button>
|
||
</div>
|
||
</div>
|
||
${textField("nazev_zakazky", "Název zakázky", true)}
|
||
${ORDER_FIELDS.map(([k, l, w, h]) => fieldHtml(k, l, w, h)).join("")}
|
||
</div>
|
||
<div class="terms-subtitle">Dodací a platební podmínky</div>
|
||
<div class="inv-grid">
|
||
${TERMS_FIELDS.map(([k, l, o]) => comboField(k, l, o)).join("")}
|
||
</div>
|
||
<div class="proj-panel hidden" id="proj-panel"></div>
|
||
</div>`;
|
||
}
|
||
|
||
function fmtCzk(n) {
|
||
return (Number(n) || 0).toLocaleString("cs-CZ", {minimumFractionDigits: 2, maximumFractionDigits: 2}) + " Kč";
|
||
}
|
||
|
||
function applyOrderToInputs() {
|
||
document.querySelectorAll('[data-order]').forEach((inp) => {
|
||
const k = inp.dataset.order;
|
||
if (state.order[k] != null) {
|
||
inp.value = state.order[k];
|
||
inp.classList.toggle("is-empty", inp.value === "");
|
||
}
|
||
});
|
||
document.querySelectorAll('[data-order-date]').forEach((inp) => {
|
||
const k = inp.dataset.orderDate;
|
||
inp.value = toDateInput(state.order[k]);
|
||
});
|
||
}
|
||
|
||
async function loadProjectDefaults(code) {
|
||
if (!code) { state.orderHistory = {}; return; }
|
||
try {
|
||
const r = await fetch(`api/order-defaults?code=${encodeURIComponent(code)}`);
|
||
if (!r.ok) return;
|
||
const { defaults } = await r.json();
|
||
if (defaults && Object.keys(defaults).length) {
|
||
state.orderHistory = defaults._history || {};
|
||
delete defaults._history;
|
||
Object.assign(state.order, defaults);
|
||
// Re-render so history dropdowns reflect the selected project.
|
||
rerenderOrderCard();
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
function renderProjPanel() {
|
||
const rows = (state.projects || []).map((p) => `
|
||
<div class="proj-item" data-kod="${escapeHtmlAttr(p.kod)}">
|
||
<span class="proj-kod">${escapeHtml(p.kod)}</span>
|
||
<input class="inv-input proj-nazev" value="${escapeHtmlAttr(p.nazev || "")}" placeholder="Název zakázky">
|
||
<button class="btn btn-secondary btn-sm proj-save" type="button">Uložit</button>
|
||
<button class="btn btn-secondary btn-sm proj-del" type="button">Smazat</button>
|
||
</div>`).join("");
|
||
return `
|
||
<div class="proj-add">
|
||
<input class="inv-input" id="proj-new-kod" placeholder="Číslo zakázky (kód/NS)">
|
||
<input class="inv-input" id="proj-new-nazev" placeholder="Název zakázky">
|
||
<button class="btn btn-primary btn-sm" id="proj-add-btn" type="button">Přidat</button>
|
||
</div>
|
||
<div class="proj-list">${rows || '<p class="order-hint">Zatím žádné zakázky.</p>'}</div>`;
|
||
}
|
||
|
||
async function refreshProjects(list) {
|
||
state.projects = list || state.projects;
|
||
const sel = $("proj-select");
|
||
if (sel) sel.innerHTML = projectOptions();
|
||
const panel = $("proj-panel");
|
||
if (panel && !panel.classList.contains("hidden")) panel.innerHTML = renderProjPanel();
|
||
}
|
||
|
||
function bindOrderHandlers() {
|
||
document.querySelectorAll('[data-order]').forEach((el) => {
|
||
const upd = () => {
|
||
state.order[el.dataset.order] = el.value;
|
||
el.classList.toggle("is-empty", el.value === "");
|
||
};
|
||
el.addEventListener("input", upd);
|
||
el.addEventListener("change", upd); // <select> fires change
|
||
});
|
||
// Date pickers store DD.MM.YYYY in the order.
|
||
document.querySelectorAll('[data-order-date]').forEach((el) => {
|
||
el.addEventListener("change", () => {
|
||
state.order[el.dataset.orderDate] = fromDateInput(el.value);
|
||
el.classList.toggle("is-empty", el.value === "");
|
||
});
|
||
});
|
||
// Combobox fields: open menu on focus/toggle, filter on type, pick or
|
||
// delete a previously-used value.
|
||
document.querySelectorAll(".combo-wrap").forEach((wrap) => {
|
||
const key = wrap.dataset.combo;
|
||
const input = wrap.querySelector(".combo-input");
|
||
const menu = wrap.querySelector(".combo-menu");
|
||
const toggle = wrap.querySelector(".combo-toggle");
|
||
const open = () => { menu.innerHTML = comboMenuHtml(key, input.value); menu.classList.remove("hidden"); };
|
||
const close = () => menu.classList.add("hidden");
|
||
if (toggle) toggle.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
menu.classList.contains("hidden") ? open() : close();
|
||
});
|
||
input.addEventListener("focus", open);
|
||
input.addEventListener("input", open); // value already synced by generic handler
|
||
menu.addEventListener("click", async (e) => {
|
||
const del = e.target.closest(".combo-del");
|
||
if (del) {
|
||
e.stopPropagation();
|
||
const val = del.closest(".combo-opt").dataset.val;
|
||
const code = state.order.kod_zakazky;
|
||
if (!code) { menu.querySelector(`[data-val="${CSS.escape(val)}"]`)?.remove(); return; }
|
||
try {
|
||
const r = await fetch("api/order-history/delete", {
|
||
method: "POST", headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({ code, field: key, value: val }),
|
||
});
|
||
if (r.ok) {
|
||
state.orderHistory = (await r.json()).defaults._history || {};
|
||
menu.innerHTML = comboMenuHtml(key, input.value);
|
||
}
|
||
} catch (_) {}
|
||
return;
|
||
}
|
||
const opt = e.target.closest(".combo-opt");
|
||
if (opt) {
|
||
input.value = opt.dataset.val;
|
||
input.classList.remove("is-empty");
|
||
state.order[key] = opt.dataset.val;
|
||
close();
|
||
}
|
||
});
|
||
});
|
||
|
||
const sel = $("proj-select");
|
||
if (sel) sel.addEventListener("change", async () => {
|
||
const code = sel.value;
|
||
state.order.kod_zakazky = code;
|
||
const p = (state.projects || []).find((x) => x.kod === code);
|
||
state.order.nazev_zakazky = p ? (p.nazev || "") : "";
|
||
applyOrderToInputs();
|
||
await loadProjectDefaults(code);
|
||
});
|
||
|
||
const manage = $("proj-manage");
|
||
if (manage) manage.addEventListener("click", () => {
|
||
const panel = $("proj-panel");
|
||
panel.classList.toggle("hidden");
|
||
if (!panel.classList.contains("hidden")) {
|
||
panel.innerHTML = renderProjPanel();
|
||
panel.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
}
|
||
});
|
||
|
||
const panel = $("proj-panel");
|
||
if (panel) {
|
||
panel.addEventListener("click", async (e) => {
|
||
const addBtn = e.target.closest("#proj-add-btn");
|
||
if (addBtn) {
|
||
const kod = $("proj-new-kod").value.trim();
|
||
const nazev = $("proj-new-nazev").value.trim();
|
||
if (!kod) { alert("Zadejte číslo zakázky."); return; }
|
||
const r = await fetch("api/projects", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({kod, nazev})});
|
||
if (r.ok) { refreshProjects((await r.json()).projects); }
|
||
return;
|
||
}
|
||
const item = e.target.closest(".proj-item");
|
||
if (!item) return;
|
||
const kod = item.dataset.kod;
|
||
if (e.target.closest(".proj-save")) {
|
||
const nazev = item.querySelector(".proj-nazev").value.trim();
|
||
const r = await fetch("api/projects", {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({kod, nazev})});
|
||
if (r.ok) refreshProjects((await r.json()).projects);
|
||
} else if (e.target.closest(".proj-del")) {
|
||
if (!confirm(`Smazat zakázku ${kod}?`)) return;
|
||
const r = await fetch(`api/projects/${encodeURIComponent(kod)}`, {method:"DELETE"});
|
||
if (r.ok) refreshProjects((await r.json()).projects);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
$("order-pdf-btn").addEventListener("click", async () => {
|
||
if (!state.jobId) return;
|
||
const btn = $("order-pdf-btn");
|
||
const old = btn.textContent;
|
||
btn.disabled = true; btn.textContent = "Generuji…";
|
||
try {
|
||
// keep supplier in sync as dodavatel
|
||
state.order.dodavatel = state.data.supplier || {};
|
||
state.order.cena_celkem = sumTotals();
|
||
const r = await fetch(`api/order-pdf/${state.jobId}`, {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify({ order: state.order, items: state.data.line_items || [] }),
|
||
});
|
||
if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
|
||
const cd = r.headers.get("content-disposition") || "";
|
||
const m = cd.match(/filename="?([^"]+)"?/);
|
||
const blob = await r.blob();
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = m ? m[1] : "objednavka.pdf";
|
||
document.body.appendChild(a); a.click(); a.remove();
|
||
setTimeout(() => URL.revokeObjectURL(a.href), 60000);
|
||
} catch (err) {
|
||
alert("Chyba: " + err.message);
|
||
} finally {
|
||
btn.disabled = false; btn.textContent = old;
|
||
}
|
||
});
|
||
|
||
function escapeHtml(s) {
|
||
return String(s ?? "").replace(/[&<>"']/g, (c) =>
|
||
({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
||
}
|
||
function escapeHtmlAttr(s) { return escapeHtml(s); }
|
||
})();
|