SecurityPredicate

master
Kowalski 2026-08-03 10:29:25 +02:00
parent 2398bd6aca
commit bf9a5e1c8d
5 changed files with 289 additions and 21 deletions

View File

@ -29,28 +29,34 @@ class KufrikConnectionFactory
$identity = $this->user->getIdentity(); $identity = $this->user->getIdentity();
// Server i Databáze se načtou z Identity přihlášeného uživatele (z tabulky LOGIN) // Server, Databáze i FirmaId se načtou z Identity přihlášeného uživatele
$host = $identity->server ?? null; $host = $identity->server ?? null;
$databaseName = $identity->database ?? null; $databaseName = $identity->database ?? null;
$firmaId = $identity->firma_id ?? null;
if (empty($host) || empty($databaseName)) { if (empty($host) || empty($databaseName) || empty($firmaId)) {
throw new \LogicException('Přihlášený uživatel nemá v tabulce LOGIN definovaný SERVER nebo DATABASE.'); throw new \LogicException('Přihlášený uživatel nemá v tabulce LOGIN definovaný SERVER, DATABASE nebo FIRMA_ID.');
} }
// Dynamický DSN řetězec // Dynamický DSN řetězec
$dsn = "sqlsrv:Server={$host};Database={$databaseName};LoginTimeout=3;"; $dsn = "sqlsrv:Server={$host};Database={$databaseName};LoginTimeout=3;";
return new Connection( $connection = new Connection(
$dsn, $dsn,
$this->dbUser, $this->dbUser,
$this->dbPassword, $this->dbPassword,
[ [
'lazy' => false, 'lazy' => false, // Spojení musí vzniknout ihned
'driverOptions' => [ 'driverOptions' => [
\PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_UTF8, \PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_UTF8,
] ]
] ]
); );
// NASTAVENÍ RLS KONTEXTU PRO MS SQL SERVER
$connection->query('EXEC sp_set_session_context @key = N\'FirmaId\', @value = ?', (int)$firmaId);
return $connection;
} }
public function createExplorer(): Explorer public function createExplorer(): Explorer

View File

