diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..7a974e1 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1 @@ -body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; -} - -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} - -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-links { - display: flex; - gap: 1rem; -} - -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); -} - -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; -} - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file +:root{--kk-bg:#f6f7f8;--kk-surface:#ffffff;--kk-text:#151719;--kk-muted:#667085;--kk-border:#d9dee5;--kk-soft:#eef1f4;--kk-primary:#1f5f45;--kk-primary-dark:#123c2b;--kk-radius:10px;--kk-radius-sm:6px;--kk-shadow:0 10px 30px rgba(20,24,28,.06)}*{box-sizing:border-box}body{margin:0;background:var(--kk-bg);color:var(--kk-text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:14px;line-height:1.5}.kk-navbar{background:rgba(255,255,255,.94);border-bottom:1px solid var(--kk-border);backdrop-filter:blur(10px)}.navbar-brand{font-weight:750;letter-spacing:-.02em}.brand-mark{display:inline-grid;place-items:center;width:28px;height:28px;border-radius:6px;background:var(--kk-primary);color:#fff;font-weight:800}.nav-link{font-weight:600;color:#39404a}.nav-link:hover{color:var(--kk-primary)}.panel{background:var(--kk-surface);border:1px solid var(--kk-border);border-radius:var(--kk-radius);box-shadow:var(--kk-shadow);padding:24px}.hero{padding:32px}.eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:800;color:var(--kk-primary);margin:0}h1{font-size:clamp(30px,4vw,48px);line-height:1.04;letter-spacing:-.05em;font-weight:820;margin:0 0 14px}h2{font-size:22px;letter-spacing:-.03em;font-weight:780;margin:0}.hero-copy{max-width:720px;color:var(--kk-muted);font-size:16px}.btn{border-radius:var(--kk-radius-sm);font-weight:700}.btn-lg{font-size:14px;padding:.75rem 1rem}.btn-dark{background:var(--kk-text);border-color:var(--kk-text)}.btn-dark:hover{background:var(--kk-primary-dark);border-color:var(--kk-primary-dark)}.btn-outline-dark{border-color:#aeb6c2}.company-card{border:1px solid var(--kk-border);border-radius:var(--kk-radius);padding:20px;background:#fafbfc}.meta-row{display:flex;justify-content:space-between;gap:16px;padding:11px 0;border-top:1px solid var(--kk-border)}.meta-row span{color:var(--kk-muted)}.meta-row strong{text-align:right}.stat-card{border:1px solid var(--kk-border);border-radius:var(--kk-radius);background:var(--kk-surface);padding:18px;min-height:98px}.stat-card span{display:block;color:var(--kk-muted);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}.stat-card strong{display:block;margin-top:8px;font-size:24px;letter-spacing:-.04em}.section-heading{margin-bottom:18px}.form-label{font-size:12px;font-weight:750;color:#344054}.form-control,.form-select{border-color:var(--kk-border);border-radius:var(--kk-radius-sm);font-size:14px}.form-control:focus,.form-select:focus{border-color:var(--kk-primary);box-shadow:0 0 0 .2rem rgba(31,95,69,.12)}.kk-table{font-size:13px;margin-bottom:0}.kk-table thead th{color:#596273;text-transform:uppercase;font-size:11px;letter-spacing:.08em;background:#fafbfc;border-bottom:1px solid var(--kk-border)}.kk-table td{border-color:#eef1f4}.badge{border-radius:999px;font-weight:750}.empty-state{display:flex;flex-direction:column;align-items:flex-start;gap:4px;padding:24px;border:1px dashed #b8c0cc;border-radius:var(--kk-radius);background:#fbfcfd;color:var(--kk-muted)}.empty-state strong{color:var(--kk-text)}.tracking-card{border:1px solid var(--kk-border);border-radius:var(--kk-radius);padding:20px;background:#fbfcfd}.tracking-card h3{font-size:26px;letter-spacing:-.04em;margin:0}.progress-steps{display:grid;gap:8px;list-style:none;padding:0;margin:0}.progress-steps li{position:relative;padding:10px 12px 10px 38px;border:1px solid var(--kk-border);border-radius:var(--kk-radius-sm);background:#fff;color:var(--kk-muted);font-weight:700}.progress-steps li:before{content:"";position:absolute;left:13px;top:13px;width:14px;height:14px;border:2px solid #c3cad5;border-radius:50%;background:#fff}.progress-steps li.done{border-color:#bed4c9;color:var(--kk-primary);background:#f6faf8}.progress-steps li.done:before{background:var(--kk-primary);border-color:var(--kk-primary)}.footer-line{color:var(--kk-muted);font-size:13px;border-top:1px solid var(--kk-border);padding-top:18px}.toast{border:1px solid var(--kk-border);box-shadow:var(--kk-shadow)}.detail-grid{display:grid;grid-template-columns:130px 1fr;gap:10px 16px}.detail-grid dt{color:var(--kk-muted);font-weight:750}.detail-grid dd{margin:0;font-weight:650}.print-body{background:#e9edf2}.print-sheet{max-width:920px;margin:24px auto;padding:0 16px}.print-actions{display:flex;justify-content:space-between;margin-bottom:12px}.doc-card{background:#fff;border:1px solid #cfd6df;border-radius:8px;padding:30px;color:#111}.doc-header{display:flex;justify-content:space-between;gap:24px;border-bottom:2px solid #111;padding-bottom:16px;margin-bottom:18px}.doc-header h1{font-size:28px;letter-spacing:-.04em;margin:0}.doc-header p{margin:5px 0 0;color:#555}.doc-number{text-align:right}.doc-number span,.doc-box span{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:#667085;font-weight:800}.doc-number strong{display:block;font-size:18px}.doc-title{text-align:center;text-transform:uppercase;letter-spacing:.14em;font-weight:850;border:1px solid #111;padding:9px;margin-bottom:16px}.doc-box{border:1px solid #cfd6df;border-radius:6px;padding:12px;min-height:78px}.doc-box strong{display:block}.doc-box small{color:#667085}.doc-table th{width:170px;background:#f6f7f8}.doc-table th,.doc-table td,.invoice-table th,.invoice-table td{border-color:#cfd6df}.invoice-table thead th{background:#111;color:#fff}.qc-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.qc-grid>div{border:1px solid #cfd6df;border-radius:6px;padding:12px}.qc-grid label{display:block;margin-top:10px}.signature-row{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:46px}.signature-row div{text-align:center;border-top:1px solid #111;padding-top:10px;font-weight:750}.total-box{border:1px solid #cfd6df;border-radius:6px;overflow:hidden}.total-box div{display:flex;justify-content:space-between;padding:10px 12px;border-bottom:1px solid #e4e8ee}.total-box div:last-child{border-bottom:0}.total-box .remaining{background:#111;color:#fff}.payment-box{margin-top:18px;border:1px solid #cfd6df;border-radius:6px;padding:12px}.payment-box p{margin:6px 0 0;color:#555}@media (max-width:768px){.panel,.hero{padding:18px}.btn-group{display:grid}.detail-grid{grid-template-columns:1fr}.doc-header,.qc-grid{grid-template-columns:1fr;display:grid}.doc-number{text-align:left}.signature-row{gap:10px}}@media print{body{background:#fff}.print-sheet{margin:0;max-width:none;padding:0}.print-actions,.kk-navbar{display:none!important}.doc-card{border:0;border-radius:0;padding:0}.btn{display:none}} diff --git a/assets/js/main.js b/assets/js/main.js index d349598..6a28d2a 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,15 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); + document.querySelectorAll('.toast').forEach((toastEl) => { + if (window.bootstrap) { + const toast = new bootstrap.Toast(toastEl, { delay: 4200 }); + toast.show(); + } + }); - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } + const moneyInputs = document.querySelectorAll('.money-input'); + moneyInputs.forEach((input) => { + input.addEventListener('input', () => { + if (Number(input.value) < 0) input.value = '0'; }); + }); }); diff --git a/index.php b/index.php index 7205f3d..0ea951e 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,332 @@ prepare("SELECT COUNT(*) AS total FROM print_orders WHERE $column LIKE :prefix"); + $stmt->execute([':prefix' => $prefix . '%']); + $count = (int)($stmt->fetch()['total'] ?? 0) + 1; + return $prefix . str_pad((string)$count, 5, '0', STR_PAD_LEFT); +} +function statusBadge(string $status): string { + return match ($status) { + 'Order diterima' => 'secondary', + 'SPK dibuat' => 'info', + 'Produksi berjalan' => 'primary', + 'QC / Finishing' => 'warning', + 'Barang selesai' => 'success', + 'Invoice / Tagihan' => 'dark', + default => 'secondary', + }; +} +function ensureSchema(PDO $pdo): void { + $pdo->exec("CREATE TABLE IF NOT EXISTS print_orders ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + order_no VARCHAR(40) NOT NULL UNIQUE, + spk_no VARCHAR(40) NOT NULL UNIQUE, + invoice_no VARCHAR(40) NOT NULL UNIQUE, + customer_name VARCHAR(140) NOT NULL, + customer_phone VARCHAR(40) NOT NULL, + customer_email VARCHAR(160) NULL, + project_name VARCHAR(180) NOT NULL, + category VARCHAR(100) NOT NULL, + quantity INT UNSIGNED NOT NULL DEFAULT 1, + size_info VARCHAR(120) NULL, + material VARCHAR(160) NULL, + finishing VARCHAR(180) NULL, + deadline DATE NULL, + operator_name VARCHAR(120) NULL, + cs_name VARCHAR(120) NULL, + status VARCHAR(40) NOT NULL DEFAULT 'Order diterima', + progress_note TEXT NULL, + unit_price DECIMAL(14,2) NOT NULL DEFAULT 0, + discount DECIMAL(14,2) NOT NULL DEFAULT 0, + paid_amount DECIMAL(14,2) NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_phone (customer_phone), + INDEX idx_status (status) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); +} + +$pdo = db(); +ensureSchema($pdo); +$errors = []; +$notice = ''; +$createdId = 0; +$statuses = ['Order diterima', 'SPK dibuat', 'Produksi berjalan', 'QC / Finishing', 'Barang selesai', 'Invoice / Tagihan']; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? ''; + if ($action === 'create_order') { + $customerName = trim((string)($_POST['customer_name'] ?? '')); + $customerPhone = preg_replace('/[^0-9+]/', '', (string)($_POST['customer_phone'] ?? '')); + $customerEmail = trim((string)($_POST['customer_email'] ?? '')); + $project = trim((string)($_POST['project_name'] ?? '')); + $category = trim((string)($_POST['category'] ?? '')); + $qty = max(1, (int)($_POST['quantity'] ?? 1)); + $deadline = trim((string)($_POST['deadline'] ?? '')) ?: null; + $unitPrice = max(0, (float)($_POST['unit_price'] ?? 0)); + $discount = max(0, (float)($_POST['discount'] ?? 0)); + $paid = max(0, (float)($_POST['paid_amount'] ?? 0)); + + foreach ([ + 'Nama customer' => $customerName, + 'Nomor telepon' => $customerPhone, + 'Nama pekerjaan' => $project, + 'Kategori' => $category, + ] as $label => $value) { + if ($value === '') { $errors[] = $label . ' wajib diisi.'; } + } + if ($customerEmail !== '' && !filter_var($customerEmail, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Format email customer tidak valid.'; } + + if (!$errors) { + $year = date('y'); + $month = date('m'); + $orderNo = nextNumber($pdo, 'ORD-' . date('Y') . '-', 'order_no'); + $invoiceNo = nextNumber($pdo, 'VI-' . $year . '-', 'invoice_no'); + $spkPrefix = str_pad((string)(((int)$pdo->query('SELECT COUNT(*) FROM print_orders')->fetchColumn()) + 1), 2, '0', STR_PAD_LEFT) . '-SPK-' . $month . '-' . date('Y'); + $spkNo = $spkPrefix; + + $stmt = $pdo->prepare('INSERT INTO print_orders (order_no, spk_no, invoice_no, customer_name, customer_phone, customer_email, project_name, category, quantity, size_info, material, finishing, deadline, operator_name, cs_name, status, progress_note, unit_price, discount, paid_amount) VALUES (:order_no, :spk_no, :invoice_no, :customer_name, :customer_phone, :customer_email, :project_name, :category, :quantity, :size_info, :material, :finishing, :deadline, :operator_name, :cs_name, :status, :progress_note, :unit_price, :discount, :paid_amount)'); + $stmt->execute([ + ':order_no' => $orderNo, + ':spk_no' => $spkNo, + ':invoice_no' => $invoiceNo, + ':customer_name' => $customerName, + ':customer_phone' => $customerPhone, + ':customer_email' => $customerEmail ?: null, + ':project_name' => $project, + ':category' => $category, + ':quantity' => $qty, + ':size_info' => trim((string)($_POST['size_info'] ?? '')) ?: null, + ':material' => trim((string)($_POST['material'] ?? '')) ?: null, + ':finishing' => trim((string)($_POST['finishing'] ?? '')) ?: null, + ':deadline' => $deadline, + ':operator_name' => trim((string)($_POST['operator_name'] ?? '')) ?: null, + ':cs_name' => trim((string)($_POST['cs_name'] ?? '')) ?: null, + ':status' => 'SPK dibuat', + ':progress_note' => trim((string)($_POST['progress_note'] ?? 'Order diterima dan SPK siap dicetak.')), + ':unit_price' => $unitPrice, + ':discount' => $discount, + ':paid_amount' => $paid, + ]); + $createdId = (int)$pdo->lastInsertId(); + $notice = 'Order berhasil dibuat. SPK dan Invoice sudah otomatis tersedia.'; + } + } elseif ($action === 'update_status') { + $id = (int)($_POST['id'] ?? 0); + $status = (string)($_POST['status'] ?? ''); + $note = trim((string)($_POST['progress_note'] ?? '')); + if ($id > 0 && in_array($status, $statuses, true)) { + $stmt = $pdo->prepare('UPDATE print_orders SET status = :status, progress_note = :note WHERE id = :id'); + $stmt->execute([':status' => $status, ':note' => $note, ':id' => $id]); + $notice = 'Status produksi berhasil diperbarui.'; + } + } +} + +$trackResult = null; +if (isset($_GET['track_order'], $_GET['track_phone'])) { + $trackOrder = trim((string)$_GET['track_order']); + $trackPhone = preg_replace('/[^0-9+]/', '', (string)$_GET['track_phone']); + $stmt = $pdo->prepare('SELECT * FROM print_orders WHERE order_no = :order_no AND customer_phone = :phone LIMIT 1'); + $stmt->execute([':order_no' => $trackOrder, ':phone' => $trackPhone]); + $trackResult = $stmt->fetch() ?: false; +} + +$orders = $pdo->query('SELECT * FROM print_orders ORDER BY created_at DESC LIMIT 25')->fetchAll(); +$totalOrders = (int)$pdo->query('SELECT COUNT(*) FROM print_orders')->fetchColumn(); +$activeOrders = (int)$pdo->query("SELECT COUNT(*) FROM print_orders WHERE status NOT IN ('Barang selesai','Invoice / Tagihan')")->fetchColumn(); +$finishedOrders = (int)$pdo->query("SELECT COUNT(*) FROM print_orders WHERE status = 'Barang selesai'")->fetchColumn(); +$finance = $pdo->query('SELECT COALESCE(SUM(quantity * unit_price - discount),0) AS billed, COALESCE(SUM(paid_amount),0) AS paid FROM print_orders')->fetch(); +$receivable = max(0, (float)$finance['billed'] - (float)$finance['paid']); ?> - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + <?= e($projectName) ?> — Order, SPK, Produksi & Invoice + + + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… +
- + + + +
+ +
+
+
+
+
+
+ + +
+
+
+

Sistem Percetakan

+

Order masuk, SPK, produksi, invoice, dan tracking customer dalam satu alur.

+

MVP awal untuk Percetakan Kenanga Kreasindo: admin membuat order, sistem menerbitkan SPK dan invoice, produksi memperbarui status, customer mengecek progres memakai nomor order + telepon.

+ +
+
+
+
Percetakan
+

Kenanga Kreasindo

+
AlamatPerum BCL Jln Kenanga Raya
+
Emailkenangakreasindo@gmail.com
+
RuntimePHP
+
+
+
+
+ +
+
Total Order
+
Produksi Aktif
+
Barang Selesai
+
Piutang
+
+ +
+
+
+
+

Penerimaan Order

+

Buat order + SPK

+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ * wajib diisi. Nomor order, SPK, dan invoice otomatis. + +
+
+
+
+ +
+
+
+

Produksi & Monitoring

Daftar order terbaru

+ Lihat order baru +
+ +
Belum ada order.Buat order pertama untuk menghasilkan SPK, tracking, dan invoice.
+ +
+ + + + + + + + + + + + + + +
OrderCustomerPekerjaanStatusTagihanAksi
· pcs
Bayar
+
+ +
+
+
+ +
+
+
+

Customer Portal

Cek progres order

+

Customer tidak perlu login. Masukkan nomor order dan telepon yang tercatat di order.

+
+
+
+
+
+
+
+ +
Hasil tracking muncul di sini.Gunakan nomor order dari admin/Kasir.
+ +
Order tidak ditemukan. Pastikan nomor order dan telepon sudah benar.
+ +
+
Order

+

, pcs

+

+
    + $s): ?> +
  1. + +
