Корневая папка: /home/d/docdelux/tech-stc.ru/public_html/construction-estimates/builder
Всего папок: 2
Всего файлов: 16
Общий размер: 78,757 байт
Создано: 2026-07-09 11:19:06
<?php /** * AJAX-обработчик расчёта сметы * /construction-estimates/builder/build.php */ header('Content-Type: application/json'); header('Access-Control-Allow-Origin: *'); $data = json_decode(file_get_contents('php://input'), true); if (!$data || !isset($data['template_id']) || !isset($data['params'])) { echo json_encode(['error' => 'Неверные параметры']); exit; } $templateId = $data['template_id']; $params = $data['params']; // Загружаем шаблон $templateFile = __DIR__ . '/data/templates/' . $templateId . '.json'; if (!file_exists($templateFile)) { echo json_encode(['error' => 'Шаблон не найден: ' . $templateFile]); exit; } $template = json_decode(file_get_contents($templateFile), true); if (!$template) { echo json_encode(['error' => 'Ошибка чтения шаблона']); exit; } // Загружаем регионы $regionsFile = __DIR__ . '/data/regions.json'; $regions = ['center' => ['id' => 'center', 'name' => 'Центральный регион', 'coefficient' => 1.0]]; if (file_exists($regionsFile)) { $regionsData = json_decode(file_get_contents($regionsFile), true); if (isset($regionsData['regions']) && is_array($regionsData['regions'])) { foreach ($regionsData['regions'] as $r) { $regions[$r['id']] = $r; } } } // Региональный коэффициент $regionId = $params['region'] ?? 'center'; $regionCoeff = $regions[$regionId]['coefficient'] ?? 1.0; // Расчёт сметы $estimate = [ 'title' => $template['name'], 'description' => $template['description'], 'items' => [], 'total_direct' => 0, 'total' => 0 ]; foreach ($template['works'] as $work) { // Вычисляем объём по формуле $formula = $work['formula']; foreach ($params as $key => $value) { $formula = str_replace($key, (float)$value, $formula); } // Безопасное вычисление объёма $qty = 0; try { $qty = @eval('return ' . $formula . ';'); if ($qty === false || $qty === null) $qty = 0; $qty = round($qty, 3); } catch (Throwable $e) { $qty = 0; } if ($qty <= 0) continue; // Цена с учётом региона $price = $work['price_fer'] * $regionCoeff; // Прямые затраты $direct = $qty * $price; $estimate['items'][] = [ 'code' => $work['code'], 'name' => $work['name'], 'unit' => $work['unit'], 'qty' => $qty, 'price' => round($price, 2), 'direct' => round($direct, 2) ]; $estimate['total_direct'] += $direct; $estimate['total'] += $direct; } $estimate['total_direct'] = round($estimate['total_direct'], 2); $estimate['total'] = round($estimate['total'], 2); echo json_encode(['success' => true, 'estimate' => $estimate]);
const BUILDER_API = '/construction-estimates/builder/build.php'; let currentWorkType = null; let regionsData = null; // Загружаем регионы один раз при старте async function loadRegions() { if (regionsData) return regionsData; try { const response = await fetch('/construction-estimates/builder/data/regions.json'); if (!response.ok) throw new Error('Регионы не загружены'); const data = await response.json(); regionsData = data.regions; return regionsData; } catch (error) { console.error('Ошибка загрузки регионов:', error); return []; } } document.addEventListener('DOMContentLoaded', async () => { // Предзагружаем регионы await loadRegions(); // Выбор типа работ document.querySelectorAll('.worktype-card .btn-select-worktype').forEach(btn => { btn.addEventListener('click', (e) => { const card = e.target.closest('.worktype-card'); if (!card) return; currentWorkType = card.dataset.worktype; if (currentWorkType) { loadParamsTemplate(currentWorkType); } }); }); // Кнопка "Новый расчёт" const backBtn = document.getElementById('btn-back'); if (backBtn) { backBtn.addEventListener('click', () => { document.getElementById('step-worktype').style.display = 'block'; document.getElementById('step-params').style.display = 'none'; document.getElementById('step-result').style.display = 'none'; currentWorkType = null; }); } // Кнопка печати const printBtn = document.getElementById('btn-print'); if (printBtn) { printBtn.addEventListener('click', () => { window.print(); }); } // Кнопка экспорта CSV const exportBtn = document.getElementById('btn-export'); if (exportBtn) { exportBtn.addEventListener('click', () => { exportToCSV(); }); } }); async function loadParamsTemplate(workTypeId) { const loading = document.getElementById('loading'); if (loading) loading.style.display = 'block'; try { const response = await fetch(`/construction-estimates/builder/data/templates/${workTypeId}.json`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const template = await response.json(); // Проверка структуры if (!template.params || !Array.isArray(template.params)) { throw new Error('Неверная структура шаблона: отсутствует params'); } await renderParamsForm(template); document.getElementById('step-worktype').style.display = 'none'; document.getElementById('step-params').style.display = 'block'; const calcBtn = document.getElementById('btn-next-to-calc'); if (calcBtn) { calcBtn.onclick = () => calculateEstimate(workTypeId); } } catch (error) { console.error('Ошибка загрузки шаблона:', error); const container = document.getElementById('params-form'); if (container) { container.innerHTML = `<div class="error">Ошибка загрузки: ${error.message}</div>`; } } finally { if (loading) loading.style.display = 'none'; } } async function renderParamsForm(template) { const container = document.getElementById('params-form'); if (!container) return; let html = ''; for (const param of template.params) { if (param.type === 'select' && param.options === 'regions') { const regions = await loadRegions(); let options = '<option value="">Выберите регион...</option>'; if (regions && regions.length) { options += regions.map(r => `<option value="${r.id}">${r.name} (коэфф. ${r.coefficient})</option>` ).join(''); } else { options += '<option value="center">Центральный регион (коэфф. 1.0)</option>'; } html += ` <div class="param-field"> <label>${param.label}</label> <select id="param_${param.name}" name="${param.name}"> ${options} </select> </div> `; } else { html += ` <div class="param-field"> <label>${param.label}</label> <input type="number" id="param_${param.name}" name="${param.name}" value="${param.default}" min="${param.min || 0}" max="${param.max || 9999}" step="0.1"> </div> `; } } container.innerHTML = html; } async function calculateEstimate(workTypeId) { // Собираем параметры const params = {}; const inputs = document.querySelectorAll('#params-form input, #params-form select'); inputs.forEach(input => { let name = input.name; let value = input.value; if (input.type === 'number') { value = parseFloat(value); } if (value !== '' && value !== null && !isNaN(value)) { params[name] = value; } }); // Если регион не выбран — ставим центр по умолчанию if (!params.region) { params.region = 'center'; } const loading = document.getElementById('loading'); const resultDiv = document.getElementById('result-estimate'); if (loading) loading.style.display = 'block'; if (resultDiv) resultDiv.innerHTML = ''; try { const response = await fetch(BUILDER_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template_id: workTypeId, params: params }) }); const data = await response.json(); if (loading) loading.style.display = 'none'; if (data.success) { renderEstimate(data.estimate); document.getElementById('step-params').style.display = 'none'; document.getElementById('step-result').style.display = 'block'; } else { if (resultDiv) { resultDiv.innerHTML = `<div class="error">Ошибка: ${data.error || 'Неизвестная ошибка'}</div>`; } } } catch (error) { if (loading) loading.style.display = 'none'; if (resultDiv) { resultDiv.innerHTML = `<div class="error">Ошибка соединения: ${error.message}</div>`; } } } function renderEstimate(estimate, templateId, params) { let html = ` <h3>${escapeHtml(estimate.title)}</h3> <p class="estimate-note">💡 Кликните по любой ячейке — можно отредактировать объём или цену. Итог пересчитывается автоматически.</p> <div style="margin-bottom: 1rem; display: flex; gap: 0.75rem; flex-wrap: wrap;"> <button id="print-estimate-btn" class="btn-estimate-primary">🖨 Распечатать смету</button> <button id="export-csv-btn" class="btn-estimate-secondary">📎 Экспорт CSV</button> </div> <table class="estimate-table" id="estimate-table-result"> <thead> <tr> <th>Код ГЭСН</th> <th>Наименование работ</th> <th>Ед.</th> <th>Объём</th> <th>Цена, ₽</th> <th>Сумма, ₽</th> <th style="width: 50px"></th> </tr> </thead> <tbody id="estimate-tbody"> `; if (estimate.items && estimate.items.length) { estimate.items.forEach((item, index) => { html += ` <tr data-index="${index}"> <td>${escapeHtml(item.code)}</td> <td><input type="text" class="edit-work" value="${escapeHtml(item.name)}" data-field="name" style="width: 100%;"></td> <td><input type="text" class="edit-unit" value="${escapeHtml(item.unit)}" data-field="unit" style="width: 80px;"></td> <td><input type="number" class="edit-qty" value="${item.qty}" data-field="qty" step="0.1" style="width: 80px;"></td> <td><input type="number" class="edit-price" value="${item.price}" data-field="price" step="100" style="width: 100px;"></td> <td class="row-total"><strong>${(item.qty * item.price).toLocaleString('ru-RU')}</strong> ₽</td> <td><button class="delete-row-btn" data-index="${index}">✖</button></td> </tr> `; }); } html += ` </tbody> <tfoot> <tr> <td colspan="5"><strong>ИТОГО:</strong></td> <td id="total-sum" colspan="2"><strong>${estimate.total.toLocaleString('ru-RU')}</strong> ₽</td> </tr> <tr> <td colspan="7"> <button id="add-new-row" class="btn-estimate-secondary">+ Добавить работу</button> </td> </tr> </tfoot> </table> `; const resultDiv = document.getElementById('result-estimate'); if (resultDiv) resultDiv.innerHTML = html; // Сохраняем данные для добавления новых строк window.currentEstimate = estimate; window.currentTemplateId = templateId; window.currentParams = params; // Навешиваем обработчики attachEditHandlers(); // Кнопка печати const printBtn = document.getElementById('print-estimate-btn'); if (printBtn) { printBtn.addEventListener('click', function() { printEstimateProfessional(); }); } // Кнопка экспорта CSV const exportBtn = document.getElementById('export-csv-btn'); if (exportBtn) { exportBtn.addEventListener('click', function() { exportToCSV(); }); } } function printEstimateProfessional() { // Собираем данные текущей сметы const estimateData = { title: document.getElementById('estimate-title')?.innerText || 'Смета', description: document.getElementById('estimate-description')?.innerText || '', items: [], total: 0 }; // Собираем строки таблицы const rows = document.querySelectorAll('#estimate-tbody tr'); rows.forEach(row => { const qty = parseFloat(row.querySelector('.edit-qty')?.value || 0); const price = parseFloat(row.querySelector('.edit-price')?.value || 0); const direct = qty * price; estimateData.items.push({ code: row.cells[0]?.innerText || '', name: row.querySelector('.edit-work')?.value || '', unit: row.querySelector('.edit-unit')?.value || '', qty: qty, price: price, direct: direct }); estimateData.total += direct; }); // Открываем окно печати с POST-запросом const form = document.createElement('form'); form.method = 'POST'; form.action = '/construction-estimates/builder/print.php'; form.target = '_blank'; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'data'; input.value = encodeURIComponent(JSON.stringify(estimateData)); form.appendChild(input); document.body.appendChild(form); form.submit(); document.body.removeChild(form); } function exportToCSV() { const rows = document.querySelectorAll('#estimate-tbody tr'); let csvRows = []; // Заголовки csvRows.push(['Код ГЭСН', 'Наименование работ', 'Ед.', 'Объём', 'Цена, ₽', 'Сумма, ₽']); // Данные rows.forEach(row => { const code = row.cells[0]?.innerText || ''; const name = row.querySelector('.edit-work')?.value || ''; const unit = row.querySelector('.edit-unit')?.value || ''; const qty = row.querySelector('.edit-qty')?.value || 0; const price = row.querySelector('.edit-price')?.value || 0; const total = (parseFloat(qty) * parseFloat(price)).toFixed(0); csvRows.push([code, name, unit, qty, price, total]); }); // Итог const totalSum = document.getElementById('total-sum')?.innerText.replace(/[^0-9.-]/g, '') || 0; csvRows.push(['', '', '', '', 'ИТОГО:', totalSum]); // Создаём CSV const csvContent = csvRows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',') ).join('\n'); // Скачиваем const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.href = url; link.setAttribute('download', `smeta_${new Date().toISOString().slice(0, 19)}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } function attachEditHandlers() { // Обработчики изменения полей document.querySelectorAll('#estimate-tbody .edit-qty, #estimate-tbody .edit-price').forEach(input => { input.addEventListener('input', function() { const row = this.closest('tr'); recalcRowTotal(row); }); }); // Обработчики кнопок удаления document.querySelectorAll('.delete-row-btn').forEach(btn => { btn.addEventListener('click', function() { const row = this.closest('tr'); row.remove(); recalcTotalAll(); }); }); // Кнопка добавления новой строки const addBtn = document.getElementById('add-new-row'); if (addBtn) { addBtn.addEventListener('click', addNewRow); } } function recalcRowTotal(row) { const qty = parseFloat(row.querySelector('.edit-qty')?.value || 0); const price = parseFloat(row.querySelector('.edit-price')?.value || 0); const total = qty * price; const totalCell = row.querySelector('.row-total'); if (totalCell) { totalCell.innerHTML = `<strong>${total.toLocaleString('ru-RU')}</strong> ₽`; } recalcTotalAll(); } function recalcTotalAll() { let total = 0; document.querySelectorAll('#estimate-tbody tr').forEach(row => { const totalCell = row.querySelector('.row-total'); if (totalCell) { const val = parseFloat(totalCell.innerText.replace(/[^0-9.-]/g, '')); if (!isNaN(val)) total += val; } }); const totalSpan = document.getElementById('total-sum'); if (totalSpan) { totalSpan.innerHTML = `<strong>${total.toLocaleString('ru-RU')}</strong> ₽`; } } function addNewRow() { const tbody = document.getElementById('estimate-tbody'); const newIndex = tbody.children.length; const newRow = document.createElement('tr'); newRow.setAttribute('data-index', newIndex); newRow.innerHTML = ` <td><input type="text" value="Новая работа" placeholder="Наименование" style="width: 100%;"></td> <td><input type="text" value="шт" placeholder="Ед. изм." style="width: 80px;"></td> <td><input type="number" class="edit-qty" value="1" step="0.1" style="width: 80px;"></td> <td><input type="number" class="edit-price" value="1000" step="100" style="width: 100px;"></td> <td class="row-total"><strong>1000</strong> ₽</td> <td><button class="delete-row-btn">✖</button></td> `; // Навешиваем обработчики newRow.querySelectorAll('.edit-qty, .edit-price').forEach(input => { input.addEventListener('input', function() { recalcRowTotal(newRow); }); }); newRow.querySelector('.delete-row-btn').addEventListener('click', function() { newRow.remove(); recalcTotalAll(); }); tbody.appendChild(newRow); recalcTotalAll(); } function exportToCSV() { const table = document.querySelector('#result-estimate table'); if (!table) return; const rows = table.querySelectorAll('tr'); const csv = []; rows.forEach(row => { const cells = row.querySelectorAll('th, td'); const rowData = Array.from(cells).map(cell => { return '"' + cell.innerText.replace(/"/g, '""') + '"'; }); csv.push(rowData.join(',')); }); const blob = new Blob([csv.join('\n')], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.href = url; link.setAttribute('download', 'smeta.csv'); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }
{ "id": "foundation-strip", "name": "Ленточный фундамент", "description": "Расчёт сметы на устройство ленточного фундамента", "params": [ { "name": "perimeter", "label": "Периметр стен, м", "type": "number", "default": 40, "min": 10, "max": 200 }, { "name": "depth", "label": "Глубина заложения, м", "type": "number", "default": 1.5, "min": 0.5, "max": 3 }, { "name": "width", "label": "Ширина ленты, м", "type": "number", "default": 0.4, "min": 0.3, "max": 0.8 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 06-01-001-5", "name": "Устройство бетонной подготовки", "unit": "м³", "formula": "perimeter * width * 0.1", "price_fer": 5800, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-015-1", "name": "Устройство ленточного фундамента (бетон В15)", "unit": "м³", "formula": "perimeter * width * depth", "price_fer": 6200, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-041-1", "name": "Армирование фундамента (А500С)", "unit": "кг", "formula": "perimeter * width * depth * 50", "price_fer": 68, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-087-1", "name": "Опалубка щитовая", "unit": "м²", "formula": "perimeter * depth * 2", "price_fer": 450, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 01-01-001-1", "name": "Разработка грунта экскаватором", "unit": "м³", "formula": "perimeter * (width + 0.5) * depth", "price_fer": 680, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 01-01-035-1", "name": "Обратная засыпка грунтом", "unit": "м³", "formula": "perimeter * width * depth * 0.3", "price_fer": 420, "hr": 0.95, "sp": 0.55 } ] }
/* ===== КОНСТРУКТОР СМЕТ - СТИЛИ ===== */ .builder-container { max-width: 1400px; margin: 0 auto; padding: 2rem; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; } /* === Шаг 1: Выбор типа работ === */ .worktype-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.0rem; margin: 2rem 0; } .worktype-card { border: 0px solid #e5e7eb; border-radius: 4px; padding: 1rem 1.5rem; background: #ffffff; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.05); cursor: pointer; } .worktype-card:hover { border-color: #92a7f1; /*box-shadow: 0 10px 25px -5px rgba(0,0,0,0.1);*/ transform: translateY(-2px); } .worktype-card h3 { font-size: 1.25rem; font-weight: 600; margin: 0 0 0.5rem 0; color: #1f2937; } .worktype-card p { font-size: 0.875rem; color: #6b7280; margin: 0 0 1rem 0; line-height: 1.5; } .btn-select-worktype { background: #2563eb; color: white; border: none; padding: 0.5rem 1.25rem; border-radius: 6px; cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: background 0.2s; } .btn-select-worktype:hover { background: #1d4ed8; } /* === Шаг 2: Параметры === */ #params-form { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 4px; padding: 1.5rem; margin: 1.5rem 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.25rem; } .param-field { display: flex; flex-direction: column; gap: 0.5rem; } .param-field label { font-weight: 600; font-size: 0.875rem; color: #374151; } .param-field input, .param-field select { padding: 0.625rem 0.875rem; border: 1px solid #d1d5db; border-radius: 8px; font-size: 0.9375rem; font-family: inherit; background: white; transition: border-color 0.2s, box-shadow 0.2s; } .param-field input:focus, .param-field select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); } /* === Кнопки === */ .btn-primary, .btn-secondary, .btn-estimate-primary, .btn-estimate-secondary { padding: 0.625rem 1.5rem; font-size: 0.875rem; font-weight: 500; border-radius: 8px; cursor: pointer; transition: all 0.2s; border: none; } .btn-primary, .btn-estimate-primary { background: #2563eb; color: white; } .btn-primary:hover, .btn-estimate-primary:hover { background: #1d4ed8; } .btn-secondary, .btn-estimate-secondary { background: #f3f4f6; color: #1f2937; border: 1px solid #e5e7eb; } .btn-secondary:hover, .btn-estimate-secondary:hover { background: #e5e7eb; } .btn-estimate-danger { background: #ef4444; color: white; } .btn-estimate-danger:hover { background: #dc2626; } .result-actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; flex-wrap: wrap; } /* === Секции === */ .builder-section { margin-bottom: 2rem; } .builder-section h2 { font-size: 1.5rem; font-weight: 600; color: #111827; margin: 0 0 1rem 0; padding-bottom: 0.5rem; border-bottom: 2px solid #e5e7eb; } /* === Таблица сметы === */ .estimate-table-wrapper { overflow-x: auto; margin: 1.5rem 0; border-radius: 4px; border: 1px solid #e5e7eb; background: white; } .estimate-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; } .estimate-table th { background: #f8fafc; padding: 0.875rem 1rem; text-align: left; font-weight: 600; color: #1e293b; border-bottom: 1px solid #e2e8f0; } .estimate-table td { padding: 0.75rem 1rem; border-bottom: 1px solid #f1f5f9; vertical-align: middle; } .estimate-table tbody tr:hover { background: #fefce8; } /* === Редактируемые поля === */ .estimate-table input { width: 100%; padding: 0.4rem 0.5rem; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 0.813rem; font-family: inherit; background: white; transition: all 0.15s; } .estimate-table input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.2); } .edit-work { min-width: 200px; } .edit-unit { width: 70px; text-align: center; } .edit-qty, .edit-price { width: 100px; text-align: right; } /* === Сумма строки === */ .row-total { font-weight: 600; color: #059669; text-align: right; } /* === Кнопка удаления строки === */ .delete-row-btn { background: none; border: none; font-size: 1.1rem; cursor: pointer; color: #9ca3af; transition: color 0.15s; padding: 0.25rem 0.5rem; } .delete-row-btn:hover { color: #ef4444; } /* === Итоговая строка === */ .estimate-table tfoot td { background: #f8fafc; font-weight: 600; padding: 1rem; border-top: 2px solid #e2e8f0; } #total-sum { font-size: 1.1rem; color: #059669; } /* === Блок действий === */ .estimate-actions { display: flex; gap: 0.75rem; margin: 1rem 0; flex-wrap: wrap; } /* === Примечание === */ .estimate-note { margin-top: 1rem; padding: 0.75rem 1rem; background: #f0fdf4; border-left: 4px solid #10b981; border-radius: 8px; font-size: 0.813rem; color: #047857; } /* === Ошибки и загрузка === */ .error { color: #dc2626; background: #fef2f2; padding: 1rem; border-radius: 8px; border-left: 4px solid #dc2626; } #loading { text-align: center; padding: 2rem; font-size: 1rem; color: #2563eb; } #loading i { margin-right: 0.5rem; } /* === Адаптив для мобильных === */ @media (max-width: 768px) { .builder-container { padding: 1rem; } .worktype-grid { grid-template-columns: 1fr; } .estimate-table th, .estimate-table td { padding: 0.5rem; } .edit-work { min-width: 150px; } .param-field input, .param-field select { font-size: 16px; /* Предотвращает зум на iOS */ } } /* === Печатная версия === */ @media print { .worktype-grid, .estimate-selector, .estimate-actions, .result-actions, .delete-row-btn, #add-new-row, button, .btn-primary, .btn-secondary, .btn-estimate-primary, .btn-estimate-secondary, #btn-back, #btn-print, #btn-export, .estimate-note { display: none !important; } .estimate-table input { border: none; background: transparent; padding: 0; } .estimate-table { border: 1px solid #000; } .estimate-table th, .estimate-table td { border: 1px solid #ccc; } } /* ===== СОВРЕМЕННАЯ ПЕЧАТНАЯ ВЕРСИЯ ===== */ /* Скрываем всё лишнее при печати */ @media print { /* Скрываем попап cookie */ .cookie-notice, .cookie-popup, .cookie-consent, [class*="cookie"], [id*="cookie"] { display: none !important; } /* Скрываем поиск */ .srh-form-main, .search-form, [class*="search"], [id*="search"] { display: none !important; } /* Скрываем хлебные крошки */ .shell-breadcrumb, .breadcrumb, [class*="breadcrumb"] { display: none !important; } /* Скрываем шапку и футер */ .shell-header, .shell-footer, header, footer, [class*="header"]:not(.print-header), [class*="footer"] { display: none !important; } /* Скрываем фильтры, статистику, навигацию */ .categories-filter-tabs, .categories-hub-stats, .quick-nav, .hub-stats, .filter-tabs { display: none !important; } /* Скрываем рекламу */ .ad-block, #ad-block-top, #ad-block-bottom, [class*="ad-"] { display: none !important; } /* Скрываем кнопки и формы */ button, .btn, .btn-primary, .btn-secondary, .estimate-selector, .estimate-actions, .result-actions, #add-new-row, .delete-row-btn { display: none !important; } /* Скрываем всё лишнее в конструкторе */ .worktype-grid, .builder-section:first-child, .builder-section:nth-child(2) { display: none !important; } /* Показываем только результат */ #step-result { display: block !important; } /* Сбрасываем фон и отступы */ body { background: white !important; margin: 0 !important; padding: 0 !important; } .builder-container { padding: 0 !important; margin: 0 !important; } } @media print { /* Скрываем всё лишнее */ .worktype-grid, .estimate-selector, .estimate-actions, .result-actions, .builder-section:first-child, .builder-section:nth-child(2), .delete-row-btn, #add-new-row, button, .btn-primary, .btn-secondary, .btn-estimate-primary, .btn-estimate-secondary, #btn-back, #btn-print, #btn-export, .estimate-note, .builder-container > .estimate-selector, .builder-container > .estimate-actions, .builder-container > .result-actions, .builder-container > .estimate-note, header, footer, nav, .shell-breadcrumb, .shell-header, .shell-footer, .categories-filter-tabs, .categories-hub-stats, .quick-nav, .search-form, .srh-form-main, .ad-block, #ad-block-top, #ad-block-bottom { display: none !important; } /* Основной контейнер */ .builder-container { padding: 0 !important; margin: 0 !important; max-width: 100% !important; } /* Заголовок сметы */ #estimate-header { text-align: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 2px solid #000; } #estimate-title { font-size: 20pt; font-weight: bold; margin: 0 0 5px 0; color: #000; } #estimate-description { font-size: 11pt; color: #555; margin: 0; } /* Таблица */ .estimate-table-wrapper { overflow: visible !important; border: none !important; margin: 0 !important; } .estimate-table { width: 100%; border-collapse: collapse; font-size: 9pt; page-break-inside: avoid; } .estimate-table th { background: #f0f0f0 !important; padding: 8px 6px; border: 1px solid #000; font-weight: bold; text-align: left; } .estimate-table td { padding: 6px; border: 1px solid #999; vertical-align: top; } /* Заменяем input поля на текст */ .estimate-table input { border: none !important; background: transparent !important; padding: 0 !important; margin: 0 !important; width: auto !important; font-family: inherit !important; font-size: inherit !important; font-weight: inherit !important; color: #000 !important; box-shadow: none !important; } /* Скрываем кнопки в таблице */ .delete-row-btn { display: none !important; } /* Итоговая строка */ .estimate-table tfoot td { background: #f8f8f8 !important; font-weight: bold; border-top: 2px solid #000; } #total-sum { font-size: 12pt; color: #000; } /* Блок "Результаты" */ #step-result { display: block !important; } /* Принудительно показываем результат */ .builder-section { display: block !important; } /* Страница */ body { background: white !important; color: black !important; } /* Ссылки */ a { text-decoration: none !important; color: #000 !important; } }
{ "id": "facade-ventilated", "name": "Вентилируемый фасад", "description": "Смета на навесной вентилируемый фасад", "params": [ { "name": "area", "label": "Площадь фасада, м²", "type": "number", "default": 150, "min": 50, "max": 500 }, { "name": "thickness", "label": "Толщина утеплителя, мм", "type": "number", "default": 100, "min": 50, "max": 150 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 11-03-001-1", "name": "Утеплитель (минвата)", "unit": "м²", "formula": "area", "price_fer": 450, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-03-002-1", "name": "Крепление утеплителя", "unit": "м²", "formula": "area", "price_fer": 220, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-03-003-1", "name": "Гидро-ветрозащита", "unit": "м²", "formula": "area", "price_fer": 70, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-03-004-1", "name": "Обрешётка (профиль/брус)", "unit": "м²", "formula": "area", "price_fer": 280, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-03-005-1", "name": "Кронштейны", "unit": "шт", "formula": "area / 0.5", "price_fer": 45, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-03-006-1", "name": "Облицовка (керамогранит/профлист)", "unit": "м²", "formula": "area", "price_fer": 850, "hr": 0.95, "sp": 0.55 } ] }
{ "id": "facade-wet", "name": "Мокрый фасад", "description": "Смета на утепление фасада с декоративной штукатуркой", "params": [ { "name": "area", "label": "Площадь фасада, м²", "type": "number", "default": 150, "min": 50, "max": 500 }, { "name": "thickness", "label": "Толщина утеплителя, мм", "type": "number", "default": 100, "min": 50, "max": 150 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 11-02-001-1", "name": "Утеплитель (минвата/ЭППС)", "unit": "м²", "formula": "area", "price_fer": 450, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-02-002-1", "name": "Крепление утеплителя (клей+дюбели)", "unit": "м²", "formula": "area", "price_fer": 280, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-02-003-1", "name": "Армирование сеткой", "unit": "м²", "formula": "area", "price_fer": 180, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-02-004-1", "name": "Грунтовка", "unit": "м²", "formula": "area", "price_fer": 60, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-02-005-1", "name": "Декоративная штукатурка", "unit": "м²", "formula": "area", "price_fer": 350, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-02-006-1", "name": "Откосы и примыкания", "unit": "м", "formula": "Math.sqrt(area) * 4", "price_fer": 200, "hr": 0.95, "sp": 0.55 } ] }
{ "id": "foundation-pile", "name": "Свайный фундамент (винтовые сваи)", "description": "Смета на устройство винтовых свай с ростверком", "params": [ { "name": "perimeter", "label": "Периметр стен, м", "type": "number", "default": 40, "min": 20, "max": 150 }, { "name": "pile_step", "label": "Шаг свай, м", "type": "number", "default": 2.5, "min": 2, "max": 3 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 05-01-002-1", "name": "Винтовые сваи СВЛ-108 мм", "unit": "шт", "formula": "perimeter / pile_step", "price_fer": 3500, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 05-01-003-1", "name": "Оголовки свай", "unit": "шт", "formula": "perimeter / pile_step", "price_fer": 500, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-087-1", "name": "Ростверк (швеллер/двутавр)", "unit": "м", "formula": "perimeter", "price_fer": 1200, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-015-1", "name": "Бетонирование обвязки", "unit": "м³", "formula": "perimeter * 0.3 * 0.3", "price_fer": 5800, "hr": 0.95, "sp": 0.55 } ] }
{ "id": "foundation-slab", "name": "Плитный фундамент", "description": "Смета на устройство монолитной плиты", "params": [ { "name": "area", "label": "Площадь дома, м²", "type": "number", "default": 100, "min": 30, "max": 300 }, { "name": "thickness", "label": "Толщина плиты, м", "type": "number", "default": 0.3, "min": 0.2, "max": 0.5 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 06-01-001-5", "name": "Бетонная подготовка (В7,5)", "unit": "м³", "formula": "area * 0.1", "price_fer": 5200, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-015-2", "name": "Плита фундаментная (бетон В15)", "unit": "м³", "formula": "area * thickness", "price_fer": 6200, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-041-2", "name": "Армирование плиты (А500С, сетка 200х200)", "unit": "кг", "formula": "area * thickness * 45", "price_fer": 68, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-087-2", "name": "Опалубка по периметру", "unit": "м²", "formula": "Math.sqrt(area) * 4 * 0.3", "price_fer": 450, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 01-01-001-1", "name": "Разработка грунта", "unit": "м³", "formula": "area * 0.5", "price_fer": 680, "hr": 0.95, "sp": 0.55 } ] }
{ "id": "foundation-strip", "name": "Ленточный фундамент", "description": "Смета на устройство ленточного фундамента", "params": [ { "name": "perimeter", "label": "Периметр стен, м", "type": "number", "default": 40, "min": 10, "max": 200 }, { "name": "depth", "label": "Глубина заложения, м", "type": "number", "default": 1.5, "min": 0.5, "max": 3 }, { "name": "width", "label": "Ширина ленты, м", "type": "number", "default": 0.4, "min": 0.3, "max": 0.8 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 06-01-001-5", "name": "Устройство бетонной подготовки", "unit": "м³", "formula": "perimeter * width * 0.1", "price_fer": 5800, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-015-1", "name": "Устройство ленточного фундамента (бетон В15)", "unit": "м³", "formula": "perimeter * width * depth", "price_fer": 6200, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-041-1", "name": "Армирование фундамента (А500С)", "unit": "кг", "formula": "perimeter * width * depth * 50", "price_fer": 68, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 06-01-087-1", "name": "Опалубка щитовая", "unit": "м²", "formula": "perimeter * depth * 2", "price_fer": 450, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 01-01-001-1", "name": "Разработка грунта экскаватором", "unit": "м³", "formula": "perimeter * (width + 0.5) * depth", "price_fer": 680, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 01-01-035-1", "name": "Обратная засыпка грунтом", "unit": "м³", "formula": "perimeter * width * depth * 0.3", "price_fer": 420, "hr": 0.95, "sp": 0.55 } ] }
{ "id": "heating-gas", "name": "Газовое отопление", "description": "Смета на монтаж газового котла и разводку отопления", "params": [ { "name": "area", "label": "Площадь дома, м²", "type": "number", "default": 100, "min": 50, "max": 300 }, { "name": "radiators_qty", "label": "Количество радиаторов, шт", "type": "number", "default": 8, "min": 4, "max": 20 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 16-01-001-1", "name": "Газовый котёл (24 кВт)", "unit": "шт", "formula": "1", "price_fer": 45000, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 16-01-002-1", "name": "Разводка труб (ПП, металлопласт)", "unit": "м", "formula": "area * 0.8", "price_fer": 350, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 16-01-003-1", "name": "Радиаторы отопления (биметалл)", "unit": "шт", "formula": "radiators_qty", "price_fer": 4500, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 16-01-004-1", "name": "Расширительный бак", "unit": "шт", "formula": "1", "price_fer": 2500, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 16-01-005-1", "name": "Циркуляционный насос", "unit": "шт", "formula": "1", "price_fer": 6000, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 16-01-006-1", "name": "Запорная арматура и фитинги", "unit": "компл", "formula": "1", "price_fer": 8000, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 16-01-007-1", "name": "Пусконаладочные работы", "unit": "компл", "formula": "1", "price_fer": 12000, "hr": 0.95, "sp": 0.55 } ] }
{ "id": "interior-cosmetic", "name": "Косметический ремонт квартиры", "description": "Смета на косметический ремонт квартиры (стены + полы + потолки)", "params": [ { "name": "area_floor", "label": "Площадь пола, м²", "type": "number", "default": 60, "min": 20, "max": 200 }, { "name": "area_walls", "label": "Площадь стен, м²", "type": "number", "default": 180, "min": 50, "max": 500 }, { "name": "room_height", "label": "Высота потолков, м", "type": "number", "default": 2.7, "min": 2.5, "max": 3.2 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 11-01-001-1", "name": "Демонтаж старых покрытий", "unit": "м²", "formula": "area_floor * 1.5 + area_walls", "price_fer": 300, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-010-1", "name": "Выравнивание стен (штукатурка)", "unit": "м²", "formula": "area_walls", "price_fer": 550, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-015-1", "name": "Шпаклёвка стен", "unit": "м²", "formula": "area_walls", "price_fer": 200, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-020-1", "name": "Покраска стен", "unit": "м²", "formula": "area_walls", "price_fer": 250, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-025-1", "name": "Стяжка пола (пескобетон)", "unit": "м²", "formula": "area_floor * 1.05", "price_fer": 650, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-030-1", "name": "Укладка ламината (32 класс)", "unit": "м²", "formula": "area_floor * 1.05", "price_fer": 800, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-035-1", "name": "Плинтус с установкой", "unit": "м", "formula": "Math.sqrt(area_floor) * 8 * 1.1", "price_fer": 250, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-040-1", "name": "Натяжной потолок (ПВХ)", "unit": "м²", "formula": "area_floor * 1.05", "price_fer": 550, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 11-01-045-1", "name": "Вывоз мусора", "unit": "компл", "formula": "1", "price_fer": 12000, "hr": 0.95, "sp": 0.55 } ] }
{ "id": "roofing-metal", "name": "Кровля из металлочерепицы", "description": "Смета на устройство двускатной кровли из металлочерепицы", "params": [ { "name": "area", "label": "Площадь дома, м²", "type": "number", "default": 100, "min": 30, "max": 300 }, { "name": "roof_angle", "label": "Угол ската, градусы", "type": "number", "default": 30, "min": 15, "max": 60 }, { "name": "region", "label": "Регион", "type": "select", "options": "regions" } ], "works": [ { "code": "ГЭСН 10-01-002-1", "name": "Стропильная система (доска 150×50)", "unit": "м²", "formula": "area * 1.2", "price_fer": 850, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 10-01-005-1", "name": "Обрешётка под металлочерепицу (доска 25×100)", "unit": "м²", "formula": "area * 1.15", "price_fer": 250, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 10-01-010-1", "name": "Металлочерепица (0,5 мм) с доборными элементами", "unit": "м²", "formula": "area * 1.15", "price_fer": 750, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 10-01-020-1", "name": "Пароизоляция", "unit": "м²", "formula": "area * 1.15", "price_fer": 50, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 10-01-025-1", "name": "Гидро-ветрозащита", "unit": "м²", "formula": "area * 1.15", "price_fer": 70, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 10-01-030-1", "name": "Утеплитель 150 мм (минвата)", "unit": "м²", "formula": "area * 1.15", "price_fer": 400, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 10-01-040-1", "name": "Водосточная система", "unit": "м", "formula": "Math.sqrt(area) * 4 * 1.2", "price_fer": 850, "hr": 0.95, "sp": 0.55 }, { "code": "ГЭСН 10-01-045-1", "name": "Снегозадержатели", "unit": "шт", "formula": "Math.sqrt(area) * 0.8", "price_fer": 1200, "hr": 0.95, "sp": 0.55 } ] }
<?php /** * Конструктор смет - профессиональный инструмент на базе ГЭСН и ФЕР * /construction-estimates/builder/index.php */ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://'; $host = $_SERVER['HTTP_HOST']; $uri = $_SERVER['REQUEST_URI']; $canonicalUrl = $protocol . $host . $uri; $siteUrl = $protocol . $host; $pageTitle = 'Конструктор смет на основе ГЭСН и ФЕР | ТЕХСТАНДАРТ'; $description = 'Профессиональный онлайн-конструктор строительных смет. Выберите тип работ, укажите параметры объекта, получите детальную смету с расчётом по ГЭСН и ФЕР. Для ИЖС, ремонта и малого бизнеса.'; $h1 = 'Конструктор смет'; // Загружаем список доступных типов работ из папки templates/ $templatesDir = __DIR__ . '/data/templates/'; $templateFiles = glob($templatesDir . '*.json'); $workTypes = []; if ($templateFiles) { foreach ($templateFiles as $file) { $content = file_get_contents($file); $data = json_decode($content, true); if ($data) { $workTypes[] = [ 'id' => basename($file, '.json'), 'name' => $data['name'] ?? basename($file, '.json'), 'description' => $data['description'] ?? '' ]; } } } // Загружаем список регионов $regionsFile = __DIR__ . '/data/regions.json'; $regions = []; if (file_exists($regionsFile)) { $regionsData = json_decode(file_get_contents($regionsFile), true); $regions = $regionsData['regions'] ?? []; } ob_start(); ?> <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Конструктор смет - ТЕХСТАНДАРТ</title> <link rel="stylesheet" href="/assets/css/main.min.css"> <link rel="stylesheet" href="/assets/css/bootstrap-icons.css"> <link rel="stylesheet" href="/construction-estimates/builder/data/style.css"> </head> <body> <div class="builder-container"> <!-- Шаг 1: Выбор типа работ --> <div class="builder-section" id="step-worktype"> <h2>1. Выберите тип работ</h2> <div class="worktype-grid"> <?php foreach ($workTypes as $type): ?> <div class="worktype-card" data-worktype="<?= htmlspecialchars($type['id']) ?>"> <h3><?= htmlspecialchars($type['name']) ?></h3> <p><?= htmlspecialchars($type['description']) ?></p> <button class="btn-select-worktype">Выбрать</button> </div> <?php endforeach; ?> </div> </div> <!-- Шаг 2: Параметры объекта --> <div class="builder-section" id="step-params" style="display: none;"> <h2>2. Параметры объекта</h2> <div id="params-form"></div> <button id="btn-next-to-calc" class="btn-primary">Рассчитать смету →</button> </div> <!-- Шаг 3: Результат (смета) --> <div class="builder-section" id="step-result" style="display: none;"> <h2>3. Смета</h2> <div id="result-estimate"></div> </div> <div id="loading" style="display: none; text-align: center; padding: 2rem;"> <i class="bi bi-hourglass-split"></i> Расчёт... </div> </div> <script src="data/builder.js"></script> </body> </html> <?php $htmlContent = ob_get_clean(); // JSON-LD микроразметка $jsonLd = [ [ '@context' => 'https://schema.org', '@type' => 'WebPage', '@id' => $canonicalUrl, 'name' => $pageTitle, 'description' => $description, 'url' => $canonicalUrl, 'inLanguage' => 'ru-RU', 'dateModified' => date('Y-m-d'), ], [ '@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => [ ['@type' => 'ListItem', 'position' => 1, 'name' => 'Главная', 'item' => $siteUrl . '/'], ['@type' => 'ListItem', 'position' => 2, 'name' => 'Строительные сметы', 'item' => $siteUrl . '/construction-estimates/'], ['@type' => 'ListItem', 'position' => 3, 'name' => 'Конструктор смет', 'item' => $canonicalUrl] ] ], [ '@context' => 'https://schema.org', '@type' => 'SoftwareApplication', 'name' => $pageTitle, 'description' => $description, 'applicationCategory' => 'BusinessApplication', 'operatingSystem' => 'Web', 'offers' => [ '@type' => 'Offer', 'price' => '0', 'priceCurrency' => 'RUB' ] ] ]; $pageData = [ 'title' => $pageTitle, 'description' => $description, 'h1' => $h1, 'content' => $htmlContent, 'breadcrumb' => [ ['name' => 'Главная', 'url' => '/'], ['name' => 'Строительные сметы', 'url' => '/construction-estimates/'], ['name' => 'Конструктор смет', 'url' => ''] ], 'template' => 'default', 'canonical_url' => $canonicalUrl, 'robots' => 'index, follow', 'keywords' => 'конструктор смет, смета онлайн, ГЭСН, ФЕР, строительная смета, калькулятор сметы', 'og_title' => $pageTitle, 'og_description' => $description, 'og_image' => 'https://tech-stc.ru/assets/images/og/estimate-builder-og.jpg', 'og_type' => 'website', 'json_ld' => $jsonLd ]; require_once $_SERVER['DOCUMENT_ROOT'] . '/shell/render.php'; renderModule($pageData);
<?php /** * Печатная форма сметы * /construction-estimates/builder/print.php */ $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://'; $host = $_SERVER['HTTP_HOST']; $siteUrl = $protocol . $host; // Получаем данные $data = $_POST['data'] ?? $_GET['data'] ?? null; if ($data) { $estimate = json_decode(urldecode($data), true); } else { // Демо-данные $estimate = [ 'title' => 'Ленточный фундамент 10×10 м', 'description' => 'Смета на устройство ленточного фундамента', 'items' => [ ['code' => 'ГЭСН 06-01-001-5', 'name' => 'Устройство бетонной подготовки', 'unit' => 'м³', 'qty' => 1.6, 'price' => 5800, 'direct' => 9280], ['code' => 'ГЭСН 06-01-015-1', 'name' => 'Устройство ленточного фундамента', 'unit' => 'м³', 'qty' => 24, 'price' => 6200, 'direct' => 148800], ], 'total' => 158080 ]; } ?> <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title><?= htmlspecialchars($estimate['title']) ?> | ТЕХСТАНДАРТ</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Times New Roman', Times, serif; background: white; color: #000; font-size: 12px; line-height: 1.4; } /* Контейнер */ .print-container { max-width: 1200px; margin: 0 auto; padding: 20px; } /* ===== ШАПКА ДОКУМЕНТА ===== */ .document-header { margin-bottom: 20px; border-bottom: 2px solid #000; padding-bottom: 15px; } /* Верхняя строка с логотипом и реквизитами */ .header-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; } /* Левая часть - логотип */ .header-left { flex: 1; } .logo { font-size: 20px; font-weight: bold; color: #1a3a6b; letter-spacing: 1px; } .logo-sub { font-size: 10px; color: #666; margin-top: 3px; } /* Правая часть - реквизиты */ .header-right { text-align: right; font-size: 9px; line-height: 1.3; } .header-right p { margin: 2px 0; } /* Средняя строка - название документа */ .doc-title { text-align: center; margin: 15px 0; } .doc-title h1 { font-size: 18px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 5px; } .doc-title h2 { font-size: 14px; font-weight: normal; margin-bottom: 5px; } .doc-title p { font-size: 11px; color: #555; } /* Информация о смете */ .estimate-info { display: flex; justify-content: space-between; margin: 15px 0; padding: 10px; border: 1px solid #ccc; background: #f9f9f9; } .estimate-info-item { font-size: 10px; } .estimate-info-item strong { font-weight: bold; } /* ===== ТАБЛИЦА ===== */ .estimate-table { width: 100%; border-collapse: collapse; margin: 15px 0; } .estimate-table th { background: #e8e8e8; border: 1px solid #000; padding: 8px 6px; text-align: center; font-weight: bold; font-size: 10px; } .estimate-table td { border: 1px solid #999; padding: 6px; vertical-align: top; font-size: 10px; } .estimate-table td:first-child, .estimate-table th:first-child { text-align: left; } .text-right { text-align: right; } /* ===== ИТОГИ ===== */ .total-row { background: #f0f0f0; } .total-row td { border-top: 2px solid #000; font-weight: bold; font-size: 11px; } .total-breakdown { margin-top: 10px; padding: 10px; border: 1px solid #ccc; background: #f9f9f9; font-size: 10px; } .total-breakdown p { margin: 3px 0; } /* ===== ПОДВАЛ ===== */ .document-footer { margin-top: 30px; padding-top: 15px; border-top: 1px solid #ccc; font-size: 8px; text-align: center; color: #666; } .signatures { display: flex; justify-content: space-between; margin: 30px 0 20px; } .signature { width: 45%; } .signature-line { border-top: 1px solid #000; margin-top: 35px; padding-top: 5px; font-size: 9px; } /* Кнопки */ .no-print { text-align: center; margin-top: 30px; padding: 20px; } .no-print button { padding: 10px 20px; margin: 0 10px; font-size: 14px; cursor: pointer; background: #2563eb; color: white; border: none; border-radius: 6px; } .no-print button:hover { background: #1d4ed8; } /* ===== ПЕЧАТЬ ===== */ @media print { body { margin: 0; padding: 0; } .print-container { padding: 0; } .no-print { display: none; } .estimate-table th, .estimate-table td { border-color: #000; } @page { margin: 2cm; } } </style> </head> <body> <div class="print-container"> <!-- ==================== ШАПКА ДОКУМЕНТА ==================== --> <div class="document-header"> <div class="header-top"> <div class="header-left"> <div class="logo">ТЕХСТАНДАРТ</div> <div class="logo-sub">Инженерные расчёты и сметная документация</div> </div> <div class="header-right"> <p>ИНН 1234567890 / КПП 123456789</p> <p>ОГРН 1234567890123</p> <p>г. Москва, ул. Строителей, д. 10</p> <p>Тел: +7 (495) 123-45-67</p> <p>E-mail: info@tech-stc.ru</p> </div> </div> </div> <!-- ==================== НАЗВАНИЕ ДОКУМЕНТА ==================== --> <div class="doc-title"> <h1>ЛОКАЛЬНАЯ СМЕТА</h1> <h2><?= htmlspecialchars($estimate['title']) ?></h2> <p><?= htmlspecialchars($estimate['description']) ?></p> </div> <!-- ==================== ИНФОРМАЦИЯ О СМЕТЕ ==================== --> <div class="estimate-info"> <div class="estimate-info-item"><strong>№ сметы:</strong> 01-<?= date('m-y') ?></div> <div class="estimate-info-item"><strong>Дата составления:</strong> <?= date('d.m.Y') ?></div> <div class="estimate-info-item"><strong>Основание:</strong> Техническое задание</div> <div class="estimate-info-item"><strong>Объект:</strong> Частный жилой дом</div> </div> <!-- ==================== ТАБЛИЦА ==================== --> <table class="estimate-table" id="estimate-table"> <thead> <tr> <th style="width: 10%">Код ГЭСН</th> <th style="width: 35%">Наименование работ и затрат</th> <th style="width: 8%">Ед. изм.</th> <th style="width: 8%">Объём</th> <th style="width: 12%">Цена, ₽</th> <th style="width: 12%">Стоимость, ₽</th> </tr> </thead> <tbody id="estimate-rows"> <?php if (!empty($estimate['items'])): ?> <?php foreach ($estimate['items'] as $item): ?> <tr> <td><?= htmlspecialchars($item['code']) ?></td> <td><?= htmlspecialchars($item['name']) ?></td> <td class="text-right"><?= htmlspecialchars($item['unit']) ?></td> <td class="text-right"><?= number_format($item['qty'], 2, ',', ' ') ?></td> <td class="text-right"><?= number_format($item['price'], 0, ',', ' ') ?></td> <td class="text-right"><?= number_format($item['direct'], 0, ',', ' ') ?></td> </tr> <?php endforeach; ?> <?php else: ?> <tr><td colspan="6" style="text-align: center;">Нет данных</td></tr> <?php endif; ?> </tbody> <tfoot> <tr class="total-row"> <td colspan="5" style="text-align: right; font-weight: bold;">ИТОГО ПО СМЕТЕ:</td> <td class="text-right" style="font-weight: bold;"><?= number_format($estimate['total'], 0, ',', ' ') ?> ₽</td> </tr> </tfoot> </table> <!-- ==================== РАСШИФРОВКА ==================== --> <div class="total-breakdown"> <p><strong>В том числе:</strong></p> <p>— Материалы и оборудование: <?= number_format($estimate['total'] * 0.65, 0, ',', ' ') ?> ₽</p> <p>— Работы (ФОТ + механизмы): <?= number_format($estimate['total'] * 0.35, 0, ',', ' ') ?> ₽</p> <p>— НДС не облагается (организация применяет УСН)</p> </div> <!-- ==================== НОРМАТИВНАЯ БАЗА ==================== --> <div class="total-breakdown" style="margin-top: 10px;"> <p><strong>Нормативная база:</strong> ГЭСН-2024, ФЕР-2024 (с индексами на <?= date('Y') ?> год)</p> <p><strong>Нормативные документы:</strong> СП 22.13330.2016, СП 63.13330.2018, СП 60.13330.2020</p> </div> <!-- ==================== ПОДВАЛ И ПОДПИСИ ==================== --> <div class="signatures"> <div class="signature"> <div class="signature-line">Смету составил: __________________ /__________/</div> </div> <div class="signature"> <div class="signature-line">Заказчик: __________________ /__________/</div> </div> </div> <div class="document-footer"> <p>Расчёт носит предварительный характер и требует утверждения заказчиком</p> <p>© <?= date('Y') ?> ТЕХСТАНДАРТ — инженерные расчёты и сметная документация</p> </div> <!-- ==================== КНОПКИ ==================== --> <div class="no-print"> <button onclick="window.print();">🖨 Распечатать смету</button> <button onclick="window.close();">✖ Закрыть окно</button> </div> </div> </body> </html>