707 lines
37 KiB
PHP
707 lines
37 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require_once 'db/config.php';
|
|
|
|
$current_branch_id = (int)($_GET['branch_id'] ?? 1);
|
|
$db = db();
|
|
|
|
$branches = $db->query("SELECT * FROM branches")->fetchAll();
|
|
$current_branch = array_filter($branches, fn($b) => $b['id'] == $current_branch_id);
|
|
$current_branch = reset($current_branch);
|
|
|
|
$products = $db->query("SELECT p.*, c.name as category_name FROM products p LEFT JOIN categories c ON p.category_id = c.id WHERE p.branch_id = $current_branch_id AND p.stock > 0 ORDER BY p.name ASC")->fetchAll();
|
|
$members = $db->query("SELECT * FROM members ORDER BY name ASC")->fetchAll();
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>POS - Kasir</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
|
<link href="assets/css/custom.css?v=<?php echo time(); ?>" rel="stylesheet">
|
|
<style>
|
|
.pos-container { height: calc(100vh - 80px); }
|
|
.product-grid { height: 100%; overflow-y: auto; }
|
|
.cart-panel { height: 100%; display: flex; flex-direction: column; background: #fff; border-left: 1px solid #e2e8f0; }
|
|
.cart-items { flex-grow: 1; overflow-y: auto; background-color: #f8fafc; }
|
|
.product-card { cursor: pointer; transition: 0.2s; border: 1px solid transparent; }
|
|
.product-card:hover { border-color: var(--accent-color); background-color: #f0fdf4; }
|
|
.bg-member { background-color: #f0fdf4 !important; border-color: #10b981 !important; }
|
|
|
|
.cart-item {
|
|
background: white;
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
padding: 12px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
border: 1px solid #e2e8f0;
|
|
transition: all 0.2s;
|
|
}
|
|
.cart-item:hover {
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
|
border-color: #cbd5e1;
|
|
}
|
|
|
|
/* Print Styles */
|
|
#printSection { display: none; }
|
|
@media print {
|
|
body * { visibility: hidden; }
|
|
#printSection, #printSection * { visibility: visible; }
|
|
#printSection { display: block; position: absolute; left: 0; top: 0; width: 100%; }
|
|
.no-print { display: none !important; }
|
|
}
|
|
.receipt-container { width: 80mm; font-family: 'Courier New', Courier, monospace; font-size: 12px; }
|
|
.invoice-container { width: 100%; font-family: Arial, sans-serif; }
|
|
|
|
.qty-input {
|
|
width: 70px;
|
|
text-align: center;
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 6px;
|
|
padding: 4px;
|
|
font-weight: bold;
|
|
color: #1e293b;
|
|
}
|
|
.qty-input:focus {
|
|
outline: none;
|
|
border-color: #3b82f6;
|
|
ring: 2px rgba(59, 130, 246, 0.2);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-light">
|
|
<nav class="navbar navbar-light bg-white border-bottom p-3 no-print">
|
|
<div class="container-fluid">
|
|
<div class="d-flex align-items-center">
|
|
<a href="index.php?branch_id=<?php echo $current_branch_id; ?>" class="btn btn-outline-dark me-3">
|
|
<i class="bi bi-arrow-left"></i>
|
|
</a>
|
|
<span class="navbar-brand mb-0 h1">Kasir - <?php echo htmlspecialchars($current_branch['name']); ?></span>
|
|
</div>
|
|
<div class="ms-auto d-flex align-items-center">
|
|
<a href="vouchers.php?branch_id=<?php echo $current_branch_id; ?>" class="btn btn-outline-primary me-3">
|
|
<i class="bi bi-ticket-perforated me-1"></i> Voucher
|
|
</a>
|
|
<div class="text-end me-3">
|
|
<div class="small text-muted">Waktu Sekarang</div>
|
|
<div class="fw-bold" id="clock">00:00:00</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container-fluid pos-container no-print">
|
|
<div class="row h-100">
|
|
<!-- Product Section -->
|
|
<div class="col-md-8 p-4 product-grid">
|
|
<div class="row mb-3">
|
|
<div class="col-md-8">
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span>
|
|
<input type="text" id="productSearch" class="form-control border-start-0" placeholder="Cari produk atau scan barcode...">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<select class="form-select" id="categoryFilter">
|
|
<option value="">Semua Kategori</option>
|
|
<?php
|
|
$categories = $db->query("SELECT * FROM categories")->fetchAll();
|
|
foreach($categories as $cat): ?>
|
|
<option value="<?php echo $cat['id']; ?>"><?php echo htmlspecialchars($cat['name']); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3" id="productContainer">
|
|
<?php if (empty($products)): ?>
|
|
<div class="col-12 text-center py-5">
|
|
<i class="bi bi-box-seam display-1 text-muted"></i>
|
|
<p class="mt-3">Produk tidak tersedia. Silakan <a href="products.php?branch_id=<?php echo $current_branch_id; ?> ">tambah produk</a> terlebih dahulu.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($products as $p): ?>
|
|
<div class="col-md-3 product-item" data-name="<?php echo strtolower(htmlspecialchars($p['name'])); ?>" data-category="<?php echo $p['category_id']; ?>">
|
|
<div class="card product-card h-100 shadow-sm border-0" onclick='addToCart(<?php echo json_encode($p); ?>)'>
|
|
<div class="card-body p-3">
|
|
<div class="small text-muted mb-1"><?php echo htmlspecialchars($p['category_name'] ?? 'Umum'); ?></div>
|
|
<h6 class="card-title mb-1"><?php echo htmlspecialchars($p['name']); ?></h6>
|
|
<div class="d-flex flex-column">
|
|
<div class="small text-muted text-decoration-line-through">Rp <?php echo number_format((float)$p['selling_price'], 0, ',', '.'); ?></div>
|
|
<div class="fw-bold text-success">Rp <?php echo number_format((float)$p['member_price'], 0, ',', '.'); ?> <span class="badge bg-soft-success text-success small" style="font-size: 10px;">MEMBER</span></div>
|
|
</div>
|
|
<div class="small text-muted mt-2">Stok: <?php echo $p['stock']; ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cart Section -->
|
|
<div class="col-md-4 p-0 cart-panel shadow-sm">
|
|
<div class="p-3 border-bottom bg-white d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Keranjang Belanja</h5>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="clearCart()">Bersihkan</button>
|
|
</div>
|
|
|
|
<!-- Member Selection -->
|
|
<div class="p-3 border-bottom bg-light">
|
|
<label class="form-label small fw-bold mb-1">Pilih Member (Opsional)</label>
|
|
<select class="form-select mb-2" id="memberSelect" onchange="updateMember()">
|
|
<option value="">-- Pelanggan Umum --</option>
|
|
<?php foreach ($members as $m): ?>
|
|
<option value="<?php echo $m['id']; ?>" data-name="<?php echo htmlspecialchars($m['name']); ?>" data-code="<?php echo htmlspecialchars($m['code']); ?>" data-points="<?php echo $m['points']; ?>"><?php echo htmlspecialchars($m['name']); ?> (<?php echo htmlspecialchars($m['code']); ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<div id="memberInfo" class="mt-2 small d-none">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span>Poin: <span class="fw-bold" id="memberPointsText">0</span></span>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="usePoints" onchange="renderCart()">
|
|
<label class="form-check-label" for="usePoints">Tukar Poin</label>
|
|
</div>
|
|
</div>
|
|
<div id="pointsRedemptionInput" class="mt-2 d-none">
|
|
<input type="number" id="pointsToUse" class="form-control form-control-sm" placeholder="Jumlah poin..." oninput="renderCart()">
|
|
<small class="text-muted">1 Poin = Rp 1</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Voucher Section -->
|
|
<div class="mt-3">
|
|
<label class="form-label small fw-bold mb-1">Voucher Promo</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" id="voucherCode" class="form-control" placeholder="Kode voucher...">
|
|
<button class="btn btn-primary" type="button" onclick="applyVoucher()">Gunakan</button>
|
|
</div>
|
|
<div id="voucherStatus" class="small mt-1 d-none"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cart-items p-3" id="cartItems">
|
|
<!-- Items injected by JS -->
|
|
</div>
|
|
|
|
<div class="p-3 border-top bg-light">
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span>Subtotal</span>
|
|
<span id="subtotal">Rp 0</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2 text-success d-none" id="memberDiscountRow">
|
|
<span>Hemat Member</span>
|
|
<span id="memberSavings">- Rp 0</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2 text-danger d-none" id="pointsDiscountRow">
|
|
<span>Tukar Poin</span>
|
|
<span id="pointsDiscount">- Rp 0</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2 text-danger d-none" id="voucherDiscountRow">
|
|
<span>Diskon Voucher</span>
|
|
<span id="voucherDiscount">- Rp 0</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2 text-info d-none" id="pointsEarnedRow">
|
|
<span>Poin Didapat</span>
|
|
<span id="pointsEarned">0 Pts</span>
|
|
</div>
|
|
<hr>
|
|
<div class="d-flex justify-content-between mb-3">
|
|
<h4 class="mb-0">Total Akhir</h4>
|
|
<h4 class="mb-0 text-primary" id="total">Rp 0</h4>
|
|
</div>
|
|
<button class="btn btn-primary w-100 py-3 fw-bold shadow" id="payBtn" disabled data-bs-toggle="modal" data-bs-target="#payModal">
|
|
PROSES PEMBAYARAN
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Payment Modal -->
|
|
<div class="modal fade" id="payModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header border-0">
|
|
<h5 class="modal-title">Selesaikan Pembayaran</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center mb-4">
|
|
<h6 class="text-muted mb-1">Total yang Harus Dibayar</h6>
|
|
<h2 class="text-primary mb-0" id="modalTotal">Rp 0</h2>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Metode Pembayaran</label>
|
|
<div class="btn-group w-100" role="group">
|
|
<input type="radio" class="btn-check" name="payMethod" id="payCash" value="cash" checked onchange="togglePaymentInputs()">
|
|
<label class="btn btn-outline-primary" for="payCash">Tunai</label>
|
|
|
|
<input type="radio" class="btn-check" name="payMethod" id="payTransfer" value="transfer" onchange="togglePaymentInputs()">
|
|
<label class="btn btn-outline-primary" for="payTransfer">Transfer</label>
|
|
|
|
<input type="radio" class="btn-check" name="payMethod" id="payCredit" value="credit" onchange="togglePaymentInputs()">
|
|
<label class="btn btn-outline-primary" for="payCredit" id="payCreditLabel">Kredit</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="cashInputs">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Pilihan Cepat</label>
|
|
<div class="row g-2">
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(50000)">50rb</button></div>
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(100000)">100rb</button></div>
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(150000)">150rb</button></div>
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(200000)">200rb</button></div>
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(250000)">250rb</button></div>
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(300000)">300rb</button></div>
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(400000)">400rb</button></div>
|
|
<div class="col-3"><button type="button" class="btn btn-outline-secondary w-100 btn-sm" onclick="setCash(500000)">500rb</button></div>
|
|
<div class="col-12"><button type="button" class="btn btn-secondary w-100 btn-sm" onclick="setCash('exact')">Uang Pas</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Uang Diterima</label>
|
|
<input type="number" id="cashReceived" class="form-control form-control-lg" placeholder="Masukkan jumlah...">
|
|
</div>
|
|
<div class="alert alert-secondary d-flex justify-content-between mb-0">
|
|
<span>Kembalian</span>
|
|
<span class="fw-bold" id="changeAmount">Rp 0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" id="cancelPay">Batal</button>
|
|
<button type="button" class="btn btn-success px-4" id="confirmPay" onclick="processPayment()">Selesai & Cetak</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Print Section (Hidden) -->
|
|
<div id="printSection"></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
let cart = [];
|
|
let selectedMemberId = null;
|
|
let selectedMemberName = '';
|
|
let selectedMemberCode = '';
|
|
let selectedMemberPoints = 0;
|
|
let appliedVoucher = null;
|
|
|
|
const subtotalEl = document.getElementById('subtotal');
|
|
const totalEl = document.getElementById('total');
|
|
const cartItemsEl = document.getElementById('cartItems');
|
|
const payBtn = document.getElementById('payBtn');
|
|
const memberSelect = document.getElementById('memberSelect');
|
|
const memberInfo = document.getElementById('memberInfo');
|
|
const memberPointsText = document.getElementById('memberPointsText');
|
|
const memberDiscountRow = document.getElementById('memberDiscountRow');
|
|
const memberSavingsEl = document.getElementById('memberSavings');
|
|
const pointsDiscountRow = document.getElementById('pointsDiscountRow');
|
|
const pointsDiscountEl = document.getElementById('pointsDiscount');
|
|
const voucherDiscountRow = document.getElementById('voucherDiscountRow');
|
|
const voucherDiscountEl = document.getElementById('voucherDiscount');
|
|
const pointsEarnedRow = document.getElementById('pointsEarnedRow');
|
|
const pointsEarnedEl = document.getElementById('pointsEarned');
|
|
const payCredit = document.getElementById('payCredit');
|
|
const payCreditLabel = document.getElementById('payCreditLabel');
|
|
const usePointsToggle = document.getElementById('usePoints');
|
|
const pointsToUseInput = document.getElementById('pointsToUse');
|
|
const pointsRedemptionInput = document.getElementById('pointsRedemptionInput');
|
|
|
|
// Product search & filter
|
|
document.getElementById('productSearch').addEventListener('input', filterProducts);
|
|
document.getElementById('categoryFilter').addEventListener('change', filterProducts);
|
|
|
|
function filterProducts() {
|
|
const search = document.getElementById('productSearch').value.toLowerCase();
|
|
const category = document.getElementById('categoryFilter').value;
|
|
const items = document.querySelectorAll('.product-item');
|
|
|
|
items.forEach(item => {
|
|
const nameMatch = item.dataset.name.includes(search);
|
|
const catMatch = !category || item.dataset.category === category;
|
|
item.classList.toggle('d-none', !(nameMatch && catMatch));
|
|
});
|
|
}
|
|
|
|
function updateMember() {
|
|
selectedMemberId = memberSelect.value;
|
|
if (selectedMemberId) {
|
|
const option = memberSelect.options[memberSelect.selectedIndex];
|
|
selectedMemberName = option.dataset.name;
|
|
selectedMemberCode = option.dataset.code;
|
|
selectedMemberPoints = parseInt(option.dataset.points);
|
|
memberPointsText.innerText = new Intl.NumberFormat('id-ID').format(selectedMemberPoints);
|
|
memberInfo.classList.remove('d-none');
|
|
memberDiscountRow.classList.remove('d-none');
|
|
pointsEarnedRow.classList.remove('d-none');
|
|
payCredit.disabled = false;
|
|
payCreditLabel.classList.remove('disabled');
|
|
} else {
|
|
selectedMemberName = '';
|
|
selectedMemberCode = '';
|
|
selectedMemberPoints = 0;
|
|
memberInfo.classList.add('d-none');
|
|
memberDiscountRow.classList.add('d-none');
|
|
pointsEarnedRow.classList.add('d-none');
|
|
usePointsToggle.checked = false;
|
|
pointsToUseInput.value = '';
|
|
payCredit.disabled = true;
|
|
payCreditLabel.classList.add('disabled');
|
|
if (payCredit.checked) document.getElementById('payCash').checked = true;
|
|
togglePaymentInputs();
|
|
}
|
|
renderCart();
|
|
}
|
|
|
|
async function applyVoucher() {
|
|
const code = document.getElementById('voucherCode').value.trim();
|
|
const statusEl = document.getElementById('voucherStatus');
|
|
if (!code) return;
|
|
|
|
try {
|
|
const response = await fetch(`api/check_voucher.php?code=${code}`);
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
appliedVoucher = result.voucher;
|
|
statusEl.innerText = `Voucher berhasil digunakan: Rp ${new Intl.NumberFormat('id-ID').format(appliedVoucher.discount_amount)}`;
|
|
statusEl.className = 'small mt-1 text-success';
|
|
statusEl.classList.remove('d-none');
|
|
} else {
|
|
appliedVoucher = null;
|
|
statusEl.innerText = result.error || 'Kode voucher tidak valid';
|
|
statusEl.className = 'small mt-1 text-danger';
|
|
statusEl.classList.remove('d-none');
|
|
}
|
|
renderCart();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
function addToCart(product) {
|
|
const existing = cart.find(item => item.id === product.id);
|
|
if (existing) {
|
|
existing.qty++;
|
|
} else {
|
|
cart.push({...product, qty: 1});
|
|
}
|
|
renderCart();
|
|
}
|
|
|
|
function removeFromCart(id) {
|
|
cart = cart.filter(item => item.id !== id);
|
|
renderCart();
|
|
}
|
|
|
|
function updateQty(id, qty) {
|
|
const item = cart.find(i => i.id === id);
|
|
if (item) {
|
|
item.qty = Math.max(0, parseInt(qty) || 0);
|
|
if (item.qty === 0) removeFromCart(id);
|
|
else renderCart();
|
|
}
|
|
}
|
|
|
|
function renderCart() {
|
|
cartItemsEl.innerHTML = '';
|
|
let subtotal = 0;
|
|
let total = 0;
|
|
let savings = 0;
|
|
|
|
if (cart.length === 0) {
|
|
cartItemsEl.innerHTML = '<div class="text-center py-5 text-muted"><i class="bi bi-cart-x display-4"></i><p class="mt-2">Keranjang masih kosong</p></div>';
|
|
payBtn.disabled = true;
|
|
} else {
|
|
payBtn.disabled = false;
|
|
cart.forEach(item => {
|
|
const price = selectedMemberId ? item.member_price : item.selling_price;
|
|
const itemTotal = item.qty * price;
|
|
|
|
subtotal += item.qty * item.selling_price;
|
|
total += itemTotal;
|
|
savings += (item.qty * item.selling_price) - itemTotal;
|
|
|
|
const div = document.createElement('div');
|
|
div.className = `cart-item ${selectedMemberId ? 'bg-member' : ''}`;
|
|
div.innerHTML = `
|
|
<div class="d-flex align-items-start">
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold text-dark mb-1" style="font-size: 1.05rem;">${item.name}</div>
|
|
<div class="text-muted small">
|
|
Harga Satuan: Rp ${new Intl.NumberFormat('id-ID').format(price)}
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-sm text-danger" onclick="removeFromCart(${item.id})"><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center mt-3">
|
|
<div class="d-flex align-items-center">
|
|
<span class="small text-muted me-2">Jumlah:</span>
|
|
<input type="number" class="qty-input" value="${item.qty}" onchange="updateQty(${item.id}, this.value)">
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="small text-muted">Subtotal Item</div>
|
|
<div class="fw-bold text-primary" style="font-size: 1.1rem;">
|
|
Rp ${new Intl.NumberFormat('id-ID').format(itemTotal)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
cartItemsEl.appendChild(div);
|
|
});
|
|
}
|
|
|
|
// Handle points redemption
|
|
let pointsDiscount = 0;
|
|
if (usePointsToggle.checked) {
|
|
pointsRedemptionInput.classList.remove('d-none');
|
|
let ptsToUse = parseInt(pointsToUseInput.value) || 0;
|
|
if (ptsToUse > selectedMemberPoints) {
|
|
ptsToUse = selectedMemberPoints;
|
|
pointsToUseInput.value = ptsToUse;
|
|
}
|
|
pointsDiscount = ptsToUse;
|
|
} else {
|
|
pointsRedemptionInput.classList.add('d-none');
|
|
}
|
|
|
|
// Handle voucher
|
|
let voucherDiscount = 0;
|
|
if (appliedVoucher) {
|
|
voucherDiscount = parseFloat(appliedVoucher.discount_amount);
|
|
voucherDiscountRow.classList.remove('d-none');
|
|
} else {
|
|
voucherDiscountRow.classList.add('d-none');
|
|
}
|
|
|
|
total = Math.max(0, total - pointsDiscount - voucherDiscount);
|
|
|
|
subtotalEl.innerText = 'Rp ' + new Intl.NumberFormat('id-ID').format(subtotal);
|
|
totalEl.innerText = 'Rp ' + new Intl.NumberFormat('id-ID').format(total);
|
|
memberSavingsEl.innerText = '- Rp ' + new Intl.NumberFormat('id-ID').format(savings);
|
|
|
|
if (pointsDiscount > 0) {
|
|
pointsDiscountRow.classList.remove('d-none');
|
|
pointsDiscountEl.innerText = '- Rp ' + new Intl.NumberFormat('id-ID').format(pointsDiscount);
|
|
} else {
|
|
pointsDiscountRow.classList.add('d-none');
|
|
}
|
|
|
|
if (voucherDiscount > 0) {
|
|
voucherDiscountEl.innerText = '- Rp ' + new Intl.NumberFormat('id-ID').format(voucherDiscount);
|
|
}
|
|
|
|
const earned = selectedMemberId ? Math.floor(total / 10000) : 0;
|
|
pointsEarnedEl.innerText = earned + ' Poin';
|
|
document.getElementById('modalTotal').innerText = 'Rp ' + new Intl.NumberFormat('id-ID').format(total);
|
|
updateChange();
|
|
}
|
|
|
|
function clearCart() {
|
|
if (confirm('Bersihkan semua item di keranjang?')) {
|
|
cart = [];
|
|
appliedVoucher = null;
|
|
document.getElementById('voucherCode').value = '';
|
|
document.getElementById('voucherStatus').classList.add('d-none');
|
|
renderCart();
|
|
}
|
|
}
|
|
|
|
// Clock
|
|
setInterval(() => {
|
|
const now = new Date();
|
|
document.getElementById('clock').innerText = now.toLocaleTimeString('id-ID');
|
|
}, 1000);
|
|
|
|
// Payment Logic
|
|
const cashReceivedInput = document.getElementById('cashReceived');
|
|
const changeAmountEl = document.getElementById('changeAmount');
|
|
const cashInputs = document.getElementById('cashInputs');
|
|
|
|
function togglePaymentInputs() {
|
|
const method = document.querySelector('input[name="payMethod"]:checked').value;
|
|
if (method === 'cash') {
|
|
cashInputs.classList.remove('d-none');
|
|
} else {
|
|
cashInputs.classList.add('d-none');
|
|
cashReceivedInput.value = '';
|
|
updateChange();
|
|
}
|
|
}
|
|
|
|
function setCash(amount) {
|
|
const currentTotal = parseInt(totalEl.innerText.replace(/[^0-9]/g, ''));
|
|
if (amount === 'exact') {
|
|
cashReceivedInput.value = currentTotal;
|
|
} else {
|
|
cashReceivedInput.value = amount;
|
|
}
|
|
updateChange();
|
|
}
|
|
|
|
function updateChange() {
|
|
const currentTotal = parseInt(totalEl.innerText.replace(/[^0-9]/g, ''));
|
|
const cash = parseFloat(cashReceivedInput.value) || 0;
|
|
const change = cash - currentTotal;
|
|
changeAmountEl.innerText = 'Rp ' + new Intl.NumberFormat('id-ID').format(Math.max(0, change));
|
|
}
|
|
|
|
cashReceivedInput.addEventListener('input', updateChange);
|
|
|
|
async function processPayment() {
|
|
const currentTotal = parseInt(totalEl.innerText.replace(/[^0-9]/g, ''));
|
|
const method = document.querySelector('input[name="payMethod"]:checked').value;
|
|
const cash = parseFloat(cashReceivedInput.value) || 0;
|
|
const change = Math.max(0, cash - currentTotal);
|
|
|
|
if (method === 'cash' && cash < currentTotal) {
|
|
alert('Uang yang diterima kurang dari total pembayaran!');
|
|
return;
|
|
}
|
|
|
|
const confirmBtn = document.getElementById('confirmPay');
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Memproses...';
|
|
|
|
const payload = {
|
|
member_id: selectedMemberId,
|
|
total: currentTotal,
|
|
payment_method: method,
|
|
cash_received: method === 'cash' ? cash : currentTotal,
|
|
change_amount: method === 'cash' ? change : 0,
|
|
points_redeemed: usePointsToggle.checked ? parseInt(pointsToUseInput.value) || 0 : 0,
|
|
voucher_id: appliedVoucher ? appliedVoucher.id : null,
|
|
items: cart.map(item => ({
|
|
id: item.id,
|
|
qty: item.qty,
|
|
price: selectedMemberId ? item.member_price : item.selling_price,
|
|
name: item.name
|
|
}))
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('api/process_sale.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
payload.invoice_no = result.invoice_no;
|
|
payload.date = new Date().toLocaleString('id-ID');
|
|
|
|
if (selectedMemberId) {
|
|
renderInvoice(payload);
|
|
} else {
|
|
renderReceipt(payload);
|
|
}
|
|
|
|
window.print();
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + result.error);
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.innerText = 'Selesai & Cetak';
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
alert('Terjadi kesalahan jaringan.');
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.innerText = 'Selesai & Cetak';
|
|
}
|
|
}
|
|
|
|
function renderReceipt(data) {
|
|
const printEl = document.getElementById('printSection');
|
|
let itemsHtml = '';
|
|
data.items.forEach(item => {
|
|
itemsHtml += `<div style="display: flex; justify-content: space-between;"><span>${item.name} x ${item.qty}</span><span>${new Intl.NumberFormat('id-ID').format(item.qty * item.price)}</span></div>`;
|
|
});
|
|
|
|
printEl.innerHTML = `
|
|
<div class="receipt-container mx-auto">
|
|
<div style="text-align: center; margin-bottom: 10px;">
|
|
<h4 style="margin: 0;"><?php echo htmlspecialchars($current_branch['name']); ?></h4>
|
|
<div style="font-size: 10px;"><?php echo htmlspecialchars($current_branch['location'] ?? ''); ?></div>
|
|
</div>
|
|
<div style="border-top: 1px dashed #000; margin: 5px 0;"></div>
|
|
<div style="font-size: 10px; margin-bottom: 10px;">
|
|
<div>No: ${data.invoice_no}</div>
|
|
<div>Tgl: ${data.date}</div>
|
|
<div>Kasir: Admin</div>
|
|
</div>
|
|
<div style="border-top: 1px dashed #000; margin: 5px 0;"></div>
|
|
${itemsHtml}
|
|
${data.points_redeemed > 0 ? `<div style="display: flex; justify-content: space-between; font-size: 10px;"><span>Tukar Poin</span><span>-Rp ${new Intl.NumberFormat('id-ID').format(data.points_redeemed)}</span></div>` : ''}
|
|
${data.voucher_id ? `<div style="display: flex; justify-content: space-between; font-size: 10px;"><span>Voucher</span><span>-Rp ${new Intl.NumberFormat('id-ID').format(appliedVoucher.discount_amount)}</span></div>` : ''}
|
|
<div style="border-top: 1px dashed #000; margin: 5px 0;"></div>
|
|
<div style="display: flex; justify-content: space-between; font-weight: bold;">
|
|
<span>TOTAL</span>
|
|
<span>Rp ${new Intl.NumberFormat('id-ID').format(data.total)}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span>Metode</span>
|
|
<span>${data.payment_method.toUpperCase()}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span>Bayar</span>
|
|
<span>Rp ${new Intl.NumberFormat('id-ID').format(data.cash_received)}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span>Kembali</span>
|
|
<span>Rp ${new Intl.NumberFormat('id-ID').format(data.change_amount)}</span>
|
|
</div>
|
|
<div style="border-top: 1px dashed #000; margin: 5px 0;"></div>
|
|
<div style="text-align: center; margin-top: 10px; font-size: 10px;">
|
|
Terima kasih atas kunjungan Anda!
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderInvoice(data) {
|
|
const printEl = document.getElementById('printSection');
|
|
let itemsHtml = '';
|
|
data.items.forEach((item, index) => {
|
|
itemsHtml += `<tr><td>${index + 1}</td><td>${item.name}</td><td class="text-end">${item.qty}</td><td class="text-end">Rp ${new Intl.NumberFormat('id-ID').format(item.price)}</td><td class="text-end">Rp ${new Intl.NumberFormat('id-ID').format(item.qty * item.price)}</td></tr>`;
|
|
});
|
|
|
|
printEl.innerHTML = `
|
|
<div class="invoice-container p-4">
|
|
<div class="row mb-4">
|
|
<div class="col-6">
|
|
<h3>NOTA FAKTUR</h3>
|
|
<h5 class="text-primary"><?php echo htmlspecialchars($current_branch['name']); ?></h5>
|
|
<p class="small text-muted">${data.date}</p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<p class="mb-1"><strong>No Invoice:</strong> ${data.invoice_no}</p>
|
|
<p class="mb-1"><strong>Pelanggan:</strong> ${selectedMemberName}</p>
|
|
<p class="mb-0"><strong>Kode:</strong> ${selectedMemberCode}</p>
|
|
</div>
|
|
</div>
|
|
<table class="table table-bordered">
|
|
<thead class="bg-light">
|
|
<tr><th>#</th><th>Nama Produk</th><th class="text-end">Qty</th><th class="text-end">Harga</th><th class="text-end">Subtotal</th></tr>
|
|
</thead>
|
|
<tbody>${itemsHtml}</tbody>
|
|
<tfoot>
|
|
${data.points_redeemed > 0 ? `<tr><td colspan="4" class="text-end">Tukar Poin</td><td class="text-end">- Rp ${new Intl.NumberFormat('id-ID').format(data.points_redeemed)}</td></tr>` : ''}
|
|
${data.voucher_id ? `<tr><td colspan="4" class="text-end">Diskon Voucher</td><td class="text-end">- Rp ${new Intl.NumberFormat('id-ID').format(appliedVoucher.discount_amount)}</td></tr>` : ''}
|
|
<tr><td colspan="4" class="text-end fw-bold">TOTAL AKHIR</td><td class="text-end fw-bold">Rp ${new Intl.NumberFormat('id-ID').format(data.total)}</td></tr>
|
|
<tr><td colspan="4" class="text-end">Metode Pembayaran</td><td class="text-end">${data.payment_method.toUpperCase()}</td></tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|