+
+ +
+
+
+
+ + + + diff --git a/invoice.php b/invoice.php new file mode 100644 index 0000000..e32b80a --- /dev/null +++ b/invoice.php @@ -0,0 +1,15 @@ +prepare('SELECT * FROM print_orders WHERE id = :id LIMIT 1'); +$stmt->execute([':id' => max(0, (int)($_GET['id'] ?? 0))]); +$order = $stmt->fetch(); +if (!$order) { http_response_code(404); echo 'Invoice tidak ditemukan.'; exit; } +$subtotal = (float)$order['quantity'] * (float)$order['unit_price']; +$total = max(0, $subtotal - (float)$order['discount']); +$remaining = max(0, $total - (float)$order['paid_amount']); +?> +Invoice <?= e($order['invoice_no']) ?> — Kenanga Kreasindo

Kenanga Kreasindo

Perum BCL Jln Kenanga Raya · kenangakreasindo@gmail.com

Invoice No
Company / Customer
ProjectOrder:
ItemCategoryQtyDiscPrice/UnitAmount
Subtotal
Disc
Total
Paid
Remaining payment
Informasi Pembayaran

Transfer ke rekening resmi Kenanga Kreasindo. Nomor rekening dapat diisi pada iterasi berikutnya sesuai rekening aktif.

