40281-vm/index.php
2026-06-18 09:37:21 +00:00

333 lines
21 KiB
PHP

<?php
require_once __DIR__ . '/db/config.php';
@date_default_timezone_set('Asia/Jakarta');
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Sistem manajemen percetakan Kenanga Kreasindo untuk order, SPK, produksi, invoice, dan tracking customer.';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Kenanga Kreasindo Printing';
function e(string $value): string { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); }
function rupiah(float $value): string { return 'Rp ' . number_format($value, 0, ',', '.'); }
function nextNumber(PDO $pdo, string $prefix, string $column): string {
$stmt = $pdo->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']);
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($projectName) ?> — Order, SPK, Produksi & Invoice</title>
<meta name="description" content="<?= e($projectDescription) ?>">
<meta property="og:title" content="<?= e($projectName) ?>">
<meta property="og:description" content="<?= e($projectDescription) ?>">
<meta property="twitter:title" content="<?= e($projectName) ?>">
<meta property="twitter:description" content="<?= e($projectDescription) ?>">
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= e($projectImageUrl) ?>">
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
</head>
<body>
<nav class="navbar navbar-expand-lg kk-navbar sticky-top">
<div class="container-fluid px-3 px-lg-4">
<a class="navbar-brand d-flex align-items-center gap-2" href="#top" aria-label="Kenanga Kreasindo Home">
<span class="brand-mark">K</span><span>Kenanga Kreasindo</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu" aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navMenu">
<div class="navbar-nav ms-auto gap-lg-2">
<a class="nav-link" href="#order">Order</a>
<a class="nav-link" href="#produksi">Produksi</a>
<a class="nav-link" href="#tracking">Tracking Customer</a>
<a class="nav-link" href="#laporan">Keuangan</a>
</div>
</div>
</div>
</nav>
<main id="top" class="container-fluid px-3 px-lg-4 py-4">
<?php if ($notice || $errors): ?>
<div class="toast-container position-fixed top-0 end-0 p-3">
<div class="toast show" role="status" aria-live="polite" aria-atomic="true">
<div class="toast-header"><strong class="me-auto"><?= $errors ? 'Validasi belum lengkap' : 'Berhasil' ?></strong><button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button></div>
<div class="toast-body"><?= $errors ? e(implode(' ', $errors)) : e($notice) ?></div>
</div>
</div>
<?php endif; ?>
<section class="hero panel mb-4" aria-labelledby="heroTitle">
<div class="row g-4 align-items-center">
<div class="col-lg-7">
<p class="eyebrow mb-2">Sistem Percetakan</p>
<h1 id="heroTitle">Order masuk, SPK, produksi, invoice, dan tracking customer dalam satu alur.</h1>
<p class="hero-copy mb-4">MVP awal untuk Percetakan Kenanga Kreasindo: admin membuat order, sistem menerbitkan SPK dan invoice, produksi memperbarui status, customer mengecek progres memakai nomor order + telepon.</p>
<div class="d-flex flex-wrap gap-2">
<a href="#order" class="btn btn-dark btn-lg">Buat Order & SPK</a>
<a href="#tracking" class="btn btn-outline-dark btn-lg">Cek Status Customer</a>
</div>
</div>
<div class="col-lg-5">
<div class="company-card">
<div class="small text-muted">Percetakan</div>
<h2 class="h4 mb-3">Kenanga Kreasindo</h2>
<div class="meta-row"><span>Alamat</span><strong>Perum BCL Jln Kenanga Raya</strong></div>
<div class="meta-row"><span>Email</span><strong>kenangakreasindo@gmail.com</strong></div>
<div class="meta-row"><span>Runtime</span><strong>PHP <?= e(PHP_VERSION) ?></strong></div>
</div>
</div>
</div>
</section>
<section id="laporan" class="row g-3 mb-4" aria-label="Ringkasan laporan">
<div class="col-6 col-lg-3"><div class="stat-card"><span>Total Order</span><strong><?= $totalOrders ?></strong></div></div>
<div class="col-6 col-lg-3"><div class="stat-card"><span>Produksi Aktif</span><strong><?= $activeOrders ?></strong></div></div>
<div class="col-6 col-lg-3"><div class="stat-card"><span>Barang Selesai</span><strong><?= $finishedOrders ?></strong></div></div>
<div class="col-6 col-lg-3"><div class="stat-card"><span>Piutang</span><strong><?= e(rupiah($receivable)) ?></strong></div></div>
</section>
<div class="row g-4">
<section id="order" class="col-xl-5" aria-labelledby="orderTitle">
<div class="panel h-100">
<div class="section-heading">
<p class="eyebrow">Penerimaan Order</p>
<h2 id="orderTitle">Buat order + SPK</h2>
</div>
<form method="post" class="kk-form" novalidate>
<input type="hidden" name="action" value="create_order">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Nama Customer *</label><input class="form-control" name="customer_name" required></div>
<div class="col-md-6"><label class="form-label">Telepon *</label><input class="form-control" name="customer_phone" required placeholder="08..."></div>
<div class="col-12"><label class="form-label">Email</label><input class="form-control" name="customer_email" type="email" placeholder="opsional"></div>
<div class="col-md-8"><label class="form-label">Nama Pekerjaan *</label><input class="form-control" name="project_name" required placeholder="Contoh: Banner Grand Opening"></div>
<div class="col-md-4"><label class="form-label">Kategori *</label><select class="form-select" name="category" required><option value="Digital Printing">Digital Printing</option><option value="Offset">Offset</option><option value="Merchandise">Merchandise</option><option value="Apparel">Apparel</option><option value="Lainnya">Lainnya</option></select></div>
<div class="col-md-4"><label class="form-label">Qty *</label><input class="form-control" name="quantity" type="number" min="1" value="1" required></div>
<div class="col-md-4"><label class="form-label">Ukuran</label><input class="form-control" name="size_info" placeholder="A3 / 3x1 m"></div>
<div class="col-md-4"><label class="form-label">Deadline</label><input class="form-control" name="deadline" type="date"></div>
<div class="col-md-6"><label class="form-label">Bahan</label><input class="form-control" name="material" placeholder="Flexi 280 / Art carton"></div>
<div class="col-md-6"><label class="form-label">Finishing</label><input class="form-control" name="finishing" placeholder="Laminasi, potong, mata ayam"></div>
<div class="col-md-6"><label class="form-label">CS</label><input class="form-control" name="cs_name" placeholder="Nama CS"></div>
<div class="col-md-6"><label class="form-label">Operator</label><input class="form-control" name="operator_name" placeholder="Nama operator"></div>
<div class="col-md-4"><label class="form-label">Harga / Unit</label><input class="form-control money-input" name="unit_price" type="number" min="0" value="0"></div>
<div class="col-md-4"><label class="form-label">Diskon</label><input class="form-control money-input" name="discount" type="number" min="0" value="0"></div>
<div class="col-md-4"><label class="form-label">Terbayar</label><input class="form-control money-input" name="paid_amount" type="number" min="0" value="0"></div>
<div class="col-12"><label class="form-label">Detail Pesanan / Catatan SPK</label><textarea class="form-control" name="progress_note" rows="3" placeholder="Instruksi file, warna, referensi, catatan operator..."></textarea></div>
</div>
<div class="d-flex justify-content-between align-items-center mt-4">
<small class="text-muted">* wajib diisi. Nomor order, SPK, dan invoice otomatis.</small>
<button class="btn btn-dark" type="submit">Simpan Order</button>
</div>
</form>
</div>
</section>
<section id="produksi" class="col-xl-7" aria-labelledby="productionTitle">
<div class="panel h-100">
<div class="section-heading d-flex justify-content-between align-items-start gap-3">
<div><p class="eyebrow">Produksi & Monitoring</p><h2 id="productionTitle">Daftar order terbaru</h2></div>
<?php if ($createdId): ?><a class="btn btn-outline-dark btn-sm" href="order_detail.php?id=<?= $createdId ?>">Lihat order baru</a><?php endif; ?>
</div>
<?php if (!$orders): ?>
<div class="empty-state"><strong>Belum ada order.</strong><span>Buat order pertama untuk menghasilkan SPK, tracking, dan invoice.</span></div>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle kk-table">
<thead><tr><th>Order</th><th>Customer</th><th>Pekerjaan</th><th>Status</th><th>Tagihan</th><th>Aksi</th></tr></thead>
<tbody>
<?php foreach ($orders as $order): $amount = ((float)$order['quantity'] * (float)$order['unit_price']) - (float)$order['discount']; ?>
<tr>
<td><strong><?= e($order['order_no']) ?></strong><div class="small text-muted"><?= e(date('d M Y', strtotime($order['created_at']))) ?></div></td>
<td><?= e($order['customer_name']) ?><div class="small text-muted"><?= e($order['customer_phone']) ?></div></td>
<td><?= e($order['project_name']) ?><div class="small text-muted"><?= e($order['category']) ?> · <?= (int)$order['quantity'] ?> pcs</div></td>
<td><span class="badge text-bg-<?= e(statusBadge($order['status'])) ?>"><?= e($order['status']) ?></span></td>
<td><?= e(rupiah(max(0, $amount))) ?><div class="small text-muted">Bayar <?= e(rupiah((float)$order['paid_amount'])) ?></div></td>
<td><div class="btn-group btn-group-sm"><a class="btn btn-outline-dark" href="order_detail.php?id=<?= (int)$order['id'] ?>">Detail</a><a class="btn btn-outline-dark" href="spk.php?id=<?= (int)$order['id'] ?>">SPK</a><a class="btn btn-outline-dark" href="invoice.php?id=<?= (int)$order['id'] ?>">Invoice</a></div></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</section>
</div>
<section id="tracking" class="panel mt-4" aria-labelledby="trackingTitle">
<div class="row g-4 align-items-start">
<div class="col-lg-5">
<div class="section-heading"><p class="eyebrow">Customer Portal</p><h2 id="trackingTitle">Cek progres order</h2></div>
<p class="text-muted">Customer tidak perlu login. Masukkan nomor order dan telepon yang tercatat di order.</p>
<form method="get" class="row g-2">
<div class="col-md-6"><label class="form-label">Nomor Order</label><input class="form-control" name="track_order" value="<?= e((string)($_GET['track_order'] ?? '')) ?>" placeholder="ORD-2026-00001" required></div>
<div class="col-md-6"><label class="form-label">Telepon</label><input class="form-control" name="track_phone" value="<?= e((string)($_GET['track_phone'] ?? '')) ?>" placeholder="08..." required></div>
<div class="col-12"><button class="btn btn-dark" type="submit">Cek Status</button></div>
</form>
</div>
<div class="col-lg-7">
<?php if ($trackResult === null): ?>
<div class="empty-state"><strong>Hasil tracking muncul di sini.</strong><span>Gunakan nomor order dari admin/Kasir.</span></div>
<?php elseif ($trackResult === false): ?>
<div class="alert alert-warning mb-0">Order tidak ditemukan. Pastikan nomor order dan telepon sudah benar.</div>
<?php else: ?>
<div class="tracking-card">
<div class="d-flex justify-content-between gap-3 flex-wrap"><div><span class="small text-muted">Order</span><h3><?= e($trackResult['order_no']) ?></h3></div><span class="badge text-bg-<?= e(statusBadge($trackResult['status'])) ?> align-self-start"><?= e($trackResult['status']) ?></span></div>
<p class="mb-2"><strong><?= e($trackResult['project_name']) ?></strong> — <?= e($trackResult['category']) ?>, <?= (int)$trackResult['quantity'] ?> pcs</p>
<p class="text-muted mb-3"><?= nl2br(e((string)$trackResult['progress_note'])) ?></p>
<ol class="progress-steps">
<?php $current = array_search($trackResult['status'], $statuses, true); foreach ($statuses as $i => $s): ?>
<li class="<?= $i <= (int)$current ? 'done' : '' ?>"><span><?= e($s) ?></span></li>
<?php endforeach; ?>
</ol>
</div>
<?php endif; ?>
</div>
</div>
</section>
</main>
<footer class="container-fluid px-3 px-lg-4 pb-4">
<div class="footer-line">© <?= date('Y') ?> Kenanga Kreasindo · Perum BCL Jln Kenanga Raya · kenangakreasindo@gmail.com</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>