From 497708b5bbc4905d70d753d6d977f154adde40e3 Mon Sep 17 00:00:00 2001 From: Kowalski Date: Sat, 31 Oct 2026 09:31:03 +0100 Subject: [PATCH] Admin menu. --- app/Model/Facade/DatabazeFacade.php | 396 ++++++++++++++++++++++++++-- app/UI/@layout.latte | 10 +- app/UI/Admin/AdminPresenter.php | 201 +++++++++++++- app/UI/Admin/databaze.latte | 128 ++------- app/UI/Admin/firmy.latte | 166 ++++++++++++ app/UI/Admin/uzivatele.latte | 206 +++++++++++++++ config/common.neon | 5 + config/services.neon | 11 +- sql/KufrikDB.sql | 16 +- sql/mark_kufrik_database.sql | 28 ++ 10 files changed, 1015 insertions(+), 152 deletions(-) create mode 100644 app/UI/Admin/firmy.latte create mode 100644 app/UI/Admin/uzivatele.latte create mode 100644 sql/mark_kufrik_database.sql diff --git a/app/Model/Facade/DatabazeFacade.php b/app/Model/Facade/DatabazeFacade.php index 982a63b..836856b 100644 --- a/app/Model/Facade/DatabazeFacade.php +++ b/app/Model/Facade/DatabazeFacade.php @@ -2,35 +2,188 @@ namespace App\Model\Facade; +use App\Model\Database\KufrikConnectionFactory; use Nette\Database\Connection; use Nette\Http\FileUpload; +use Nette\Security\Passwords; use Nette\Utils\FileSystem; use Nette\Utils\Json; +use Nette\Utils\Validators; class DatabazeFacade { + private const MASTER_DB = 'kufrik_master'; + private const SYSTEM_DBS = ['master', 'tempdb', 'model', 'msdb', self::MASTER_DB]; + + /** Extended property na datové DB — jen takto označené se vypisují v Admin/Databáze. */ + private const APP_PROPERTY = 'kufrik_app'; + private const APP_PROPERTY_VALUE = '1'; + public function __construct( private Connection $masterDb, - private string $sdfStorageDir + private string $sdfStorageDir, + private Passwords $passwords, + private KufrikConnectionFactory $kufrikConnectionFactory, + private string $tenantServer, + private string $tenantDatabase ) { } /** - * Seznam databází z master tabulky LOGIN (jeden login = jedna databáze). + * Seznam datových databází Kufříku (označené extended property kufrik_app=1). * * @return \stdClass[] */ - public function getAll(): array + public function getDatabases(): array + { + $rows = $this->masterDb->fetchAll( + "SELECT name AS [DATABASE] + FROM sys.databases + WHERE state = 0 + AND name NOT IN (?, ?, ?, ?, ?) + ORDER BY name", + ...self::SYSTEM_DBS + ); + + $stats = []; + foreach ($this->masterDb->fetchAll( + "SELECT [DATABASE], [SERVER], + COUNT(*) AS USERS, + COUNT(DISTINCT FIRMA_ID) AS FIRMY + FROM dbo.LOGIN + WHERE [DATABASE] <> ? + GROUP BY [DATABASE], [SERVER]", + self::MASTER_DB + ) as $row) { + $stats[(string) $row->DATABASE] = $row; + } + + $result = []; + foreach ($rows as $row) { + $name = (string) $row->DATABASE; + if (!$this->isKufrikDatabase($name)) { + continue; + } + $stat = $stats[$name] ?? null; + $result[] = (object) [ + 'DATABASE' => $name, + 'SERVER' => $stat->SERVER ?? $this->tenantServer, + 'USERS' => (int) ($stat->USERS ?? 0), + 'FIRMY' => (int) ($stat->FIRMY ?? 0), + ]; + } + + return $result; + } + + /** + * @return array database => database + */ + public function getDatabaseSelectItems(): array + { + $items = []; + foreach ($this->getDatabases() as $db) { + $items[$db->DATABASE] = $db->DATABASE; + } + if ($items === [] && $this->tenantDatabase !== '') { + $items[$this->tenantDatabase] = $this->tenantDatabase; + } + + return $items; + } + + /** + * Firmy z vybrané datové DB. + * + * @return \stdClass[] + */ + public function getFirmy(string $database, ?string $server = null): array + { + $server ??= $this->tenantServer; + $this->assertTenantDatabase($database); + + $explorer = $this->kufrikConnectionFactory->createTenantExplorer($server, $database, 1); + $userCounts = []; + foreach ($this->masterDb->fetchAll( + 'SELECT FIRMA_ID, COUNT(*) AS C + FROM dbo.LOGIN + WHERE [DATABASE] = ? AND [SERVER] = ? + GROUP BY FIRMA_ID', + $database, + $server + ) as $row) { + $userCounts[(int) $row->FIRMA_ID] = (int) $row->C; + } + + $result = []; + foreach ($explorer->table('FIRMA')->order('NAZEV ASC') as $firma) { + $firmaId = (int) $firma->ID; + $loginId = $this->findLoginIdForFirma($database, $firmaId, $server); + $item = (object) [ + 'ID' => $firmaId, + 'NAZEV' => (string) $firma->NAZEV, + 'IC' => $firma->IC, + 'DIC' => $firma->DIC, + 'EMAIL' => $firma->EMAIL, + 'TELEFON' => $firma->TELEFON, + 'MESTO' => $firma->MESTO, + 'DATABASE' => $database, + 'SERVER' => $server, + 'USERS' => $userCounts[$firmaId] ?? 0, + 'LOGIN_ID' => $loginId, + ]; + if ($loginId) { + $meta = $this->readSdfMeta($loginId); + $item->SDF_SOUBOR = $meta['originalName'] ?? null; + $item->SDF_NAHRANO = isset($meta['uploadedAt']) ? new \DateTimeImmutable($meta['uploadedAt']) : null; + $item->SDF_IMPORTOVANO = isset($meta['importedAt']) ? new \DateTimeImmutable($meta['importedAt']) : null; + } else { + $item->SDF_SOUBOR = null; + $item->SDF_NAHRANO = null; + $item->SDF_IMPORTOVANO = null; + } + $result[] = $item; + } + + return $result; + } + + /** + * @return array + */ + public function getFirmaSelectItems(string $database, ?string $server = null): array + { + $items = []; + foreach ($this->getFirmy($database, $server) as $firma) { + $items[(int) $firma->ID] = $firma->NAZEV . ' (#' . $firma->ID . ')'; + } + + return $items; + } + + /** + * Uživatelé z master tabulky LOGIN. + * + * @return \stdClass[] + */ + public function getUzivatele(): array { $rows = $this->masterDb->fetchAll( 'SELECT ID, EMAIL, POSLEDNI_PRIHLASENI, AKTIVNI, [DATABASE], [SERVER], FIRMA_ID, IS_ADMIN FROM dbo.LOGIN - ORDER BY [DATABASE], EMAIL' + ORDER BY [DATABASE], FIRMA_ID, EMAIL' ); + $firmaNamesByDb = []; $result = []; foreach ($rows as $row) { - $result[] = $this->hydrate($row); + $item = $this->hydrateLogin($row); + $db = (string) $item->DATABASE; + if (!isset($firmaNamesByDb[$db])) { + $firmaNamesByDb[$db] = $this->loadFirmaNames($db, (string) $item->SERVER); + } + $item->FIRMA_NAZEV = $firmaNamesByDb[$db][(int) $item->FIRMA_ID] ?? null; + $result[] = $item; } return $result; @@ -44,8 +197,129 @@ class DatabazeFacade WHERE ID = ?', $id ); + if (!$row) { + return null; + } - return $row ? $this->hydrate($row) : null; + $item = $this->hydrateLogin($row); + $names = $this->loadFirmaNames((string) $item->DATABASE, (string) $item->SERVER); + $item->FIRMA_NAZEV = $names[(int) $item->FIRMA_ID] ?? null; + + return $item; + } + + public function createFirma(string $firmaNazev, string $database, ?string $server = null): int + { + $server ??= $this->tenantServer; + $firmaNazev = trim($firmaNazev); + $this->assertTenantDatabase($database); + + if ($firmaNazev === '') { + throw new \InvalidArgumentException('Zadejte název firmy.'); + } + + $explorer = $this->kufrikConnectionFactory->createTenantExplorer($server, $database, 1); + $firmaRow = $explorer->table('FIRMA')->insert([ + 'NAZEV' => mb_substr($firmaNazev, 0, 255), + 'PLATCE_DPH' => 0, + ]); + + return (int) $firmaRow->ID; + } + + /** + * Založí uživatele (vždy role User, nikdy admin). + */ + public function createUzivatel( + string $email, + string $password, + string $database, + int $firmaId, + ?string $server = null + ): int { + $server ??= $this->tenantServer; + $email = mb_strtolower(trim($email)); + $this->assertTenantDatabase($database); + + if (!Validators::isEmail($email)) { + throw new \InvalidArgumentException('Zadejte platný e-mail uživatele.'); + } + if (mb_strlen($password) < 6) { + throw new \InvalidArgumentException('Heslo musí mít alespoň 6 znaků.'); + } + if ($this->emailExists($email)) { + throw new \InvalidArgumentException('Uživatel s tímto e-mailem už existuje.'); + } + if (!$this->firmaExists($database, $firmaId, $server)) { + throw new \InvalidArgumentException('Vybraná firma v databázi neexistuje.'); + } + + $this->masterDb->query( + 'INSERT INTO dbo.LOGIN (EMAIL, HESLO_HASH, AKTIVNI, [DATABASE], [SERVER], FIRMA_ID, IS_ADMIN) + VALUES (?, ?, 1, ?, ?, ?, 0)', + $email, + $this->passwords->hash($password), + $database, + $server, + $firmaId + ); + + $loginId = (int) $this->masterDb->fetchField('SELECT ID FROM dbo.LOGIN WHERE EMAIL = ?', $email); + if (!$loginId) { + throw new \RuntimeException('Uživatele se nepodařilo založit.'); + } + + return $loginId; + } + + public function updateUserCredentials(int $loginId, string $email, ?string $password = null): void + { + $login = $this->getById($loginId); + if (!$login) { + throw new \InvalidArgumentException('Uživatel nebyl nalezen.'); + } + + $email = mb_strtolower(trim($email)); + if (!Validators::isEmail($email)) { + throw new \InvalidArgumentException('Zadejte platný e-mail uživatele.'); + } + if ($this->emailExists($email, $loginId)) { + throw new \InvalidArgumentException('Uživatel s tímto e-mailem už existuje.'); + } + + if ($password !== null && $password !== '') { + if (mb_strlen($password) < 6) { + throw new \InvalidArgumentException('Heslo musí mít alespoň 6 znaků.'); + } + $this->masterDb->query( + 'UPDATE dbo.LOGIN SET EMAIL = ?, HESLO_HASH = ? WHERE ID = ?', + $email, + $this->passwords->hash($password), + $loginId + ); + return; + } + + $this->masterDb->query( + 'UPDATE dbo.LOGIN SET EMAIL = ? WHERE ID = ?', + $email, + $loginId + ); + } + + public function findLoginIdForFirma(string $database, int $firmaId, ?string $server = null): ?int + { + $server ??= $this->tenantServer; + $id = $this->masterDb->fetchField( + 'SELECT TOP 1 ID FROM dbo.LOGIN + WHERE [DATABASE] = ? AND [SERVER] = ? AND FIRMA_ID = ? + ORDER BY IS_ADMIN DESC, ID ASC', + $database, + $server, + $firmaId + ); + + return $id ? (int) $id : null; } public function getSdfFilePath(int $loginId): ?string @@ -66,29 +340,11 @@ class DatabazeFacade FileSystem::write($this->sdfMetaPath($loginId), Json::encode($meta, pretty: true)); } - /** - * Položky selectu: ID => „databáze — e-mail“. - * - * @return array - */ - public function getSelectItems(): array - { - $items = []; - foreach ($this->getAll() as $login) { - $items[(int) $login->ID] = $login->DATABASE . ' — ' . $login->EMAIL; - } - - return $items; - } - - /** - * Uloží nahraný .sdf k loginu. - */ public function saveSdf(int $loginId, FileUpload $file): void { $exists = $this->masterDb->fetchField('SELECT ID FROM dbo.LOGIN WHERE ID = ?', $loginId); if (!$exists) { - throw new \InvalidArgumentException('Vybraná databáze (login) neexistuje.'); + throw new \InvalidArgumentException('Uživatel / login pro import neexistuje.'); } if (!$file->isOk()) { @@ -101,7 +357,6 @@ class DatabazeFacade } FileSystem::createDir($this->sdfStorageDir); - $file->move($this->sdfPath($loginId)); $this->clearSdfImportMeta($loginId); @@ -111,10 +366,95 @@ class DatabazeFacade 'size' => $file->getSize(), 'loginId' => $loginId, ]; - FileSystem::write($this->sdfMetaPath($loginId), Json::encode($meta, pretty: true)); } + public function getDefaultTenantDatabase(): string + { + return $this->tenantDatabase; + } + + public function getDefaultTenantServer(): string + { + return $this->tenantServer; + } + + private function assertTenantDatabase(string $database): void + { + if ($database === '' || in_array($database, self::SYSTEM_DBS, true)) { + throw new \InvalidArgumentException('Neplatná datová databáze.'); + } + if (!$this->isKufrikDatabase($database)) { + throw new \InvalidArgumentException( + "Databáze „{$database}“ není označená jako Kufřík (chybí metadata kufrik_app)." + ); + } + } + + /** + * Databáze patří do Kufříku, pokud má extended property kufrik_app = 1. + */ + private function isKufrikDatabase(string $database): bool + { + if (!preg_match('/^[A-Za-z0-9_]+$/', $database)) { + return false; + } + + try { + // sqlsrv + Nette fetchField nefunguje spolehlivě (column '0') → pojmenovaný sloupec + $row = $this->masterDb->fetch( + "SELECT TOP 1 CONVERT(nvarchar(100), value) AS prop_value + FROM [{$database}].sys.extended_properties + WHERE class = 0 AND major_id = 0 AND minor_id = 0 AND name = ?", + self::APP_PROPERTY + ); + + return $row !== null && (string) $row->prop_value === self::APP_PROPERTY_VALUE; + } catch (\Throwable) { + return false; + } + } + + private function firmaExists(string $database, int $firmaId, string $server): bool + { + $explorer = $this->kufrikConnectionFactory->createTenantExplorer($server, $database, 1); + return (bool) $explorer->table('FIRMA')->get($firmaId); + } + + private function emailExists(string $email, ?int $exceptId = null): bool + { + if ($exceptId) { + return (bool) $this->masterDb->fetchField( + 'SELECT ID FROM dbo.LOGIN WHERE EMAIL = ? AND ID <> ?', + $email, + $exceptId + ); + } + + return (bool) $this->masterDb->fetchField( + 'SELECT ID FROM dbo.LOGIN WHERE EMAIL = ?', + $email + ); + } + + /** + * @return array + */ + private function loadFirmaNames(string $database, string $server): array + { + try { + $this->assertTenantDatabase($database); + $explorer = $this->kufrikConnectionFactory->createTenantExplorer($server, $database, 1); + $names = []; + foreach ($explorer->table('FIRMA')->select('ID, NAZEV') as $firma) { + $names[(int) $firma->ID] = (string) $firma->NAZEV; + } + return $names; + } catch (\Throwable) { + return []; + } + } + /** * @return array{originalName?: string, uploadedAt?: string, size?: int}|array{} */ @@ -143,7 +483,7 @@ class DatabazeFacade return $meta; } - private function hydrate(object $row): \stdClass + private function hydrateLogin(object $row): \stdClass { $item = (object) (array) $row; $meta = $this->readSdfMeta((int) $item->ID); diff --git a/app/UI/@layout.latte b/app/UI/@layout.latte index 7ac53cd..c848b56 100644 --- a/app/UI/@layout.latte +++ b/app/UI/@layout.latte @@ -235,9 +235,13 @@ Databáze - - - Statistika + + + Firmy + + + + Uživatelé diff --git a/app/UI/Admin/AdminPresenter.php b/app/UI/Admin/AdminPresenter.php index fd19a70..7b401ce 100644 --- a/app/UI/Admin/AdminPresenter.php +++ b/app/UI/Admin/AdminPresenter.php @@ -15,6 +15,9 @@ class AdminPresenter extends BasePresenter { private const SDF_MAX_BYTES = 50 * 1024 * 1024; + /** @persistent */ + public string $db = ''; + public function __construct( private DatabazeFacade $databazeFacade, private SdfKufrikImporter $sdfImporter @@ -29,6 +32,10 @@ class AdminPresenter extends BasePresenter if (!$this->getUser()->isInRole('admin')) { $this->error('Sekce Admin je dostupná pouze administrátorovi systému.', 403); } + + if ($this->db === '') { + $this->db = $this->databazeFacade->getDefaultTenantDatabase(); + } } public function actionDefault(): void @@ -38,22 +45,192 @@ class AdminPresenter extends BasePresenter public function renderDatabaze(): void { - $loginy = $this->databazeFacade->getAll(); - $aktivniCount = 0; + $databases = $this->databazeFacade->getDatabases(); + $this->template->databases = $databases; + $this->template->celkemCount = count($databases); + } - foreach ($loginy as $login) { - if ($login->AKTIVNI) { + public function renderFirmy(): void + { + $databases = $this->databazeFacade->getDatabaseSelectItems(); + if ($databases !== [] && !isset($databases[$this->db])) { + $this->db = (string) array_key_first($databases); + } + + $firmy = $this->db !== '' + ? $this->databazeFacade->getFirmy($this->db, $this->databazeFacade->getDefaultTenantServer()) + : []; + + $this->template->databases = $databases; + $this->template->selectedDb = $this->db; + $this->template->firmy = $firmy; + $this->template->celkemCount = count($firmy); + } + + public function renderUzivatele(): void + { + $uzivatele = $this->databazeFacade->getUzivatele(); + $aktivniCount = 0; + foreach ($uzivatele as $u) { + if ($u->AKTIVNI) { $aktivniCount++; } } - $this->template->loginy = $loginy; - $this->template->celkemCount = count($loginy); + $this->template->uzivatele = $uzivatele; + $this->template->celkemCount = count($uzivatele); $this->template->aktivniCount = $aktivniCount; + $this->template->databases = $this->databazeFacade->getDatabaseSelectItems(); + $this->template->defaultDb = $this->db !== '' + ? $this->db + : $this->databazeFacade->getDefaultTenantDatabase(); + $this->template->firmaItems = $this->databazeFacade->getFirmaSelectItems( + $this->template->defaultDb, + $this->databazeFacade->getDefaultTenantServer() + ); } - public function renderStatistika(): void + protected function createComponentAddFirmaForm(): Form { + $form = new Form(); + + $form->addText('firmaNazev', 'Název firmy / OSVČ:') + ->setRequired('Zadejte název firmy.') + ->addRule($form::MaxLength, 'Název může mít nejvýše 255 znaků.', 255); + + $form->addSubmit('send', 'Přidat firmu'); + $form->onSuccess[] = [$this, 'addFirmaFormSucceeded']; + + return $form; + } + + public function addFirmaFormSucceeded(Form $form, \stdClass $data): void + { + try { + if ($this->db === '') { + throw new \InvalidArgumentException('Nejdřív vyberte databázi.'); + } + $firmaId = $this->databazeFacade->createFirma( + $data->firmaNazev, + $this->db, + $this->databazeFacade->getDefaultTenantServer() + ); + $this->flashMessage("Firma „{$data->firmaNazev}“ (#{$firmaId}) byla založena.", 'success'); + } catch (AbortException $e) { + throw $e; + } catch (\Throwable $e) { + $form->addError($e->getMessage()); + return; + } + + $this->redirect('this'); + } + + protected function createComponentAddUzivatelForm(): Form + { + $form = new Form(); + $databases = $this->databazeFacade->getDatabaseSelectItems(); + $defaultDb = isset($databases[$this->db]) + ? $this->db + : ($databases !== [] ? (string) array_key_first($databases) : ''); + + $form->addSelect('database', 'Databáze:') + ->setItems($databases) + ->setDefaultValue($defaultDb) + ->setRequired('Vyberte databázi.'); + + $form->addSelect('firmaId', 'Firma:') + ->setItems($defaultDb !== '' + ? $this->databazeFacade->getFirmaSelectItems($defaultDb) + : []) + ->setPrompt('— vyberte firmu —') + ->setRequired('Vyberte firmu.'); + + $form->addEmail('email', 'E-mail:') + ->setRequired('Zadejte e-mail.') + ->addRule($form::MaxLength, 'E-mail může mít nejvýše 100 znaků.', 100); + + $form->addPassword('password', 'Heslo:') + ->setRequired('Zadejte heslo.') + ->addRule($form::MinLength, 'Heslo musí mít alespoň 6 znaků.', 6); + + $form->addPassword('passwordVerify', 'Heslo znovu:') + ->setRequired('Zadejte heslo znovu.') + ->addRule($form::Equal, 'Hesla se neshodují.', $form['password']); + + $form->addSubmit('send', 'Přidat uživatele'); + $form->onSuccess[] = [$this, 'addUzivatelFormSucceeded']; + + return $form; + } + + public function addUzivatelFormSucceeded(Form $form, \stdClass $data): void + { + try { + $this->databazeFacade->createUzivatel( + $data->email, + $data->password, + $data->database, + (int) $data->firmaId, + $this->databazeFacade->getDefaultTenantServer() + ); + $this->flashMessage('Uživatel byl založen s rolí User.', 'success'); + } catch (AbortException $e) { + throw $e; + } catch (\Throwable $e) { + $form->addError($e->getMessage()); + return; + } + + $this->redirect('this'); + } + + protected function createComponentUserForm(): Form + { + $form = new Form(); + + $form->addHidden('loginId') + ->addRule($form::Integer) + ->setRequired(); + + $form->addEmail('email', 'E-mail / přihlašovací jméno:') + ->setRequired('Zadejte e-mail.') + ->addRule($form::MaxLength, 'E-mail může mít nejvýše 100 znaků.', 100); + + $form->addPassword('password', 'Nové heslo:') + ->setNullable() + ->addCondition($form::Filled) + ->addRule($form::MinLength, 'Heslo musí mít alespoň 6 znaků.', 6); + + $form->addPassword('passwordVerify', 'Heslo znovu:') + ->setNullable() + ->addConditionOn($form['password'], $form::Filled) + ->setRequired('Zadejte heslo znovu.') + ->addRule($form::Equal, 'Hesla se neshodují.', $form['password']); + + $form->addSubmit('send', 'Uložit uživatele'); + $form->onSuccess[] = [$this, 'userFormSucceeded']; + + return $form; + } + + public function userFormSucceeded(Form $form, \stdClass $data): void + { + try { + $password = $data->password !== '' ? $data->password : null; + $this->databazeFacade->updateUserCredentials( + (int) $data->loginId, + $data->email, + $password + ); + $this->flashMessage('Údaje uživatele byly uloženy.', 'success'); + } catch (AbortException $e) { + throw $e; + } catch (\Throwable $e) { + $this->flashMessage('Uložení uživatele selhalo: ' . $e->getMessage(), 'danger'); + } + + $this->redirect('this'); } protected function createComponentSdfImportForm(): Form @@ -61,8 +238,8 @@ class AdminPresenter extends BasePresenter $form = new Form(); $form->addHidden('loginId') - ->addRule($form::Integer, 'Vyberte databázi.') - ->setRequired('Vyberte databázi.'); + ->addRule($form::Integer, 'Vyberte firmu s uživatelem.') + ->setRequired('Vyberte firmu s uživatelem.'); $iniLimits = array_filter([ Helpers::iniGetSize('upload_max_filesize'), @@ -102,13 +279,11 @@ class AdminPresenter extends BasePresenter $stats = $this->sdfImporter->import($loginId); $this->flashMessage( sprintf( - 'Import ze SDF do databáze %s dokončen. Původní data byla smazána. Zákazníci: %d. Faktury: %d. Položky: %d. Platby: %d. Nastavení: %s.', - $stats['database'], + 'Import ze SDF dokončen. Původní data firmy byla smazána. Zákazníci: %d. Faktury: %d. Položky: %d. Platby: %d.', $stats['zakazniciInserted'], $stats['fakturyInserted'], $stats['polozkyInserted'], - $stats['platbyInserted'], - !empty($stats['nastaveniImported']) ? 'přeneseno' : 'bez změn' + $stats['platbyInserted'] ), 'success' ); diff --git a/app/UI/Admin/databaze.latte b/app/UI/Admin/databaze.latte index c72a804..06bcdec 100644 --- a/app/UI/Admin/databaze.latte +++ b/app/UI/Admin/databaze.latte @@ -1,38 +1,22 @@ {block content} -
-
-

Přehled databází

-
- -
- - - -
- -
-
    -
  • {$error}
  • -
+
+

Databáze

+

+ Jen databáze označené metadaty kufrik_app=1 (bez systémových DB a kufrik_master). +

-
+
-
Celkem databází
+
Databází
{$celkemCount} ks
-
-
-
Aktivní
-
{$aktivniCount} ks
-
-
@@ -42,71 +26,27 @@ Databáze Server - Login - Firma ID - Poslední přihlášení - Stav - Role - Poslední import SDF + Firem + Uživatelů Akce - {foreach $loginy as $login} - - - {$login->DATABASE} - - {$login->SERVER} - - {$login->EMAIL} - - {$login->FIRMA_ID} - - {if $login->POSLEDNI_PRIHLASENI} - {$login->POSLEDNI_PRIHLASENI|date:'d.m.Y H:i'} - {else} - — - {/if} - - - {if $login->AKTIVNI} - Aktivní - {else} - Neaktivní - {/if} - - - {if $login->IS_ADMIN} - Admin - {else} - Uživatel - {/if} - - - {if $login->SDF_SOUBOR} -
{$login->SDF_SOUBOR}
-
- nahráno {$login->SDF_NAHRANO|date:'d.m.Y H:i'} - {if $login->SDF_VELIKOST_TEXT} - · {$login->SDF_VELIKOST_TEXT} - {/if} -
-
- importováno {$login->SDF_IMPORTOVANO|date:'d.m.Y H:i'} -
- {else} - — - {/if} + {foreach $databases as $db} + + + {$db->DATABASE} + {$db->SERVER} + {$db->FIRMY} + {$db->USERS} - + + + + + + {/foreach} @@ -123,33 +63,11 @@ {/syntax} diff --git a/app/UI/Admin/firmy.latte b/app/UI/Admin/firmy.latte new file mode 100644 index 0000000..a06a69d --- /dev/null +++ b/app/UI/Admin/firmy.latte @@ -0,0 +1,166 @@ +{block content} + + + +
+
+

Firmy

+

+ Tabulka FIRMA ve vybrané datové databázi. +

+
+ +
+
+
+
    +
  • {$error}
  • +
+
+
+ +
+
+
+ + +
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ +
+ + + +
+ +
+
+
+
Firem v {$selectedDb}
+
{$celkemCount} ks
+
+
+
+ +
+
+ + + + + + + + + + + + + + {foreach $firmy as $firma} + + + + + + + + + + {/foreach} + +
NázevIDIČ / DIČKontaktUživatelůSDFAkce
+ {$firma->NAZEV} + {$firma->ID} + {if $firma->IC || $firma->DIC} +
IČ: {$firma->IC}
+
DIČ: {$firma->DIC}
+ {else} + — + {/if} +
+ {if $firma->EMAIL || $firma->TELEFON || $firma->MESTO} +
{$firma->EMAIL}
+
{$firma->TELEFON}
+
{$firma->MESTO}
+ {else} + — + {/if} +
{$firma->USERS} + {if $firma->SDF_SOUBOR} +
{$firma->SDF_SOUBOR}
+
+ importováno {$firma->SDF_IMPORTOVANO|date:'d.m.Y H:i'} +
+ {else} + — + {/if} +
+ {if $firma->LOGIN_ID} + + {else} + Nejdřív založte uživatele + {/if} +
+
+
+
+ + + + +{syntax off} + +{/syntax} diff --git a/app/UI/Admin/uzivatele.latte b/app/UI/Admin/uzivatele.latte new file mode 100644 index 0000000..c7005dd --- /dev/null +++ b/app/UI/Admin/uzivatele.latte @@ -0,0 +1,206 @@ +{block content} + + + +
+
+

Uživatelé

+

+ Tabulka LOGIN v kufrik_master. Každý uživatel míří na databázi a firmu. Noví uživatelé mají vždy roli User. +

+
+ +
+

Přidat uživatele

+
+
+
+
    +
  • {$error}
  • +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+

+ Po změně databáze obnovte stránku s parametrem ?db=..., aby se načetly firmy, nebo přidejte firmu nejdřív v agendě Firmy. +

+
+ +
+
+
+
Uživatelů
+
{$celkemCount} ks
+
+
+
+
+
Aktivní
+
{$aktivniCount} ks
+
+
+
+ +
+
+ + + + + + + + + + + + + + {foreach $uzivatele as $u} + + + + + + + + + + {/foreach} + +
E-mailDatabázeFirmaRolePoslední přihlášeníStavAkce
+ {$u->EMAIL} + {$u->DATABASE} + {$u->FIRMA_NAZEV ?: ('#' . $u->FIRMA_ID)} + (#{$u->FIRMA_ID}) + + {if $u->IS_ADMIN} + Admin + {else} + User + {/if} + + {if $u->POSLEDNI_PRIHLASENI} + {$u->POSLEDNI_PRIHLASENI|date:'d.m.Y H:i'} + {else} + — + {/if} + + {if $u->AKTIVNI} + Aktivní + {else} + Neaktivní + {/if} + + +
+
+
+
+ + + + + + +{syntax off} + +{/syntax} diff --git a/config/common.neon b/config/common.neon index 51a0a6e..de13b84 100644 --- a/config/common.neon +++ b/config/common.neon @@ -4,6 +4,11 @@ parameters: user: kufrik password: 'giteaKufrik_2026!' + # Sdílená tenant DB (zatím jen localhost / jedna databáze) + tenantDb: + server: localhost + database: kufrik + # Nahrané SDF soubory (SQL Server Compact) a heslo staré databáze sdfStorageDir: %appDir%/../data/sdf sdfPassword: 'Kmetix2016_' diff --git a/config/services.neon b/config/services.neon index fb3e479..2320c5c 100644 --- a/config/services.neon +++ b/config/services.neon @@ -6,8 +6,15 @@ services: # Authenticator si vyžádá připojení k Master DB - App\Model\Security\Authenticator(@database.default.connection) - # Seznam databází (LOGIN) z Master DB - - App\Model\Facade\DatabazeFacade(@database.default.connection, %sdfStorageDir%) + # Firmy / přístupy (LOGIN) ve sdílené tenant DB + - App\Model\Facade\DatabazeFacade( + @database.default.connection, + %sdfStorageDir%, + @security.passwords, + @kufrikConnectionFactory, + %tenantDb.server%, + %tenantDb.database% + ) - App\Model\Sdf\SdfCompactExporter(%sdfExportScript%, %sdfPassword%) - App\Model\Sdf\SdfKufrikImporter diff --git a/sql/KufrikDB.sql b/sql/KufrikDB.sql index 6cd29ca..005bb48 100644 --- a/sql/KufrikDB.sql +++ b/sql/KufrikDB.sql @@ -100,7 +100,7 @@ GO ALTER DATABASE [kufrik] SET AUTO_CLOSE OFF WITH NO_WAIT; GO ---user (stejný i pro master DB): +-- user (stejný i pro master DB): USE [kufrik] GO CREATE USER [kufrik] FOR LOGIN [kufrik] @@ -110,6 +110,20 @@ GO ALTER ROLE [db_owner] ADD MEMBER [kufrik] GO +-- Metadata: označení datové DB aplikace Kufřík (Admin vypisuje jen takto označené) +-- Property na databázi = sp_addextendedproperty bez level parametrů (v kontextu USE [kufrik]) +IF NOT EXISTS ( + SELECT 1 + FROM sys.extended_properties + WHERE class = 0 AND major_id = 0 AND minor_id = 0 AND name = N'kufrik_app' +) +BEGIN + EXEC sys.sp_addextendedproperty + @name = N'kufrik_app', + @value = N'1'; +END +GO + -- ========================================== -- 1. FIRMA HLAVNÍ TABULKA - vázaná na LOGIN z master_DB (taky bude tenant - izolace dat) -- ========================================== diff --git a/sql/mark_kufrik_database.sql b/sql/mark_kufrik_database.sql new file mode 100644 index 0000000..a91af4a --- /dev/null +++ b/sql/mark_kufrik_database.sql @@ -0,0 +1,28 @@ +-- Označí existující datovou databázi jako Kufřík (extended property). +-- Admin → Databáze vypisuje jen DB s kufrik_app = 1. +-- Property se nastavuje UVNITŘ databáze (bez @level0type = DATABASE). + +USE [kufrik] +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.extended_properties + WHERE class = 0 AND major_id = 0 AND minor_id = 0 AND name = N'kufrik_app' +) +BEGIN + EXEC sys.sp_addextendedproperty + @name = N'kufrik_app', + @value = N'1'; + PRINT N'Databáze [kufrik] byla označena (kufrik_app=1).'; +END +ELSE +BEGIN + PRINT N'Databáze [kufrik] už je označená.'; +END +GO + +SELECT name, CONVERT(nvarchar(100), value) AS value +FROM sys.extended_properties +WHERE class = 0 AND major_id = 0 AND minor_id = 0; +GO