feat: filemanager s NAS pro projekty

- NasFileManager.php - filesystem helper (browse, upload, download, delete, rename, mkdir)
- project-files.php API - CRUD operace nad soubory projektu
- ProjectFileManager.jsx - React komponenta v detailu projektu
- Automaticke vytvoreni slozky pri vytvoreni projektu (rucne i z objednavky)
- Prejmenovani slozky pri zmene nazvu projektu
- Checkbox "Smazat i soubory na disku" pri mazani projektu/objednavky
- Path traversal ochrana, MIME validace, blocklist nebezpecnych typu
- Bily spinner v primary tlacitkach, ConfirmModal message jako div
- Case-insensitive rename fix pro Windows filesystem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 13:06:34 +01:00
parent 9e3c95e576
commit 45fd930f76
69 changed files with 2776 additions and 71 deletions

View File

@@ -128,6 +128,11 @@ function handleGetDetail(PDO $pdo, int $id): void
$stmt->execute([$id]);
$order['project'] = $stmt->fetch() ?: null;
if ($order['project']) {
$fm = new NasFileManager();
$order['project']['has_nas_folder'] = $fm->projectFolderExists($order['project']['project_number']);
}
// Get linked invoice
$stmt = $pdo->prepare('SELECT id, invoice_number, status FROM invoices WHERE order_id = ? LIMIT 1');
$stmt->execute([$id]);
@@ -352,6 +357,10 @@ function handleCreateOrder(PDO $pdo): void
$pdo->commit();
$pdo->query("SELECT RELEASE_LOCK('boha_order_number')");
// Vytvorit slozku na NAS pro novy projekt
$fm = new NasFileManager();
$fm->createProjectFolder($orderNumber, $projectName);
AuditLog::logCreate('orders_order', $orderId, [
'order_number' => $orderNumber,
'quotation_number' => $quotation['quotation_number'],
@@ -482,6 +491,9 @@ function handleUpdateOrder(PDO $pdo, int $id): void
function handleDeleteOrder(PDO $pdo, int $id): void
{
$input = getJsonInput();
$deleteFiles = (bool) ($input['delete_files'] ?? false);
$stmt = $pdo->prepare(
'SELECT id, order_number, quotation_id FROM orders WHERE id = ?'
);
@@ -492,8 +504,21 @@ function handleDeleteOrder(PDO $pdo, int $id): void
errorResponse('Objednávka nebyla nalezena', 404);
}
// Nacist projekt pred smazanim (pro NAS slozku)
$stmt = $pdo->prepare('SELECT project_number FROM projects WHERE order_id = ?');
$stmt->execute([$id]);
$project = $stmt->fetch();
$pdo->beginTransaction();
try {
// Delete project notes
if ($project) {
$stmt = $pdo->prepare(
'DELETE FROM project_notes WHERE project_id IN (SELECT id FROM projects WHERE order_id = ?)'
);
$stmt->execute([$id]);
}
// Delete project linked to this order
$stmt = $pdo->prepare('DELETE FROM projects WHERE order_id = ?');
$stmt->execute([$id]);
@@ -515,10 +540,16 @@ function handleDeleteOrder(PDO $pdo, int $id): void
$pdo->commit();
// Smazat NAS slozku pokud pozadovano
if ($deleteFiles && $project) {
$fm = new NasFileManager();
$fm->deleteProjectFolder($project['project_number']);
}
AuditLog::logDelete('orders_order', $id, [
'order_number' => $order['order_number'],
'quotation_id' => $order['quotation_id'],
], "Smazána objednávka '{$order['order_number']}'");
], "Smazána objednávka '{$order['order_number']}'" . ($deleteFiles ? ' (včetně souborů)' : ''));
successResponse(null, 'Objednávka byla smazána');
} catch (PDOException $e) {

View File

@@ -0,0 +1,236 @@
<?php
/**
* Project Files API handlery
*
* Vsechny operace se soubory projektu na NAS.
*/
declare(strict_types=1);
/**
* @param array<string, mixed> $authData
*/
function handleFilesList(PDO $pdo, array $authData): void
{
$projectId = (int) ($_GET['project_id'] ?? 0);
if (!$projectId) {
errorResponse('ID projektu je povinné');
}
$project = getProjectForFiles($pdo, $projectId);
$path = $_GET['path'] ?? '';
$fm = new NasFileManager();
if (!$fm->isConfigured()) {
errorResponse('Souborový systém není nakonfigurován', 500);
}
$result = $fm->listFiles($project['project_number'], $path);
if ($result === null) {
errorResponse('Složka nebyla nalezena', 404);
}
$result['project_number'] = $project['project_number'];
$result['folder_exists'] = true;
successResponse($result);
}
/**
* @param array<string, mixed> $authData
*/
function handleFilesDownload(PDO $pdo, array $authData): void
{
$projectId = (int) ($_GET['project_id'] ?? 0);
if (!$projectId) {
errorResponse('ID projektu je povinné');
}
$project = getProjectForFiles($pdo, $projectId);
$path = $_GET['path'] ?? '';
if ($path === '') {
errorResponse('Cesta k souboru je povinná');
}
$fm = new NasFileManager();
$error = $fm->downloadFile($project['project_number'], $path);
if ($error !== null) {
errorResponse($error, 404);
}
}
/**
* @param array<string, mixed> $authData
*/
function handleFilesUpload(PDO $pdo, array $authData): void
{
$projectId = (int) ($_GET['project_id'] ?? 0);
if (!$projectId) {
errorResponse('ID projektu je povinné');
}
$project = getProjectForFiles($pdo, $projectId);
$path = $_GET['path'] ?? '';
$fm = new NasFileManager();
if (!$fm->isConfigured()) {
errorResponse('Souborový systém není nakonfigurován', 500);
}
// Vytvorit slozku pokud neexistuje
if (!$fm->projectFolderExists($project['project_number'])) {
$fm->createProjectFolder($project['project_number'], $project['name']);
}
if (empty($_FILES['file'])) {
errorResponse('Nebyl nahrán žádný soubor');
}
$error = $fm->uploadFile($project['project_number'], $path, $_FILES['file']);
if ($error !== null) {
errorResponse($error);
}
AuditLog::logCreate(
'project_file',
$projectId,
['file' => $_FILES['file']['name'] ?? '', 'path' => $path],
"Nahrán soubor do projektu '{$project['project_number']}'"
);
successResponse(null, 'Soubor byl nahrán');
}
/**
* @param array<string, mixed> $authData
*/
function handleFilesCreateFolder(PDO $pdo, array $authData): void
{
$projectId = (int) ($_GET['project_id'] ?? 0);
if (!$projectId) {
errorResponse('ID projektu je povinné');
}
$project = getProjectForFiles($pdo, $projectId);
$input = getJsonInput();
$path = $input['path'] ?? '';
$folderName = trim($input['folder_name'] ?? '');
if ($folderName === '') {
errorResponse('Název složky je povinný');
}
if (mb_strlen($folderName) > 100) {
errorResponse('Název složky je příliš dlouhý (max 100 znaků)');
}
$fm = new NasFileManager();
if (!$fm->isConfigured()) {
errorResponse('Souborový systém není nakonfigurován', 500);
}
// Vytvorit projektovou slozku pokud neexistuje
if (!$fm->projectFolderExists($project['project_number'])) {
$fm->createProjectFolder($project['project_number'], $project['name']);
}
$error = $fm->createFolder($project['project_number'], $path, $folderName);
if ($error !== null) {
errorResponse($error);
}
AuditLog::logCreate(
'project_file',
$projectId,
['folder' => $folderName, 'path' => $path],
"Vytvořena složka '$folderName' v projektu '{$project['project_number']}'"
);
successResponse(null, 'Složka byla vytvořena');
}
/**
* @param array<string, mixed> $authData
*/
function handleFilesMove(PDO $pdo, array $authData): void
{
$projectId = (int) ($_GET['project_id'] ?? 0);
if (!$projectId) {
errorResponse('ID projektu je povinné');
}
$project = getProjectForFiles($pdo, $projectId);
$input = getJsonInput();
$fromPath = $input['from_path'] ?? '';
$toPath = $input['to_path'] ?? '';
if ($fromPath === '' || $toPath === '') {
errorResponse('Zdrojová i cílová cesta jsou povinné');
}
$fm = new NasFileManager();
$error = $fm->moveItem($project['project_number'], $fromPath, $toPath);
if ($error !== null) {
errorResponse($error);
}
AuditLog::logUpdate(
'project_file',
$projectId,
['path' => $fromPath],
['path' => $toPath],
"Přesun/přejmenování v projektu '{$project['project_number']}'"
);
successResponse(null, 'Soubor byl přesunut');
}
/**
* @param array<string, mixed> $authData
*/
function handleFilesDelete(PDO $pdo, array $authData): void
{
$projectId = (int) ($_GET['project_id'] ?? 0);
if (!$projectId) {
errorResponse('ID projektu je povinné');
}
$project = getProjectForFiles($pdo, $projectId);
$path = $_GET['path'] ?? '';
if ($path === '') {
errorResponse('Cesta k souboru je povinná');
}
$fm = new NasFileManager();
$error = $fm->deleteItem($project['project_number'], $path);
if ($error !== null) {
errorResponse($error);
}
AuditLog::logDelete(
'project_file',
$projectId,
['path' => $path],
"Smazán soubor/složka v projektu '{$project['project_number']}'"
);
successResponse(null, 'Soubor byl smazán');
}
/**
* Nacte projekt z DB pro file operace
*
* @return array<string, mixed>
*/
function getProjectForFiles(PDO $pdo, int $projectId): array
{
$stmt = $pdo->prepare('SELECT id, project_number, name FROM projects WHERE id = ?');
$stmt->execute([$projectId]);
$project = $stmt->fetch();
if (!$project) {
errorResponse('Projekt nebyl nalezen', 404);
}
return $project;
}

View File

@@ -121,6 +121,10 @@ function handleCreateProject(PDO $pdo): void
'customer_id' => $customerId,
], "Ručně vytvořen projekt '$projectNumber'");
// Vytvorit slozku na NAS
$fm = new NasFileManager();
$fm->createProjectFolder($projectNumber, $name);
successResponse([
'project_id' => $projectId,
'project_number' => $projectNumber,
@@ -134,6 +138,9 @@ function handleCreateProject(PDO $pdo): void
function handleDeleteProject(PDO $pdo, int $id): void
{
$input = getJsonInput();
$deleteFiles = (bool) ($input['delete_files'] ?? false);
$stmt = $pdo->prepare(
'SELECT id, project_number, name, order_id, status FROM projects WHERE id = ?'
);
@@ -161,12 +168,18 @@ function handleDeleteProject(PDO $pdo, int $id): void
$pdo->commit();
// Smazat slozku na NAS pokud pozadovano
if ($deleteFiles) {
$fm = new NasFileManager();
$fm->deleteProjectFolder($project['project_number']);
}
AuditLog::logUpdate(
'projects_project',
$id,
['status' => $project['status']],
['status' => 'deleted'],
"Smazán ruční projekt '{$project['project_number']}'"
"Smazán ruční projekt '{$project['project_number']}'" . ($deleteFiles ? ' (včetně souborů)' : '')
);
successResponse(null, 'Projekt byl smazán');
@@ -254,6 +267,10 @@ function handleGetDetail(PDO $pdo, int $id): void
errorResponse('Projekt nebyl nalezen', 404);
}
// Kontrola existence slozky na NAS
$fm = new NasFileManager();
$project['has_nas_folder'] = $fm->projectFolderExists($project['project_number']);
successResponse($project);
}
@@ -344,6 +361,12 @@ function handleUpdateProject(PDO $pdo, int $id): void
$pdo->commit();
// Prejmenovani slozky na NAS pokud se zmenil nazev
if ($name !== $project['name']) {
$fm = new NasFileManager();
$fm->renameProjectFolder($project['project_number'], $name);
}
AuditLog::logUpdate(
'projects_project',
$id,

View File

@@ -16,6 +16,7 @@ require_once dirname(__DIR__) . '/config.php';
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
require_once dirname(__DIR__) . '/includes/AuditLog.php';
require_once dirname(__DIR__) . '/includes/PaginationHelper.php';
require_once dirname(__DIR__) . '/includes/NasFileManager.php';
require_once __DIR__ . '/handlers/orders-handlers.php';
setCorsHeaders();

81
dist/api/admin/project-files.php vendored Normal file
View File

@@ -0,0 +1,81 @@
<?php
/**
* Project Files API
*
* GET ?project_id=X&path=... - seznam souboru
* GET ?action=download&project_id=X&path= - stazeni souboru
* POST ?action=upload&project_id=X&path= - upload (FormData)
* POST ?action=create_folder&project_id=X - nova podslozka
* PUT ?action=move&project_id=X - presun/prejmenovani
* DELETE ?project_id=X&path=... - smazani souboru/slozky
*/
declare(strict_types=1);
require_once dirname(__DIR__) . '/config.php';
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
require_once dirname(__DIR__) . '/includes/AuditLog.php';
require_once dirname(__DIR__) . '/includes/NasFileManager.php';
require_once __DIR__ . '/handlers/project-files-handlers.php';
setCorsHeaders();
setSecurityHeaders();
setNoCacheHeaders();
header('Content-Type: application/json; charset=utf-8');
$authData = JWTAuth::requireAuth();
AuditLog::setUser($authData['user_id'], $authData['user']['username'] ?? 'unknown');
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
try {
$pdo = db();
switch ($method) {
case 'GET':
requirePermission($authData, 'projects.view');
if ($action === 'download') {
handleFilesDownload($pdo, $authData);
} else {
handleFilesList($pdo, $authData);
}
break;
case 'POST':
requirePermission($authData, 'projects.files');
if ($action === 'upload') {
handleFilesUpload($pdo, $authData);
} elseif ($action === 'create_folder') {
handleFilesCreateFolder($pdo, $authData);
} else {
errorResponse('Neznámá akce', 400);
}
break;
case 'PUT':
requirePermission($authData, 'projects.files');
if ($action === 'move') {
handleFilesMove($pdo, $authData);
} else {
errorResponse('Neznámá akce', 400);
}
break;
case 'DELETE':
requirePermission($authData, 'projects.files');
handleFilesDelete($pdo, $authData);
break;
default:
errorResponse('Metoda není povolena', 405);
}
} catch (PDOException $e) {
error_log('Project Files API error: ' . $e->getMessage());
if (DEBUG_MODE) {
errorResponse('Chyba databáze: ' . $e->getMessage(), 500);
} else {
errorResponse('Chyba databáze', 500);
}
}

View File

@@ -19,6 +19,7 @@ require_once dirname(__DIR__) . '/config.php';
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
require_once dirname(__DIR__) . '/includes/AuditLog.php';
require_once dirname(__DIR__) . '/includes/PaginationHelper.php';
require_once dirname(__DIR__) . '/includes/NasFileManager.php';
require_once __DIR__ . '/handlers/projects-handlers.php';
setCorsHeaders();

521
dist/api/includes/NasFileManager.php vendored Normal file
View File

@@ -0,0 +1,521 @@
<?php
/**
* NAS File Manager - filesystem operace pro projektove slozky
*
* Pracuje s namapovanym diskem (NAS_FILES_PATH).
* Vsechny cesty jsou validovany proti path traversal.
*/
declare(strict_types=1);
class NasFileManager
{
private string $basePath;
public function __construct()
{
$this->basePath = rtrim(str_replace('\\', '/', NAS_FILES_PATH), '/');
}
public function isConfigured(): bool
{
return $this->basePath !== '' && is_dir($this->basePath);
}
/**
* Vytvori projektovou slozku na NAS
*/
public function createProjectFolder(string $projectNumber, string $projectName): bool
{
if (!$this->isConfigured()) {
return false;
}
$folderName = $this->buildFolderName($projectNumber, $projectName);
$fullPath = $this->basePath . '/' . $folderName;
if (is_dir($fullPath)) {
return true;
}
return mkdir($fullPath, 0775, true);
}
/**
* Smaze projektovou slozku rekurzivne
*/
public function deleteProjectFolder(string $projectNumber): bool
{
if (!$this->isConfigured()) {
return false;
}
$folderPath = $this->findProjectFolder($projectNumber);
if ($folderPath === null) {
return true;
}
return $this->deleteRecursive($folderPath);
}
/**
* Kontrola existence projektove slozky
*/
public function projectFolderExists(string $projectNumber): bool
{
return $this->findProjectFolder($projectNumber) !== null;
}
/**
* Prejmenovani slozky pri zmene nazvu projektu
*/
public function renameProjectFolder(string $projectNumber, string $newName): bool
{
if (!$this->isConfigured()) {
return false;
}
$currentPath = $this->findProjectFolder($projectNumber);
if ($currentPath === null) {
return false;
}
$newFolderName = $this->buildFolderName($projectNumber, $newName);
$newPath = $this->basePath . '/' . $newFolderName;
if ($currentPath === $newPath) {
return true;
}
return rename($currentPath, $newPath);
}
/**
* Seznam souboru a slozek v dane ceste
*
* @return array{path: string, items: list<array<string, mixed>>}|null
*/
public function listFiles(string $projectNumber, string $subPath = ''): ?array
{
$dirPath = $this->resolveProjectPath($projectNumber, $subPath);
if ($dirPath === null || !is_dir($dirPath)) {
return null;
}
$entries = scandir($dirPath);
if ($entries === false) {
return null;
}
$items = [];
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$fullPath = $dirPath . '/' . $entry;
$isDir = is_dir($fullPath);
$item = [
'name' => $entry,
'type' => $isDir ? 'folder' : 'file',
'modified' => date('Y-m-d H:i', filemtime($fullPath) ?: 0),
];
if ($isDir) {
$item['item_count'] = $this->countItems($fullPath);
} else {
$size = filesize($fullPath);
$item['size'] = $size;
$item['size_formatted'] = $this->formatFileSize($size ?: 0);
$item['extension'] = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
}
$items[] = $item;
}
// Slozky prvni, pak soubory - obe abecedne
usort($items, function (array $a, array $b): int {
if ($a['type'] !== $b['type']) {
return $a['type'] === 'folder' ? -1 : 1;
}
return strnatcasecmp($a['name'], $b['name']);
});
$breadcrumb = [''];
if ($subPath !== '') {
$parts = explode('/', trim($subPath, '/'));
foreach ($parts as $part) {
$breadcrumb[] = $part;
}
}
return [
'path' => $subPath,
'items' => $items,
'breadcrumb' => $breadcrumb,
];
}
/**
* Upload souboru
*
* @param array<string, mixed> $uploadedFile $_FILES element
*/
public function uploadFile(string $projectNumber, string $subPath, array $uploadedFile): ?string
{
$dirPath = $this->resolveProjectPath($projectNumber, $subPath);
if ($dirPath === null || !is_dir($dirPath)) {
return 'Cílová složka neexistuje';
}
if (($uploadedFile['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
return 'Chyba při nahrávání souboru';
}
$size = (int) ($uploadedFile['size'] ?? 0);
if ($size > NAS_MAX_UPLOAD_SIZE) {
$maxMb = round(NAS_MAX_UPLOAD_SIZE / 1048576);
return "Soubor je příliš velký (max {$maxMb} MB)";
}
$originalName = basename($uploadedFile['name'] ?? '');
$safeName = $this->sanitizeFilename($originalName);
if ($safeName === '') {
return 'Neplatný název souboru';
}
$ext = strtolower(pathinfo($safeName, PATHINFO_EXTENSION));
if (in_array($ext, NAS_BLOCKED_EXTENSIONS, true)) {
return 'Tento typ souboru není povolen';
}
if (!empty(NAS_ALLOWED_EXTENSIONS) && !in_array($ext, NAS_ALLOWED_EXTENSIONS, true)) {
return 'Tento typ souboru není povolen';
}
// MIME validace
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($uploadedFile['tmp_name']);
if ($this->isSuspiciousMime($mime ?: '', $ext)) {
return 'Obsah souboru neodpovídá jeho příponě';
}
$destPath = $dirPath . '/' . $safeName;
// Pokud soubor existuje, pridej cislo
if (file_exists($destPath)) {
$base = pathinfo($safeName, PATHINFO_FILENAME);
$counter = 1;
do {
$safeName = $base . '_' . $counter . '.' . $ext;
$destPath = $dirPath . '/' . $safeName;
$counter++;
} while (file_exists($destPath));
}
if (!move_uploaded_file($uploadedFile['tmp_name'], $destPath)) {
return 'Nepodařilo se uložit soubor';
}
return null;
}
/**
* Smaze soubor nebo slozku
*/
public function deleteItem(string $projectNumber, string $filePath): ?string
{
if ($filePath === '' || $filePath === '/') {
return 'Nelze smazat kořenovou složku projektu';
}
$fullPath = $this->resolveProjectPath($projectNumber, $filePath);
if ($fullPath === null) {
return 'Neplatná cesta';
}
if (!file_exists($fullPath)) {
return 'Soubor nebo složka neexistuje';
}
if (is_dir($fullPath)) {
if (!$this->deleteRecursive($fullPath)) {
return 'Nepodařilo se smazat složku';
}
} else {
if (!unlink($fullPath)) {
return 'Nepodařilo se smazat soubor';
}
}
return null;
}
/**
* Presun/prejmenovani souboru nebo slozky
*/
public function moveItem(string $projectNumber, string $fromPath, string $toPath): ?string
{
if ($fromPath === '' || $fromPath === '/') {
return 'Nelze přesunout kořenovou složku';
}
$fullFrom = $this->resolveProjectPath($projectNumber, $fromPath);
$fullTo = $this->resolveProjectPath($projectNumber, $toPath);
if ($fullFrom === null || $fullTo === null) {
return 'Neplatná cesta';
}
if (!file_exists($fullFrom)) {
return 'Zdrojový soubor neexistuje';
}
// Case-insensitive FS (Windows) - povolit zmenu velikosti pismen
$sameFile = str_ireplace('\\', '/', $fullFrom) === str_ireplace('\\', '/', $fullTo);
if (file_exists($fullTo) && !$sameFile) {
return 'Cílový soubor již existuje';
}
// Validace nazvu u ciloveho souboru
$targetName = basename($toPath);
if ($this->sanitizeFilename($targetName) !== $targetName) {
return 'Neplatný cílový název';
}
if (!rename($fullFrom, $fullTo)) {
return 'Nepodařilo se přesunout soubor';
}
return null;
}
/**
* Vytvori podslozku
*/
public function createFolder(string $projectNumber, string $subPath, string $folderName): ?string
{
$dirPath = $this->resolveProjectPath($projectNumber, $subPath);
if ($dirPath === null || !is_dir($dirPath)) {
return 'Nadřazená složka neexistuje';
}
$safeName = $this->sanitizeFilename($folderName);
if ($safeName === '') {
return 'Neplatný název složky';
}
$newPath = $dirPath . '/' . $safeName;
if (file_exists($newPath)) {
return 'Složka s tímto názvem již existuje';
}
if (!mkdir($newPath, 0775)) {
return 'Nepodařilo se vytvořit složku';
}
return null;
}
/**
* Streamuje soubor ke stazeni
*/
public function downloadFile(string $projectNumber, string $filePath): ?string
{
$fullPath = $this->resolveProjectPath($projectNumber, $filePath);
if ($fullPath === null || !is_file($fullPath)) {
return 'Soubor nebyl nalezen';
}
$filename = basename($fullPath);
$size = filesize($fullPath);
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($fullPath) ?: 'application/octet-stream';
header_remove('Content-Type');
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . $this->sanitizeFilename($filename) . '"');
header('Content-Length: ' . $size);
header('Cache-Control: no-cache');
readfile($fullPath);
exit;
}
// --- Private helpers ---
/**
* Najde existujici projektovou slozku podle prefixu cisla projektu
*/
private function findProjectFolder(string $projectNumber): ?string
{
if (!$this->isConfigured()) {
return null;
}
$entries = scandir($this->basePath);
if ($entries === false) {
return null;
}
$prefix = $projectNumber . '_';
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
if (str_starts_with($entry, $prefix) && is_dir($this->basePath . '/' . $entry)) {
return $this->basePath . '/' . $entry;
}
}
return null;
}
/**
* Sestavi nazev slozky: {number}_{name_s_podtrzitky}
*/
private function buildFolderName(string $projectNumber, string $projectName): string
{
$safe = preg_replace('/[^\p{L}\p{N}_\-. ]/u', '', $projectName) ?? '';
$safe = str_replace(' ', '_', trim($safe));
$safe = preg_replace('/_+/', '_', $safe) ?? $safe;
$safe = mb_substr($safe, 0, 200);
return $projectNumber . '_' . $safe;
}
/**
* Resolve a project sub-path to a safe absolute path
* Returns null if path traversal detected
*/
private function resolveProjectPath(string $projectNumber, string $subPath): ?string
{
$folderPath = $this->findProjectFolder($projectNumber);
if ($folderPath === null) {
return null;
}
if ($subPath === '' || $subPath === '/') {
return $folderPath;
}
// Zakladni path traversal ochrana
if (str_contains($subPath, "\0") || str_contains($subPath, '..')) {
return null;
}
$subPath = str_replace('\\', '/', $subPath);
$subPath = trim($subPath, '/');
$candidate = $folderPath . '/' . $subPath;
$normalBase = str_replace('\\', '/', $folderPath);
// realpath kontrola - soubor/slozka musi existovat pro existujici cesty
if (file_exists($candidate)) {
$real = realpath($candidate);
$normalReal = str_replace('\\', '/', (string) $real);
if ($real === false || !str_starts_with($normalReal, $normalBase)) {
return null;
}
return $normalReal;
}
// Pro nove soubory/slozky - kontrola rodice
$parentDir = dirname($candidate);
if (file_exists($parentDir)) {
$realParent = realpath($parentDir);
$normalParent = str_replace('\\', '/', (string) $realParent);
if ($realParent === false || !str_starts_with($normalParent, $normalBase)) {
return null;
}
}
return $candidate;
}
private function deleteRecursive(string $path): bool
{
if (is_file($path)) {
return unlink($path);
}
$entries = scandir($path);
if ($entries === false) {
return false;
}
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$fullPath = $path . '/' . $entry;
if (!$this->deleteRecursive($fullPath)) {
return false;
}
}
return rmdir($path);
}
private function countItems(string $dirPath): int
{
$entries = scandir($dirPath);
if ($entries === false) {
return 0;
}
return max(0, count($entries) - 2);
}
public function sanitizeFilename(string $name): string
{
$name = basename($name);
$name = preg_replace('/[\x00-\x1f\x7f<>:"\/\\\\|?*]/', '', $name) ?? '';
$name = trim($name, '. ');
if (mb_strlen($name) > 255) {
$ext = pathinfo($name, PATHINFO_EXTENSION);
$base = mb_substr(pathinfo($name, PATHINFO_FILENAME), 0, 250 - mb_strlen($ext));
$name = $ext ? $base . '.' . $ext : $base;
}
return $name;
}
public function formatFileSize(int $bytes): string
{
if ($bytes < 1024) {
return $bytes . ' B';
}
if ($bytes < 1048576) {
return round($bytes / 1024, 1) . ' KB';
}
if ($bytes < 1073741824) {
return round($bytes / 1048576, 1) . ' MB';
}
return round($bytes / 1073741824, 1) . ' GB';
}
/**
* Detekce podezrelych MIME typů (napr. exe maskujici se jako jpg)
*/
private function isSuspiciousMime(string $mime, string $ext): bool
{
$executableMimes = [
'application/x-executable',
'application/x-msdos-program',
'application/x-dosexec',
'application/x-msdownload',
];
if (in_array($mime, $executableMimes, true)) {
return true;
}
// PHP soubory
if (str_contains($mime, 'php') || str_contains($mime, 'x-httpd')) {
return true;
}
return false;
}
}

View File

@@ -36,3 +36,12 @@ define('INCLUDES_PATH', API_ROOT . '/includes');
// Rate limiting
define('RATE_LIMIT_STORAGE_PATH', dirname(__DIR__) . '/rate_limits');
// NAS File Manager
define('NAS_FILES_PATH', env('NAS_FILES_PATH', ''));
define('NAS_MAX_UPLOAD_SIZE', (int) env('NAS_MAX_UPLOAD_SIZE', 52428800));
define(
'NAS_ALLOWED_EXTENSIONS',
array_filter(array_map('trim', explode(',', (string) env('NAS_ALLOWED_EXTENSIONS', ''))))
);
define('NAS_BLOCKED_EXTENSIONS', ['exe', 'bat', 'sh', 'php', 'htaccess', 'env', 'cmd', 'com', 'msi', 'ps1']);

View File

@@ -1 +0,0 @@
{"window_start":1773394755,"count":1}

View File

@@ -1 +1 @@
{"window_start":1773397817,"count":1}
{"window_start":1773403455,"count":8}

View File

@@ -1 +0,0 @@
{"window_start":1773399442,"count":1}

View File

@@ -1 +0,0 @@
{"window_start":1773394748,"count":1}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-CNxd7jIT.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-BrM8fzBu.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
<!DOCTYPE html>
<html lang="cs">
<head>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/OrderDetail-CV53xEih.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/Orders-CtS3KkKW.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/ProjectDetail-TbZLFSAA.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/assets/Projects-DvLHy4pA.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-CNxd7jIT.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-BrM8fzBu.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
<!DOCTYPE html>
<html lang="cs">
<head>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/index-S7b0Xjr1.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-CNxd7jIT.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-BrM8fzBu.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};

4
dist/index.html vendored
View File

@@ -29,11 +29,11 @@
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Urbanist:wght@400;500;600;700;800&display=swap"
rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-CNxd7jIT.js"></script>
<script type="module" crossorigin src="/assets/index-BrM8fzBu.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BVs3cwbi.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-animation-0s3FMHwK.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-Dyr8OjFr.js">
<link rel="stylesheet" crossorigin href="/assets/index-D_wrslmx.css">
<link rel="stylesheet" crossorigin href="/assets/index-S7b0Xjr1.css">
</head>
<body style="background-color: var(--bg-primary, #12121a);">

View File

@@ -3,7 +3,7 @@
'name' => 'boha/website',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '308941449e1b5f1ec6752c297d286e6633c11fed',
'reference' => '9e3c95e5764f4ec5167c09b9f9cf05a58891a0aa',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'boha/website' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '308941449e1b5f1ec6752c297d286e6633c11fed',
'reference' => '9e3c95e5764f4ec5167c09b9f9cf05a58891a0aa',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),