Admin menu.
parent
fa02f23d7f
commit
497708b5bb
|
|
@ -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<string, string> 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<int, string>
|
||||
*/
|
||||
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<int, string>
|
||||
*/
|
||||
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<int, string>
|
||||
*/
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -235,9 +235,13 @@
|
|||
<i class="bi bi-database-fill"></i>
|
||||
<span>Databáze</span>
|
||||
</a>
|
||||
<a n:href="Admin:statistika" n:class="btn, btn-action, $presenter->isLinkCurrent('Admin:statistika') ? 'btn-primary active' : 'btn-outline-primary'">
|
||||
<i class="bi bi-graph-up"></i>
|
||||
<span>Statistika</span>
|
||||
<a n:href="Admin:firmy" n:class="btn, btn-action, $presenter->isLinkCurrent('Admin:firmy') ? 'btn-primary active' : 'btn-outline-primary'">
|
||||
<i class="bi bi-building"></i>
|
||||
<span>Firmy</span>
|
||||
</a>
|
||||
<a n:href="Admin:uzivatele" n:class="btn, btn-action, $presenter->isLinkCurrent('Admin:uzivatele') ? 'btn-primary active' : 'btn-outline-primary'">
|
||||
<i class="bi bi-people"></i>
|
||||
<span>Uživatelé</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,38 +1,22 @@
|
|||
{block content}
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h3 mb-0">Přehled databází</h2>
|
||||
</div>
|
||||
|
||||
<form n:name="sdfImportForm" class="d-none">
|
||||
<input n:name="loginId">
|
||||
<input n:name="sdf" accept=".sdf">
|
||||
<button n:name="send">Import</button>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-danger" n:if="$presenter['sdfImportForm']->hasErrors()">
|
||||
<ul class="mb-0 ps-3">
|
||||
<li n:foreach="$presenter['sdfImportForm']->errors as $error">{$error}</li>
|
||||
</ul>
|
||||
<div class="mb-3">
|
||||
<h2 class="h3 mb-1">Databáze</h2>
|
||||
<p class="text-muted small mb-0">
|
||||
Jen databáze označené metadaty <code>kufrik_app=1</code> (bez systémových DB a <code>kufrik_master</code>).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-primary border-4">
|
||||
<div class="text-muted small">Celkem databází</div>
|
||||
<div class="text-muted small">Databází</div>
|
||||
<div class="fs-4 fw-bold text-primary">{$celkemCount} ks</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-success border-4">
|
||||
<div class="text-muted small">Aktivní</div>
|
||||
<div class="fs-4 fw-bold text-success">{$aktivniCount} ks</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 p-3 bg-white">
|
||||
|
|
@ -42,71 +26,27 @@
|
|||
<tr>
|
||||
<th>Databáze</th>
|
||||
<th>Server</th>
|
||||
<th>Login</th>
|
||||
<th class="text-center">Firma ID</th>
|
||||
<th>Poslední přihlášení</th>
|
||||
<th class="text-center">Stav</th>
|
||||
<th class="text-center">Role</th>
|
||||
<th>Poslední import SDF</th>
|
||||
<th class="text-center">Firem</th>
|
||||
<th class="text-center">Uživatelů</th>
|
||||
<th class="text-end" data-orderable="false">Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $loginy as $login}
|
||||
<tr n:class="!$login->AKTIVNI ? 'table-light text-muted'">
|
||||
<td data-label="Databáze" class="fw-bold" data-order="{$login->DATABASE}">
|
||||
<i class="bi bi-database me-1 text-muted"></i>{$login->DATABASE}
|
||||
</td>
|
||||
<td data-label="Server">{$login->SERVER}</td>
|
||||
<td data-label="Login">
|
||||
<a href="mailto:{$login->EMAIL}" class="text-decoration-none">{$login->EMAIL}</a>
|
||||
</td>
|
||||
<td data-label="Firma ID" class="text-center">{$login->FIRMA_ID}</td>
|
||||
<td data-label="Poslední přihlášení" data-order="{if $login->POSLEDNI_PRIHLASENI}{$login->POSLEDNI_PRIHLASENI|date:'YmdHis'}{/if}">
|
||||
{if $login->POSLEDNI_PRIHLASENI}
|
||||
{$login->POSLEDNI_PRIHLASENI|date:'d.m.Y H:i'}
|
||||
{else}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td data-label="Stav" class="text-center" data-order="{$login->AKTIVNI}">
|
||||
{if $login->AKTIVNI}
|
||||
<span class="badge bg-success">Aktivní</span>
|
||||
{else}
|
||||
<span class="badge bg-secondary">Neaktivní</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td data-label="Role" class="text-center" data-order="{$login->IS_ADMIN}">
|
||||
{if $login->IS_ADMIN}
|
||||
<span class="badge bg-dark">Admin</span>
|
||||
{else}
|
||||
<span class="badge bg-light text-dark border">Uživatel</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td data-label="Poslední import SDF" data-order="{$login->SDF_SOUBOR}">
|
||||
{if $login->SDF_SOUBOR}
|
||||
<div class="fw-semibold">{$login->SDF_SOUBOR}</div>
|
||||
<div class="small text-muted">
|
||||
nahráno {$login->SDF_NAHRANO|date:'d.m.Y H:i'}
|
||||
{if $login->SDF_VELIKOST_TEXT}
|
||||
· {$login->SDF_VELIKOST_TEXT}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="small text-success" n:if="$login->SDF_IMPORTOVANO">
|
||||
importováno {$login->SDF_IMPORTOVANO|date:'d.m.Y H:i'}
|
||||
</div>
|
||||
{else}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
{foreach $databases as $db}
|
||||
<tr>
|
||||
<td data-label="Databáze" class="fw-bold">
|
||||
<i class="bi bi-database me-1 text-muted"></i>{$db->DATABASE}
|
||||
</td>
|
||||
<td data-label="Server">{$db->SERVER}</td>
|
||||
<td data-label="Firem" class="text-center">{$db->FIRMY}</td>
|
||||
<td data-label="Uživatelů" class="text-center">{$db->USERS}</td>
|
||||
<td class="text-end text-nowrap cell-actions">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-success js-sdf-import"
|
||||
data-login-id="{$login->ID}"
|
||||
data-database="{$login->DATABASE}"
|
||||
title="Smaže stávající zákazníky, faktury, položky a platby a nahradí je daty ze SDF">
|
||||
<i class="bi bi-box-arrow-in-down me-1"></i>Import
|
||||
</button>
|
||||
<a n:href="Admin:firmy, db => $db->DATABASE" class="btn btn-sm btn-outline-primary me-1" title="Firmy v databázi">
|
||||
<i class="bi bi-building"></i>
|
||||
</a>
|
||||
<a n:href="Admin:uzivatele, db => $db->DATABASE" class="btn btn-sm btn-outline-secondary" title="Uživatelé databáze">
|
||||
<i class="bi bi-people"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
|
|
@ -123,33 +63,11 @@
|
|||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#databaze-table').DataTable({
|
||||
language: {
|
||||
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/cs.json'
|
||||
},
|
||||
language: { url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/cs.json' },
|
||||
order: [[0, 'asc']],
|
||||
pageLength: 25,
|
||||
autoWidth: false
|
||||
});
|
||||
|
||||
const $form = $('form[id$="-sdfImportForm"], #frm-sdfImportForm').first();
|
||||
const $loginId = $form.find('[name="loginId"]');
|
||||
const $file = $form.find('input[type="file"]');
|
||||
|
||||
$(document).on('click', '.js-sdf-import', function() {
|
||||
const database = $(this).data('database');
|
||||
if (!confirm('VAROVÁNÍ: Import ze SDF do databáze ' + database + ' SMAŽE všechna stávající data této firmy (zákazníky, faktury, položky a platby) a nahradí je obsahem souboru.\n\nOpravdu chcete pokračovat?')) {
|
||||
return;
|
||||
}
|
||||
$loginId.val($(this).data('login-id'));
|
||||
$file.val('');
|
||||
$file.trigger('click');
|
||||
});
|
||||
|
||||
$file.on('change', function() {
|
||||
if (this.files && this.files.length) {
|
||||
$form.trigger('submit');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{/syntax}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
{block content}
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
<div class="mb-3">
|
||||
<h2 class="h3 mb-1">Firmy</h2>
|
||||
<p class="text-muted small mb-0">
|
||||
Tabulka <code>FIRMA</code> ve vybrané datové databázi.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm p-3 bg-white mb-4">
|
||||
<div class="mb-2" n:if="$presenter['addFirmaForm']->hasErrors()">
|
||||
<div class="alert alert-danger mb-0">
|
||||
<ul class="mb-0 ps-3">
|
||||
<li n:foreach="$presenter['addFirmaForm']->errors as $error">{$error}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<form method="get" action="{link this}">
|
||||
<label class="form-label" for="dbFilter">Databáze</label>
|
||||
<select name="db" id="dbFilter" class="form-select" onchange="this.form.submit()">
|
||||
<option n:foreach="$databases as $name => $label" value="{$name}" {if $selectedDb === $name}selected{/if}>{$label}</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<form n:name="addFirmaForm" class="row g-2 align-items-end">
|
||||
<div class="col-md-8">
|
||||
<label n:name="firmaNazev" class="form-label">Název firmy / OSVČ</label>
|
||||
<input n:name="firmaNazev" class="form-control" placeholder="např. Jan Novák">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button n:name="send" class="btn btn-primary w-100">
|
||||
<i class="bi bi-plus-lg me-1"></i>Přidat
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form n:name="sdfImportForm" class="d-none">
|
||||
<input n:name="loginId">
|
||||
<input n:name="sdf" accept=".sdf">
|
||||
<button n:name="send">Import</button>
|
||||
</form>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-primary border-4">
|
||||
<div class="text-muted small">Firem v {$selectedDb}</div>
|
||||
<div class="fs-4 fw-bold text-primary">{$celkemCount} ks</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 p-3 bg-white">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-cards align-middle w-100" id="firmy-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Název</th>
|
||||
<th class="text-center">ID</th>
|
||||
<th>IČ / DIČ</th>
|
||||
<th>Kontakt</th>
|
||||
<th class="text-center">Uživatelů</th>
|
||||
<th>SDF</th>
|
||||
<th class="text-end" data-orderable="false">Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $firmy as $firma}
|
||||
<tr>
|
||||
<td data-label="Název" class="fw-bold">
|
||||
<i class="bi bi-building me-1 text-muted"></i>{$firma->NAZEV}
|
||||
</td>
|
||||
<td data-label="ID" class="text-center">{$firma->ID}</td>
|
||||
<td data-label="IČ / DIČ">
|
||||
{if $firma->IC || $firma->DIC}
|
||||
<div n:if="$firma->IC">IČ: {$firma->IC}</div>
|
||||
<div class="small text-muted" n:if="$firma->DIC">DIČ: {$firma->DIC}</div>
|
||||
{else}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td data-label="Kontakt">
|
||||
{if $firma->EMAIL || $firma->TELEFON || $firma->MESTO}
|
||||
<div n:if="$firma->EMAIL">{$firma->EMAIL}</div>
|
||||
<div class="small text-muted" n:if="$firma->TELEFON">{$firma->TELEFON}</div>
|
||||
<div class="small text-muted" n:if="$firma->MESTO">{$firma->MESTO}</div>
|
||||
{else}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td data-label="Uživatelů" class="text-center">{$firma->USERS}</td>
|
||||
<td data-label="SDF">
|
||||
{if $firma->SDF_SOUBOR}
|
||||
<div class="fw-semibold">{$firma->SDF_SOUBOR}</div>
|
||||
<div class="small text-success" n:if="$firma->SDF_IMPORTOVANO">
|
||||
importováno {$firma->SDF_IMPORTOVANO|date:'d.m.Y H:i'}
|
||||
</div>
|
||||
{else}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-end text-nowrap cell-actions">
|
||||
{if $firma->LOGIN_ID}
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-success js-sdf-import"
|
||||
data-login-id="{$firma->LOGIN_ID}"
|
||||
data-firma="{$firma->NAZEV}"
|
||||
title="Import ze SDF (smaže data firmy)">
|
||||
<i class="bi bi-box-arrow-in-down me-1"></i>Import
|
||||
</button>
|
||||
{else}
|
||||
<span class="small text-muted">Nejdřív založte uživatele</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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).ready(function() {
|
||||
$('#firmy-table').DataTable({
|
||||
language: { url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/cs.json' },
|
||||
order: [[0, 'asc']],
|
||||
pageLength: 25,
|
||||
autoWidth: false
|
||||
});
|
||||
|
||||
const $importForm = $('form[id$="-sdfImportForm"], #frm-sdfImportForm').first();
|
||||
const $importLoginId = $importForm.find('[name="loginId"]');
|
||||
const $importFile = $importForm.find('input[type="file"]');
|
||||
|
||||
$(document).on('click', '.js-sdf-import', function() {
|
||||
const firma = $(this).data('firma');
|
||||
if (!confirm('VAROVÁNÍ: Import ze SDF pro firmu ' + firma + ' SMAŽE všechna stávající data této firmy.\n\nOpravdu pokračovat?')) {
|
||||
return;
|
||||
}
|
||||
$importLoginId.val($(this).data('login-id'));
|
||||
$importFile.val('');
|
||||
$importFile.trigger('click');
|
||||
});
|
||||
|
||||
$importFile.on('change', function() {
|
||||
if (this.files && this.files.length) {
|
||||
$importForm.trigger('submit');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{/syntax}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
{block content}
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<div class="container-fluid py-3">
|
||||
<div class="mb-3">
|
||||
<h2 class="h3 mb-1">Uživatelé</h2>
|
||||
<p class="text-muted small mb-0">
|
||||
Tabulka <code>LOGIN</code> v <code>kufrik_master</code>. Každý uživatel míří na databázi a firmu. Noví uživatelé mají vždy roli User.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm p-3 bg-white mb-4">
|
||||
<h3 class="h6 text-muted mb-2">Přidat uživatele</h3>
|
||||
<form n:name="addUzivatelForm" class="row g-3 align-items-end">
|
||||
<div class="col-12" n:if="$form->hasErrors()">
|
||||
<div class="alert alert-danger mb-0">
|
||||
<ul class="mb-0 ps-3">
|
||||
<li n:foreach="$form->errors as $error">{$error}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label n:name="database" class="form-label">Databáze</label>
|
||||
<select n:name="database" class="form-select" id="addUserDatabase"></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label n:name="firmaId" class="form-label">Firma</label>
|
||||
<select n:name="firmaId" class="form-select" id="addUserFirma"></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label n:name="email" class="form-label">E-mail</label>
|
||||
<input n:name="email" class="form-control" placeholder="jan@firma.cz">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label n:name="password" class="form-label">Heslo</label>
|
||||
<input n:name="password" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label n:name="passwordVerify" class="form-label">Heslo znovu</label>
|
||||
<input n:name="passwordVerify" class="form-control">
|
||||
</div>
|
||||
<div class="col-12 col-md-auto">
|
||||
<button n:name="send" class="btn btn-primary">
|
||||
<i class="bi bi-person-plus me-1"></i>Přidat
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="small text-muted mt-2 mb-0">
|
||||
Po změně databáze obnovte stránku s parametrem <code>?db=...</code>, aby se načetly firmy, nebo přidejte firmu nejdřív v agendě Firmy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-primary border-4">
|
||||
<div class="text-muted small">Uživatelů</div>
|
||||
<div class="fs-4 fw-bold text-primary">{$celkemCount} ks</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card border-0 shadow-sm p-3 bg-white border-start border-success border-4">
|
||||
<div class="text-muted small">Aktivní</div>
|
||||
<div class="fs-4 fw-bold text-success">{$aktivniCount} ks</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 p-3 bg-white">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-cards align-middle w-100" id="uzivatele-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>E-mail</th>
|
||||
<th>Databáze</th>
|
||||
<th>Firma</th>
|
||||
<th class="text-center">Role</th>
|
||||
<th>Poslední přihlášení</th>
|
||||
<th class="text-center">Stav</th>
|
||||
<th class="text-end" data-orderable="false">Akce</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{foreach $uzivatele as $u}
|
||||
<tr n:class="!$u->AKTIVNI ? 'table-light text-muted'">
|
||||
<td data-label="E-mail" class="fw-bold">
|
||||
<a href="mailto:{$u->EMAIL}" class="text-decoration-none">{$u->EMAIL}</a>
|
||||
</td>
|
||||
<td data-label="Databáze">{$u->DATABASE}</td>
|
||||
<td data-label="Firma">
|
||||
{$u->FIRMA_NAZEV ?: ('#' . $u->FIRMA_ID)}
|
||||
<span class="small text-muted">(#{$u->FIRMA_ID})</span>
|
||||
</td>
|
||||
<td data-label="Role" class="text-center">
|
||||
{if $u->IS_ADMIN}
|
||||
<span class="badge bg-dark">Admin</span>
|
||||
{else}
|
||||
<span class="badge bg-light text-dark border">User</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td data-label="Poslední přihlášení">
|
||||
{if $u->POSLEDNI_PRIHLASENI}
|
||||
{$u->POSLEDNI_PRIHLASENI|date:'d.m.Y H:i'}
|
||||
{else}
|
||||
<span class="text-muted">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td data-label="Stav" class="text-center">
|
||||
{if $u->AKTIVNI}
|
||||
<span class="badge bg-success">Aktivní</span>
|
||||
{else}
|
||||
<span class="badge bg-secondary">Neaktivní</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-end text-nowrap cell-actions">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary js-user-edit"
|
||||
data-login-id="{$u->ID}"
|
||||
data-email="{$u->EMAIL}"
|
||||
data-label="{$u->EMAIL}"
|
||||
title="Upravit e-mail / heslo">
|
||||
<i class="bi bi-person-gear"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="userEditModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form n:name="userForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-person-gear me-1"></i>Uživatel — <span id="userEditLabel"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zavřít"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input n:name="loginId">
|
||||
<div class="mb-3">
|
||||
<label n:name="email" class="form-label">E-mail / přihlašovací jméno</label>
|
||||
<input n:name="email" class="form-control">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label n:name="password" class="form-label">Nové heslo</label>
|
||||
<input n:name="password" class="form-control" placeholder="ponechte prázdné = beze změny">
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label n:name="passwordVerify" class="form-label">Heslo znovu</label>
|
||||
<input n:name="passwordVerify" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Zrušit</button>
|
||||
<button n:name="send" class="btn btn-primary">Uložit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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).ready(function() {
|
||||
$('#uzivatele-table').DataTable({
|
||||
language: { url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/cs.json' },
|
||||
order: [[0, 'asc']],
|
||||
pageLength: 25,
|
||||
autoWidth: false
|
||||
});
|
||||
|
||||
const userModalEl = document.getElementById('userEditModal');
|
||||
const userModal = userModalEl ? new bootstrap.Modal(userModalEl) : null;
|
||||
const $userForm = $('form[id$="-userForm"], #frm-userForm').first();
|
||||
|
||||
$(document).on('click', '.js-user-edit', function() {
|
||||
$userForm.find('[name="loginId"]').val($(this).data('login-id'));
|
||||
$userForm.find('[name="email"]').val($(this).data('email'));
|
||||
$userForm.find('[name="password"]').val('');
|
||||
$userForm.find('[name="passwordVerify"]').val('');
|
||||
$('#userEditLabel').text($(this).data('label'));
|
||||
if (userModal) {
|
||||
userModal.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Při změně databáze v add form načti firmy přes redirect s ?db=
|
||||
$('#addUserDatabase').on('change', function() {
|
||||
const db = $(this).val();
|
||||
if (db) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('db', db);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{/syntax}
|
||||
|
|
@ -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_'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
-- ==========================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue