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:
33
dist/api/admin/handlers/orders-handlers.php
vendored
33
dist/api/admin/handlers/orders-handlers.php
vendored
@@ -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) {
|
||||
|
||||
236
dist/api/admin/handlers/project-files-handlers.php
vendored
Normal file
236
dist/api/admin/handlers/project-files-handlers.php
vendored
Normal 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;
|
||||
}
|
||||
25
dist/api/admin/handlers/projects-handlers.php
vendored
25
dist/api/admin/handlers/projects-handlers.php
vendored
@@ -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,
|
||||
|
||||
1
dist/api/admin/orders.php
vendored
1
dist/api/admin/orders.php
vendored
@@ -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
81
dist/api/admin/project-files.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
1
dist/api/admin/projects.php
vendored
1
dist/api/admin/projects.php
vendored
@@ -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
521
dist/api/includes/NasFileManager.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
9
dist/api/includes/constants.php
vendored
9
dist/api/includes/constants.php
vendored
@@ -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']);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"window_start":1773394755,"count":1}
|
||||
@@ -1 +1 @@
|
||||
{"window_start":1773397817,"count":1}
|
||||
{"window_start":1773403455,"count":8}
|
||||
@@ -1 +0,0 @@
|
||||
{"window_start":1773399442,"count":1}
|
||||
@@ -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
@@ -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
1
dist/assets/OrderDetail-CV53xEih.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/OrderDetail-DgTv224i.js
vendored
1
dist/assets/OrderDetail-DgTv224i.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/Orders-Bm_dTJbR.js
vendored
1
dist/assets/Orders-Bm_dTJbR.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/Orders-CtS3KkKW.js
vendored
Normal file
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
1
dist/assets/ProjectDetail-Dg0G_KTk.js
vendored
1
dist/assets/ProjectDetail-Dg0G_KTk.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/ProjectDetail-TbZLFSAA.js
vendored
Normal file
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
1
dist/assets/Projects-DvLHy4pA.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/Projects-Pu7lx6LE.js
vendored
1
dist/assets/Projects-Pu7lx6LE.js
vendored
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,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
1
dist/assets/index-D_wrslmx.css
vendored
1
dist/assets/index-D_wrslmx.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-S7b0Xjr1.css
vendored
Normal file
1
dist/assets/index-S7b0Xjr1.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
4
dist/index.html
vendored
@@ -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);">
|
||||
|
||||
4
dist/vendor/composer/installed.php
vendored
4
dist/vendor/composer/installed.php
vendored
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user