*/ function getValidTransitions(string $currentStatus): array { $map = [ 'prijata' => ['v_realizaci', 'stornovana'], 'v_realizaci' => ['dokoncena', 'stornovana'], 'dokoncena' => [], 'stornovana' => [], ]; return $map[$currentStatus] ?? []; } // --- Number generation --- function generateOrderNumber(PDO $pdo): string { return generateSharedNumber($pdo); } // --- Handlers --- function handleGetList(PDO $pdo): void { $sortMap = [ 'OrderNumber' => 'o.order_number', 'order_number' => 'o.order_number', 'CreatedAt' => 'o.created_at', 'created_at' => 'o.created_at', 'Status' => 'o.status', 'status' => 'o.status', 'Currency' => 'o.currency', 'currency' => 'o.currency', ]; $p = PaginationHelper::parseParams($sortMap); $where = 'WHERE 1=1'; $params = []; if ($p['search']) { $where .= ' AND (o.order_number LIKE ? OR q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)'; $searchParam = "%{$p['search']}%"; $params = [$searchParam, $searchParam, $searchParam, $searchParam]; } $from = "FROM orders o LEFT JOIN quotations q ON o.quotation_id = q.id LEFT JOIN customers c ON o.customer_id = c.id"; $result = PaginationHelper::paginate( $pdo, "SELECT COUNT(*) {$from} {$where}", "SELECT o.id, o.order_number, o.quotation_id, o.status, o.currency, o.created_at, o.apply_vat, o.vat_rate, q.quotation_number, q.project_code, c.name as customer_name, (SELECT COALESCE(SUM(CASE WHEN oi.is_included_in_total THEN oi.quantity * oi.unit_price ELSE 0 END), 0) FROM order_items oi WHERE oi.order_id = o.id) as total, (SELECT inv.id FROM invoices inv WHERE inv.order_id = o.id LIMIT 1) as invoice_id, (SELECT inv.invoice_number FROM invoices inv WHERE inv.order_id = o.id LIMIT 1) as invoice_number {$from} {$where} ORDER BY {$p['sort']} {$p['order']}", $params, $p ); successResponse([ 'orders' => $result['items'], 'pagination' => $result['pagination'], ]); } function handleGetDetail(PDO $pdo, int $id): void { // BLOB vynechany - stahuje se pres action=attachment $stmt = $pdo->prepare(' SELECT o.id, o.order_number, o.customer_order_number, o.attachment_name, o.quotation_id, o.customer_id, o.status, o.currency, o.language, o.vat_rate, o.apply_vat, o.exchange_rate, o.scope_title, o.scope_description, o.notes, o.created_at, o.modified_at, q.quotation_number, q.project_code, c.name as customer_name FROM orders o LEFT JOIN quotations q ON o.quotation_id = q.id LEFT JOIN customers c ON o.customer_id = c.id WHERE o.id = ? '); $stmt->execute([$id]); $order = $stmt->fetch(); if (!$order) { errorResponse('Objednávka nebyla nalezena', 404); } // Get items $stmt = $pdo->prepare('SELECT * FROM order_items WHERE order_id = ? ORDER BY position'); $stmt->execute([$id]); $order['items'] = $stmt->fetchAll(); // Get sections $stmt = $pdo->prepare('SELECT * FROM order_sections WHERE order_id = ? ORDER BY position'); $stmt->execute([$id]); $order['sections'] = $stmt->fetchAll(); // Get customer if ($order['customer_id']) { $stmt = $pdo->prepare( 'SELECT id, name, company_id, vat_id, street, city, postal_code, country, custom_fields FROM customers WHERE id = ?' ); $stmt->execute([$order['customer_id']]); $order['customer'] = $stmt->fetch(); } // Get linked project $stmt = $pdo->prepare('SELECT id, project_number, name, status FROM projects WHERE order_id = ?'); $stmt->execute([$id]); $order['project'] = $stmt->fetch() ?: null; // Get linked invoice $stmt = $pdo->prepare('SELECT id, invoice_number, status FROM invoices WHERE order_id = ? LIMIT 1'); $stmt->execute([$id]); $order['invoice'] = $stmt->fetch() ?: null; // Valid transitions $order['valid_transitions'] = getValidTransitions($order['status']); successResponse($order); } function handleGetAttachment(PDO $pdo, int $id): void { $stmt = $pdo->prepare('SELECT attachment_data, attachment_name FROM orders WHERE id = ?'); $stmt->execute([$id]); $row = $stmt->fetch(); if (!$row || !$row['attachment_data']) { errorResponse('Příloha nebyla nalezena', 404); } $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->buffer($row['attachment_data']); if ($mime !== 'application/pdf') { errorResponse('Příloha není platný PDF soubor', 415); } header_remove('Content-Type'); header('Content-Type: application/pdf'); $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($row['attachment_name'] ?: 'priloha.pdf')); header('Content-Disposition: attachment; filename="' . $safeName . '"'); header('Content-Length: ' . strlen($row['attachment_data'])); echo $row['attachment_data']; exit; } function handleCreateOrder(PDO $pdo): void { // Podporuje JSON i FormData (kvuli nahravani prilohy) $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; if (str_contains($contentType, 'multipart/form-data')) { $quotationId = (int)($_POST['quotationId'] ?? 0); $customerOrderNumber = trim($_POST['customerOrderNumber'] ?? ''); } else { $input = getJsonInput(); $quotationId = (int)($input['quotationId'] ?? 0); $customerOrderNumber = trim($input['customerOrderNumber'] ?? ''); } if (!$quotationId) { errorResponse('ID nabídky je povinné'); } if ($customerOrderNumber === '') { errorResponse('Číslo objednávky zákazníka je povinné'); } if (mb_strlen($customerOrderNumber) > 100) { errorResponse('Číslo objednávky zákazníka je příliš dlouhé (max 100 znaků)'); } // Validace prilohy $attachmentData = null; $attachmentName = null; if (!empty($_FILES['attachment']['tmp_name'])) { $file = $_FILES['attachment']; if ($file['error'] !== UPLOAD_ERR_OK) { errorResponse('Chyba při nahrávání souboru'); } $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); if ($mime !== 'application/pdf') { errorResponse('Příloha musí být ve formátu PDF'); } if ($file['size'] > 10 * 1024 * 1024) { errorResponse('Příloha nesmí být větší než 10 MB'); } $attachmentData = file_get_contents($file['tmp_name']); $attachmentName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name'])); } // Verify quotation exists and has no order yet $stmt = $pdo->prepare('SELECT * FROM quotations WHERE id = ?'); $stmt->execute([$quotationId]); $quotation = $stmt->fetch(); if (!$quotation) { errorResponse('Nabídka nebyla nalezena', 404); } if ($quotation['order_id']) { errorResponse('Tato nabídka již má objednávku'); } // Get quotation items and sections $stmt = $pdo->prepare('SELECT * FROM quotation_items WHERE quotation_id = ? ORDER BY position'); $stmt->execute([$quotationId]); $quotationItems = $stmt->fetchAll(); $stmt = $pdo->prepare('SELECT * FROM scope_sections WHERE quotation_id = ? ORDER BY position'); $stmt->execute([$quotationId]); $quotationSections = $stmt->fetchAll(); // Lock for concurrent number generation $locked = $pdo->query("SELECT GET_LOCK('boha_order_number', 5)")->fetchColumn(); if (!$locked) { errorResponse('Nepodařilo se získat zámek pro číslo objednávky, zkuste to znovu', 503); } $pdo->beginTransaction(); try { $orderNumber = generateOrderNumber($pdo); $stmt = $pdo->prepare(" INSERT INTO orders ( order_number, customer_order_number, attachment_data, attachment_name, quotation_id, customer_id, status, currency, language, vat_rate, apply_vat, exchange_rate, scope_title, scope_description, created_at, modified_at ) VALUES (?, ?, ?, ?, ?, ?, 'prijata', ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) "); $stmt->execute([ $orderNumber, $customerOrderNumber, $attachmentData, $attachmentName, $quotationId, $quotation['customer_id'], $quotation['currency'] ?? 'EUR', $quotation['language'] ?? 'EN', $quotation['vat_rate'] ?? 0, $quotation['apply_vat'] ?? 0, $quotation['exchange_rate'], $quotation['scope_title'] ?? '', $quotation['scope_description'] ?? '', ]); $orderId = (int)$pdo->lastInsertId(); // Copy items if (!empty($quotationItems)) { $itemStmt = $pdo->prepare(' INSERT INTO order_items ( order_id, description, item_description, quantity, unit, unit_price, is_included_in_total, position, modified_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()) '); foreach ($quotationItems as $item) { $itemStmt->execute([ $orderId, $item['description'] ?? '', $item['item_description'] ?? '', $item['quantity'] ?? 1, $item['unit'] ?? '', $item['unit_price'] ?? 0, $item['is_included_in_total'] ?? 1, $item['position'] ?? 0, ]); } } // Copy sections if (!empty($quotationSections)) { $sectionStmt = $pdo->prepare(' INSERT INTO order_sections ( order_id, title, title_cz, content, position, modified_at ) VALUES (?, ?, ?, ?, ?, NOW()) '); foreach ($quotationSections as $section) { $sectionStmt->execute([ $orderId, $section['title'] ?? '', $section['title_cz'] ?? '', $section['content'] ?? '', $section['position'] ?? 0, ]); } } // Create project with same number $projectName = $quotation['project_code'] ?: ($quotation['customer_name'] ?? 'Projekt ' . $orderNumber); // Need customer name if (!$quotation['project_code'] && $quotation['customer_id']) { $custStmt = $pdo->prepare('SELECT name FROM customers WHERE id = ?'); $custStmt->execute([$quotation['customer_id']]); $custName = $custStmt->fetchColumn(); if ($custName) { $projectName = $custName; } } $stmt = $pdo->prepare(" INSERT INTO projects ( project_number, name, customer_id, quotation_id, order_id, status, start_date, created_at, modified_at ) VALUES (?, ?, ?, ?, ?, 'aktivni', CURDATE(), NOW(), NOW()) "); $stmt->execute([ $orderNumber, $projectName, $quotation['customer_id'], $quotationId, $orderId, ]); $projectId = (int)$pdo->lastInsertId(); // Update quotation with back-reference $stmt = $pdo->prepare('UPDATE quotations SET order_id = ?, modified_at = NOW() WHERE id = ?'); $stmt->execute([$orderId, $quotationId]); $pdo->commit(); $pdo->query("SELECT RELEASE_LOCK('boha_order_number')"); AuditLog::logCreate('orders_order', $orderId, [ 'order_number' => $orderNumber, 'quotation_number' => $quotation['quotation_number'], 'project_id' => $projectId, ], "Vytvořena objednávka '$orderNumber' z nabídky '{$quotation['quotation_number']}'"); successResponse([ 'order_id' => $orderId, 'order_number' => $orderNumber, 'project_id' => $projectId, 'project_number' => $orderNumber, ], 'Objednávka byla vytvořena'); } catch (PDOException $e) { $pdo->rollBack(); $pdo->query("SELECT RELEASE_LOCK('boha_order_number')"); throw $e; } } function handleUpdateOrder(PDO $pdo, int $id): void { $stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?'); $stmt->execute([$id]); $order = $stmt->fetch(); if (!$order) { errorResponse('Objednávka nebyla nalezena', 404); } $input = getJsonInput(); $newStatus = $input['status'] ?? null; $notes = $input['notes'] ?? null; $newOrderNumber = isset($input['order_number']) ? trim($input['order_number']) : null; // Delkove limity if ($notes !== null && mb_strlen($notes) > 5000) { errorResponse('Poznámky jsou příliš dlouhé (max 5000 znaků)'); } if ($newOrderNumber !== null && mb_strlen($newOrderNumber) > 50) { errorResponse('Číslo objednávky je příliš dlouhé (max 50 znaků)'); } // Validate status transition if ($newStatus && $newStatus !== $order['status']) { $valid = getValidTransitions($order['status']); if (!in_array($newStatus, $valid)) { errorResponse("Neplatný přechod stavu z '{$order['status']}' na '$newStatus'"); } } // Validate order number uniqueness if ($newOrderNumber !== null && $newOrderNumber !== $order['order_number']) { if (empty($newOrderNumber)) { errorResponse('Číslo objednávky nesmí být prázdné'); } $stmt = $pdo->prepare('SELECT id FROM orders WHERE order_number = ? AND id != ?'); $stmt->execute([$newOrderNumber, $id]); if ($stmt->fetch()) { errorResponse('Toto číslo objednávky již existuje'); } } $pdo->beginTransaction(); try { $updates = []; $params = []; if ($newOrderNumber !== null && $newOrderNumber !== $order['order_number']) { $updates[] = 'order_number = ?'; $params[] = $newOrderNumber; // Sync project number $stmt = $pdo->prepare('UPDATE projects SET project_number = ?, modified_at = NOW() WHERE order_id = ?'); $stmt->execute([$newOrderNumber, $id]); } if ($newStatus !== null) { $updates[] = 'status = ?'; $params[] = $newStatus; } if ($notes !== null) { $updates[] = 'notes = ?'; $params[] = $notes; } if (!empty($updates)) { $updates[] = 'modified_at = NOW()'; $params[] = $id; $sql = 'UPDATE orders SET ' . implode(', ', $updates) . ' WHERE id = ?'; $stmt = $pdo->prepare($sql); $stmt->execute($params); } // Sync project status with order status if ($newStatus && $newStatus !== $order['status']) { $projectStatus = null; if ($newStatus === 'stornovana') { $projectStatus = 'zruseny'; } elseif ($newStatus === 'dokoncena') { $projectStatus = 'dokonceny'; } elseif ($newStatus === 'v_realizaci') { $projectStatus = 'aktivni'; } if ($projectStatus) { $stmt = $pdo->prepare('UPDATE projects SET status = ?, modified_at = NOW() WHERE order_id = ?'); $stmt->execute([$projectStatus, $id]); } } $pdo->commit(); AuditLog::logUpdate( 'orders_order', $id, ['status' => $order['status'], 'notes' => $order['notes']], ['status' => $newStatus ?? $order['status'], 'notes' => $notes ?? $order['notes']], "Upravena objednávka '{$order['order_number']}'" ); successResponse(null, 'Objednávka byla aktualizována'); } catch (PDOException $e) { $pdo->rollBack(); throw $e; } } function handleDeleteOrder(PDO $pdo, int $id): void { $stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?'); $stmt->execute([$id]); $order = $stmt->fetch(); if (!$order) { errorResponse('Objednávka nebyla nalezena', 404); } $pdo->beginTransaction(); try { // Delete project linked to this order $stmt = $pdo->prepare('DELETE FROM projects WHERE order_id = ?'); $stmt->execute([$id]); // Delete order items and sections $stmt = $pdo->prepare('DELETE FROM order_items WHERE order_id = ?'); $stmt->execute([$id]); $stmt = $pdo->prepare('DELETE FROM order_sections WHERE order_id = ?'); $stmt->execute([$id]); // Delete order $stmt = $pdo->prepare('DELETE FROM orders WHERE id = ?'); $stmt->execute([$id]); // Remove back-reference from quotation $stmt = $pdo->prepare('UPDATE quotations SET order_id = NULL, modified_at = NOW() WHERE order_id = ?'); $stmt->execute([$id]); $pdo->commit(); AuditLog::logDelete('orders_order', $id, [ 'order_number' => $order['order_number'], 'quotation_id' => $order['quotation_id'], ], "Smazána objednávka '{$order['order_number']}'"); successResponse(null, 'Objednávka byla smazána'); } catch (PDOException $e) { $pdo->rollBack(); throw $e; } }