diff --git a/app/Model/Facade/DatabazeFacade.php b/app/Model/Facade/DatabazeFacade.php index 2e95868..c344cd5 100644 --- a/app/Model/Facade/DatabazeFacade.php +++ b/app/Model/Facade/DatabazeFacade.php @@ -358,6 +358,12 @@ class DatabazeFacade public function getSdfFilePath(int $loginId): ?string { + // JSON export funguje i na Linuxu; .sdf vyžaduje Windows + SQL Compact. + $export = $this->sdfExportPath($loginId); + if (is_file($export)) { + return $export; + } + $path = $this->sdfPath($loginId); return is_file($path) ? $path : null; } @@ -386,12 +392,25 @@ class DatabazeFacade } $extension = strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION)); - if ($extension !== 'sdf') { - throw new \InvalidArgumentException('Soubor musí mít příponu .sdf.'); + if (!in_array($extension, ['sdf', 'json'], true)) { + throw new \InvalidArgumentException('Soubor musí mít příponu .sdf nebo .json (export).'); } FileSystem::createDir($this->sdfStorageDir); - $file->move($this->sdfPath($loginId)); + + $target = $extension === 'json' + ? $this->sdfExportPath($loginId) + : $this->sdfPath($loginId); + + // Nový upload přepíše druhý formát, ať import nebere starý soubor. + $other = $extension === 'json' + ? $this->sdfPath($loginId) + : $this->sdfExportPath($loginId); + if (is_file($other)) { + FileSystem::delete($other); + } + + $file->move($target); $this->clearSdfImportMeta($loginId); $meta = [ @@ -399,6 +418,7 @@ class DatabazeFacade 'uploadedAt' => (new \DateTimeImmutable())->format('c'), 'size' => $file->getSize(), 'loginId' => $loginId, + 'format' => $extension, ]; FileSystem::write($this->sdfMetaPath($loginId), Json::encode($meta, pretty: true)); } @@ -490,12 +510,12 @@ class DatabazeFacade } /** - * @return array{originalName?: string, uploadedAt?: string, size?: int}|array{} + * @return array{originalName?: string, uploadedAt?: string, size?: int, format?: string}|array{} */ private function readSdfMeta(int $loginId): array { - $sdf = $this->sdfPath($loginId); - if (!is_file($sdf)) { + $source = $this->getSdfFilePath($loginId); + if ($source === null) { return []; } @@ -510,9 +530,10 @@ class DatabazeFacade } } - $meta['size'] = filesize($sdf) ?: ($meta['size'] ?? null); - $meta['uploadedAt'] ??= date('c', filemtime($sdf) ?: time()); - $meta['originalName'] ??= $loginId . '.sdf'; + $meta['size'] = filesize($source) ?: ($meta['size'] ?? null); + $meta['uploadedAt'] ??= date('c', filemtime($source) ?: time()); + $meta['originalName'] ??= basename($source); + $meta['format'] ??= strtolower(pathinfo($source, PATHINFO_EXTENSION)); return $meta; } @@ -550,6 +571,11 @@ class DatabazeFacade return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.sdf'; } + private function sdfExportPath(int $loginId): string + { + return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.export.json'; + } + private function sdfMetaPath(int $loginId): string { return $this->sdfStorageDir . DIRECTORY_SEPARATOR . $loginId . '.json'; diff --git a/app/Model/Sdf/SdfCompactExporter.php b/app/Model/Sdf/SdfCompactExporter.php index 6f7accc..fdec821 100644 --- a/app/Model/Sdf/SdfCompactExporter.php +++ b/app/Model/Sdf/SdfCompactExporter.php @@ -14,7 +14,7 @@ class SdfCompactExporter } /** - * Přečte SQL Compact .sdf a vrátí tabulky jako pole. + * Přečte SQL Compact .sdf (Windows) nebo hotový JSON export a vrátí tabulky jako pole. * * @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array, konfigurace: array} */ @@ -23,38 +23,67 @@ class SdfCompactExporter if (!is_file($sdfPath)) { throw new \RuntimeException('SDF soubor nebyl nalezen.'); } + + $extension = strtolower(pathinfo($sdfPath, PATHINFO_EXTENSION)); + if ($extension === 'json') { + return $this->normalizeExport(Json::decode(FileSystem::read($sdfPath), forceArrays: true)); + } + if (!is_file($this->scriptPath)) { throw new \RuntimeException('Exportní skript bin/sdf-export.ps1 chybí.'); } + $powershell = $this->findPowershell(); + if ($powershell === null) { + throw new \RuntimeException( + 'Čtení .sdf vyžaduje Windows a PowerShell se SQL Server Compact 4.0. ' + . 'Na Linux serveru nejdřív na Windows spusťte bin/sdf-export.ps1 ' + . 'a nahrajte výsledný .json.' + ); + } + $workDir = dirname($sdfPath) . DIRECTORY_SEPARATOR . 'export-' . bin2hex(random_bytes(4)); FileSystem::createDir($workDir); $outFile = $workDir . DIRECTORY_SEPARATOR . 'export.json'; try { - $this->runPowershell($sdfPath, $outFile); + $this->runPowershell($powershell, $sdfPath, $outFile); $raw = FileSystem::read($outFile); - $data = Json::decode($raw, forceArrays: true); - if (!is_array($data)) { - throw new \RuntimeException('Export SDF nevrátil platná data.'); - } - - return [ - 'uzivatel' => $data['uzivatel'] ?? [], - 'zakaznik' => $data['zakaznik'] ?? [], - 'faktura' => $data['faktura'] ?? [], - 'polozka' => $data['polozka'] ?? [], - 'platba' => $data['platba'] ?? [], - 'konfigurace' => $data['konfigurace'] ?? [], - ]; + return $this->normalizeExport(Json::decode($raw, forceArrays: true)); } finally { FileSystem::delete($workDir); } } - private function runPowershell(string $sdfPath, string $outFile): void + /** + * @return array{uzivatel: array, zakaznik: array, faktura: array, polozka: array, platba: array, konfigurace: array} + */ + private function normalizeExport(mixed $data): array + { + if (!is_array($data)) { + throw new \RuntimeException('Export SDF nevrátil platná data.'); + } + + $hasTables = isset($data['uzivatel']) || isset($data['zakaznik']) || isset($data['faktura']); + if (!$hasTables) { + throw new \RuntimeException( + 'JSON nemá očekávanou strukturu exportu (uzivatel/zakaznik/faktura). ' + . 'Vygenerujte ho přes bin/sdf-export.ps1.' + ); + } + + return [ + 'uzivatel' => $data['uzivatel'] ?? [], + 'zakaznik' => $data['zakaznik'] ?? [], + 'faktura' => $data['faktura'] ?? [], + 'polozka' => $data['polozka'] ?? [], + 'platba' => $data['platba'] ?? [], + 'konfigurace' => $data['konfigurace'] ?? [], + ]; + } + + private function runPowershell(string $powershell, string $sdfPath, string $outFile): void { - $powershell = $this->findPowershell(); $cmd = sprintf( '%s -NoProfile -NonInteractive -ExecutionPolicy Bypass -File %s -SdfPath %s -OutFile %s -Password %s', $this->winArg($powershell), @@ -85,8 +114,12 @@ class SdfCompactExporter } } - private function findPowershell(): string + private function findPowershell(): ?string { + if (PHP_OS_FAMILY !== 'Windows') { + return null; + } + $candidates = [ getenv('SystemRoot') . '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', diff --git a/app/Model/Sdf/SdfKufrikImporter.php b/app/Model/Sdf/SdfKufrikImporter.php index e14b6a3..650b5cc 100644 --- a/app/Model/Sdf/SdfKufrikImporter.php +++ b/app/Model/Sdf/SdfKufrikImporter.php @@ -39,7 +39,7 @@ class SdfKufrikImporter $sdfPath = $this->databazeFacade->getSdfFilePath($loginId); if ($sdfPath === null) { - throw new \RuntimeException('K této databázi ještě není nahraný soubor SDF.'); + throw new \RuntimeException('K této databázi ještě není nahraný soubor SDF ani JSON export.'); } $exported = $this->sdfExporter->export($sdfPath); diff --git a/app/UI/Admin/AdminPresenter.php b/app/UI/Admin/AdminPresenter.php index dccc7fd..ea2826a 100644 --- a/app/UI/Admin/AdminPresenter.php +++ b/app/UI/Admin/AdminPresenter.php @@ -248,8 +248,8 @@ class AdminPresenter extends BasePresenter $maxBytes = min(self::SDF_MAX_BYTES, ...($iniLimits ?: [self::SDF_MAX_BYTES])); $maxMb = max(1, (int) floor($maxBytes / (1024 * 1024))); - $form->addUpload('sdf', 'Soubor SDF:') - ->setRequired('Vyberte soubor s příponou .sdf.') + $form->addUpload('sdf', 'Soubor SDF / JSON:') + ->setRequired('Vyberte soubor .sdf nebo JSON export.') ->addRule($form::MaxFileSize, "Soubor může mít nejvýše {$maxMb} MB.", $maxBytes) ->addRule( function (UploadControl $control): bool { @@ -258,9 +258,11 @@ class AdminPresenter extends BasePresenter return false; } - return strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION)) === 'sdf'; + $ext = strtolower(pathinfo($file->getUntrustedName(), PATHINFO_EXTENSION)); + + return in_array($ext, ['sdf', 'json'], true); }, - 'Soubor musí mít příponu .sdf.' + 'Soubor musí mít příponu .sdf nebo .json.' ); $form->addSubmit('send', 'Import'); diff --git a/app/UI/Admin/firmy.latte b/app/UI/Admin/firmy.latte index a06a69d..702fe3e 100644 --- a/app/UI/Admin/firmy.latte +++ b/app/UI/Admin/firmy.latte @@ -46,7 +46,7 @@
- +
@@ -114,7 +114,7 @@ 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)"> + title="Import ze SDF / JSON exportu (smaže data firmy)"> Import {else} @@ -148,7 +148,7 @@ $(document).ready(function() { $(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?')) { + if (!confirm('VAROVÁNÍ: Import ze SDF/JSON pro firmu ' + firma + ' SMAŽE všechna stávající data této firmy.\n\nNa Linux serveru nahrajte JSON z bin/sdf-export.ps1 (Windows).\n\nOpravdu pokračovat?')) { return; } $importLoginId.val($(this).data('login-id'));