GUI, kopírování faktur a DIČ má 12 znaků.

master
Kowalski 2026-11-01 19:15:31 +01:00
parent 4471344edc
commit 84e5cafecc
9 changed files with 195 additions and 169 deletions

View File

@ -336,10 +336,7 @@ class FakturaFacade
'O_DIC' => self::nullIfEmpty($data['O_DIC'] ?? null),
];
if (!$isNew) {
return $fromForm;
}
if ($isNew) {
$defaults = $this->getDodavatelDefaults($firmaId);
foreach ($defaults as $key => $value) {
if ($fromForm[$key] === null || $fromForm[$key] === '') {
@ -366,8 +363,49 @@ class FakturaFacade
}
}
}
}
return $fromForm;
return self::clampSnapshot($fromForm);
}
/**
* Ořízne snapshot na délky sloupců FAKTURA (ochrana před truncací SQL Serveru).
*
* @param array<string, string|null> $snapshot
* @return array<string, string|null>
*/
private static function clampSnapshot(array $snapshot): array
{
$limits = [
'D_PRAJM' => 511,
'D_ULICE' => 50,
'D_MESTO' => 50,
'D_PSC' => 50,
'D_IC' => 8,
'D_DIC' => 12,
'D_BANKA' => 100,
'D_TELEFON' => 20,
'D_EMAIL' => 100,
'D_WEB' => 100,
'O_PRAJM' => 511,
'O_ULICE' => 50,
'O_MESTO' => 50,
'O_PSC' => 5,
'O_IC' => 8,
'O_DIC' => 10,
];
foreach ($limits as $key => $max) {
if (!isset($snapshot[$key]) || $snapshot[$key] === null) {
continue;
}
$text = (string) $snapshot[$key];
if (mb_strlen($text) > $max) {
$snapshot[$key] = mb_substr($text, 0, $max);
}
}
return $snapshot;
}
private static function nullIfEmpty(mixed $value): ?string
@ -601,70 +639,6 @@ class FakturaFacade
$this->kufrikExplorer->table('FAKTURA')->where('ID', $id)->delete();
}
/**
* Kopie faktury: nové číslo, dnešní data, stejný odběratel a položky, bez plateb.
*
* @return int ID nové faktury
*/
public function duplicate(int $id, int $firmaId): int
{
$zdroj = $this->getById($id);
if (!$zdroj) {
throw new \InvalidArgumentException('Faktura nebyla nalezena.');
}
$dnes = new \DateTimeImmutable('today');
$dalsi = $this->getNextCislo($firmaId, $dnes);
$datumVystaveni = $dnes->format('Y-m-d');
$datumSplatnosti = $dnes
->modify('+' . $dalsi['splatnostDny'] . ' days')
->format('Y-m-d');
$novaId = $this->saveFaktura([
'ZAKAZNIK_ID' => (int) $zdroj->ZAKAZNIK,
'CISLO' => $dalsi['cislo'],
'CISLO_ABS' => $dalsi['cisloAbs'],
'FORMA_UHRADY' => (int) ($zdroj->FORMA_UHRADY ?? 1),
'DATUM_VYSTAVENI' => $datumVystaveni,
'DATUM_SPLATNOSTI' => $datumSplatnosti,
'DATUM_DUZP' => $datumVystaveni,
'STAV' => self::STAV_VYSTAVENO,
'CINNOSTI' => $zdroj->CINNOSTI,
// Zachovat upravené údaje dodavatele / odběratele ze zdroje
'D_PRAJM' => $zdroj->D_PRAJM,
'D_ULICE' => $zdroj->D_ULICE,
'D_MESTO' => $zdroj->D_MESTO,
'D_PSC' => $zdroj->D_PSC,
'D_IC' => $zdroj->D_IC,
'D_DIC' => $zdroj->D_DIC,
'D_BANKA' => $zdroj->D_BANKA,
'D_TELEFON' => $zdroj->D_TELEFON,
'D_EMAIL' => $zdroj->D_EMAIL,
'D_WEB' => $zdroj->D_WEB,
'O_PRAJM' => $zdroj->O_PRAJM,
'O_ULICE' => $zdroj->O_ULICE,
'O_MESTO' => $zdroj->O_MESTO,
'O_PSC' => $zdroj->O_PSC,
'O_IC' => $zdroj->O_IC,
'O_DIC' => $zdroj->O_DIC,
], null, $firmaId);
$polozky = [];
foreach ($this->getPolozky($id) as $polozka) {
$polozky[] = [
'NAZEV' => $polozka->NAZEV,
'POCET' => $polozka->POCET,
'JEDNOTKA' => $polozka->JEDNOTKA,
'CENA_JEDNOTKA' => $polozka->CENA_JEDNOTKA,
'SLEVA' => $polozka->SLEVA,
];
}
$this->savePolozky($novaId, $polozky, $firmaId);
$this->syncStav($novaId);
return $novaId;
}
/**
* Číslo účtu dodavatele ve formátu „účet/kód banky“.
*/

View File

@ -25,7 +25,7 @@
</script>
{* naše vlastní styly *}
<link href="{$basePath}/css/layout.css?v={$appVersion ?? '1.0'}-9" rel="stylesheet">
<link href="{$basePath}/css/layout.css?v={$appVersion ?? '1.0'}-10" rel="stylesheet">
</head>
<body>
@ -218,27 +218,30 @@
</div>
{/if}
<!-- Toasty -->
<div n:foreach="$flashes as $flash" class="alert alert-{$flash->type} alert-dismissible fade show auto-dismiss mb-1 shadow-sm" role="alert">
<!-- Flash zprávy jako plovoucí vrstva (neposouvá obsah pod sebou) -->
<div class="flash-toast-container" aria-live="polite" aria-atomic="true">
<div n:foreach="$flashes as $flash" class="alert alert-{$flash->type} alert-dismissible fade show auto-dismiss flash-toast shadow" role="alert">
{if $flash->type === 'danger' || $flash->type === 'error'}
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{elseif $flash->type === 'warning'}
<i class="bi bi-exclamation-circle-fill me-2"></i>
{elseif $flash->type === 'info'}
<i class="bi bi-info-circle-fill me-2"></i>
{else}
<i class="bi bi-check-circle-fill me-2"></i>
{/if}
<strong>{$flash->message}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Zavřít"></button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Najde všechny alerty s třídou auto-dismiss
const alerts = document.querySelectorAll('.alert.auto-dismiss');
alerts.forEach(alert => {
document.querySelectorAll('.flash-toast-container .alert.auto-dismiss').forEach(alert => {
setTimeout(() => {
// Přidá plynulé vytracení přes Bootstrap třídu nebo JS transition
alert.classList.remove('show');
alert.classList.add('fade');
// Po dokončení animace prvek úplně odstraní z DOMu
setTimeout(() => alert.remove(), 500);
}, 3000); // 3000 ms = 3 vteřiny
}, 3000);
});
});
</script>

View File

@ -219,21 +219,4 @@ class FakturaPresenter extends BasePresenter
$this->redirect('this');
}
/**
* Kopie faktury — nové číslo, dnešní data, stejné položky; otevře editaci.
*/
public function handleDuplicate(int $id): void
{
$firmaId = (int) $this->getUser()->getIdentity()->firma_id;
try {
$novaId = $this->fakturaFacade->duplicate($id, $firmaId);
} catch (\InvalidArgumentException $e) {
$this->flashMessage($e->getMessage(), 'danger');
$this->redirect('this');
}
$this->flashMessage('Kopie faktury byla vytvořena.', 'success');
$this->redirect('FakturaEdit:edit', $novaId);
}
}

View File

@ -137,8 +137,8 @@
<i class="bi bi-pencil"></i>
</a>
<!-- Kopie faktury -->
<a n:href="duplicate! $f->ID"
<!-- Kopie faktury (jako nová — editovatelná včetně data vystavení) -->
<a n:href="FakturaEdit:copy $f->ID"
class="btn btn-sm btn-outline-secondary me-1"
title="Vytvořit kopii">
<i class="bi bi-copy"></i>

View File

