840 lines
38 KiB
Plaintext
840 lines
38 KiB
Plaintext
{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-6">
|
||
<label n:name="DATUM_VYSTAVENI" class="form-label">Datum vystavení</label>
|
||
<input n:name="DATUM_VYSTAVENI" class="form-control">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<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-6">
|
||
<label n:name="DATUM_SPLATNOSTI" class="form-label">Datum splatnosti</label>
|
||
<input n:name="DATUM_SPLATNOSTI" class="form-control">
|
||
</div>
|
||
<div class="col-md-6" 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} |