Received
Kenanga Kreasindo
Customer
diff --git a/order_detail.php b/order_detail.php new file mode 100644 index 0000000..fa05e52 --- /dev/null +++ b/order_detail.php @@ -0,0 +1,20 @@ + 'info', 'Produksi berjalan' => 'primary', 'QC / Finishing' => 'warning', 'Barang selesai' => 'success', 'Invoice / Tagihan' => 'dark', default => 'secondary' }; } +$pdo = db(); +$id = max(0, (int)($_GET['id'] ?? 0)); +$stmt = $pdo->prepare('SELECT * FROM print_orders WHERE id = :id LIMIT 1'); +$stmt->execute([':id' => $id]); +$order = $stmt->fetch(); +if (!$order) { http_response_code(404); echo 'Order tidak ditemukan.'; exit; } +$statuses = ['Order diterima', 'SPK dibuat', 'Produksi berjalan', 'QC / Finishing', 'Barang selesai', 'Invoice / Tagihan']; +$amount = ((float)$order['quantity'] * (float)$order['unit_price']) - (float)$order['discount']; +$remaining = max(0, $amount - (float)$order['paid_amount']); +$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Detail order percetakan Kenanga Kreasindo.'; +$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; +?> +Detail <?= e($order['order_no']) ?> — Kenanga Kreasindo +
← Kembali

Detail Order

·


Pekerjaan

Project
Kategori
Qty
Ukuran
Bahan
Finishing
Deadline

Keuangan

Invoice
Total
Terbayar
Sisa Tagihan

diff --git a/spk.php b/spk.php new file mode 100644 index 0000000..5cd2d58 --- /dev/null +++ b/spk.php @@ -0,0 +1,11 @@ +prepare('SELECT * FROM print_orders WHERE id = :id LIMIT 1'); +$stmt->execute([':id' => max(0, (int)($_GET['id'] ?? 0))]); +$order = $stmt->fetch(); +if (!$order) { http_response_code(404); echo 'SPK tidak ditemukan.'; exit; } +?> +SPK <?= e($order['spk_no']) ?> — Kenanga Kreasindo

Kenanga Kreasindo

Perum BCL Jln Kenanga Raya · kenangakreasindo@gmail.com

No SPK
Surat Perintah Kerja
PIC / Customer
Deadline / PengambilanCS:
Pekerjaan
Kategori / Qty · pcs
Bahan
Ukuran
Finishing
Detail Pesanan
Operator

QC Checklist

Printing
Finishing
Operator
Checker QC
Customer