@ -109,10 +109,10 @@
<div n:class="tab-pane, fade, $presenter->isLinkCurrent('Faktura:*') ? 'show active'" id="toolbar-invoices" role="tabpanel"> <div n:class="tab-pane, fade, $presenter->isLinkCurrent('Faktura:*') ? 'show active'" id="toolbar-invoices" role="tabpanel">
<div class="toolbar-group"> <div class="toolbar-group">
<div class="action-buttons"> <div class="action-buttons">
<button type="button" class="btn btn-outline-primary btn-action"> <a n:href="Faktura:" type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-folder-fill"></i> <i class="bi bi-folder-fill"></i>
<span>Seznam faktur</span> <span>Seznam faktur</span>
</button> </a>
<button type="button" class="btn btn-success btn-action"> <button type="button" class="btn btn-success btn-action">
<i class="bi bi-plus-square-fill"></i> <i class="bi bi-plus-square-fill"></i>
<span>Přidat fakturu</span> <span>Přidat fakturu</span>
@ -134,17 +134,36 @@
</button> </button>
</div> </div>
<!-- Filtr pro Faktury --> <!-- Filtr pro Faktury v Toolbaru -->
<form class="filter-panel ms-auto ms-lg-0"> <form n:attr="method => 'get', action => $presenter->link('Faktura:default')" class="filter-panel ms-auto ms-lg-0">
<i class="bi bi-funnel-fill text-muted fs-5 ms-1"></i> <i class="bi bi-funnel-fill text-muted fs-5 ms-1"></i>
<select class="form-select form-select-sm" style="width: 85px;">
<option selected>2026</option> {var $aktualniRok = (int) date('Y')}
<option>2025</option> {var $vybranyRok = $presenter->rok ?? $aktualniRok}
{var $vybranyMesic = $presenter->mesic ?? ''}
<!-- Výběr Roku -->
<select name="rok" class="form-select form-select-sm" style="width: 90px;" onchange="this.form.submit()">
{for $r = $aktualniRok; $r >= $aktualniRok - 3; $r--}
<option value="{$r}" {if $vybranyRok == $r}selected{/if}>{$r}</option>
{/for}
</select> </select>
<select class="form-select form-select-sm" style="width: 140px;">
<option selected>(všechny měsíce)</option> <!-- Výběr Měsíce -->
<option>Leden</option> <select name="mesic" class="form-select form-select-sm" style="width: 140px;" onchange="this.form.submit()">
<option>Únor</option> <option value="" {if $vybranyMesic === ''}selected{/if}>(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> </select>
</form> </form>
</div> </div>

View File

@ -7,19 +7,113 @@ use Nette\Database\Explorer;
class FakturaPresenter extends BasePresenter class FakturaPresenter extends BasePresenter
{ {
/** @persistent */
public int $rok;
/** @persistent */
public string $mesic = '';
public function __construct( public function __construct(
private Explorer $kufrikExplorer private Explorer $kufrikExplorer
) { ) {
parent::__construct(); parent::__construct();
$this->rok = (int) date('Y');
} }
public function actionDefault(): void public function renderDefault(): void
{ {
// Seznam faktur $firmaId = $this->getUser()->getIdentity()->firma_id ?? null;
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;
} }
public function actionAdd(): void // 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
$celkemSum = 0;
$neuhrazenoCount = 0;
foreach ($faktury as $f) {
$suma = (float) ($f->CASTKA_CELKEM ?? 0);
$celkemSum += $suma;
if ($f->STAV !== 'PAID') {
$neuhrazenoCount++;
}
}
$this->template->faktury = $faktury;
$this->template->celkemSum = $celkemSum;
$this->template->neuhrazenoCount = $neuhrazenoCount;
$this->template->vybranyRok = $this->rok;
$this->template->vybranyMesic = $this->mesic;
}
public function handleUhradit(int $id): void
{ {
// Přidání faktury $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]);
$this->flashMessage(
$novyStav === 'PAID' ? 'Faktura byla označena jako zaplacená.' : 'Stav faktury byl změněn na neuhrazeno.',
'success'
);
}
$this->redirect('this');
}
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->flashMessage('Faktura byla smazána.', 'info');
}
$this->redirect('this');
} }
} }

View File

@ -0,0 +1,149 @@
{block content}
<div class="container-fluid">
<!-- 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>
<!-- 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>
</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>
</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>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Číslo faktury</th>
<th>Odběratel</th>
<th>Vystaveno</th>
<th>Splatnost</th>
<th class="text-end">Celková částka</th>
<th class="text-center">Stav</th>
<th class="text-end">Akce</th>
</tr>
</thead>
<tbody>
{foreach $faktury as $faktura}
<tr>
<td class="fw-bold">
<a n:href="FakturaDetail:default $faktura->ID" class="text-decoration-none">
{$faktura->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>
</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'}
</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">
<i class="bi bi-eye"></i>
</a>
<!-- Úprava -->
<a n:href="FakturaEdit:edit $faktura->ID"
class="btn btn-outline-primary"
title="Upravit fakturu">
<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">
<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}
</tbody>
</table>
</div>
</div>
</div>
<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);
});
});
</script>

View File

@ -41,7 +41,7 @@ class SignPresenter extends Presenter
$this->flashMessage('Přihlášení proběhlo úspěšně.', 'success'); $this->flashMessage('Přihlášení proběhlo úspěšně.', 'success');
// Přesměrování do hlavní části aplikace (např. HomePresenter) // Přesměrování do hlavní části aplikace (např. HomePresenter)
$this->redirect('Home:'); $this->redirect('Faktura:');
} catch (AuthenticationException $e) { } catch (AuthenticationException $e) {
$form->addError('Nespravný e-mail nebo heslo.'); $form->addError('Nespravný e-mail nebo heslo.');