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…
+
+
+
+
+
+
+
+
+
+
= $errors ? e(implode(' ', $errors)) : e($notice) ?>
+
+
+
+
+
+
+
+
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
+
Alamat Perum BCL Jln Kenanga Raya
+
Email kenangakreasindo@gmail.com
+
Runtime PHP = e(PHP_VERSION) ?>
+
+
+
+
+
+
+ Total Order = $totalOrders ?>
+ Produksi Aktif = $activeOrders ?>
+ Barang Selesai = $finishedOrders ?>
+ Piutang = e(rupiah($receivable)) ?>
+
+
+
+
+
+
+
Penerimaan Order
+
Buat order + SPK
+
+
+
+
+
+
+
+
+
Produksi & Monitoring
Daftar order terbaru
+
Lihat order baru
+
+
+
Belum ada order. Buat order pertama untuk menghasilkan SPK, tracking, dan invoice.
+
+
+
+ Order Customer Pekerjaan Status Tagihan Aksi
+
+
+
+ = e($order['order_no']) ?> = e(date('d M Y', strtotime($order['created_at']))) ?>
+ = e($order['customer_name']) ?>= e($order['customer_phone']) ?>
+ = e($order['project_name']) ?>= e($order['category']) ?> · = (int)$order['quantity'] ?> pcs
+ = e($order['status']) ?>
+ = e(rupiah(max(0, $amount))) ?>Bayar = e(rupiah((float)$order['paid_amount'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Customer Portal
Cek progres order
+
Customer tidak perlu login. Masukkan nomor order dan telepon yang tercatat di order.
+
+ Nomor Order
+ Telepon
+ Cek Status
+
+
+
+
+
Hasil tracking muncul di sini. Gunakan nomor order dari admin/Kasir.
+
+
Order tidak ditemukan. Pastikan nomor order dan telepon sudah benar.
+
+
+
Order
= e($trackResult['order_no']) ?> = e($trackResult['status']) ?>
+
= e($trackResult['project_name']) ?> — = e($trackResult['category']) ?>, = (int)$trackResult['quantity'] ?> pcs
+
= nl2br(e((string)$trackResult['progress_note'])) ?>
+
+ $s): ?>
+ = e($s) ?>
+
+
+
+
+
+
+
+
+
+
+
+
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 Company / Customer = e($order['customer_name']) ?> = e($order['customer_phone']) ?>
Project = e($order['project_name']) ?> Order: = e($order['order_no']) ?>
Item Category Qty Disc Price/Unit Amount = e($order['project_name']) ?> = e($order['category']) ?> = (int)$order['quantity'] ?> = e(rupiah((float)$order['discount'])) ?> = e(rupiah((float)$order['unit_price'])) ?> = e(rupiah($total)) ?>
Subtotal = e(rupiah($subtotal)) ?>
Disc = e(rupiah((float)$order['discount'])) ?>
Total = e(rupiah($total)) ?>
Paid = e(rupiah((float)$order['paid_amount'])) ?>
Remaining payment = e(rupiah($remaining)) ?>
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
= e($order['order_no']) ?> = e($order['customer_name']) ?> · = e($order['customer_phone']) ?>
= e($order['status']) ?> Pekerjaan Project = e($order['project_name']) ?> Kategori = e($order['category']) ?> Qty = (int)$order['quantity'] ?> Ukuran = e((string)($order['size_info'] ?: '-')) ?> Bahan = e((string)($order['material'] ?: '-')) ?> Finishing = e((string)($order['finishing'] ?: '-')) ?> Deadline = e($order['deadline'] ? date('d M Y', strtotime($order['deadline'])) : '-') ?> Keuangan Invoice = e($order['invoice_no']) ?> Total = e(rupiah(max(0, $amount))) ?> Terbayar = e(rupiah((float)$order['paid_amount'])) ?> Sisa Tagihan = e(rupiah($remaining)) ?> Update Status >= e($status) ?>
Catatan Progress = e((string)$order['progress_note']) ?>
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 Surat Perintah Kerja
PIC / Customer = e($order['customer_name']) ?> = e($order['customer_phone']) ?>
Deadline / Pengambilan = e($order['deadline'] ? date('d M Y', strtotime($order['deadline'])) : '-') ?> CS: = e((string)($order['cs_name'] ?: '-')) ?>
Pekerjaan = e($order['project_name']) ?> Kategori / Qty = e($order['category']) ?> · = (int)$order['quantity'] ?> pcs Bahan = e((string)($order['material'] ?: '-')) ?> Ukuran = e((string)($order['size_info'] ?: '-')) ?> Finishing = e((string)($order['finishing'] ?: '-')) ?> Detail Pesanan = nl2br(e((string)$order['progress_note'])) ?> Operator = e((string)($order['operator_name'] ?: '-')) ?>
QC Checklist Operator
Checker QC
Customer