Kufrik/app/UI/FakturaEdit/edit.latte

840 lines
38 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{block content}
<!-- Načtení SortableJS pro Drag & Drop -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
<div class="container-fluid">
<div class="content-card my-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h4 mb-0">
<i class="bi {if $isEdit}bi-pencil-square{else}bi-file-earmark-plus-fill{/if} me-2"></i>
{if $isEdit}Úprava faktury{else}Nová faktura{/if}
</h2>
<a n:href="Faktura:default" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Zpět na seznam
</a>
</div>
<form n:name="fakturaForm" id="faktura-form" class="row g-3">
<div n:if="$form->hasErrors()" class="col-12">
<div class="alert alert-danger mb-0">
<ul class="mb-0">
<li n:foreach="$form->errors as $error">{$error}</li>
</ul>
</div>
</div>
<!-- Základní informace -->
<div class="col-12">
<h5 class="mb-3"><i class="bi bi-info-circle me-2"></i>Základní informace</h5>
</div>
<div class="col-md-6">
<label n:name="CISLO" class="form-label fw-bold">Číslo faktury *</label>
<input n:name="CISLO" class="form-control">
<input n:name="CISLO_ABS">
</div>
<div class="col-md-6">
<label n:name="ZAKAZNIK_ID" class="form-label fw-bold">Zákazník *</label>
<select n:name="ZAKAZNIK_ID" class="form-select" data-suggest="zakaznik" {if $isEdit}data-locked="1"{/if}></select>
</div>
<div class="col-md-4">
<label n:name="DATUM_VYSTAVENI" class="form-label">Datum vystavení</label>
<input n:name="DATUM_VYSTAVENI" class="form-control">
</div>
<div class="col-md-4">
<label n:name="FORMA_UHRADY" class="form-label">Forma úhrady</label>
<select n:name="FORMA_UHRADY" class="form-select"></select>
</div>
<div class="col-md-4">
<label n:name="DATUM_SPLATNOSTI" class="form-label">Datum splatnosti</label>
<input n:name="DATUM_SPLATNOSTI" class="form-control">
</div>
<div class="col-md-4" n:if="$isEdit">
<label class="form-label">Stav úhrady</label>
<div class="form-control bg-light">
<span class="badge {if $stavUhrady === 'PAID'}bg-success{elseif $stavUhrady === 'PARTIAL'}bg-info text-dark{else}bg-warning text-dark{/if}">
{if $stavUhrady === 'PAID'}Zaplaceno{elseif $stavUhrady === 'PARTIAL'}Částečně uhrazeno{else}Neuhrazeno{/if}
</span>
<span class="ms-2 text-muted">zbývá {$zbyva|number:2, ',', ' '} CZK</span>
</div>
</div>
<!-- Detailní informace (rozbalovací) -->
<div class="col-12 mt-2">
<div class="border rounded">
<button class="btn btn-light w-100 text-start d-flex justify-content-between align-items-center px-3 py-2"
type="button"
data-bs-toggle="collapse"
data-bs-target="#detailniInformace"
aria-expanded="false"
aria-controls="detailniInformace">
<span class="fw-semibold"><i class="bi bi-person-vcard me-2"></i>Detailní informace</span>
<i class="bi bi-chevron-down"></i>
</button>
<div class="collapse" id="detailniInformace">
<div class="p-3 border-top">
<div class="row g-3">
<div class="col-md-6">
<h6 class="text-muted mb-3">Dodavatel</h6>
<div class="mb-2">
<label n:name="D_PRAJM" class="form-label small mb-1">Dodavatel</label>
<input n:name="D_PRAJM" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label n:name="D_ULICE" class="form-label small mb-1">Ulice</label>
<input n:name="D_ULICE" class="form-control form-control-sm">
</div>
<div class="row g-2 mb-2">
<div class="col-4">
<label n:name="D_PSC" class="form-label small mb-1">PSČ</label>
<input n:name="D_PSC" class="form-control form-control-sm">
</div>
<div class="col-8">
<label n:name="D_MESTO" class="form-label small mb-1">Město</label>
<input n:name="D_MESTO" class="form-control form-control-sm">
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-6">
<label n:name="D_IC" class="form-label small mb-1">IČ</label>
<input n:name="D_IC" class="form-control form-control-sm">
</div>
<div class="col-6">
<label n:name="D_DIC" class="form-label small mb-1">DIČ</label>
<input n:name="D_DIC" class="form-control form-control-sm">
</div>
</div>
<div class="mb-2">
<label n:name="D_TELEFON" class="form-label small mb-1">Telefon</label>
<input n:name="D_TELEFON" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label n:name="D_EMAIL" class="form-label small mb-1">E-mail</label>
<input n:name="D_EMAIL" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label n:name="D_BANKA" class="form-label small mb-1">Bankovní účet</label>
<input n:name="D_BANKA" class="form-control form-control-sm">
</div>
<div class="mb-0">
<label n:name="D_WEB" class="form-label small mb-1">Web</label>
<input n:name="D_WEB" class="form-control form-control-sm">
</div>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-3">Odběratel</h6>
<div class="mb-2">
<label n:name="O_PRAJM" class="form-label small mb-1">Odběratel</label>
<input n:name="O_PRAJM" class="form-control form-control-sm">
</div>
<div class="mb-2">
<label n:name="O_ULICE" class="form-label small mb-1">Ulice</label>
<input n:name="O_ULICE" class="form-control form-control-sm">
</div>
<div class="row g-2 mb-2">
<div class="col-4">
<label n:name="O_PSC" class="form-label small mb-1">PSČ</label>
<input n:name="O_PSC" class="form-control form-control-sm">
</div>
<div class="col-8">
<label n:name="O_MESTO" class="form-label small mb-1">Město</label>
<input n:name="O_MESTO" class="form-control form-control-sm">
</div>
</div>
<div class="row g-2 mb-2">
<div class="col-6">
<label n:name="O_IC" class="form-label small mb-1">IČ</label>
<input n:name="O_IC" class="form-control form-control-sm">
</div>
<div class="col-6">
<label n:name="O_DIC" class="form-label small mb-1">DIČ</label>
<input n:name="O_DIC" class="form-control form-control-sm">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<hr class="my-4">
<!-- Položky Faktury -->
<div class="col-12">
<h5 class="mb-3"><i class="bi bi-list-check me-2"></i>Položky faktury</h5>
<div class="table-responsive">
<table class="table table-bordered table-cards align-middle" id="items-table">
<thead class="table-light">
<tr>
<th style="width: 40px;" class="text-center"></th> <!-- Sloupec pro Drag Handle -->
<th>Název položky / služby</th>
<th style="width: 100px;">Počet</th>
<th style="width: 90px;">Jedn.</th>
<th style="width: 130px;">Cena/Jedn.</th>
<th style="width: 100px;">Sleva %</th>
<th style="width: 170px;" class="text-end">Celkem</th>
<th style="width: 50px;"></th>
</tr>
</thead>
<tbody id="items-body">
{foreach $polozky as $index => $p}
<tr class="item-row">
<td data-label="Pořadí" class="text-center align-middle drag-handle" style="cursor: grab;">
<i class="bi bi-grip-vertical text-muted fs-5"></i>
</td>
<td data-label="Název"><input type="text" name="polozky[{$index}][NAZEV]" value="{$p->NAZEV}" class="form-control item-nazev" placeholder="Začněte psát název práce nebo zboží" required autocomplete="off"></td>
<td data-label="Počet"><input type="number" step="any" name="polozky[{$index}][POCET]" value="{$p->POCET}" class="form-control item-pocet text-end" required></td>
<td data-label="Jedn."><input type="text" name="polozky[{$index}][JEDNOTKA]" value="{$p->JEDNOTKA}" class="form-control"></td>
<td data-label="Cena/Jedn."><input type="number" step="0.01" name="polozky[{$index}][CENA_JEDNOTKA]" value="{$p->CENA_JEDNOTKA}" class="form-control item-cena text-end" required></td>
<td data-label="Sleva %"><input type="number" step="0.1" name="polozky[{$index}][SLEVA]" value="{$p->SLEVA}" class="form-control item-sleva text-end"></td>
<td data-label="Celkem"><input type="number" step="0.01" value="{$p->CENA}" class="form-control item-celkem text-end fw-bold" title="Úprava přepočítá počet"></td>
<td class="text-center cell-actions">
<button type="button" class="btn btn-outline-danger btn-sm btn-remove-row"><i class="bi bi-trash"></i></button>
</td>
</tr>
{/foreach}
</tbody>
<tfoot>
<tr>
<td colspan="6" class="text-end fw-bold fs-5 cell-span">Celková částka:</td>
<td colspan="2" class="text-end fw-bold fs-5 text-primary text-nowrap" id="total-sum">0,00 CZK</td>
</tr>
</tfoot>
</table>
</div>
<button type="button" class="btn btn-outline-success btn-sm" id="btn-add-item">
<i class="bi bi-plus-circle me-1"></i>Přidat položku
</button>
</div>
<hr class="my-4">
<div class="col-12">
<label n:name="CINNOSTI" class="form-label fw-bold">Činnosti / Popis plnění</label>
<textarea n:name="CINNOSTI" class="form-control" rows="3" placeholder="Rozsah vykonaných prací..."></textarea>
</div>
<div class="col-12 mt-4">
<button n:name="save" class="btn btn-primary me-2">
<i class="bi bi-check-lg me-1"></i>Uložit fakturu
</button>
<a n:href="Faktura:default" class="btn btn-outline-secondary">Zrušit</a>
</div>
</form>
<div n:if="$isEdit" id="uhrady" class="mt-4 pt-3 border-top">
<h5 class="mb-3"><i class="bi bi-cash-coin me-2"></i>Úhrady</h5>
<p class="text-muted small">
Celkem {$celkemFaktury|number:2, ',', ' '} CZK,
uhrazeno {$zaplaceno|number:2, ',', ' '} CZK,
zbývá {$zbyva|number:2, ',', ' '} CZK.
</p>
<form n:name="platbyForm">
<div n:if="$presenter['platbyForm']->hasErrors()" class="alert alert-danger">
<ul class="mb-0">
<li n:foreach="$presenter['platbyForm']->errors as $error">{$error}</li>
</ul>
</div>
<div class="table-responsive mb-3">
<table class="table table-sm table-cards align-middle">
<thead class="table-light">
<tr>
<th style="width: 160px;">Datum</th>
<th style="width: 160px;" class="text-end">Částka</th>
<th>Poznámka</th>
<th style="width: 50px;"></th>
</tr>
</thead>
<tbody>
<tr n:if="count($platby) === 0">
<td colspan="4" class="text-muted cell-span">Zatím žádná platba.</td>
</tr>
<tr n:foreach="$form['platby']->getComponents() as $platbaId => $row">
<td data-label="Datum">{input $row['datum'] class: 'form-control'}</td>
<td data-label="Částka">{input $row['castka'] class: 'form-control text-end'}</td>
<td data-label="Poznámka">{input $row['poznamka'] class: 'form-control'}</td>
<td class="text-end cell-actions">
<a n:href="smazatPlatbu! platbaId => $platbaId"
class="btn btn-sm btn-outline-danger"
title="Smazat platbu"
onclick="return confirm('Opravdu smazat tuto platbu?');">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
<tr>
<td data-label="Datum">
{input $form['nova']['datum'] class: 'form-control'}
</td>
<td data-label="Částka">
{input $form['nova']['castka'] class: 'form-control text-end'}
</td>
<td data-label="Poznámka">
{input $form['nova']['poznamka'] class: 'form-control', placeholder: 'Nová platba'}
</td>
<td class="cell-actions"></td>
</tr>
</tbody>
</table>
</div>
<div class="mb-3">
<button type="button" class="btn btn-sm btn-outline-secondary me-1" id="edit-platba-dnes" data-zbyva="{$zbyvaInput}">Dnes</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="edit-platba-splatnost" data-date="{$splatnostIso}" data-zbyva="{$zbyvaInput}">Den splatnosti</button>
</div>
<button n:name="save" class="btn btn-success">
<i class="bi bi-check-lg me-1"></i>Uložit platby
</button>
</form>
</div>
</div>
</div>
<script n:if="$isEdit">
function prefillPlatba(date, zbyva) {
if (date) {
document.getElementById('edit-platba-datum').value = date;
}
const castka = Number(zbyva);
document.getElementById('edit-platba-castka').value = castka > 0 ? castka.toFixed(2) : '';
}
document.getElementById('edit-platba-dnes').addEventListener('click', function () {
const d = new Date();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
prefillPlatba(d.getFullYear() + '-' + m + '-' + day, this.getAttribute('data-zbyva'));
});
document.getElementById('edit-platba-splatnost').addEventListener('click', function () {
prefillPlatba(this.getAttribute('data-date'), this.getAttribute('data-zbyva'));
});
</script>
<script type="application/json" id="katalog-polozek">{$katalogPolozekJson|noescape}</script>
<script type="application/json" id="zakaznici-detail">{$zakazniciDetailJson|noescape}</script>
{syntax off}
<script>
document.addEventListener('DOMContentLoaded', () => {
let rowIndex = document.querySelectorAll('.item-row').length;
const tbody = document.getElementById('items-body');
const btnAdd = document.getElementById('btn-add-item');
// 1. Inicializace SortableJS na <tbody>
if (typeof Sortable !== 'undefined') {
new Sortable(tbody, {
handle: '.drag-handle', // Přetahovat lze uchopením za ikonu
animation: 150,
ghostClass: 'table-active', // Zvýraznění přetahovaného řádku
onEnd: function () {
reindexRows();
calculateTotals();
}
});
}
// 2. Přereindexování name atributů podle nového pořadí řádků v DOMu
function reindexRows() {
document.querySelectorAll('.item-row').forEach((row, index) => {
row.querySelectorAll('input').forEach(input => {
const name = input.getAttribute('name');
if (name) {
input.setAttribute('name', name.replace(/polozky\[\d+\]/, `polozky[${index}]`));
}
});
});
}
// 3. Výpočet součtů (celkem ← počet × cena × (1 − sleva))
function roundMoney(value) {
return Math.round((value + Number.EPSILON) * 100) / 100;
}
function roundPocet(value) {
return Math.round((value + Number.EPSILON) * 10000) / 10000;
}
function rowCelkem(row) {
const pocet = parseFloat(row.querySelector('.item-pocet').value) || 0;
const cena = parseFloat(row.querySelector('.item-cena').value) || 0;
const sleva = parseFloat(row.querySelector('.item-sleva').value) || 0;
return (pocet * cena) * (1 - (sleva / 100));
}
function calculateTotals() {
let grandTotal = 0;
document.querySelectorAll('.item-row').forEach(row => {
const celkem = rowCelkem(row);
grandTotal += celkem;
const celkemInput = row.querySelector('.item-celkem');
if (celkemInput && document.activeElement !== celkemInput) {
celkemInput.value = roundMoney(celkem).toFixed(2);
}
});
document.getElementById('total-sum').textContent = grandTotal.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' CZK';
}
// Úprava celkově: přepočítá počet, cena/jedn. a sleva zůstávají
function recalculatePocetFromCelkem(row) {
const celkem = parseFloat(row.querySelector('.item-celkem').value) || 0;
const cena = parseFloat(row.querySelector('.item-cena').value) || 0;
const sleva = parseFloat(row.querySelector('.item-sleva').value) || 0;
const faktor = cena * (1 - (sleva / 100));
const pocetInput = row.querySelector('.item-pocet');
if (!pocetInput || Math.abs(faktor) < 1e-9) {
return;
}
pocetInput.value = String(roundPocet(celkem / faktor));
}
function updateGrandTotalFromInputs() {
let grandTotal = 0;
document.querySelectorAll('.item-row').forEach(row => {
const celkemInput = row.querySelector('.item-celkem');
if (celkemInput && document.activeElement === celkemInput) {
grandTotal += parseFloat(celkemInput.value) || 0;
} else {
grandTotal += rowCelkem(row);
}
});
document.getElementById('total-sum').textContent = grandTotal.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' CZK';
}
function fold(value) {
return String(value).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
}
function formatMoney(value) {
return Number(value).toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function bindSuggest(input, options) {
if (input.dataset.suggestBound) {
return;
}
input.dataset.suggestBound = '1';
input.setAttribute('autocomplete', 'off');
input.setAttribute('role', 'combobox');
input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-expanded', 'false');
const menu = document.createElement('ul');
menu.className = 'suggest-menu';
menu.hidden = true;
menu.setAttribute('role', 'listbox');
document.body.appendChild(menu);
input._suggestMenu = menu;
let activeIndex = -1;
let visible = [];
function closeMenu() {
menu.hidden = true;
input.setAttribute('aria-expanded', 'false');
activeIndex = -1;
}
function placeMenu() {
const rect = input.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
menu.style.left = Math.max(8, rect.left) + 'px';
menu.style.width = Math.max(rect.width, 220) + 'px';
menu.hidden = false;
const height = menu.offsetHeight;
if (spaceBelow < height + 8 && rect.top > spaceBelow) {
menu.style.top = Math.max(8, rect.top - height - 4) + 'px';
} else {
menu.style.top = (rect.bottom + 4) + 'px';
}
}
function render() {
const query = input.value;
const foldedQuery = fold(query).trim();
let matched = options.items.filter(item => {
if (!foldedQuery) {
return true;
}
const hay = fold(item.label + ' ' + (item.meta || ''));
return foldedQuery.split(/\s+/).every(word => hay.includes(word));
});
if (!options.keepOrder) {
matched.sort((a, b) => {
const score = item => {
if (!foldedQuery) {
return 1;
}
const label = fold(item.label);
if (label.startsWith(foldedQuery)) {
return 0;
}
if (label.includes(foldedQuery)) {
return 1;
}
return 2;
};
return score(a) - score(b);
});
}
visible = matched.slice(0, options.limit || 40);
menu.innerHTML = '';
if (!visible.length) {
if (!foldedQuery && options.freeText) {
closeMenu();
return;
}
const empty = document.createElement('li');
empty.className = 'suggest-empty';
empty.textContent = options.emptyText || 'Nic nenalezeno';
menu.appendChild(empty);
activeIndex = -1;
placeMenu();
input.setAttribute('aria-expanded', 'true');
return;
}
visible.forEach((item, index) => {
const li = document.createElement('li');
li.className = 'suggest-option' + (index === activeIndex ? ' is-active' : '');
li.setAttribute('role', 'option');
li.textContent = item.label;
if (item.meta) {
const meta = document.createElement('small');
meta.textContent = item.meta;
li.appendChild(meta);
}
li.addEventListener('mousedown', event => {
event.preventDefault();
choose(index);
});
li.addEventListener('mouseenter', () => {
activeIndex = index;
menu.querySelectorAll('.suggest-option').forEach((node, i) => {
node.classList.toggle('is-active', i === activeIndex);
});
});
menu.appendChild(li);
});
if (activeIndex >= visible.length) {
activeIndex = visible.length - 1;
}
placeMenu();
input.setAttribute('aria-expanded', 'true');
const current = menu.querySelectorAll('.suggest-option')[activeIndex];
if (current) {
current.classList.add('is-active');
current.scrollIntoView({ block: 'nearest' });
}
}
function choose(index) {
const item = visible[index];
if (!item) {
return;
}
input.value = item.label;
closeMenu();
options.onPick(item, input);
}
function acceptExact() {
const typed = input.value.trim();
if (typed === '') {
options.onClear?.(input);
return;
}
const foldedTyped = fold(typed);
const hits = options.items.filter(item => fold(item.label) === foldedTyped);
if (hits.length === 1) {
input.value = hits[0].label;
options.onPick(hits[0], input);
return;
}
options.onClear?.(input);
}
input.addEventListener('focus', () => {
document.querySelectorAll('.suggest-menu').forEach(other => {
if (other !== menu) {
other.hidden = true;
}
});
activeIndex = -1;
render();
});
input.addEventListener('input', () => {
activeIndex = -1;
if (!options.freeText) {
options.onClear?.(input);
}
render();
});
input.addEventListener('keydown', event => {
if (event.key === 'ArrowDown') {
event.preventDefault();
if (menu.hidden) {
render();
}
if (!visible.length) {
return;
}
activeIndex = (activeIndex + 1) % visible.length;
render();
return;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
if (!visible.length) {
return;
}
activeIndex = activeIndex <= 0 ? visible.length - 1 : activeIndex - 1;
render();
return;
}
if (event.key === 'Enter' && !menu.hidden) {
event.preventDefault();
if (activeIndex >= 0) {
choose(activeIndex);
} else if (visible.length === 1) {
choose(0);
}
return;
}
if (event.key === 'Escape') {
closeMenu();
}
});
input.addEventListener('blur', () => {
window.setTimeout(() => {
if (!options.freeText) {
acceptExact();
}
closeMenu();
}, 120);
});
const followInput = event => {
if (menu.hidden || event.target === menu) {
return;
}
placeMenu();
};
window.addEventListener('resize', followInput);
window.addEventListener('scroll', followInput, true);
}
let zakazniciDetail = {};
try {
zakazniciDetail = JSON.parse(document.getElementById('zakaznici-detail').textContent || '{}');
} catch (error) {
zakazniciDetail = {};
}
function fillOdberatel(zakaznikId) {
const detail = zakazniciDetail[String(zakaznikId)];
if (!detail) {
return;
}
const set = (name, value) => {
const el = document.querySelector('[name="' + name + '"]');
if (el) {
el.value = value || '';
}
};
set('O_PRAJM', detail.nazev);
set('O_ULICE', detail.ulice);
set('O_PSC', detail.psc);
set('O_MESTO', detail.mesto);
set('O_IC', detail.ic);
set('O_DIC', detail.dic);
}
function initZakaznikSuggest() {
const select = document.querySelector('select[data-suggest="zakaznik"]');
if (!select) {
return;
}
if (select.dataset.locked === '1') {
return;
}
const items = Array.from(select.options)
.filter(option => option.value !== '')
.map(option => ({
value: option.value,
label: option.textContent.trim(),
}));
const wrap = document.createElement('div');
wrap.className = 'suggest';
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control';
input.placeholder = 'Začněte psát jméno, město nebo IČ';
input.id = select.id ? select.id + '-suggest' : 'zakaznik-suggest';
const label = document.querySelector('label[for="' + select.id + '"]');
if (label) {
label.htmlFor = input.id;
}
select.parentNode.insertBefore(wrap, select);
wrap.appendChild(input);
wrap.appendChild(select);
select.classList.add('suggest-native');
select.setAttribute('aria-hidden', 'true');
select.removeAttribute('required');
select.tabIndex = -1;
const selected = select.selectedOptions[0];
if (select.value && selected) {
input.value = selected.textContent.trim();
}
bindSuggest(input, {
items,
emptyText: 'Žádný zákazník neodpovídá',
onPick(item) {
select.value = item.value;
fillOdberatel(item.value);
},
onClear() {
select.value = '';
},
});
}
// Chevron v rozbalovacím panelu
const detailCollapse = document.getElementById('detailniInformace');
if (detailCollapse) {
const toggleBtn = document.querySelector('[data-bs-target="#detailniInformace"]');
const chevron = toggleBtn?.querySelector('.bi-chevron-down, .bi-chevron-up');
detailCollapse.addEventListener('show.bs.collapse', () => {
chevron?.classList.replace('bi-chevron-down', 'bi-chevron-up');
});
detailCollapse.addEventListener('hide.bs.collapse', () => {
chevron?.classList.replace('bi-chevron-up', 'bi-chevron-down');
});
}
let katalog = [];
try {
katalog = JSON.parse(document.getElementById('katalog-polozek').textContent || '[]');
} catch (error) {
katalog = [];
}
const katalogItems = katalog.map(item => ({
label: item.nazev,
meta: (item.jednotka ? item.jednotka + ' · ' : '') + formatMoney(item.cena) + ' CZK',
payload: item,
}));
function bindPolozkaSuggest(input) {
if (!input) {
return;
}
input.placeholder = input.placeholder || 'Začněte psát název práce nebo zboží';
bindSuggest(input, {
items: katalogItems,
freeText: true,
keepOrder: true,
emptyText: 'V předchozích fakturách nic podobného není',
onPick(item) {
const row = input.closest('.item-row');
if (!row) {
return;
}
const jednotka = row.querySelector('input[name*="[JEDNOTKA]"]');
const cena = row.querySelector('.item-cena');
if (jednotka && item.payload.jednotka) {
jednotka.value = item.payload.jednotka;
}
if (cena) {
cena.value = Number(item.payload.cena).toFixed(2);
}
calculateTotals();
},
});
}
initZakaznikSuggest();
document.querySelectorAll('.item-nazev').forEach(bindPolozkaSuggest);
// Přidání nového řádku
btnAdd.addEventListener('click', () => {
const tr = document.createElement('tr');
tr.className = 'item-row';
tr.innerHTML = `
<td data-label="Pořadí" class="text-center align-middle drag-handle" style="cursor: grab;">
<i class="bi bi-grip-vertical text-muted fs-5"></i>
</td>
<td data-label="Název"><input type="text" name="polozky[${rowIndex}][NAZEV]" class="form-control item-nazev" placeholder="Začněte psát název práce nebo zboží" required autocomplete="off"></td>
<td data-label="Počet"><input type="number" step="any" name="polozky[${rowIndex}][POCET]" value="1" class="form-control item-pocet text-end" required></td>
<td data-label="Jedn."><input type="text" name="polozky[${rowIndex}][JEDNOTKA]" value="ks" class="form-control"></td>
<td data-label="Cena/Jedn."><input type="number" step="0.01" name="polozky[${rowIndex}][CENA_JEDNOTKA]" value="0" class="form-control item-cena text-end" required></td>
<td data-label="Sleva %"><input type="number" step="0.1" name="polozky[${rowIndex}][SLEVA]" value="0" class="form-control item-sleva text-end"></td>
<td data-label="Celkem"><input type="number" step="0.01" value="0" class="form-control item-celkem text-end fw-bold" title="Úprava přepočítá počet"></td>
<td class="text-center cell-actions">
<button type="button" class="btn btn-outline-danger btn-sm btn-remove-row"><i class="bi bi-trash"></i></button>
</td>
`;
tbody.appendChild(tr);
bindPolozkaSuggest(tr.querySelector('.item-nazev'));
rowIndex++;
reindexRows();
calculateTotals();
});
// Event delegation pro smazání a přepočet
tbody.addEventListener('click', (e) => {
if (e.target.closest('.btn-remove-row')) {
const row = e.target.closest('.item-row');
row.querySelectorAll('.item-nazev').forEach(input => input._suggestMenu?.remove());
row.remove();
reindexRows();
calculateTotals();
}
});
tbody.addEventListener('input', (e) => {
if (e.target.matches('.item-celkem')) {
recalculatePocetFromCelkem(e.target.closest('.item-row'));
updateGrandTotalFromInputs();
return;
}
if (e.target.matches('.item-pocet, .item-cena, .item-sleva')) {
calculateTotals();
}
});
tbody.addEventListener('focusout', (e) => {
if (e.target.matches('.item-celkem')) {
recalculatePocetFromCelkem(e.target.closest('.item-row'));
calculateTotals();
}
});
if (document.querySelectorAll('.item-row').length === 0) {
btnAdd.click();
} else {
calculateTotals();
}
});
</script>
{/syntax}