Našeptávače
parent
6a39a66e73
commit
302edd094b
|
|
@ -124,6 +124,49 @@ class FakturaFacade
|
||||||
", $fakturaId)->fetchAll();
|
", $fakturaId)->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Názvy položek pro našeptávač. Jednotka a cena jsou z posledního použití,
|
||||||
|
* pořadí je podle počtu výskytů na fakturách za poslední rok.
|
||||||
|
*
|
||||||
|
* @return list<array{nazev: string, jednotka: string, cena: float}>
|
||||||
|
*/
|
||||||
|
public function getKatalogPolozek(): array
|
||||||
|
{
|
||||||
|
$od = (new \DateTimeImmutable('today'))->modify('-1 year')->format('Y-m-d');
|
||||||
|
$rows = $this->kufrikExplorer->query("
|
||||||
|
SELECT katalog.NAZEV, katalog.JEDNOTKA, katalog.CENA_JEDNOTKA
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
P.NAZEV,
|
||||||
|
P.JEDNOTKA,
|
||||||
|
P.CENA_JEDNOTKA,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY P.NAZEV ORDER BY P.ID DESC) AS rn
|
||||||
|
FROM dbo.POLOZKA P
|
||||||
|
WHERE P.NAZEV IS NOT NULL AND LTRIM(RTRIM(P.NAZEV)) <> ''
|
||||||
|
) AS katalog
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT P.NAZEV, COUNT(*) AS POCET
|
||||||
|
FROM dbo.POLOZKA P
|
||||||
|
INNER JOIN dbo.FAKTURA F ON F.ID = P.FAKTURA
|
||||||
|
WHERE F.DATUM_VYSTAVENI >= ?
|
||||||
|
GROUP BY P.NAZEV
|
||||||
|
) AS pouziti ON pouziti.NAZEV = katalog.NAZEV
|
||||||
|
WHERE katalog.rn = 1
|
||||||
|
ORDER BY ISNULL(pouziti.POCET, 0) DESC, katalog.NAZEV ASC
|
||||||
|
", $od)->fetchAll();
|
||||||
|
|
||||||
|
$katalog = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$katalog[] = [
|
||||||
|
'nazev' => (string) $row->NAZEV,
|
||||||
|
'jednotka' => trim((string) ($row->JEDNOTKA ?? '')),
|
||||||
|
'cena' => round((float) $row->CENA_JEDNOTKA, 2),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $katalog;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maska, období číslování a výchozí splatnost uložené u firmy.
|
* Maska, období číslování a výchozí splatnost uložené u firmy.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{* naše vlastní styly *}
|
{* naše vlastní styly *}
|
||||||
<link href="{$basePath}/css/layout.css?v={$appVersion ?? '1.0'}-6" rel="stylesheet">
|
<link href="{$basePath}/css/layout.css?v={$appVersion ?? '1.0'}-7" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,10 @@ class FakturaEditPresenter extends BasePresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->template->katalogPolozekJson = json_encode(
|
||||||
|
$this->fakturaFacade->getKatalogPolozek(),
|
||||||
|
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
$this->template->polozky = $polozky;
|
$this->template->polozky = $polozky;
|
||||||
$this->template->platby = $platby;
|
$this->template->platby = $platby;
|
||||||
$this->template->celkemFaktury = $celkem;
|
$this->template->celkemFaktury = $celkem;
|
||||||
|
|
@ -135,16 +139,28 @@ class FakturaEditPresenter extends BasePresenter
|
||||||
{
|
{
|
||||||
$form = new Form;
|
$form = new Form;
|
||||||
|
|
||||||
// 1. Načteme všechny zákazníky (parametr 'vse' projde bez filtru AKTIVNI)
|
$zakaznici = $this->zakaznikFacade->getFiltered('', 'aktivni');
|
||||||
$zakaznici = $this->zakaznikFacade->getFiltered('', 'vse');
|
if ($this->id) {
|
||||||
|
$faktura = $this->fakturaFacade->getById($this->id);
|
||||||
|
$zakaznikId = $faktura ? (int) $faktura->ZAKAZNIK : 0;
|
||||||
|
$uzJeVSeznamu = false;
|
||||||
|
foreach ($zakaznici as $z) {
|
||||||
|
if ((int) $z->ID === $zakaznikId) {
|
||||||
|
$uzJeVSeznamu = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($zakaznikId && !$uzJeVSeznamu) {
|
||||||
|
$stavajici = $this->zakaznikFacade->getById($zakaznikId);
|
||||||
|
if ($stavajici) {
|
||||||
|
$zakaznici[] = $stavajici;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$zakazniciPairs = [];
|
$zakazniciPairs = [];
|
||||||
foreach ($zakaznici as $z) {
|
foreach ($zakaznici as $z) {
|
||||||
$nazev = trim(($z->PRIJMENI ?? '') . ' ' . ($z->JMENO ?? ''));
|
$zakazniciPairs[(int) $z->ID] = $this->nazevZakaznika($z);
|
||||||
if (!empty($z->IC)) {
|
|
||||||
$nazev .= " (IČ: {$z->IC})";
|
|
||||||
}
|
|
||||||
// Kličem musí být int
|
|
||||||
$zakazniciPairs[(int) $z->ID] = $nazev;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$form->addSelect('ZAKAZNIK_ID', 'Odběratel:', $zakazniciPairs)
|
$form->addSelect('ZAKAZNIK_ID', 'Odběratel:', $zakazniciPairs)
|
||||||
|
|
@ -184,6 +200,23 @@ class FakturaEditPresenter extends BasePresenter
|
||||||
return $form;
|
return $form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function nazevZakaznika(object $zakaznik): string
|
||||||
|
{
|
||||||
|
$nazev = trim(($zakaznik->PRIJMENI ?? '') . ' ' . ($zakaznik->JMENO ?? ''));
|
||||||
|
$doplnek = [];
|
||||||
|
if (!empty($zakaznik->MESTO)) {
|
||||||
|
$doplnek[] = (string) $zakaznik->MESTO;
|
||||||
|
}
|
||||||
|
if (!empty($zakaznik->IC)) {
|
||||||
|
$doplnek[] = 'IČ: ' . $zakaznik->IC;
|
||||||
|
}
|
||||||
|
if ($doplnek) {
|
||||||
|
$nazev .= ' (' . implode(', ', $doplnek) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $nazev;
|
||||||
|
}
|
||||||
|
|
||||||
public function fakturaFormSucceeded(Form $form, \stdClass $data): void
|
public function fakturaFormSucceeded(Form $form, \stdClass $data): void
|
||||||
{
|
{
|
||||||
$firmaId = $this->getUser()->getIdentity()->firma_id;
|
$firmaId = $this->getUser()->getIdentity()->firma_id;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<!-- Hlavička Faktury -->
|
<!-- Hlavička Faktury -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label n:name="ZAKAZNIK_ID" class="form-label fw-bold">Odběratel *</label>
|
<label n:name="ZAKAZNIK_ID" class="form-label fw-bold">Odběratel *</label>
|
||||||
<select n:name="ZAKAZNIK_ID" class="form-select"></select>
|
<select n:name="ZAKAZNIK_ID" class="form-select" data-suggest="zakaznik" {if $isEdit}data-locked="1"{/if}></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label n:name="CISLO" class="form-label fw-bold">Číslo faktury *</label>
|
<label n:name="CISLO" class="form-label fw-bold">Číslo faktury *</label>
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
<td data-label="Pořadí" class="text-center align-middle drag-handle" style="cursor: grab;">
|
<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>
|
<i class="bi bi-grip-vertical text-muted fs-5"></i>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Název"><input type="text" name="polozky[{$index}][NAZEV]" value="{$p->NAZEV}" class="form-control item-nazev" required></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="0.01" name="polozky[{$index}][POCET]" value="{$p->POCET}" class="form-control item-pocet text-end" required></td>
|
<td data-label="Počet"><input type="number" step="0.01" 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="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="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>
|
||||||
|
|
@ -209,6 +209,8 @@ document.getElementById('edit-platba-splatnost').addEventListener('click', funct
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script type="application/json" id="katalog-polozek">{$katalogPolozekJson|noescape}</script>
|
||||||
|
|
||||||
{syntax off}
|
{syntax off}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
@ -260,6 +262,328 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.getElementById('total-sum').textContent = grandTotal.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' CZK';
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initZakaznikSuggest() {
|
||||||
|
const select = document.querySelector('select[data-suggest="zakaznik"]');
|
||||||
|
if (!select || 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;
|
||||||
|
},
|
||||||
|
onClear() {
|
||||||
|
select.value = '';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Přidání nového řádku
|
||||||
btnAdd.addEventListener('click', () => {
|
btnAdd.addEventListener('click', () => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
|
|
@ -268,7 +592,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
<td data-label="Pořadí" class="text-center align-middle drag-handle" style="cursor: grab;">
|
<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>
|
<i class="bi bi-grip-vertical text-muted fs-5"></i>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Název"><input type="text" name="polozky[${rowIndex}][NAZEV]" class="form-control item-nazev" placeholder="Popis práce / zboží" required></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="0.01" name="polozky[${rowIndex}][POCET]" value="1" class="form-control item-pocet text-end" required></td>
|
<td data-label="Počet"><input type="number" step="0.01" 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="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="Cena/Jedn."><input type="number" step="0.01" name="polozky[${rowIndex}][CENA_JEDNOTKA]" value="0" class="form-control item-cena text-end" required></td>
|
||||||
|
|
@ -279,6 +603,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
|
bindPolozkaSuggest(tr.querySelector('.item-nazev'));
|
||||||
rowIndex++;
|
rowIndex++;
|
||||||
reindexRows();
|
reindexRows();
|
||||||
calculateTotals();
|
calculateTotals();
|
||||||
|
|
@ -287,7 +612,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Event delegation pro smazání a přepočet
|
// Event delegation pro smazání a přepočet
|
||||||
tbody.addEventListener('click', (e) => {
|
tbody.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.btn-remove-row')) {
|
if (e.target.closest('.btn-remove-row')) {
|
||||||
e.target.closest('.item-row').remove();
|
const row = e.target.closest('.item-row');
|
||||||
|
row.querySelectorAll('.item-nazev').forEach(input => input._suggestMenu?.remove());
|
||||||
|
row.remove();
|
||||||
reindexRows();
|
reindexRows();
|
||||||
calculateTotals();
|
calculateTotals();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -832,4 +832,62 @@ a:hover {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Našeptávač zákazníka a položek faktury */
|
||||||
|
.suggest {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.suggest-native {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1080;
|
||||||
|
max-height: 280px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
list-style: none;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: 0 8px 24px rgba(20, 50, 100, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-menu[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-option {
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-option.is-active,
|
||||||
|
.suggest-option:hover {
|
||||||
|
background: var(--primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-option small {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggest-empty {
|
||||||
|
padding: 0.45rem 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue