Zákazníci

master
Kowalski 2026-08-03 10:51:27 +02:00
parent bf9a5e1c8d
commit 584cb2318a
8 changed files with 488 additions and 89 deletions

View File

@ -0,0 +1,109 @@
<?php
namespace App\Model\Facade;
use Nette\Database\Explorer;
class ZakaznikFacade
{
public function __construct(
private Explorer $kufrikExplorer
) {
}
/**
* Vrátí vyfiltrovaný seznam zákazníků
*/
public function getFiltered(string $search = '', string $zobrazit = 'aktivni'): array
{
$where = [];
$params = [];
if ($zobrazit === 'aktivni') {
$where[] = 'Z.AKTIVNI = 1';
} elseif ($zobrazit === 'neaktivni') {
$where[] = 'Z.AKTIVNI = 0';
}
if ($search !== '') {
$where[] = '(Z.PRIJMENI LIKE ? OR Z.JMENO LIKE ? OR Z.IC LIKE ? OR Z.MESTO LIKE ?)';
$term = '%' . $search . '%';
$params = array_merge($params, [$term, $term, $term, $term]);
}
$whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "
SELECT Z.*
FROM dbo.ZAKAZNIK Z
{$whereSql}
ORDER BY Z.PRIJMENI ASC, Z.JMENO ASC
";
return $this->kufrikExplorer->query($sql, ...$params)->fetchAll();
}
/**
* Detail zákazníka podle ID
*/
public function getById(int $id)
{
return $this->kufrikExplorer->table('ZAKAZNIK')->where('ID', $id)->fetch();
}
/**
* Uložení / Aktualizace zákazníka
*/
public function save(array $data, ?int $id = null, int $firmaId): void
{
// Sanitizace prázdných řetězců na NULL pro volitelné položky
$values = [
'FIRMA_ID' => $firmaId,
'JMENO' => $data['JMENO'] ?: null,
'PRIJMENI' => $data['PRIJMENI'],
'ULICE' => $data['ULICE'] ?: null,
'MESTO' => $data['MESTO'] ?: null,
'PSC' => $data['PSC'] ?: null,
'IC' => $data['IC'] ?: null,
'DIC' => $data['DIC'] ?: null,
'TELEFON' => $data['TELEFON'] ?: null,
'EMAIL' => $data['EMAIL'] ?: null,
'AKTIVNI' => (int) ($data['AKTIVNI'] ?? 1),
];
if ($id) {
unset($values['FIRMA_ID']); // FIRMA_ID se při úpravě nemění
$this->kufrikExplorer->table('ZAKAZNIK')->where('ID', $id)->update($values);
} else {
$this->kufrikExplorer->table('ZAKAZNIK')->insert($values);
}
}
/**
* Přepnutí stavu Aktivní / Neaktivní
*/
public function toggleAktivni(int $id): bool
{
$zakaznik = $this->getById($id);
if (!$zakaznik) {
return false;
}
$novyStav = !$zakaznik->AKTIVNI;
// OPRAVA: Provedeme update přímo přes Selection podle sloupce 'ID'
$this->kufrikExplorer->table('ZAKAZNIK')
->where('ID', $id)
->update(['AKTIVNI' => (int) $novyStav]);
return $novyStav;
}
/**
* Smazání zákazníka
*/
public function delete(int $id): void
{
$this->kufrikExplorer->table('ZAKAZNIK')->where('ID', $id)->delete();
}
}

View File

@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Model\Login;
/**
* Třída pro uchování a dešifrování názvu serveru a databáze, které uživatel použil při přihlášení.
* Parseruje název serveru, který může být ve tvaru: [server]@[databáze].
*/
class ServerCredentials
{
/**
* Název serveru (bez databáze).
* @var string
*/
public string $server;
/**
* Název databáze. Server connection string.
* @var string
*/
public string $databaze;
public function __construct(
/**
* Server connection string. Ten co zadává uživatel do přihlašovacího formuláře.
* @var string
*/
public string $server_string,
) {
$this->parseServer($server_string);
}
private function parseServer($server_string)
{
$this->server = $server_string; //default
$this->databaze = "psx"; //default
if (str_contains($server_string, "@")) { //je tam zavináč
$str_arr = explode("@", $server_string);
if (count($str_arr) <> 2)
return; //tady něco nehraje
$this->server = trim($str_arr[0]);
$this->databaze = trim($str_arr[1]);
}
}
}

View File

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Model\Login;
use Nette;
/**
* Naše vlastní indetita přihlášeného uživatele.
*/
class UserIdentity extends Nette\Security\SimpleIdentity
{
public function __construct(
int $id,
array $roles,
/*
* Server connection string.
*/
public string $dsn,
public ServerCredentials $credentials,
public array $dostupneDatabaze,
public string $username,
public string $password,
public ?string $pracovnik,
public ?int $pracovnikID,
public bool $vynutitGPS
) {
parent::__construct($id, $roles, null);
}
}

View File

@ -172,14 +172,14 @@
<!-- ZÁKAZNÍCI --> <!-- ZÁKAZNÍCI -->
<div n:class="tab-pane, fade, $presenter->isLinkCurrent('Zakaznik:*') ? 'show active'" id="toolbar-customers" role="tabpanel"> <div n:class="tab-pane, fade, $presenter->isLinkCurrent('Zakaznik:*') ? 'show active'" id="toolbar-customers" role="tabpanel">
<div class="action-buttons"> <div class="action-buttons">
<button type="button" class="btn btn-outline-primary btn-action"> <a n:href="Zakaznik:default" type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-people-fill"></i> <i class="bi bi-people-fill"></i>
<span>Seznam</span> <span>Seznam</span>
</button> </a>
<button type="button" class="btn btn-success btn-action"> <a n:href="ZakaznikEdit:new" type="button" class="btn btn-success btn-action">
<i class="bi bi-person-plus-fill"></i> <i class="bi bi-person-plus-fill"></i>
<span>Přidat zákazníka</span> <span>Přidat zákazníka</span>
</button> </a>
</div> </div>
</div> </div>

View File

@ -3,23 +3,51 @@
namespace App\UI\Zakaznik; namespace App\UI\Zakaznik;
use App\UI\BasePresenter; use App\UI\BasePresenter;
use Nette\Database\Explorer; use App\Model\Facade\ZakaznikFacade;
class ZakaznikPresenter extends BasePresenter class ZakaznikPresenter extends BasePresenter
{ {
/** @persistent */
public string $search = '';
/** @persistent */
public string $zobrazit = 'aktivni';
public function __construct( public function __construct(
private Explorer $kufrikExplorer private ZakaznikFacade $zakaznikFacade
) { ) {
parent::__construct(); parent::__construct();
} }
public function actionDefault(): void public function renderDefault(): void
{ {
// Seznam zákazníků $this->template->zakaznici = $this->zakaznikFacade->getFiltered($this->search, $this->zobrazit);
$this->template->search = $this->search;
$this->template->zobrazit = $this->zobrazit;
} }
public function actionAdd(): void public function handleToggleAktivni(int $id): void
{ {
// Přidání zákazníka $novyStav = $this->zakaznikFacade->toggleAktivni($id);
$this->flashMessage(
$novyStav ? 'Zákazník byl aktivován.' : 'Zákazník byl deaktivován.',
'success'
);
$this->redirect('this');
}
public function handleDelete(int $id): void
{
try {
$this->zakaznikFacade->delete($id);
$this->flashMessage('Zákazník byl úspěšně smazán.', 'info');
} catch (\PDOException $e) {
// Pokud je navázán na FAKTURA, FK v MS SQL odmítne smazání
$this->flashMessage('Zákazníka nelze smazat, protože má v systému vystavené faktury. Můžete jej deaktivovat.', 'warning');
}
$this->redirect('this');
} }
} }

View File

@ -0,0 +1,150 @@
{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>
<!-- Vyhledávací a filtrační lišta nad tabulkou -->
<div class="content-card my-3">
<form n:attr="method => 'get', action => $presenter->link('Zakaznik:default')" class="row g-2 align-items-center">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text bg-white text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" value="{$search}" class="form-control" placeholder="Hledat podle příjmení, jména, IČ nebo města...">
</div>
</div>
<div class="col-md-3">
<select name="zobrazit" class="form-select" onchange="this.form.submit()">
<option value="aktivni" {if $zobrazit === 'aktivni'}selected{/if}>Pouze aktivní zákazníci</option>
<option value="vse" {if $zobrazit === 'vse'}selected{/if}>Všichni zákazníci</option>
<option value="neaktivni" {if $zobrazit === 'neaktivni'}selected{/if}>Pouze neaktivní</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary w-100">
<i class="bi bi-funnel me-1"></i>Filtrovat
</button>
</div>
<div class="col-md-2 text-end">
<a n:href="ZakaznikEdit:new" class="btn btn-success w-100">
<i class="bi bi-person-plus-fill me-1"></i>Nový zákazník
</a>
</div>
</form>
</div>
<!-- Tabulka Zákazníků -->
<div class="content-card my-3">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Jméno / Firma</th>
<th>Adresa</th>
<th>IČ / DIČ</th>
<th>Kontakt</th>
<th class="text-center">Stav</th>
<th class="text-end">Akce</th>
</tr>
</thead>
<tbody>
{foreach $zakaznici as $z}
<tr n:class="!$z->AKTIVNI ? 'table-light text-muted'">
<td class="fw-bold">
{$z->PRIJMENI} {$z->JMENO}
</td>
<td>
{if $z->ULICE || $z->MESTO}
<div>{$z->ULICE}</div>
<div class="small text-muted">{$z->PSC} {$z->MESTO}</div>
{else}
<span class="text-muted small">—</span>
{/if}
</td>
<td>
{if $z->IC}
<div>IČ: {$z->IC}</div>
<div class="small text-muted" n:if="$z->DIC">DIČ: {$z->DIC}</div>
{else}
<span class="text-muted small">—</span>
{/if}
</td>
<td>
{if $z->TELEFON}
<div><i class="bi bi-telephone me-1 text-muted"></i>{$z->TELEFON}</div>
{/if}
{if $z->EMAIL}
<div class="small"><i class="bi bi-envelope me-1 text-muted"></i><a href="mailto:{$z->EMAIL}" class="text-decoration-none">{$z->EMAIL}</a></div>
{/if}
{if !$z->TELEFON && !$z->EMAIL}
<span class="text-muted small">—</span>
{/if}
</td>
<td class="text-center">
{if $z->AKTIVNI}
<span class="badge bg-success-subtle text-success border border-success">Aktivní</span>
{else}
<span class="badge bg-secondary-subtle text-secondary border border-secondary">Neaktivní</span>
{/if}
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<!-- Úprava -->
<a n:href="ZakaznikEdit:edit $z->ID"
class="btn btn-outline-primary"
title="Upravit zákazníka">
<i class="bi bi-pencil"></i>
</a>
<!-- Přepnutí stavu Aktivní / Neaktivní -->
<a n:href="toggleAktivni! $z->ID"
class="btn {$z->AKTIVNI ? 'btn-outline-secondary' : 'btn-outline-success'}"
title="{$z->AKTIVNI ? 'Deaktivovat' : 'Aktivovat'}">
<i n:class="bi, $z->AKTIVNI ? 'bi-person-x' : 'bi-person-check'"></i>
</a>
<!-- Smazání -->
<a n:href="delete! $z->ID"
class="btn btn-outline-danger"
onclick="return confirm('Opravdu chcete smazat tohoto zákazníka?');"
title="Smazat zákazníka">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{else}
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<i class="bi bi-people fs-2 d-block mb-2"></i>
Nebyli nalezeni žádní zákazníci odpovídající zadaným kritériím.
</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

@ -0,0 +1,101 @@
<?php
namespace App\UI\ZakaznikEdit;
use App\UI\BasePresenter;
use App\Model\Facade\ZakaznikFacade;
use Nette\Application\UI\Form;
class ZakaznikEditPresenter extends BasePresenter
{
/** @persistent */
public ?int $id = null;
public function __construct(
private ZakaznikFacade $zakaznikFacade
) {
parent::__construct();
}
public function actionNew(): void
{
$this->id = null;
$this->setView('edit'); // <--- Zde přikážeme Nette použít edit.latte
}
public function actionEdit(int $id): void
{
$this->id = $id;
$zakaznik = $this->zakaznikFacade->getById($id);
if (!$zakaznik) {
$this->flashMessage('Zákazník nebyl nalezen.', 'danger');
$this->redirect('Zakaznik:default');
}
$this['zakaznikForm']->setDefaults($zakaznik->toArray());
}
public function renderEdit(?int $id = null): void
{
$this->template->isEdit = (bool) $this->id;
}
public function renderNew(): void
{
$this->template->isEdit = false;
}
protected function createComponentZakaznikForm(): Form
{
$form = new Form;
// Základní údaje
$form->addText('PRIJMENI', 'Příjmení / Název firmy:')
->setRequired('Zadejte příjmení nebo název firmy.');
$form->addText('JMENO', 'Jméno:');
// IČO / DIČ
$form->addText('IC', 'IČ:')
->setMaxLength(8);
$form->addText('DIC', 'DIČ:')
->setMaxLength(10);
// Adresa
$form->addText('ULICE', 'Ulice a ČP:');
$form->addText('MESTO', 'Město:');
$form->addText('PSC', 'PSČ:')
->setMaxLength(5);
// Kontaktní údaje
$form->addText('TELEFON', 'Telefon:');
$form->addEmail('EMAIL', 'E-mail:');
// Příznak aktivity
$form->addCheckbox('AKTIVNI', 'Aktivní zákazník')
->setDefaultValue(true);
$form->addSubmit('save', 'Uložit zákazníka')
->setHtmlAttribute('class', 'btn btn-primary');
$form->onSuccess[] = [$this, 'zakaznikFormSucceeded'];
return $form;
}
public function zakaznikFormSucceeded(Form $form, \stdClass $data): void
{
$firmaId = $this->getUser()->getIdentity()->firma_id;
$this->zakaznikFacade->save((array) $data, $this->id, $firmaId);
$this->flashMessage(
$this->id ? 'Údaje zákazníka byly upraveny.' : 'Nový zákazník byl úspěšně vytvořen.',
'success'
);
$this->redirect('Zakaznik:default');
}
}

View File

@ -0,0 +1,90 @@
{block content}
<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-person-plus-fill{/if} me-2"></i>
{if $isEdit}Úprava zákazníka{else}Nový zákazník{/if}
</h2>
<a n:href="Zakaznik: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="zakaznikForm" class="row g-3">
<!-- Chybové hlášky formuláře -->
<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>
<!-- Identifikace / Název -->
<div class="col-md-6">
<label n:name="PRIJMENI" class="form-label fw-bold">Příjmení / Název firmy *</label>
<input n:name="PRIJMENI" class="form-control" placeholder="např. Novák nebo Auto-Servis s.r.o.">
</div>
<div class="col-md-6">
<label n:name="JMENO" class="form-label">Jméno (nepovinné pro firmy)</label>
<input n:name="JMENO" class="form-control" placeholder="např. Jan">
</div>
<!-- IČ / DIČ -->
<div class="col-md-6">
<label n:name="IC" class="form-label">IČ</label>
<input n:name="IC" class="form-control" placeholder="8 číslic">
</div>
<div class="col-md-6">
<label n:name="DIC" class="form-label">DIČ</label>
<input n:name="DIC" class="form-control" placeholder="CZ12345678">
</div>
<hr class="my-4">
<!-- Adresa -->
<div class="col-md-5">
<label n:name="ULICE" class="form-label">Ulice a číslo popisné</label>
<input n:name="ULICE" class="form-control">
</div>
<div class="col-md-5">
<label n:name="MESTO" class="form-label">Město</label>
<input n:name="MESTO" class="form-control">
</div>
<div class="col-md-2">
<label n:name="PSC" class="form-label">PSČ</label>
<input n:name="PSC" class="form-control">
</div>
<hr class="my-4">
<!-- Kontakty -->
<div class="col-md-6">
<label n:name="TELEFON" class="form-label">Telefon</label>
<input n:name="TELEFON" class="form-control" placeholder="+420 ...">
</div>
<div class="col-md-6">
<label n:name="EMAIL" class="form-label">E-mail</label>
<input n:name="EMAIL" class="form-control" placeholder="email@domain.cz">
</div>
<!-- Aktivita -->
<div class="col-12">
<div class="form-check">
<input n:name="AKTIVNI" class="form-check-input">
<label n:name="AKTIVNI" class="form-check-label fw-bold">Aktivní zákazník (zobrazovat v nabídkách)</label>
</div>
</div>
<!-- Tlačítka -->
<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 zákazníka
</button>
<a n:href="Zakaznik:default" class="btn btn-outline-secondary">Zrušit</a>
</div>
</form>
</div>
</div>