@ -12,6 +12,11 @@ class FakturaEditPresenter extends BasePresenter
/** @persistent */
public ?int $id = null;
/** @var list<object> Položky předvyplněné při kopii (ještě neuložená faktura). */
private array $kopiePolozky = [];
private ?int $ensureZakaznikId = null;
public function __construct(
private FakturaFacade $fakturaFacade,
private ZakaznikFacade $zakaznikFacade
@ -37,6 +42,51 @@ class FakturaEditPresenter extends BasePresenter
] + $this->fakturaFacade->getDodavatelDefaults($firmaId));
}
/**
* Kopie existující faktury jako nová (všechna pole editovatelná, uloží se až po uložení).
*/
public function actionCopy(int $id): void
{
$this->id = null;
$this->setView('edit');
$zdroj = $this->fakturaFacade->getById($id);
if (!$zdroj) {
$this->flashMessage('Faktura nebyla nalezena.', 'danger');
$this->redirect('Faktura:default');
}
$firmaId = (int) $this->getUser()->getIdentity()->firma_id;
$dnes = new \DateTimeImmutable('today');
$dalsi = $this->fakturaFacade->getNextCislo($firmaId, $dnes);
$datumVystaveni = $dnes->format('Y-m-d');
if ($zdroj->ZAKAZNIK !== null) {
$this->ensureZakaznikId = (int) $zdroj->ZAKAZNIK;
}
$this['fakturaForm']->setDefaults([
'ZAKAZNIK_ID' => $this->ensureZakaznikId,
'CISLO' => $dalsi['cislo'],
'CISLO_ABS' => $dalsi['cisloAbs'],
'FORMA_UHRADY' => (int) ($zdroj->FORMA_UHRADY ?? 1),
'DATUM_VYSTAVENI' => $datumVystaveni,
'DATUM_SPLATNOSTI' => $dnes
->modify('+' . $dalsi['splatnostDny'] . ' days')
->format('Y-m-d'),
'DATUM_DUZP' => $datumVystaveni,
'CINNOSTI' => $zdroj->CINNOSTI,
'O_PRAJM' => $zdroj->O_PRAJM,
'O_ULICE' => $zdroj->O_ULICE,
'O_MESTO' => $zdroj->O_MESTO,
'O_PSC' => $zdroj->O_PSC,
'O_IC' => $zdroj->O_IC,
'O_DIC' => $zdroj->O_DIC,
] + $this->fakturaFacade->getDodavatelDefaults($firmaId));
$this->kopiePolozky = $this->fakturaFacade->getPolozky($id);
}
public function actionEdit(int $id): void
{
$this->id = $id;
@ -52,6 +102,7 @@ class FakturaEditPresenter extends BasePresenter
// Explicitně přiřadíme ID zákazníka
if ($faktura->ZAKAZNIK !== null) {
$defaults['ZAKAZNIK_ID'] = (int) $faktura->ZAKAZNIK;
$this->ensureZakaznikId = (int) $faktura->ZAKAZNIK;
}
$defaults['FORMA_UHRADY'] = (int) ($faktura->FORMA_UHRADY ?? 1);
@ -92,6 +143,11 @@ class FakturaEditPresenter extends BasePresenter
$this->prepareEditTemplate();
}
public function renderCopy(): void
{
$this->prepareEditTemplate();
}
public function renderEdit(?int $id = null): void
{
$this->prepareEditTemplate();
@ -101,7 +157,9 @@ class FakturaEditPresenter extends BasePresenter
{
$this->template->isEdit = (bool) $this->id;
$polozky = $this->id ? $this->fakturaFacade->getPolozky($this->id) : [];
$polozky = $this->id
? $this->fakturaFacade->getPolozky($this->id)
: $this->kopiePolozky;
$platby = $this->id ? $this->fakturaFacade->getPlatby($this->id) : [];
$celkem = 0.0;
@ -157,9 +215,8 @@ class FakturaEditPresenter extends BasePresenter
$form = new Form;
$zakaznici = $this->zakaznikFacade->getFiltered('', 'aktivni');
if ($this->id) {
$faktura = $this->fakturaFacade->getById($this->id);
$zakaznikId = $faktura ? (int) $faktura->ZAKAZNIK : 0;
$zakaznikId = $this->ensureZakaznikId ?? 0;
if ($zakaznikId) {
$uzJeVSeznamu = false;
foreach ($zakaznici as $z) {
if ((int) $z->ID === $zakaznikId) {
@ -167,7 +224,7 @@ class FakturaEditPresenter extends BasePresenter
break;
}
}
if ($zakaznikId && !$uzJeVSeznamu) {
if (!$uzJeVSeznamu) {
$stavajici = $this->zakaznikFacade->getById($zakaznikId);
if ($stavajici) {
$zakaznici[] = $stavajici;
@ -218,8 +275,8 @@ class FakturaEditPresenter extends BasePresenter
$form->addText('D_PSC', 'PSČ:')->setMaxLength(50);
$form->addText('D_MESTO', 'Město:')->setMaxLength(50);
$form->addText('D_IC', 'IČ:')->setMaxLength(8);
$form->addText('D_DIC', 'DIČ:')->setMaxLength(10);
$form->addText('D_TELEFON', 'Telefon:')->setMaxLength(15);
$form->addText('D_DIC', 'DIČ:')->setMaxLength(12);
$form->addText('D_TELEFON', 'Telefon:')->setMaxLength(20);
$form->addText('D_EMAIL', 'E-mail:')->setMaxLength(100);
$form->addText('D_BANKA', 'Bankovní účet:')->setMaxLength(100);
$form->addText('D_WEB', 'Web:')->setMaxLength(100);
@ -259,9 +316,7 @@ class FakturaEditPresenter extends BasePresenter
];
}
if ($this->id) {
$faktura = $this->fakturaFacade->getById($this->id);
$zakaznikId = $faktura ? (int) $faktura->ZAKAZNIK : 0;
$zakaznikId = $this->ensureZakaznikId ?? 0;
if ($zakaznikId && !isset($map[(string) $zakaznikId])) {
$stavajici = $this->zakaznikFacade->getById($zakaznikId);
if ($stavajici) {
@ -275,7 +330,6 @@ class FakturaEditPresenter extends BasePresenter
];
}
}
}
return $map;
}

View File

@ -2,13 +2,6 @@
<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">
@ -135,16 +128,3 @@
</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

@ -206,9 +206,9 @@ CREATE TABLE [dbo].[FAKTURA](
[D_MESTO] [nvarchar](50) NULL,
[D_PSC] [nvarchar](50) NULL,
[D_IC] [nvarchar](8) NULL,
[D_DIC] [nvarchar](10) NULL,
[D_DIC] [nvarchar](12) NULL, -- shodně s FIRMA.DIC
[D_BANKA] [nvarchar](100) NULL,
[D_TELEFON] [nvarchar](15) NULL,
[D_TELEFON] [nvarchar](20) NULL, -- shodně s FIRMA.TELEFON
[D_EMAIL] [nvarchar](100) NULL,
[D_WEB] [nvarchar](100) NULL,

View File

@ -0,0 +1,14 @@
-- Sjednocení délky DIČ / telefonu snapshotu dodavatele s tabulkou FIRMA.
-- Spouštět v každé klientské databázi (např. kufrik).
IF COL_LENGTH('dbo.FAKTURA', 'D_DIC') IS NOT NULL
BEGIN
ALTER TABLE dbo.FAKTURA ALTER COLUMN [D_DIC] [nvarchar](12) NULL;
END
GO
IF COL_LENGTH('dbo.FAKTURA', 'D_TELEFON') IS NOT NULL
BEGIN
ALTER TABLE dbo.FAKTURA ALTER COLUMN [D_TELEFON] [nvarchar](20) NULL;
END
GO

View File

@ -383,6 +383,24 @@ body {
color: #2f6b58;
}
/* Flash / toast — vrstva nad obsahem, neovlivní layout */
.flash-toast-container {
position: fixed;
top: 4.75rem;
right: 1rem;
z-index: 1080;
width: min(28rem, calc(100vw - 2rem));
display: flex;
flex-direction: column;
gap: 0.5rem;
pointer-events: none;
}
.flash-toast-container .flash-toast {
pointer-events: auto;
margin-bottom: 0;
box-shadow: 0 0.5rem 1.25rem rgba(28, 43, 66, 0.18);
}
/* --- 7. JEMNĚJŠÍ PALETA NAD BOOTSTRAPEM --- */
a {
color: #1f62c0;