From 297ba39c4f82e0a704bcdd2d6b9945d7d6ed4235 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Mon, 9 Mar 2026 23:50:11 +0200 Subject: [PATCH] Add CuituNet Intra customer management CMS Password-protected intranet for managing fiber internet customers: - Customer table (company, address, speed, price) - Click row to view full details (contact & billing info) - Add, edit, delete customers - Search and sortable columns - Total billing summary - PHP + vanilla JS + JSON storage Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + api.php | 153 ++++++++++++++++ data/.gitkeep | 0 index.html | 160 +++++++++++++++++ script.js | 337 +++++++++++++++++++++++++++++++++++ server.py | 21 +++ style.css | 474 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1146 insertions(+) create mode 100644 .gitignore create mode 100644 api.php create mode 100644 data/.gitkeep create mode 100644 index.html create mode 100644 script.js create mode 100644 server.py create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe48ed2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data/customers.json diff --git a/api.php b/api.php new file mode 100644 index 0000000..42107cc --- /dev/null +++ b/api.php @@ -0,0 +1,153 @@ + 'Kirjaudu sisään']); + exit; + } +} + +function loadCustomers(): array { + $data = file_get_contents(DATA_FILE); + return json_decode($data, true) ?: []; +} + +function saveCustomers(array $customers): void { + file_put_contents(DATA_FILE, json_encode($customers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); +} + +function generateId(): string { + return bin2hex(random_bytes(8)); +} + +switch ($action) { + case 'login': + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $password = $input['password'] ?? ''; + if ($password === ADMIN_PASSWORD) { + $_SESSION['authenticated'] = true; + echo json_encode(['success' => true]); + } else { + http_response_code(401); + echo json_encode(['error' => 'Väärä salasana']); + } + break; + + case 'logout': + session_destroy(); + echo json_encode(['success' => true]); + break; + + case 'check_auth': + echo json_encode(['authenticated' => isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true]); + break; + + case 'customers': + requireAuth(); + if ($method === 'GET') { + $customers = loadCustomers(); + echo json_encode($customers); + } + break; + + case 'customer': + requireAuth(); + if ($method === 'POST') { + $input = json_decode(file_get_contents('php://input'), true); + $customers = loadCustomers(); + $customer = [ + 'id' => generateId(), + 'yritys' => trim($input['yritys'] ?? ''), + 'asennusosoite' => trim($input['asennusosoite'] ?? ''), + 'liittymanopeus' => trim($input['liittymanopeus'] ?? ''), + 'hinta' => floatval($input['hinta'] ?? 0), + 'yhteyshenkilö' => trim($input['yhteyshenkilö'] ?? ''), + 'puhelin' => trim($input['puhelin'] ?? ''), + 'sahkoposti' => trim($input['sahkoposti'] ?? ''), + 'laskutusosoite' => trim($input['laskutusosoite'] ?? ''), + 'laskutussahkoposti' => trim($input['laskutussahkoposti'] ?? ''), + 'ytunnus' => trim($input['ytunnus'] ?? ''), + 'lisatiedot' => trim($input['lisatiedot'] ?? ''), + 'luotu' => date('Y-m-d H:i:s'), + ]; + if (empty($customer['yritys'])) { + http_response_code(400); + echo json_encode(['error' => 'Yrityksen nimi vaaditaan']); + break; + } + $customers[] = $customer; + saveCustomers($customers); + echo json_encode($customer); + } + break; + + case 'customer_update': + requireAuth(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $id = $input['id'] ?? ''; + $customers = loadCustomers(); + $found = false; + foreach ($customers as &$c) { + if ($c['id'] === $id) { + $c['yritys'] = trim($input['yritys'] ?? $c['yritys']); + $c['asennusosoite'] = trim($input['asennusosoite'] ?? $c['asennusosoite']); + $c['liittymanopeus'] = trim($input['liittymanopeus'] ?? $c['liittymanopeus']); + $c['hinta'] = floatval($input['hinta'] ?? $c['hinta']); + $c['yhteyshenkilö'] = trim($input['yhteyshenkilö'] ?? $c['yhteyshenkilö']); + $c['puhelin'] = trim($input['puhelin'] ?? $c['puhelin']); + $c['sahkoposti'] = trim($input['sahkoposti'] ?? $c['sahkoposti']); + $c['laskutusosoite'] = trim($input['laskutusosoite'] ?? $c['laskutusosoite']); + $c['laskutussahkoposti'] = trim($input['laskutussahkoposti'] ?? $c['laskutussahkoposti']); + $c['ytunnus'] = trim($input['ytunnus'] ?? $c['ytunnus']); + $c['lisatiedot'] = trim($input['lisatiedot'] ?? $c['lisatiedot']); + $c['muokattu'] = date('Y-m-d H:i:s'); + $found = true; + echo json_encode($c); + break; + } + } + unset($c); + if (!$found) { + http_response_code(404); + echo json_encode(['error' => 'Asiakasta ei löydy']); + break; + } + saveCustomers($customers); + break; + + case 'customer_delete': + requireAuth(); + if ($method !== 'POST') break; + $input = json_decode(file_get_contents('php://input'), true); + $id = $input['id'] ?? ''; + $customers = loadCustomers(); + $customers = array_values(array_filter($customers, fn($c) => $c['id'] !== $id)); + saveCustomers($customers); + echo json_encode(['success' => true]); + break; + + default: + http_response_code(404); + echo json_encode(['error' => 'Tuntematon toiminto']); + break; +} diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..8e51d48 --- /dev/null +++ b/index.html @@ -0,0 +1,160 @@ + + + + + + CuituNet Intra - Asiakashallinta + + + + + + + + + + + + + + + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..a7bbba7 --- /dev/null +++ b/script.js @@ -0,0 +1,337 @@ +const API = 'api.php'; +let customers = []; +let sortField = 'yritys'; +let sortAsc = true; +let currentDetailId = null; + +// Elements +const loginScreen = document.getElementById('login-screen'); +const dashboard = document.getElementById('dashboard'); +const loginForm = document.getElementById('login-form'); +const loginError = document.getElementById('login-error'); +const searchInput = document.getElementById('search-input'); +const tbody = document.getElementById('customer-tbody'); +const noCustomers = document.getElementById('no-customers'); +const customerCount = document.getElementById('customer-count'); +const totalBilling = document.getElementById('total-billing'); +const customerModal = document.getElementById('customer-modal'); +const detailModal = document.getElementById('detail-modal'); +const customerForm = document.getElementById('customer-form'); + +// API helpers +async function apiCall(action, method = 'GET', body = null) { + const opts = { method, credentials: 'include' }; + if (body) { + opts.headers = { 'Content-Type': 'application/json' }; + opts.body = JSON.stringify(body); + } + const res = await fetch(`${API}?action=${action}`, opts); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Virhe'); + return data; +} + +// Auth +async function checkAuth() { + try { + const data = await apiCall('check_auth'); + if (data.authenticated) { + showDashboard(); + } + } catch (e) { /* not logged in */ } +} + +loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const password = document.getElementById('login-password').value; + try { + await apiCall('login', 'POST', { password }); + loginError.style.display = 'none'; + showDashboard(); + } catch (err) { + loginError.textContent = err.message; + loginError.style.display = 'block'; + } +}); + +document.getElementById('btn-logout').addEventListener('click', async () => { + await apiCall('logout'); + dashboard.style.display = 'none'; + loginScreen.style.display = 'flex'; + document.getElementById('login-password').value = ''; +}); + +async function showDashboard() { + loginScreen.style.display = 'none'; + dashboard.style.display = 'block'; + await loadCustomers(); +} + +// Customers +async function loadCustomers() { + customers = await apiCall('customers'); + renderTable(); +} + +function renderTable() { + const query = searchInput.value.toLowerCase().trim(); + let filtered = customers; + if (query) { + filtered = customers.filter(c => + c.yritys.toLowerCase().includes(query) || + c.asennusosoite.toLowerCase().includes(query) || + (c.yhteyshenkilö && c.yhteyshenkilö.toLowerCase().includes(query)) + ); + } + + // Sort + filtered.sort((a, b) => { + let va = a[sortField] ?? ''; + let vb = b[sortField] ?? ''; + if (sortField === 'hinta') { + va = parseFloat(va) || 0; + vb = parseFloat(vb) || 0; + } else { + va = String(va).toLowerCase(); + vb = String(vb).toLowerCase(); + } + if (va < vb) return sortAsc ? -1 : 1; + if (va > vb) return sortAsc ? 1 : -1; + return 0; + }); + + if (filtered.length === 0) { + tbody.innerHTML = ''; + noCustomers.style.display = 'block'; + document.getElementById('customer-table').style.display = 'none'; + } else { + noCustomers.style.display = 'none'; + document.getElementById('customer-table').style.display = 'table'; + tbody.innerHTML = filtered.map(c => ` + + ${esc(c.yritys)} + ${esc(c.asennusosoite)} + ${esc(c.liittymanopeus)} + ${formatPrice(c.hinta)} + + + + + + `).join(''); + } + + updateSummary(filtered); +} + +function updateSummary(filtered) { + const count = customers.length; + const total = customers.reduce((sum, c) => sum + (parseFloat(c.hinta) || 0), 0); + customerCount.textContent = `${count} asiakasta`; + totalBilling.textContent = `Laskutus yhteensä: ${formatPrice(total)}/kk`; +} + +function formatPrice(val) { + return parseFloat(val || 0).toFixed(2).replace('.', ',') + ' €'; +} + +function esc(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +// Search +searchInput.addEventListener('input', () => renderTable()); + +// Sort +document.querySelectorAll('th[data-sort]').forEach(th => { + th.addEventListener('click', () => { + const field = th.dataset.sort; + if (sortField === field) { + sortAsc = !sortAsc; + } else { + sortField = field; + sortAsc = true; + } + renderTable(); + }); +}); + +// Row click -> detail +tbody.addEventListener('click', (e) => { + const row = e.target.closest('tr'); + if (!row) return; + const id = row.dataset.id; + showDetail(id); +}); + +function showDetail(id) { + const c = customers.find(x => x.id === id); + if (!c) return; + currentDetailId = id; + + document.getElementById('detail-title').textContent = c.yritys; + document.getElementById('detail-body').innerHTML = ` +
+

Liittymätiedot

+
+
+
Yritys
+
${esc(c.yritys) || '-'}
+
+
+
Y-tunnus
+
${esc(c.ytunnus) || '-'}
+
+
+
Asennusosoite
+
${esc(c.asennusosoite) || '-'}
+
+
+
Nopeus
+
${esc(c.liittymanopeus) || '-'}
+
+
+
Hinta
+
${formatPrice(c.hinta)}
+
+
+
+
+

Yhteystiedot

+
+
+
Yhteyshenkilö
+
${esc(c.yhteyshenkilö) || '-'}
+
+
+
Puhelin
+
${c.puhelin ? `${esc(c.puhelin)}` : '-'}
+
+
+
Sähköposti
+
${c.sahkoposti ? `${esc(c.sahkoposti)}` : '-'}
+
+
+
+
+

Laskutustiedot

+
+
+
Laskutusosoite
+
${esc(c.laskutusosoite) || '-'}
+
+
+
Laskutussähköposti
+
${c.laskutussahkoposti ? `${esc(c.laskutussahkoposti)}` : '-'}
+
+
+
+ ${c.lisatiedot ? ` +
+

Lisätiedot

+

${esc(c.lisatiedot)}

+
` : ''} + `; + + detailModal.style.display = 'flex'; +} + +// Detail modal actions +document.getElementById('detail-close').addEventListener('click', () => detailModal.style.display = 'none'); +document.getElementById('detail-cancel').addEventListener('click', () => detailModal.style.display = 'none'); +document.getElementById('detail-edit').addEventListener('click', () => { + detailModal.style.display = 'none'; + editCustomer(currentDetailId); +}); +document.getElementById('detail-delete').addEventListener('click', () => { + const c = customers.find(x => x.id === currentDetailId); + if (c) { + detailModal.style.display = 'none'; + deleteCustomer(currentDetailId, c.yritys); + } +}); + +// Add/Edit modal +document.getElementById('btn-add').addEventListener('click', () => openCustomerForm()); +document.getElementById('modal-close').addEventListener('click', () => customerModal.style.display = 'none'); +document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none'); + +function openCustomerForm(customer = null) { + document.getElementById('modal-title').textContent = customer ? 'Muokkaa asiakasta' : 'Lisää asiakas'; + document.getElementById('form-submit').textContent = customer ? 'Päivitä' : 'Tallenna'; + document.getElementById('form-id').value = customer ? customer.id : ''; + document.getElementById('form-yritys').value = customer ? customer.yritys : ''; + document.getElementById('form-ytunnus').value = customer ? customer.ytunnus : ''; + document.getElementById('form-asennusosoite').value = customer ? customer.asennusosoite : ''; + document.getElementById('form-liittymanopeus').value = customer ? customer.liittymanopeus : ''; + document.getElementById('form-hinta').value = customer ? customer.hinta : ''; + document.getElementById('form-yhteyshenkilo').value = customer ? customer.yhteyshenkilö : ''; + document.getElementById('form-puhelin').value = customer ? customer.puhelin : ''; + document.getElementById('form-sahkoposti').value = customer ? customer.sahkoposti : ''; + document.getElementById('form-laskutusosoite').value = customer ? customer.laskutusosoite : ''; + document.getElementById('form-laskutussahkoposti').value = customer ? customer.laskutussahkoposti : ''; + document.getElementById('form-lisatiedot').value = customer ? customer.lisatiedot : ''; + customerModal.style.display = 'flex'; + document.getElementById('form-yritys').focus(); +} + +function editCustomer(id) { + const c = customers.find(x => x.id === id); + if (c) openCustomerForm(c); +} + +async function deleteCustomer(id, name) { + if (!confirm(`Poistetaanko asiakas "${name}"?`)) return; + await apiCall('customer_delete', 'POST', { id }); + await loadCustomers(); +} + +customerForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const id = document.getElementById('form-id').value; + const data = { + yritys: document.getElementById('form-yritys').value, + ytunnus: document.getElementById('form-ytunnus').value, + asennusosoite: document.getElementById('form-asennusosoite').value, + liittymanopeus: document.getElementById('form-liittymanopeus').value, + hinta: document.getElementById('form-hinta').value, + yhteyshenkilö: document.getElementById('form-yhteyshenkilo').value, + puhelin: document.getElementById('form-puhelin').value, + sahkoposti: document.getElementById('form-sahkoposti').value, + laskutusosoite: document.getElementById('form-laskutusosoite').value, + laskutussahkoposti: document.getElementById('form-laskutussahkoposti').value, + lisatiedot: document.getElementById('form-lisatiedot').value, + }; + + if (id) { + data.id = id; + await apiCall('customer_update', 'POST', data); + } else { + await apiCall('customer', 'POST', data); + } + + customerModal.style.display = 'none'; + await loadCustomers(); +}); + +// Close modals on backdrop click +customerModal.addEventListener('click', (e) => { + if (e.target === customerModal) customerModal.style.display = 'none'; +}); +detailModal.addEventListener('click', (e) => { + if (e.target === detailModal) detailModal.style.display = 'none'; +}); + +// ESC to close modals +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + customerModal.style.display = 'none'; + detailModal.style.display = 'none'; + } +}); + +// Init +checkAuth(); diff --git a/server.py b/server.py new file mode 100644 index 0000000..c2074c5 --- /dev/null +++ b/server.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import subprocess +import sys + +PORT = 3001 + +def main(): + print(f"CuituNet Intra käynnistyy osoitteessa http://localhost:{PORT}") + try: + subprocess.run( + ["php", "-S", f"localhost:{PORT}"], + check=True + ) + except KeyboardInterrupt: + print("\nSammutettiin.") + except FileNotFoundError: + print("PHP ei löydy. Asenna PHP ensin.") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/style.css b/style.css new file mode 100644 index 0000000..e9357c9 --- /dev/null +++ b/style.css @@ -0,0 +1,474 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #f0f2f5; + color: #1a1a2e; + min-height: 100vh; +} + +/* Login */ +.login-screen { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: linear-gradient(135deg, #0f3460, #16213e); +} + +.login-box { + background: #fff; + padding: 3rem; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); + text-align: center; + width: 100%; + max-width: 400px; +} + +.login-box h1 { + font-size: 1.8rem; + margin-bottom: 0.5rem; + color: #0f3460; +} + +.login-box p { + color: #666; + margin-bottom: 1.5rem; +} + +.login-box input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + margin-bottom: 1rem; + transition: border-color 0.2s; +} + +.login-box input:focus { + outline: none; + border-color: #0f3460; +} + +.login-box button { + width: 100%; + padding: 12px; + background: #0f3460; + color: #fff; + border: none; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: background 0.2s; +} + +.login-box button:hover { + background: #16213e; +} + +.error { + color: #e74c3c; + margin-top: 1rem; + font-size: 0.9rem; +} + +/* Header */ +header { + background: #0f3460; + color: #fff; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.header-left h1 { + font-size: 1.4rem; +} + +.subtitle { + font-size: 0.85rem; + opacity: 0.8; +} + +.header-right { + display: flex; + gap: 0.75rem; +} + +/* Buttons */ +.btn-primary { + background: #2ecc71; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + font-weight: 600; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #27ae60; +} + +.btn-secondary { + background: transparent; + color: #fff; + border: 2px solid rgba(255,255,255,0.3); + padding: 8px 18px; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.2s; +} + +.btn-secondary:hover { + border-color: #fff; + background: rgba(255,255,255,0.1); +} + +.btn-danger { + background: #e74c3c; + color: #fff; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + transition: background 0.2s; +} + +.btn-danger:hover { + background: #c0392b; +} + +/* Search */ +.search-bar { + padding: 1rem 2rem; +} + +.search-bar input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + background: #fff; + transition: border-color 0.2s; +} + +.search-bar input:focus { + outline: none; + border-color: #0f3460; +} + +/* Table */ +.table-container { + padding: 0 2rem; + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; + background: #fff; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0,0,0,0.06); +} + +thead th { + background: #16213e; + color: #fff; + padding: 14px 16px; + text-align: left; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + user-select: none; + white-space: nowrap; +} + +thead th:hover { + background: #1a2744; +} + +tbody tr { + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background 0.15s; +} + +tbody tr:hover { + background: #f7f9fc; +} + +tbody td { + padding: 12px 16px; + font-size: 0.95rem; +} + +.price-cell { + font-weight: 600; + color: #0f3460; +} + +.actions-cell { + white-space: nowrap; +} + +.actions-cell button { + background: none; + border: none; + cursor: pointer; + padding: 4px 8px; + font-size: 1.1rem; + border-radius: 4px; + transition: background 0.15s; +} + +.actions-cell button:hover { + background: #f0f0f0; +} + +.empty-state { + text-align: center; + padding: 3rem; + color: #888; + font-size: 1.1rem; +} + +/* Summary */ +.summary-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.25rem 2rem; + margin: 1rem 2rem 2rem; + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0,0,0,0.06); + font-weight: 600; +} + +#total-billing { + font-size: 1.15rem; + color: #0f3460; +} + +/* Modal */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.modal-content { + background: #fff; + border-radius: 16px; + width: 100%; + max-width: 700px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0,0,0,0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid #eee; +} + +.modal-header h2 { + font-size: 1.3rem; + color: #0f3460; +} + +.modal-close { + background: none; + border: none; + font-size: 1.8rem; + cursor: pointer; + color: #999; + line-height: 1; +} + +.modal-close:hover { + color: #333; +} + +/* Form */ +form { + padding: 1.5rem; +} + +form h3 { + font-size: 1rem; + color: #0f3460; + margin: 1.25rem 0 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #f0f2f5; +} + +form h3:first-child { + margin-top: 0; +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.form-group { + display: flex; + flex-direction: column; +} + +.form-group.full-width { + grid-column: 1 / -1; +} + +.form-group label { + font-size: 0.85rem; + font-weight: 600; + color: #555; + margin-bottom: 0.3rem; +} + +.form-group input, +.form-group textarea { + padding: 10px 12px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 0.95rem; + transition: border-color 0.2s; + font-family: inherit; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #0f3460; +} + +.form-actions { + display: flex; + gap: 0.75rem; + padding: 1.25rem 1.5rem; + border-top: 1px solid #eee; + justify-content: flex-end; +} + +.form-actions .btn-secondary { + color: #666; + border-color: #ddd; +} + +.form-actions .btn-secondary:hover { + color: #333; + border-color: #999; +} + +/* Detail view */ +#detail-body { + padding: 1.5rem; +} + +.detail-section { + margin-bottom: 1.5rem; +} + +.detail-section h3 { + font-size: 1rem; + color: #0f3460; + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid #f0f2f5; +} + +.detail-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem 2rem; +} + +.detail-item { + padding: 0.4rem 0; +} + +.detail-label { + font-size: 0.8rem; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detail-value { + font-size: 0.95rem; + font-weight: 500; + color: #1a1a2e; + word-break: break-word; +} + +.detail-value.empty { + color: #ccc; + font-style: italic; +} + +/* Responsive */ +@media (max-width: 768px) { + header { + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .form-grid, + .detail-grid { + grid-template-columns: 1fr; + } + + .summary-bar { + flex-direction: column; + gap: 0.5rem; + text-align: center; + } + + .table-container { + padding: 0 1rem; + } + + .search-bar { + padding: 1rem; + } + + .summary-bar { + margin: 1rem; + } + + thead th, + tbody td { + padding: 10px 8px; + font-size: 0.85rem; + } +}