- Handler funkce extrahovany z API souboru do api/admin/handlers/ - config.php rozdeleny na helpers.php (funkce) a constants.php (konstanty) - require_once odstranen z class souboru (AuditLog, JWTAuth, LeaveNotification) - vendor/autoload.php presunuto do config.php bootstrap - totp-handlers.php: pridany use deklarace pro TwoFactorAuth - phpstan.neon: bootstrapFiles, scanDirectories, dynamicConstantNames - Opraveny chybejici routing bloky v totp.php a session.php Vysledek: phpcs 0 errors 0 warnings, PHPStan 0 errors, ESLint 0 errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
589 lines
19 KiB
PHP
589 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
function handleGetList(PDO $pdo): void
|
|
{
|
|
$search = trim($_GET['search'] ?? '');
|
|
$sort = $_GET['sort'] ?? 'created_at';
|
|
$order = strtoupper($_GET['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
|
|
$page = max(1, (int) ($_GET['page'] ?? 1));
|
|
$perPage = min(500, max(1, (int) ($_GET['per_page'] ?? 500)));
|
|
|
|
$sortMap = [
|
|
'Date' => 'q.created_at',
|
|
'CreatedAt' => 'q.created_at',
|
|
'created_at' => 'q.created_at',
|
|
'QuotationNumber' => 'q.quotation_number',
|
|
'quotation_number' => 'q.quotation_number',
|
|
'ProjectCode' => 'q.project_code',
|
|
'project_code' => 'q.project_code',
|
|
'ValidUntil' => 'q.valid_until',
|
|
'valid_until' => 'q.valid_until',
|
|
'Currency' => 'q.currency',
|
|
'currency' => 'q.currency',
|
|
];
|
|
if (!isset($sortMap[$sort])) {
|
|
errorResponse('Neplatný parametr řazení', 400);
|
|
}
|
|
$sortCol = $sortMap[$sort];
|
|
|
|
$where = 'WHERE 1=1';
|
|
$params = [];
|
|
|
|
if ($search) {
|
|
$search = mb_substr($search, 0, 100);
|
|
$where .= ' AND (q.quotation_number LIKE ? OR q.project_code LIKE ? OR c.name LIKE ?)';
|
|
$searchParam = "%{$search}%";
|
|
$params = [$searchParam, $searchParam, $searchParam];
|
|
}
|
|
|
|
// Celkovy pocet pro pagination
|
|
$countSql = "
|
|
SELECT COUNT(*)
|
|
FROM quotations q
|
|
LEFT JOIN customers c ON q.customer_id = c.id
|
|
$where
|
|
";
|
|
$stmt = $pdo->prepare($countSql);
|
|
$stmt->execute($params);
|
|
$total = (int) $stmt->fetchColumn();
|
|
|
|
$offset = ($page - 1) * $perPage;
|
|
|
|
$sql = "
|
|
SELECT q.id, q.quotation_number, q.project_code, q.created_at, q.valid_until,
|
|
q.currency, q.language, q.apply_vat, q.vat_rate, q.exchange_rate,
|
|
q.customer_id, q.order_id, q.status,
|
|
c.name as customer_name,
|
|
(SELECT COALESCE(SUM(CASE WHEN qi.is_included_in_total THEN qi.quantity * qi.unit_price ELSE 0 END), 0)
|
|
FROM quotation_items qi WHERE qi.quotation_id = q.id) as total
|
|
FROM quotations q
|
|
LEFT JOIN customers c ON q.customer_id = c.id
|
|
$where
|
|
ORDER BY $sortCol $order
|
|
LIMIT $perPage OFFSET $offset
|
|
";
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
$quotations = $stmt->fetchAll();
|
|
|
|
successResponse([
|
|
'quotations' => $quotations,
|
|
'total' => $total,
|
|
'page' => $page,
|
|
'per_page' => $perPage,
|
|
]);
|
|
}
|
|
|
|
function handleGetDetail(PDO $pdo, int $id): void
|
|
{
|
|
$stmt = $pdo->prepare('
|
|
SELECT q.*, c.name as customer_name
|
|
FROM quotations q
|
|
LEFT JOIN customers c ON q.customer_id = c.id
|
|
WHERE q.id = ?
|
|
');
|
|
$stmt->execute([$id]);
|
|
$quotation = $stmt->fetch();
|
|
|
|
if (!$quotation) {
|
|
errorResponse('Nabídka nebyla nalezena', 404);
|
|
}
|
|
|
|
// Get items
|
|
$stmt = $pdo->prepare('
|
|
SELECT * FROM quotation_items
|
|
WHERE quotation_id = ?
|
|
ORDER BY position
|
|
');
|
|
$stmt->execute([$id]);
|
|
$quotation['items'] = $stmt->fetchAll();
|
|
|
|
// Get scope sections
|
|
$stmt = $pdo->prepare('
|
|
SELECT * FROM scope_sections
|
|
WHERE quotation_id = ?
|
|
ORDER BY position
|
|
');
|
|
$stmt->execute([$id]);
|
|
$quotation['sections'] = $stmt->fetchAll();
|
|
|
|
// Get customer
|
|
if ($quotation['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([$quotation['customer_id']]);
|
|
$quotation['customer'] = $stmt->fetch();
|
|
}
|
|
|
|
// Get linked order info
|
|
if ($quotation['order_id']) {
|
|
$stmt = $pdo->prepare('SELECT id, order_number, status FROM orders WHERE id = ?');
|
|
$stmt->execute([$quotation['order_id']]);
|
|
$quotation['order'] = $stmt->fetch() ?: null;
|
|
} else {
|
|
$quotation['order'] = null;
|
|
}
|
|
|
|
successResponse($quotation);
|
|
}
|
|
|
|
function handleGetNextNumber(PDO $pdo): void
|
|
{
|
|
$settings = $pdo->query('SELECT quotation_prefix FROM company_settings LIMIT 1')->fetch();
|
|
if (!$settings) {
|
|
errorResponse('Nastavení firmy nenalezeno');
|
|
}
|
|
|
|
$year = date('Y');
|
|
$prefix = $settings['quotation_prefix'] ?: 'N';
|
|
$number = getMaxQuotationNumber($pdo, $year, $prefix) + 1;
|
|
|
|
$formatted = sprintf('%s/%s/%03d', $year, $prefix, $number);
|
|
|
|
successResponse([
|
|
'number' => $formatted,
|
|
'raw_number' => $number,
|
|
'prefix' => $prefix,
|
|
'year' => $year,
|
|
]);
|
|
}
|
|
|
|
function getMaxQuotationNumber(PDO $pdo, string $year, string $prefix): int
|
|
{
|
|
$likePattern = "{$year}/{$prefix}/%";
|
|
$stmt = $pdo->prepare("
|
|
SELECT COALESCE(MAX(CAST(SUBSTRING_INDEX(quotation_number, '/', -1) AS UNSIGNED)), 0)
|
|
FROM quotations
|
|
WHERE quotation_number LIKE ?
|
|
");
|
|
$stmt->execute([$likePattern]);
|
|
return (int) $stmt->fetchColumn();
|
|
}
|
|
|
|
function generateNextNumber(PDO $pdo): string
|
|
{
|
|
$settings = $pdo->query('SELECT quotation_prefix FROM company_settings LIMIT 1')->fetch();
|
|
|
|
$year = date('Y');
|
|
$prefix = $settings['quotation_prefix'] ?: 'N';
|
|
$number = getMaxQuotationNumber($pdo, $year, $prefix) + 1;
|
|
|
|
return sprintf('%s/%s/%03d', $year, $prefix, $number);
|
|
}
|
|
|
|
/** @param array<string, mixed> $q */
|
|
function validateQuotationInput(array $q): void
|
|
{
|
|
if (empty($q['customer_id'])) {
|
|
errorResponse('Vyberte zákazníka');
|
|
}
|
|
if (empty($q['created_at'])) {
|
|
errorResponse('Zadejte datum vytvoření');
|
|
}
|
|
if (empty($q['valid_until'])) {
|
|
errorResponse('Zadejte datum platnosti');
|
|
}
|
|
if (!empty($q['created_at']) && !empty($q['valid_until']) && $q['valid_until'] < $q['created_at']) {
|
|
errorResponse('Datum platnosti nesmí být před datem vytvoření');
|
|
}
|
|
if (empty($q['currency'])) {
|
|
errorResponse('Vyberte měnu');
|
|
}
|
|
|
|
// Validace formatu dat
|
|
foreach (['created_at', 'valid_until'] as $dateField) {
|
|
if (!empty($q[$dateField]) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $q[$dateField])) {
|
|
errorResponse("Neplatný formát data: $dateField");
|
|
}
|
|
}
|
|
// Validace meny a jazyka
|
|
if (!in_array($q['currency'] ?? '', ['EUR', 'USD', 'CZK', 'GBP'])) {
|
|
errorResponse('Neplatná měna');
|
|
}
|
|
if (!empty($q['language']) && !in_array($q['language'], ['EN', 'CZ'])) {
|
|
errorResponse('Neplatný jazyk');
|
|
}
|
|
// Validace DPH
|
|
if (isset($q['vat_rate'])) {
|
|
$rate = floatval($q['vat_rate']);
|
|
if ($rate < 0 || $rate > 100) {
|
|
errorResponse('Sazba DPH musí být mezi 0 a 100');
|
|
}
|
|
}
|
|
// Delkove limity
|
|
if (!empty($q['project_code']) && mb_strlen($q['project_code']) > 100) {
|
|
errorResponse('Kód projektu je příliš dlouhý (max 100 znaků)');
|
|
}
|
|
}
|
|
|
|
function handleCreateOffer(PDO $pdo): void
|
|
{
|
|
$input = getJsonInput();
|
|
$quotation = $input['quotation'] ?? $input;
|
|
$items = $input['items'] ?? [];
|
|
$sections = $input['sections'] ?? [];
|
|
|
|
validateQuotationInput($quotation);
|
|
|
|
// Serialize number generation across concurrent requests
|
|
$locked = $pdo->query("SELECT GET_LOCK('boha_quotation_number', 5)")->fetchColumn();
|
|
if (!$locked) {
|
|
errorResponse('Nepodařilo se získat zámek pro číslo nabídky, zkuste to znovu', 503);
|
|
}
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$quotationNumber = generateNextNumber($pdo);
|
|
|
|
$stmt = $pdo->prepare('
|
|
INSERT INTO quotations (
|
|
quotation_number, project_code, customer_id, created_at, valid_until,
|
|
currency, language, vat_rate, apply_vat, exchange_rate,
|
|
scope_title, scope_description, modified_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
|
');
|
|
|
|
$stmt->execute([
|
|
$quotationNumber,
|
|
$quotation['project_code'] ?? '',
|
|
$quotation['customer_id'] ? (int)$quotation['customer_id'] : null,
|
|
$quotation['created_at'] ?? date('Y-m-d H:i:s'),
|
|
$quotation['valid_until'] ?? date('Y-m-d H:i:s', strtotime('+30 days')),
|
|
$quotation['currency'] ?? 'EUR',
|
|
$quotation['language'] ?? 'EN',
|
|
$quotation['vat_rate'] ?? 21,
|
|
isset($quotation['apply_vat']) ? ($quotation['apply_vat'] ? 1 : 0) : 0,
|
|
$quotation['exchange_rate'] ?? null,
|
|
$quotation['scope_title'] ?? '',
|
|
$quotation['scope_description'] ?? '',
|
|
]);
|
|
|
|
$quotationId = (int)$pdo->lastInsertId();
|
|
|
|
saveItems($pdo, $quotationId, $items);
|
|
saveSections($pdo, $quotationId, $sections);
|
|
|
|
|
|
$pdo->commit();
|
|
$pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')");
|
|
|
|
AuditLog::logCreate('offers_quotation', $quotationId, [
|
|
'quotation_number' => $quotationNumber,
|
|
'project_code' => $quotation['project_code'] ?? '',
|
|
], "Vytvořena nabídka '$quotationNumber'");
|
|
|
|
successResponse([
|
|
'id' => $quotationId,
|
|
'number' => $quotationNumber,
|
|
], 'Nabídka byla vytvořena');
|
|
} catch (PDOException $e) {
|
|
$pdo->rollBack();
|
|
$pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')");
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
function handleUpdateOffer(PDO $pdo, int $id): void
|
|
{
|
|
$stmt = $pdo->prepare('SELECT * FROM quotations WHERE id = ?');
|
|
$stmt->execute([$id]);
|
|
$existing = $stmt->fetch();
|
|
|
|
if (!$existing) {
|
|
errorResponse('Nabídka nebyla nalezena', 404);
|
|
}
|
|
|
|
if ($existing['status'] === 'invalidated') {
|
|
errorResponse('Zneplatněnou nabídku nelze upravovat', 403);
|
|
}
|
|
|
|
$input = getJsonInput();
|
|
$quotation = $input['quotation'] ?? $input;
|
|
$items = $input['items'] ?? [];
|
|
$sections = $input['sections'] ?? [];
|
|
|
|
validateQuotationInput($quotation);
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$stmt = $pdo->prepare('
|
|
UPDATE quotations SET
|
|
project_code = ?,
|
|
customer_id = ?,
|
|
created_at = ?,
|
|
valid_until = ?,
|
|
currency = ?,
|
|
language = ?,
|
|
vat_rate = ?,
|
|
apply_vat = ?,
|
|
exchange_rate = ?,
|
|
scope_title = ?,
|
|
scope_description = ?,
|
|
modified_at = NOW()
|
|
WHERE id = ?
|
|
');
|
|
|
|
$stmt->execute([
|
|
$quotation['project_code'] ?? $existing['project_code'],
|
|
isset($quotation['customer_id'])
|
|
? ($quotation['customer_id'] ? (int)$quotation['customer_id'] : null)
|
|
: $existing['customer_id'],
|
|
$quotation['created_at'] ?? $existing['created_at'],
|
|
$quotation['valid_until'] ?? $existing['valid_until'],
|
|
$quotation['currency'] ?? $existing['currency'],
|
|
$quotation['language'] ?? $existing['language'],
|
|
$quotation['vat_rate'] ?? $existing['vat_rate'],
|
|
isset($quotation['apply_vat']) ? ($quotation['apply_vat'] ? 1 : 0) : $existing['apply_vat'],
|
|
array_key_exists('exchange_rate', $quotation) ? $quotation['exchange_rate'] : $existing['exchange_rate'],
|
|
$quotation['scope_title'] ?? $existing['scope_title'],
|
|
$quotation['scope_description'] ?? $existing['scope_description'],
|
|
$id,
|
|
]);
|
|
|
|
// Replace items
|
|
$stmt = $pdo->prepare('DELETE FROM quotation_items WHERE quotation_id = ?');
|
|
$stmt->execute([$id]);
|
|
saveItems($pdo, $id, $items);
|
|
|
|
// Replace sections
|
|
$stmt = $pdo->prepare('DELETE FROM scope_sections WHERE quotation_id = ?');
|
|
$stmt->execute([$id]);
|
|
saveSections($pdo, $id, $sections);
|
|
|
|
|
|
$pdo->commit();
|
|
|
|
AuditLog::logUpdate(
|
|
'offers_quotation',
|
|
$id,
|
|
['quotation_number' => $existing['quotation_number']],
|
|
['project_code' => $quotation['project_code'] ?? $existing['project_code']],
|
|
"Upravena nabídka '{$existing['quotation_number']}'"
|
|
);
|
|
|
|
successResponse(null, 'Nabídka byla aktualizována');
|
|
} catch (PDOException $e) {
|
|
$pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
function handleDuplicate(PDO $pdo, int $sourceId): void
|
|
{
|
|
$stmt = $pdo->prepare('SELECT * FROM quotations WHERE id = ?');
|
|
$stmt->execute([$sourceId]);
|
|
$source = $stmt->fetch();
|
|
|
|
if (!$source) {
|
|
errorResponse('Zdrojová nabídka nebyla nalezena', 404);
|
|
}
|
|
|
|
$stmt = $pdo->prepare('SELECT * FROM quotation_items WHERE quotation_id = ? ORDER BY position');
|
|
$stmt->execute([$sourceId]);
|
|
$sourceItems = $stmt->fetchAll();
|
|
|
|
$stmt = $pdo->prepare('SELECT * FROM scope_sections WHERE quotation_id = ? ORDER BY position');
|
|
$stmt->execute([$sourceId]);
|
|
$sourceSections = $stmt->fetchAll();
|
|
|
|
$locked = $pdo->query("SELECT GET_LOCK('boha_quotation_number', 5)")->fetchColumn();
|
|
if (!$locked) {
|
|
errorResponse('Nepodařilo se získat zámek pro číslo nabídky, zkuste to znovu', 503);
|
|
}
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$newNumber = generateNextNumber($pdo);
|
|
|
|
$stmt = $pdo->prepare('
|
|
INSERT INTO quotations (
|
|
quotation_number, project_code, customer_id, created_at, valid_until,
|
|
currency, language, vat_rate, apply_vat, exchange_rate,
|
|
scope_title, scope_description, modified_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
|
');
|
|
|
|
$stmt->execute([
|
|
$newNumber,
|
|
$source['project_code'],
|
|
$source['customer_id'],
|
|
date('Y-m-d H:i:s'),
|
|
date('Y-m-d H:i:s', strtotime('+30 days')),
|
|
$source['currency'],
|
|
$source['language'],
|
|
$source['vat_rate'],
|
|
$source['apply_vat'],
|
|
$source['exchange_rate'],
|
|
$source['scope_title'],
|
|
$source['scope_description'],
|
|
]);
|
|
|
|
$newId = (int)$pdo->lastInsertId();
|
|
|
|
$items = array_map(function ($item) {
|
|
return [
|
|
'description' => $item['description'],
|
|
'item_description' => $item['item_description'],
|
|
'quantity' => $item['quantity'],
|
|
'unit_price' => $item['unit_price'],
|
|
'is_included_in_total' => $item['is_included_in_total'],
|
|
'position' => $item['position'],
|
|
];
|
|
}, $sourceItems);
|
|
saveItems($pdo, $newId, $items);
|
|
|
|
$sections = array_map(function ($section) {
|
|
return [
|
|
'title' => $section['title'],
|
|
'title_cz' => $section['title_cz'],
|
|
'content' => $section['content'],
|
|
'position' => $section['position'],
|
|
];
|
|
}, $sourceSections);
|
|
saveSections($pdo, $newId, $sections);
|
|
|
|
$pdo->commit();
|
|
$pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')");
|
|
|
|
AuditLog::logCreate('offers_quotation', $newId, [
|
|
'quotation_number' => $newNumber,
|
|
'duplicated_from' => $source['quotation_number'],
|
|
], "Duplikována nabídka '{$source['quotation_number']}' jako '$newNumber'");
|
|
|
|
successResponse([
|
|
'id' => $newId,
|
|
'number' => $newNumber,
|
|
], 'Nabídka byla duplikována');
|
|
} catch (PDOException $e) {
|
|
$pdo->rollBack();
|
|
$pdo->query("SELECT RELEASE_LOCK('boha_quotation_number')");
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
function handleInvalidateOffer(PDO $pdo, int $id): void
|
|
{
|
|
$stmt = $pdo->prepare('SELECT quotation_number, status, order_id FROM quotations WHERE id = ?');
|
|
$stmt->execute([$id]);
|
|
$quotation = $stmt->fetch();
|
|
|
|
if (!$quotation) {
|
|
errorResponse('Nabídka nebyla nalezena', 404);
|
|
}
|
|
|
|
if ($quotation['status'] === 'invalidated') {
|
|
errorResponse('Nabídka je již zneplatněna', 400);
|
|
}
|
|
|
|
if ($quotation['order_id']) {
|
|
errorResponse('Nabídku s objednávkou nelze zneplatnit', 400);
|
|
}
|
|
|
|
$stmt = $pdo->prepare('UPDATE quotations SET status = ?, modified_at = NOW() WHERE id = ?');
|
|
$stmt->execute(['invalidated', $id]);
|
|
|
|
AuditLog::logUpdate(
|
|
'offers_quotation',
|
|
$id,
|
|
['status' => 'active'],
|
|
['status' => 'invalidated'],
|
|
"Zneplatněna nabídka '{$quotation['quotation_number']}'"
|
|
);
|
|
|
|
successResponse(null, 'Nabídka byla zneplatněna');
|
|
}
|
|
|
|
function handleDeleteQuotation(PDO $pdo, int $id): void
|
|
{
|
|
$stmt = $pdo->prepare('SELECT quotation_number FROM quotations WHERE id = ?');
|
|
$stmt->execute([$id]);
|
|
$quotation = $stmt->fetch();
|
|
|
|
|
|
if (!$quotation) {
|
|
errorResponse('Nabídka nebyla nalezena', 404);
|
|
}
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$stmt = $pdo->prepare('DELETE FROM quotation_items WHERE quotation_id = ?');
|
|
$stmt->execute([$id]);
|
|
|
|
$stmt = $pdo->prepare('DELETE FROM scope_sections WHERE quotation_id = ?');
|
|
$stmt->execute([$id]);
|
|
|
|
$stmt = $pdo->prepare('DELETE FROM quotations WHERE id = ?');
|
|
$stmt->execute([$id]);
|
|
|
|
$pdo->commit();
|
|
|
|
AuditLog::logDelete('offers_quotation', $id, [
|
|
'quotation_number' => $quotation['quotation_number'],
|
|
], "Smazána nabídka '{$quotation['quotation_number']}'");
|
|
|
|
successResponse(null, 'Nabídka byla smazána');
|
|
} catch (PDOException $e) {
|
|
$pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
/** @param list<array<string, mixed>> $items */
|
|
function saveItems(PDO $pdo, int $quotationId, array $items): void
|
|
{
|
|
if (empty($items)) {
|
|
return;
|
|
}
|
|
|
|
$stmt = $pdo->prepare('
|
|
INSERT INTO quotation_items (
|
|
quotation_id, description, item_description, quantity, unit,
|
|
unit_price, is_included_in_total, position, modified_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
|
');
|
|
|
|
foreach ($items as $i => $item) {
|
|
$stmt->execute([
|
|
$quotationId,
|
|
$item['description'] ?? '',
|
|
$item['item_description'] ?? '',
|
|
$item['quantity'] ?? 1,
|
|
$item['unit'] ?? '',
|
|
$item['unit_price'] ?? 0,
|
|
isset($item['is_included_in_total']) ? ($item['is_included_in_total'] ? 1 : 0) : 1,
|
|
$item['position'] ?? ($i + 1),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/** @param list<array<string, mixed>> $sections */
|
|
function saveSections(PDO $pdo, int $quotationId, array $sections): void
|
|
{
|
|
if (empty($sections)) {
|
|
return;
|
|
}
|
|
|
|
$stmt = $pdo->prepare('
|
|
INSERT INTO scope_sections (
|
|
quotation_id, title, title_cz, content, position, modified_at
|
|
) VALUES (?, ?, ?, ?, ?, NOW())
|
|
');
|
|
|
|
foreach ($sections as $i => $section) {
|
|
$stmt->execute([
|
|
$quotationId,
|
|
$section['title'] ?? '',
|
|
$section['title_cz'] ?? '',
|
|
$section['content'] ?? '',
|
|
$section['position'] ?? ($i + 1),
|
|
]);
|
|
}
|
|
}
|