Fakturace
parent
584cb2318a
commit
63561a8f93
|
|
@ -0,0 +1,212 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Facade;
|
||||
|
||||
use Nette\Database\Explorer;
|
||||
|
||||
class FakturaFacade
|
||||
{
|
||||
public function __construct(
|
||||
private Explorer $kufrikExplorer
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrátí seznam faktur s dopočítanou celkovou částkou z položek.
|
||||
*/
|
||||
public function getFiltered(?int $rok = null, string $mesic = ''): array
|
||||
{
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($rok) {
|
||||
$where[] = 'YEAR(F.DATUM_VYSTAVENI) = ?';
|
||||
$params[] = $rok;
|
||||
}
|
||||
|
||||
if ($mesic !== '') {
|
||||
$where[] = 'MONTH(F.DATUM_VYSTAVENI) = ?';
|
||||
$params[] = (int) $mesic;
|
||||
}
|
||||
|
||||
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
F.*,
|
||||
ISNULL((SELECT SUM(P.CENA) FROM dbo.POLOZKA P WHERE P.FAKTURA = F.ID), 0) AS CASTKA_CELKEM
|
||||
FROM dbo.FAKTURA F
|
||||
{$whereSql}
|
||||
ORDER BY F.DATUM_VYSTAVENI DESC, F.ID DESC
|
||||
";
|
||||
|
||||
return $this->kufrikExplorer->query($sql, ...$params)->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail jedné faktury podle ID
|
||||
*/
|
||||
public function getById(int $id)
|
||||
{
|
||||
return $this->kufrikExplorer->table('FAKTURA')->where('ID', $id)->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Položky faktury seřazené podle PORADI
|
||||
*/
|
||||
public function getPolozky(int $fakturaId): array
|
||||
{
|
||||
return $this->kufrikExplorer->query("
|
||||
SELECT P.*
|
||||
FROM dbo.POLOZKA P
|
||||
WHERE P.FAKTURA = ?
|
||||
ORDER BY P.PORADI ASC, P.ID ASC
|
||||
", $fakturaId)->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vygeneruje následující číslo faktury pro aktuální rok a firmu
|
||||
*/
|
||||
public function getNextCislo(int $firmaId): string
|
||||
{
|
||||
$aktualniRok = (int) date('Y');
|
||||
|
||||
// Najdeme nejvyšší CISLO_ABS pro daný rok a firmu
|
||||
$maxAbs = $this->kufrikExplorer->query("
|
||||
SELECT MAX(CISLO_ABS) AS MAX_ABS
|
||||
FROM dbo.FAKTURA
|
||||
WHERE FIRMA_ID = ? AND YEAR(DATUM_VYSTAVENI) = ?
|
||||
", $firmaId, $aktualniRok)->fetch()?->MAX_ABS;
|
||||
|
||||
// Pokud ještě v tomto roce žádná faktura neexistuje, začínáme od 1
|
||||
$dalsiCislo = ($maxAbs !== null) ? ((int) $maxAbs + 1) : 1;
|
||||
|
||||
// Formátování: RRRR + třímístné číslo doplněné nulami (např. 2026001, 2026002...)
|
||||
return $aktualniRok . str_pad((string) $dalsiCislo, 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uložení / Aktualizace hlavičky faktury + vytvoření snapshotů D_* a O_*
|
||||
*/
|
||||
public function saveFaktura(array $data, ?int $id = null, int $firmaId): int
|
||||
{
|
||||
if ($id) {
|
||||
// PŘI EDITACI: Mění se pouze měnitelné údaje (splatnost, DUZP, stav, činnosti)
|
||||
$values = [
|
||||
'DATUM_SPLATNOSTI' => $data['DATUM_SPLATNOSTI'],
|
||||
'DATUM_DUZP' => !empty($data['DATUM_DUZP']) ? $data['DATUM_DUZP'] : null,
|
||||
'STAV' => $data['STAV'] ?? 'ISSUED',
|
||||
'CINNOSTI' => !empty($data['CINNOSTI']) ? $data['CINNOSTI'] : null,
|
||||
];
|
||||
|
||||
$this->kufrikExplorer->table('FAKTURA')->where('ID', $id)->update($values);
|
||||
return $id;
|
||||
|
||||
} else {
|
||||
// PŘI NOVÉ FAKTUŘE: Ukládáme vše (včetně DATUM_VYSTAVENI, CISLO, ZAKAZNIK a snapshotů)
|
||||
$firma = $this->kufrikExplorer->table('FIRMA')->where('ID', $firmaId)->fetch();
|
||||
$zakaznikId = !empty($data['ZAKAZNIK_ID']) ? (int) $data['ZAKAZNIK_ID'] : null;
|
||||
$zakaznik = $zakaznikId ? $this->kufrikExplorer->table('ZAKAZNIK')->where('ID', $zakaznikId)->fetch() : null;
|
||||
|
||||
$oPrajm = $zakaznik ? trim(($zakaznik->PRIJMENI ?? '') . ' ' . ($zakaznik->JMENO ?? '')) : null;
|
||||
|
||||
$dBanka = null;
|
||||
if ($firma) {
|
||||
if (isset($firma->UCET, $firma->KOD_BANKY) && $firma->UCET) {
|
||||
$dBanka = $firma->UCET . '/' . $firma->KOD_BANKY;
|
||||
} elseif (isset($firma->BANKA)) {
|
||||
$dBanka = $firma->BANKA;
|
||||
}
|
||||
}
|
||||
|
||||
$values = [
|
||||
'FIRMA_ID' => $firmaId,
|
||||
'ZAKAZNIK' => $zakaznikId,
|
||||
'CISLO' => $data['CISLO'],
|
||||
'CISLO_ABS' => (int) ($data['CISLO_ABS'] ?? substr($data['CISLO'], -3)),
|
||||
'FORMA_UHRADY' => (int) ($data['FORMA_UHRADY'] ?? 1),
|
||||
'DATUM_VYSTAVENI' => $data['DATUM_VYSTAVENI'],
|
||||
'DATUM_SPLATNOSTI' => $data['DATUM_SPLATNOSTI'],
|
||||
'DATUM_DUZP' => !empty($data['DATUM_DUZP']) ? $data['DATUM_DUZP'] : null,
|
||||
'STAV' => $data['STAV'] ?? 'ISSUED',
|
||||
'CINNOSTI' => !empty($data['CINNOSTI']) ? $data['CINNOSTI'] : null,
|
||||
|
||||
// SNAPSHOT DODAVATELE
|
||||
'D_PRAJM' => $firma->NAZEV ?? null,
|
||||
'D_ULICE' => $firma->ULICE ?? null,
|
||||
'D_MESTO' => $firma->MESTO ?? null,
|
||||
'D_PSC' => $firma->PSC ?? null,
|
||||
'D_IC' => $firma->IC ?? null,
|
||||
'D_DIC' => $firma->DIC ?? null,
|
||||
'D_BANKA' => $dBanka,
|
||||
'D_TELEFON' => $firma->TELEFON ?? null,
|
||||
'D_EMAIL' => $firma->EMAIL ?? null,
|
||||
'D_WEB' => $firma->WEB ?? null,
|
||||
|
||||
// SNAPSHOT ODBĚRATELE
|
||||
'O_PRAJM' => $oPrajm,
|
||||
'O_ULICE' => $zakaznik->ULICE ?? null,
|
||||
'O_MESTO' => $zakaznik->MESTO ?? null,
|
||||
'O_PSC' => $zakaznik->PSC ?? null,
|
||||
'O_IC' => $zakaznik->IC ?? null,
|
||||
'O_DIC' => $zakaznik->DIC ?? null,
|
||||
];
|
||||
|
||||
$row = $this->kufrikExplorer->table('FAKTURA')->insert($values);
|
||||
return $row->ID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uložení položek s automatickým výpočtem PORADI
|
||||
*/
|
||||
public function savePolozky(int $fakturaId, array $polozkyData, int $firmaId): void
|
||||
{
|
||||
$this->kufrikExplorer->table('POLOZKA')->where('FAKTURA', $fakturaId)->delete();
|
||||
|
||||
$poradi = 1;
|
||||
foreach ($polozkyData as $item) {
|
||||
if (empty($item['NAZEV']) && empty($item['CENA_JEDNOTKA'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pocet = (float) ($item['POCET'] ?? 1);
|
||||
$cenaJednotka = (float) ($item['CENA_JEDNOTKA'] ?? 0);
|
||||
$sleva = (float) ($item['SLEVA'] ?? 0);
|
||||
$cenaCelkem = ($pocet * $cenaJednotka) * (1 - ($sleva / 100));
|
||||
|
||||
$this->kufrikExplorer->table('POLOZKA')->insert([
|
||||
'FAKTURA' => $fakturaId,
|
||||
'FIRMA_ID' => $firmaId,
|
||||
'PORADI' => $poradi++,
|
||||
'NAZEV' => $item['NAZEV'],
|
||||
'POCET' => $pocet,
|
||||
'JEDNOTKA' => $item['JEDNOTKA'] ?: 'ks',
|
||||
'CENA_JEDNOTKA' => $cenaJednotka,
|
||||
'SLEVA' => $sleva,
|
||||
'CENA' => $cenaCelkem,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleUhrada(int $id): string
|
||||
{
|
||||
$faktura = $this->getById($id);
|
||||
if (!$faktura) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$novyStav = ($faktura->STAV === 'PAID') ? 'ISSUED' : 'PAID';
|
||||
|
||||
$this->kufrikExplorer->table('FAKTURA')
|
||||
->where('ID', $id)
|
||||
->update(['STAV' => $novyStav]);
|
||||
|
||||
return $novyStav;
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->kufrikExplorer->table('FAKTURA')->where('ID', $id)->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -113,10 +113,10 @@
|
|||
<i class="bi bi-folder-fill"></i>
|
||||
<span>Seznam faktur</span>
|
||||
</a>
|
||||
<button type="button" class="btn btn-success btn-action">
|
||||
<a n:href="FakturaEdit:new" type="button" class="btn btn-success btn-action">
|
||||
<i class="bi bi-plus-square-fill"></i>
|
||||
<span>Přidat fakturu</span>
|
||||
</button>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary btn-action" onclick="window.print()">
|
||||
<i class="bi bi-printer-fill"></i>
|
||||
<span>Tisk</span>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\UI\Faktura;
|
||||
|
||||
use App\UI\BasePresenter;
|
||||
use Nette\Database\Explorer;
|
||||
use App\Model\Facade\FakturaFacade;
|
||||
|
||||
class FakturaPresenter extends BasePresenter
|
||||
{
|
||||
|
|
@ -14,7 +14,7 @@ class FakturaPresenter extends BasePresenter
|
|||
public string $mesic = '';
|
||||
|
||||
public function __construct(
|
||||
private Explorer $kufrikExplorer
|
||||
private FakturaFacade $fakturaFacade
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->rok = (int) date('Y');
|
||||
|
|
@ -22,52 +22,16 @@ class FakturaPresenter extends BasePresenter
|
|||
|
||||
public function renderDefault(): void
|
||||
{
|
||||
$firmaId = $this->getUser()->getIdentity()->firma_id ?? null;
|
||||
// RLS v databázi se postará o filtraci podle firmy,
|
||||
// fasáda vrátí seřazené faktury s dopočítanou sumou.
|
||||
$faktury = $this->fakturaFacade->getFiltered($this->rok, $this->mesic);
|
||||
|
||||
if (!$firmaId) {
|
||||
$this->flashMessage('K účtu není přiřazena žádná aktivní firma.', 'danger');
|
||||
$this->template->faktury = [];
|
||||
$this->template->celkemSum = 0;
|
||||
$this->template->neuhrazenoCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamické sestavení podmínek WHERE pro T-SQL
|
||||
$where = ['F.FIRMA_ID = ?'];
|
||||
$params = [$firmaId];
|
||||
|
||||
if ($this->rok) {
|
||||
$where[] = 'YEAR(F.DATUM_VYSTAVENI) = ?';
|
||||
$params[] = $this->rok;
|
||||
}
|
||||
|
||||
if ($this->mesic !== '') {
|
||||
$where[] = 'MONTH(F.DATUM_VYSTAVENI) = ?';
|
||||
$params[] = (int) $this->mesic;
|
||||
}
|
||||
|
||||
$whereSql = implode(' AND ', $where);
|
||||
|
||||
// Čisté T-SQL zaručující 100% kompatibilitu s MS SQL Serverem bez magických Nette relací
|
||||
$sql = "
|
||||
SELECT
|
||||
F.*,
|
||||
ISNULL((SELECT SUM(P.CENA) FROM dbo.POLOZKA P WHERE P.FAKTURA = F.ID), 0) AS CASTKA_CELKEM
|
||||
FROM dbo.FAKTURA F
|
||||
WHERE {$whereSql}
|
||||
ORDER BY F.DATUM_VYSTAVENI DESC, F.ID DESC
|
||||
";
|
||||
|
||||
// Provedeme dotaz přímo na Explorer connection
|
||||
$faktury = $this->kufrikExplorer->query($sql, ...$params)->fetchAll();
|
||||
|
||||
// Součty pro karty
|
||||
// Agregace pro přehledové karty nad aktuálním výběrem
|
||||
$celkemSum = 0;
|
||||
$neuhrazenoCount = 0;
|
||||
|
||||
foreach ($faktury as $f) {
|
||||
$suma = (float) ($f->CASTKA_CELKEM ?? 0);
|
||||
$celkemSum += $suma;
|
||||
$celkemSum += (float) ($f->CASTKA_CELKEM ?? 0);
|
||||
if ($f->STAV !== 'PAID') {
|
||||
$neuhrazenoCount++;
|
||||
}
|
||||
|
|
@ -76,22 +40,18 @@ class FakturaPresenter extends BasePresenter
|
|||
$this->template->faktury = $faktury;
|
||||
$this->template->celkemSum = $celkemSum;
|
||||
$this->template->neuhrazenoCount = $neuhrazenoCount;
|
||||
$this->template->vybranyRok = $this->rok;
|
||||
$this->template->vybranyMesic = $this->mesic;
|
||||
$this->template->selectedRok = $this->rok ?? (int) date('Y'); // <--- PŘIDÁNO
|
||||
$this->template->selectedMesic = $this->mesic ?? ''; // <--- PŘIDÁNO
|
||||
}
|
||||
|
||||
/**
|
||||
* Přepnutí stavu zaplaceno / neuhrazeno
|
||||
*/
|
||||
public function handleUhradit(int $id): void
|
||||
{
|
||||
$firmaId = $this->getUser()->getIdentity()->firma_id;
|
||||
$faktura = $this->kufrikExplorer->table('FAKTURA')
|
||||
->where('ID', $id)
|
||||
->where('FIRMA_ID', $firmaId)
|
||||
->fetch();
|
||||
|
||||
if ($faktura) {
|
||||
$novyStav = ($faktura->STAV === 'PAID') ? 'ISSUED' : 'PAID';
|
||||
$faktura->update(['STAV' => $novyStav]);
|
||||
$novyStav = $this->fakturaFacade->toggleUhrada($id);
|
||||
|
||||
if ($novyStav !== '') {
|
||||
$this->flashMessage(
|
||||
$novyStav === 'PAID' ? 'Faktura byla označena jako zaplacená.' : 'Stav faktury byl změněn na neuhrazeno.',
|
||||
'success'
|
||||
|
|
@ -101,18 +61,13 @@ class FakturaPresenter extends BasePresenter
|
|||
$this->redirect('this');
|
||||
}
|
||||
|
||||
/**
|
||||
* Smazání faktury
|
||||
*/
|
||||
public function handleDelete(int $id): void
|
||||
{
|
||||
$firmaId = $this->getUser()->getIdentity()->firma_id;
|
||||
|
||||
$deleted = $this->kufrikExplorer->table('FAKTURA')
|
||||
->where('ID', $id)
|
||||
->where('FIRMA_ID', $firmaId)
|
||||
->delete();
|
||||
|
||||
if ($deleted) {
|
||||
$this->fakturaFacade->delete($id);
|
||||
$this->flashMessage('Faktura byla smazána.', 'info');
|
||||
}
|
||||
|
||||
$this->redirect('this');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +1,135 @@
|
|||
{block content}
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- DataTables CSS s Bootstrap 5 vzhledem -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<!-- Flash zprávy / Toast -->
|
||||
<div n:foreach="$flashes as $flash" class="alert alert-{$flash->type} alert-dismissible fade show auto-dismiss my-3 shadow-sm" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
<strong>{$flash->message}</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<div class="container-fluid py-3">
|
||||
<!-- Hlavička s přehledovými kartami -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h3 mb-0">Přehled faktur</h2>
|
||||
<a n:href="FakturaEdit:new" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle me-1"></i>Vystavit fakturu
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Souhrnné statistiky -->
|
||||
<div class="row g-3 my-2">
|
||||
<div class="col-md-6">
|
||||
<div class="content-card d-flex align-items-center p-3 border-start border-primary border-4">
|
||||
<div class="fs-1 text-primary me-3"><i class="bi bi-cash-stack"></i></div>
|
||||
<div>
|
||||
<div class="text-muted small">Celkem fakturováno ({$vybranyRok}{if $vybranyMesic}/{$vybranyMesic}{/if})</div>
|
||||
<div class="fs-4 fw-bold">{$celkemSum|number:2, ',', ' '} CZK</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-primary border-4">
|
||||
<div class="text-muted small">Celkem v tomto období</div>
|
||||
<div class="fs-4 fw-bold text-primary">{$celkemSum|number:2, ',', ' '} CZK</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="content-card d-flex align-items-center p-3 border-start border-warning border-4">
|
||||
<div class="fs-1 text-warning me-3"><i class="bi bi-exclamation-triangle"></i></div>
|
||||
<div>
|
||||
<div class="text-muted small">Neuhrazené faktury v období</div>
|
||||
<div class="fs-4 fw-bold">{$neuhrazenoCount} ks</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-warning border-4">
|
||||
<div class="text-muted small">Neuhrazené faktury</div>
|
||||
<div class="fs-4 fw-bold text-warning">{$neuhrazenoCount} ks</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hlavní tabulka faktur -->
|
||||
<div class="content-card my-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h4 mb-0" style="color: var(--header-bg);">
|
||||
<i class="bi bi-folder2-open me-2"></i>Vydané faktury
|
||||
</h2>
|
||||
<a class="btn btn-success btn-sm">
|
||||
<i class="bi bi-plus-lg me-1"></i>Vystavit novou fakturu
|
||||
</a>
|
||||
<!-- Filtrační lišta (Rok / Měsíc) -->
|
||||
<div class="card p-3 mb-4 border-0 shadow-sm bg-light">
|
||||
<form method="get" class="row g-2 align-items-center">
|
||||
<div class="col-auto">
|
||||
<label class="col-form-label fw-bold">Filtr roku/měsíce:</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<select name="rok" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="2026" {if ($vybranyRok ?? date('Y')) == 2026}selected{/if}>2026</option>
|
||||
<option value="2025" {if ($vybranyRok ?? date('Y')) == 2025}selected{/if}>2025</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<select name="mesic" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">-- Všechny měsíce --</option>
|
||||
<option value="1" {if ($vybranyMesic ?? '') == '1'}selected{/if}>Leden</option>
|
||||
<option value="2" {if ($vybranyMesic ?? '') == '2'}selected{/if}>Únor</option>
|
||||
<option value="3" {if ($vybranyMesic ?? '') == '3'}selected{/if}>Březen</option>
|
||||
<option value="4" {if ($vybranyMesic ?? '') == '4'}selected{/if}>Duben</option>
|
||||
<option value="5" {if ($vybranyMesic ?? '') == '5'}selected{/if}>Květen</option>
|
||||
<option value="6" {if ($vybranyMesic ?? '') == '6'}selected{/if}>Červen</option>
|
||||
<option value="7" {if ($vybranyMesic ?? '') == '7'}selected{/if}>Červenec</option>
|
||||
<option value="8" {if ($vybranyMesic ?? '') == '8'}selected{/if}>Srpen</option>
|
||||
<option value="9" {if ($vybranyMesic ?? '') == '9'}selected{/if}>Září</option>
|
||||
<option value="10" {if ($vybranyMesic ?? '') == '10'}selected{/if}>Říjen</option>
|
||||
<option value="11" {if ($vybranyMesic ?? '') == '11'}selected{/if}>Listopad</option>
|
||||
<option value="12" {if ($vybranyMesic ?? '') == '12'}selected{/if}>Prosinec</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tabulka Faktur -->
|
||||
<div class="card shadow-sm border-0 p-3 bg-white">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<table class="table table-hover align-middle w-100" id="faktury-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Číslo faktury</th>
|
||||
<th>Číslo</th>
|
||||
<th>Odběratel</th>
|
||||
<th>Vystaveno</th>
|
||||
<th>Splatnost</th>
|
||||
<th class="text-end">Celková částka</th>
|
||||
<th class="text-end">Částka</th>
|
||||
<th class="text-center">Stav</th>
|
||||
<th class="text-end">Akce</th>
|
||||
<th class="text-end" data-orderable="false">Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $faktury as $faktura}
|
||||
{foreach $faktury as $f}
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<a n:href="FakturaDetail:default $faktura->ID" class="text-decoration-none">
|
||||
{$faktura->CISLO}
|
||||
<!-- Číslo dokladu -->
|
||||
<td data-order="{$f->CISLO}">
|
||||
<a n:href="FakturaDetail:default $f->ID" class="fw-bold text-decoration-none">
|
||||
{$f->CISLO}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">{$faktura->O_PRAJM ?: 'Nespecifikovaný odběratel'}</div>
|
||||
<div class="text-muted small" n:if="$faktura->O_IC">IČ: {$faktura->O_IC}</div>
|
||||
<!-- Odběratel -->
|
||||
<td>{$f->O_PRAJM ?: '—'}</td>
|
||||
<!-- Datum vystavení -->
|
||||
<td data-order="{$f->DATUM_VYSTAVENI|date:'Ymd'}">
|
||||
{$f->DATUM_VYSTAVENI|date:'d.m.Y'}
|
||||
</td>
|
||||
<td>{$faktura->DATUM_VYSTAVENI|date:'d.m.Y'}</td>
|
||||
<td>
|
||||
<span n:class="$faktura->DATUM_SPLATNOSTI < new \DateTime() && $faktura->STAV !== 'PAID' ? 'text-danger fw-bold'">
|
||||
{$faktura->DATUM_SPLATNOSTI|date:'d.m.Y'}
|
||||
<!-- Datum splatnosti -->
|
||||
<td data-order="{$f->DATUM_SPLATNOSTI|date:'Ymd'}">
|
||||
{$f->DATUM_SPLATNOSTI|date:'d.m.Y'}
|
||||
</td>
|
||||
<!-- Částka celkem -->
|
||||
<td class="text-end fw-bold" data-order="{$f->CASTKA_CELKEM}">
|
||||
{$f->CASTKA_CELKEM|number:2, ',', ' '} CZK
|
||||
</td>
|
||||
<!-- Stav -->
|
||||
<td class="text-center" data-order="{$f->STAV}">
|
||||
<span class="badge {if $f->STAV === 'PAID'}bg-success{else}bg-warning text-dark{/if}">
|
||||
{if $f->STAV === 'PAID'}Zaplaceno{else}Neuhrazeno{/if}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end fw-bold fs-6">
|
||||
{($faktura->CASTKA_CELKEM ?? 0)|number:2, ',', ' '} CZK
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{if $faktura->STAV === 'PAID'}
|
||||
<span class="badge bg-success-subtle text-success border border-success">
|
||||
<i class="bi bi-check-circle me-1"></i>Uhrazeno
|
||||
</span>
|
||||
{else}
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger">
|
||||
<i class="bi bi-clock me-1"></i>Neuhrazeno
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<!-- Detail / Náhled -->
|
||||
<a n:href="FakturaDetail:default $faktura->ID"
|
||||
class="btn btn-outline-info"
|
||||
title="Detail faktury">
|
||||
<!-- Sloupec AKCE -->
|
||||
<td class="text-end text-nowrap">
|
||||
<!-- Přepnutí stavu úhrady -->
|
||||
<a n:href="uhradit! $f->ID"
|
||||
class="btn btn-sm {if $f->STAV === 'PAID'}btn-outline-warning{else}btn-outline-success{/if} me-1"
|
||||
title="{if $f->STAV === 'PAID'}Označit jako neuhrazeno{else}Označit jako zaplaceno{/if}">
|
||||
<i class="bi {if $f->STAV === 'PAID'}bi-x-circle{else}bi-check-circle{/if}"></i>
|
||||
</a>
|
||||
|
||||
<!-- Detail faktury -->
|
||||
<a n:href="FakturaDetail:default $f->ID" class="btn btn-sm btn-outline-info me-1" title="Zobrazit detail">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
|
||||
<!-- Úprava -->
|
||||
<a n:href="FakturaEdit:edit $faktura->ID"
|
||||
class="btn btn-outline-primary"
|
||||
title="Upravit fakturu">
|
||||
<!-- Editace faktury -->
|
||||
<a n:href="FakturaEdit:edit $f->ID" class="btn btn-sm btn-outline-primary me-1" title="Upravit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
|
||||
<!-- Přepnutí úhrady -->
|
||||
<a n:href="uhradit! $faktura->ID"
|
||||
class="btn {$faktura->STAV === 'PAID' ? 'btn-outline-secondary' : 'btn-outline-success'}"
|
||||
title="Změnit stav úhrady">
|
||||
<i n:class="bi, $faktura->STAV === 'PAID' ? 'bi-x-circle' : 'bi-check2-all'"></i>
|
||||
</a>
|
||||
|
||||
<!-- Smazání -->
|
||||
<a n:href="delete! $faktura->ID"
|
||||
class="btn btn-outline-danger"
|
||||
onclick="return confirm('Opravdu chcete smazat tuto fakturu včetně všech položek?');"
|
||||
title="Smazat fakturu">
|
||||
<!-- Smazání faktury -->
|
||||
<a n:href="delete! $f->ID"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
title="Smazat fakturu"
|
||||
onclick="return confirm('Opravdu si přejete smazat tuto fakturu?');">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{else}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">
|
||||
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
|
||||
Pro zadaný filtr (rok {$vybranyRok}{if $vybranyMesic}, měsíc {$vybranyMesic}{/if}) nebyly nalezeny žádné faktury.
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
|
|
@ -132,18 +137,23 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- DataTables JS & jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
{syntax off}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const alerts = document.querySelectorAll('.alert.auto-dismiss');
|
||||
alerts.forEach(alert => {
|
||||
setTimeout(() => {
|
||||
alert.classList.remove('show');
|
||||
alert.classList.add('fade');
|
||||
setTimeout(() => alert.remove(), 500);
|
||||
}, 3000);
|
||||
});
|
||||
$(document).ready(function() {
|
||||
$('#faktury-table').DataTable({
|
||||
language: {
|
||||
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/cs.json'
|
||||
},
|
||||
order: [[2, 'desc']], // Výchozí řazení podle data vystavení sestupně
|
||||
pageLength: 25
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{/syntax}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\UI\FakturaDetail;
|
||||
|
||||
use App\UI\BasePresenter;
|
||||
use App\Model\Facade\FakturaFacade;
|
||||
|
||||
class FakturaDetailPresenter extends BasePresenter
|
||||
{
|
||||
public function __construct(
|
||||
private FakturaFacade $fakturaFacade
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function renderDefault(int $id): void
|
||||
{
|
||||
$faktura = $this->fakturaFacade->getById($id);
|
||||
|
||||
if (!$faktura) {
|
||||
$this->flashMessage('Faktura nebyla nalezena.', 'danger');
|
||||
$this->redirect('Faktura:default');
|
||||
}
|
||||
|
||||
$polozky = $this->fakturaFacade->getPolozky($id);
|
||||
|
||||
// Výpočet celkové částky
|
||||
$celkem = 0;
|
||||
foreach ($polozky as $p) {
|
||||
$celkem += (float) $p->CENA;
|
||||
}
|
||||
|
||||
$this->template->faktura = $faktura;
|
||||
$this->template->polozky = $polozky;
|
||||
$this->template->celkem = $celkem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
{block content}
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
<!-- Ovládací lišta (při tisku se schová) -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
||||
<div>
|
||||
<a n:href="Faktura:default" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Zpět na seznam
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a n:href="FakturaEdit:edit $faktura->ID" class="btn btn-outline-primary me-2">
|
||||
<i class="bi bi-pencil me-1"></i>Upravit
|
||||
</a>
|
||||
<button onclick="window.print()" class="btn btn-success">
|
||||
<i class="bi bi-printer me-1"></i>Vytisknout / PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tělo faktury (Formát A4 náhled) -->
|
||||
<div class="card shadow-sm border-0 p-4 p-md-5 bg-white mx-auto invoice-box" style="max-width: 900px;">
|
||||
<!-- Hlavička faktury -->
|
||||
<div class="row pb-4 mb-4 border-bottom">
|
||||
<div class="col-6">
|
||||
<h1 class="h3 fw-bold mb-1">FAKTURA - DAŇOVÝ DOKLAD</h1>
|
||||
<div class="fs-5 text-muted">Číslo: <strong>{$faktura->CISLO}</strong></div>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<span class="badge {if $faktura->STAV === 'PAID'}bg-success{else}bg-warning text-dark{/if} fs-6 p-2 d-print-none">
|
||||
{if $faktura->STAV === 'PAID'}UHRATENO{else}NEUHRATENO{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dodavatel a Odběratel (ze snapshotů D_* a O_*) -->
|
||||
<div class="row mb-4">
|
||||
<!-- DODAVATEL -->
|
||||
<div class="col-6">
|
||||
<h6 class="text-uppercase text-muted fw-bold mb-2">Dodavatel</h6>
|
||||
<div class="fw-bold fs-5">{$faktura->D_PRAJM ?: '—'}</div>
|
||||
<div>{$faktura->D_ULICE}</div>
|
||||
<div>{$faktura->D_PSC} {$faktura->D_MESTO}</div>
|
||||
|
||||
<div class="mt-2">
|
||||
{if $faktura->D_IC}<strong>IČ:</strong> {$faktura->D_IC}<br>{/if}
|
||||
{if $faktura->D_DIC}<strong>DIČ:</strong> {$faktura->D_DIC}<br>{/if}
|
||||
</div>
|
||||
|
||||
{if $faktura->D_BANKA}
|
||||
<div class="mt-2">
|
||||
<strong>Bankovní účet:</strong><br>
|
||||
<span class="fw-bold">{$faktura->D_BANKA}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- ODBĚRATELE -->
|
||||
<div class="col-6">
|
||||
<h6 class="text-uppercase text-muted fw-bold mb-2">Odběratel</h6>
|
||||
<div class="fw-bold fs-5">{$faktura->O_PRAJM ?: '—'}</div>
|
||||
<div>{$faktura->O_ULICE}</div>
|
||||
<div>{$faktura->O_PSC} {$faktura->O_MESTO}</div>
|
||||
|
||||
<div class="mt-2">
|
||||
{if $faktura->O_IC}<strong>IČ:</strong> {$faktura->O_IC}<br>{/if}
|
||||
{if $faktura->O_DIC}<strong>DIČ:</strong> {$faktura->O_DIC}<br>{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datuma -->
|
||||
<div class="row my-3 p-3 bg-light rounded text-center">
|
||||
<div class="col-4">
|
||||
<div class="text-muted small">Datum vystavení:</div>
|
||||
<div class="fw-bold">{$faktura->DATUM_VYSTAVENI|date:'d.m.Y'}</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="text-muted small">Datum DUZP:</div>
|
||||
<div class="fw-bold">{if $faktura->DATUM_DUZP}{$faktura->DATUM_DUZP|date:'d.m.Y'}{else}—{/if}</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="text-muted small">Datum splatnosti:</div>
|
||||
<div class="fw-bold text-danger">{$faktura->DATUM_SPLATNOSTI|date:'d.m.Y'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Činnosti / Popis -->
|
||||
<div n:if="$faktura->CINNOSTI" class="mb-4">
|
||||
<h6 class="text-uppercase text-muted fw-bold mb-1">Popis plnění / Činnosti:</h6>
|
||||
<p class="mb-0 text-break" style="white-space: pre-line;">{$faktura->CINNOSTI}</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabulka položek -->
|
||||
<div class="table-responsive my-4">
|
||||
<table class="table table-striped align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;" class="text-center">#</th>
|
||||
<th>Položka</th>
|
||||
<th style="width: 90px;" class="text-end">Počet</th>
|
||||
<th style="width: 70px;">Jedn.</th>
|
||||
<th style="width: 120px;" class="text-end">Cena/J.</th>
|
||||
<th style="width: 80px;" class="text-end">Sleva</th>
|
||||
<th style="width: 130px;" class="text-end">Celkem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $polozky as $p}
|
||||
<tr>
|
||||
<td class="text-center text-muted fw-bold">{$p->PORADI}</td>
|
||||
<td>{$p->NAZEV}</td>
|
||||
<td class="text-end">{$p->POCET}</td>
|
||||
<td>{$p->JEDNOTKA}</td>
|
||||
<td class="text-end">{$p->CENA_JEDNOTKA|number:2, ',', ' '} CZK</td>
|
||||
<td class="text-end">{if $p->SLEVA > 0}{$p->SLEVA} %{else}—{/if}</td>
|
||||
<td class="text-end fw-bold">{$p->CENA|number:2, ',', ' '} CZK</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Rekapitulace a součet -->
|
||||
<div class="row justify-content-end my-3">
|
||||
<div class="col-md-5">
|
||||
<div class="d-flex justify-content-between align-items-center p-3 bg-primary text-white rounded">
|
||||
<span class="fs-5 fw-bold">Celkem k úhradě:</span>
|
||||
<span class="fs-4 fw-bold">{$celkem|number:2, ',', ' '} CZK</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media print {
|
||||
body {
|
||||
background: #fff !important;
|
||||
}
|
||||
.d-print-none {
|
||||
display: none !important;
|
||||
}
|
||||
.invoice-box {
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
namespace App\UI\FakturaEdit;
|
||||
|
||||
use App\UI\BasePresenter;
|
||||
use App\Model\Facade\FakturaFacade;
|
||||
use App\Model\Facade\ZakaznikFacade;
|
||||
use Nette\Application\UI\Form;
|
||||
|
||||
class FakturaEditPresenter extends BasePresenter
|
||||
{
|
||||
/** @persistent */
|
||||
public ?int $id = null;
|
||||
|
||||
public function __construct(
|
||||
private FakturaFacade $fakturaFacade,
|
||||
private ZakaznikFacade $zakaznikFacade
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function actionNew(): void
|
||||
{
|
||||
$this->id = null;
|
||||
$this->setView('edit');
|
||||
|
||||
$firmaId = $this->getUser()->getIdentity()->firma_id;
|
||||
$noveCislo = $this->fakturaFacade->getNextCislo($firmaId);
|
||||
|
||||
// Předplníme nové číslo faktury do formuláře
|
||||
$this['fakturaForm']->setDefaults([
|
||||
'CISLO' => $noveCislo,
|
||||
]);
|
||||
}
|
||||
|
||||
public function actionEdit(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
$faktura = $this->fakturaFacade->getById($id);
|
||||
|
||||
if (!$faktura) {
|
||||
$this->flashMessage('Faktura nebyla nalezena.', 'danger');
|
||||
$this->redirect('Faktura:default');
|
||||
}
|
||||
|
||||
$defaults = $faktura->toArray();
|
||||
|
||||
// Explicitně přiřadíme ID zákazníka
|
||||
if ($faktura->ZAKAZNIK !== null) {
|
||||
$defaults['ZAKAZNIK_ID'] = (int) $faktura->ZAKAZNIK;
|
||||
}
|
||||
|
||||
if ($faktura->DATUM_VYSTAVENI) {
|
||||
$defaults['DATUM_VYSTAVENI'] = $faktura->DATUM_VYSTAVENI->format('Y-m-d');
|
||||
}
|
||||
if ($faktura->DATUM_SPLATNOSTI) {
|
||||
$defaults['DATUM_SPLATNOSTI'] = $faktura->DATUM_SPLATNOSTI->format('Y-m-d');
|
||||
}
|
||||
if ($faktura->DATUM_DUZP) {
|
||||
$defaults['DATUM_DUZP'] = $faktura->DATUM_DUZP->format('Y-m-d');
|
||||
}
|
||||
|
||||
// 1. Nastavení výchozích hodnot
|
||||
$this['fakturaForm']->setDefaults($defaults);
|
||||
|
||||
// Společný CSS styl pro zablokované / zešedlé prvky (odpovídá Bootstrap disabled vzhledu)
|
||||
$disabledStyle = 'pointer-events: none; background-color: #e9ecef; opacity: 1;';
|
||||
|
||||
// 2. Zamknutí prvků při editaci (Odběratel, Číslo faktury, Datum vystavení)
|
||||
$this['fakturaForm']['ZAKAZNIK_ID']
|
||||
->setHtmlAttribute('style', $disabledStyle)
|
||||
->setHtmlAttribute('tabindex', '-1');
|
||||
|
||||
$this['fakturaForm']['CISLO']
|
||||
->setHtmlAttribute('readonly', true)
|
||||
->setHtmlAttribute('style', $disabledStyle)
|
||||
->setHtmlAttribute('tabindex', '-1');
|
||||
|
||||
$this['fakturaForm']['DATUM_VYSTAVENI']
|
||||
->setHtmlAttribute('readonly', true)
|
||||
->setHtmlAttribute('style', $disabledStyle)
|
||||
->setHtmlAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
public function renderEdit(?int $id = null): void
|
||||
{
|
||||
$this->template->isEdit = (bool) $this->id;
|
||||
|
||||
// Pokud upravujeme, načteme stávající položky, jinak připravíme prázdné pole pro JS/Latte
|
||||
$this->template->polozky = $this->id ? $this->fakturaFacade->getPolozky($this->id) : [];
|
||||
}
|
||||
|
||||
protected function createComponentFakturaForm(): Form
|
||||
{
|
||||
$form = new Form;
|
||||
|
||||
// 1. Načteme všechny zákazníky (parametr 'vse' projde bez filtru AKTIVNI)
|
||||
$zakaznici = $this->zakaznikFacade->getFiltered('', 'vse');
|
||||
$zakazniciPairs = [];
|
||||
foreach ($zakaznici as $z) {
|
||||
$nazev = trim(($z->PRIJMENI ?? '') . ' ' . ($z->JMENO ?? ''));
|
||||
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)
|
||||
->setPrompt('-- Vyberte zákazníka --');
|
||||
|
||||
$form->addText('CISLO', 'Číslo faktury:')
|
||||
->setDefaultValue(date('Y') . '001');
|
||||
|
||||
$form->addText('DATUM_VYSTAVENI', 'Datum vystavení:')
|
||||
->setHtmlType('date')
|
||||
->setDefaultValue(date('Y-m-d'));
|
||||
|
||||
$form->addText('DATUM_SPLATNOSTI', 'Datum splatnosti:')
|
||||
->setHtmlType('date')
|
||||
->setDefaultValue(date('Y-m-d', strtotime('+14 days')))
|
||||
->setRequired('Zadejte datum splatnosti.');
|
||||
|
||||
$form->addText('DATUM_DUZP', 'Datum UZP:')
|
||||
->setHtmlType('date');
|
||||
|
||||
$form->addSelect('STAV', 'Stav:', [
|
||||
'ISSUED' => 'Vystaveno (Neuhrazeno)',
|
||||
'PAID' => 'Zaplaceno',
|
||||
])->setDefaultValue('ISSUED');
|
||||
|
||||
$form->addTextArea('CINNOSTI', 'Činnosti / Popis plnění:');
|
||||
|
||||
$form->addSubmit('save', 'Uložit fakturu')
|
||||
->setHtmlAttribute('class', 'btn btn-primary');
|
||||
|
||||
$form->onSuccess[] = [$this, 'fakturaFormSucceeded'];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function fakturaFormSucceeded(Form $form, \stdClass $data): void
|
||||
{
|
||||
$firmaId = $this->getUser()->getIdentity()->firma_id;
|
||||
$httpRequest = $this->getHttpRequest();
|
||||
|
||||
// 1. Uložení hlavičky faktury
|
||||
$fakturaId = $this->fakturaFacade->saveFaktura((array) $data, $this->id, $firmaId);
|
||||
|
||||
// 2. Získání položek z POSTu (z dynamického formuláře z Latte/JS)
|
||||
$polozkyRaw = $httpRequest->getPost('polozky', []);
|
||||
|
||||
// 3. Uložení položek s automatickým výpočtem PORADI a celkové ceny
|
||||
$this->fakturaFacade->savePolozky($fakturaId, $polozkyRaw, $firmaId);
|
||||
|
||||
$this->flashMessage(
|
||||
$this->id ? 'Faktura byla úspěšně upravena.' : 'Nová faktura byla vystavena.',
|
||||
'success'
|
||||
);
|
||||
|
||||
$this->redirect('Faktura:default');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
{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>
|
||||
|
||||
<!-- Hlavička Faktury -->
|
||||
<div class="col-md-6">
|
||||
<label n:name="ZAKAZNIK_ID" class="form-label fw-bold">Odběratel *</label>
|
||||
<select n:name="ZAKAZNIK_ID" class="form-select"></select>
|
||||
</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">
|
||||
</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="DATUM_SPLATNOSTI" class="form-label">Datum splatnosti</label>
|
||||
<input n:name="DATUM_SPLATNOSTI" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label n:name="STAV" class="form-label">Stav úhrady</label>
|
||||
<select n:name="STAV" class="form-select"></select>
|
||||
</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 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: 130px;" 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 class="text-center align-middle drag-handle" style="cursor: grab;">
|
||||
<i class="bi bi-grip-vertical text-muted fs-5"></i>
|
||||
</td>
|
||||
<td><input type="text" name="polozky[{$index}][NAZEV]" value="{$p->NAZEV}" class="form-control item-nazev" required></td>
|
||||
<td><input type="number" step="0.01" name="polozky[{$index}][POCET]" value="{$p->POCET}" class="form-control item-pocet text-end" required></td>
|
||||
<td><input type="text" name="polozky[{$index}][JEDNOTKA]" value="{$p->JEDNOTKA}" class="form-control"></td>
|
||||
<td><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><input type="number" step="0.1" name="polozky[{$index}][SLEVA]" value="{$p->SLEVA}" class="form-control item-sleva text-end"></td>
|
||||
<td class="text-end fw-bold item-celkem-text">{$p->CENA|number:2, ',', ' '} CZK</td>
|
||||
<td class="text-center">
|
||||
<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">Celková částka:</td>
|
||||
<td class="text-end fw-bold fs-5 text-primary" id="total-sum">0,00 CZK</td>
|
||||
<td></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>
|
||||
</div>
|
||||
|
||||
{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ů
|
||||
function calculateTotals() {
|
||||
let grandTotal = 0;
|
||||
|
||||
document.querySelectorAll('.item-row').forEach(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;
|
||||
|
||||
const celkem = (pocet * cena) * (1 - (sleva / 100));
|
||||
grandTotal += celkem;
|
||||
|
||||
row.querySelector('.item-celkem-text').textContent = celkem.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' CZK';
|
||||
});
|
||||
|
||||
document.getElementById('total-sum').textContent = grandTotal.toLocaleString('cs-CZ', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' CZK';
|
||||
}
|
||||
|
||||
// Přidání nového řádku
|
||||
btnAdd.addEventListener('click', () => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'item-row';
|
||||
tr.innerHTML = `
|
||||
<td class="text-center align-middle drag-handle" style="cursor: grab;">
|
||||
<i class="bi bi-grip-vertical text-muted fs-5"></i>
|
||||
</td>
|
||||
<td><input type="text" name="polozky[${rowIndex}][NAZEV]" class="form-control item-nazev" placeholder="Popis práce / zboží" required></td>
|
||||
<td><input type="number" step="0.01" name="polozky[${rowIndex}][POCET]" value="1" class="form-control item-pocet text-end" required></td>
|
||||
<td><input type="text" name="polozky[${rowIndex}][JEDNOTKA]" value="ks" class="form-control"></td>
|
||||
<td><input type="number" step="0.01" name="polozky[${rowIndex}][CENA_JEDNOTKA]" value="0" class="form-control item-cena text-end" required></td>
|
||||
<td><input type="number" step="0.1" name="polozky[${rowIndex}][SLEVA]" value="0" class="form-control item-sleva text-end"></td>
|
||||
<td class="text-end fw-bold item-celkem-text">0,00 CZK</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm btn-remove-row"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
rowIndex++;
|
||||
reindexRows();
|
||||
calculateTotals();
|
||||
});
|
||||
|
||||
// Event delegation pro smazání a přepočet
|
||||
tbody.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.btn-remove-row')) {
|
||||
e.target.closest('.item-row').remove();
|
||||
reindexRows();
|
||||
calculateTotals();
|
||||
}
|
||||
});
|
||||
|
||||
tbody.addEventListener('input', (e) => {
|
||||
if (e.target.matches('.item-pocet, .item-cena, .item-sleva')) {
|
||||
calculateTotals();
|
||||
}
|
||||
});
|
||||
|
||||
if (document.querySelectorAll('.item-row').length === 0) {
|
||||
btnAdd.click();
|
||||
} else {
|
||||
calculateTotals();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{/syntax}
|
||||
|
|
@ -211,6 +211,7 @@ CREATE TABLE [dbo].[POLOZKA](
|
|||
[ID] [int] IDENTITY(24,1) NOT NULL,
|
||||
[FAKTURA] [int] NOT NULL,
|
||||
[FIRMA_ID] [int] NOT NULL, -- Duplikováno pro rychlé filtry a RLS
|
||||
[PORADI] [int] NOT NULL CONSTRAINT [DF_POLOZKA_PORADI] DEFAULT (1), -- Pořadí položky na faktuře
|
||||
|
||||
[NAZEV] [nvarchar](255) NOT NULL,
|
||||
[POCET] [float] NOT NULL DEFAULT 1,
|
||||
|
|
@ -225,7 +226,7 @@ CREATE TABLE [dbo].[POLOZKA](
|
|||
);
|
||||
GO
|
||||
|
||||
CREATE NONCLUSTERED INDEX [IX_POLOZKA_FAKTURA] ON [dbo].[POLOZKA]([FAKTURA] ASC);
|
||||
CREATE NONCLUSTERED INDEX [IX_POLOZKA_FAKTURA_PORADI] ON [dbo].[POLOZKA]([FAKTURA] ASC, [PORADI] ASC);
|
||||
GO
|
||||
|
||||
-- ==========================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue