๐Ÿ›’ Modul 5: Transaksi POS

Sistem kasir: pilih produk, keranjang, hitung total, dan simpan transaksi

1. Alur Transaksi POS
๐Ÿ”
Cari Produk
โ†’
๐Ÿ›’
Tambah ke
Keranjang
โ†’
๐Ÿ’ฐ
Input
Uang Bayar
โ†’
๐Ÿ’พ
Simpan ke
Database
โ†’
๐Ÿงพ
Cetak
Nota
๐Ÿ’ก Cara kerja keranjang:
Keranjang belanja disimpan di $_SESSION['keranjang']. Data ini sementara dan hilang saat browser ditutup atau session dihancurkan. Setelah transaksi selesai, data dipindahkan ke database.
2. Halaman Kasir: transaksi/index.php
๐Ÿ“„ transaksi/index.php โ€” Halaman POS / Kasir
<?php
// ============================================
// FILE: transaksi/index.php
// Halaman utama kasir / POS
// ============================================

$pageTitle = 'Transaksi POS';
require_once '../config/database.php';
require_once '../includes/functions.php';
require_once '../includes/header.php';

// Inisialisasi keranjang di session jika belum ada
if (!isset($_SESSION['keranjang'])) {
    $_SESSION['keranjang'] = [];
}

// ============================================
// AKSI KERANJANG
// Semua aksi dikirim via POST
// ============================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $aksi = $_POST['aksi'] ?? '';
    
    // ---------- TAMBAH PRODUK KE KERANJANG ----------
    if ($aksi === 'tambah') {
        $id_produk = (int)$_POST['id_produk'];
        $qty = max(1, (int)($_POST['qty'] ?? 1));
        
        // Ambil data produk dari DB
        $stmt = $pdo->prepare("SELECT * FROM produk WHERE id = ?");
        $stmt->execute([$id_produk]);
        $produk = $stmt->fetch();
        
        if ($produk) {
            // Cek stok cukup
            $qty_total_baru = $qty;
            if (isset($_SESSION['keranjang'][$id_produk])) {
                $qty_total_baru += $_SESSION['keranjang'][$id_produk]['qty'];
            }
            
            if ($qty_total_baru > $produk['stok']) {
                $_SESSION['pesan_error'] = 'Stok tidak mencukupi! Stok tersedia: ' . $produk['stok'];
            } else {
                // Tambah atau update keranjang
                if (isset($_SESSION['keranjang'][$id_produk])) {
                    $_SESSION['keranjang'][$id_produk]['qty'] += $qty;
                    $_SESSION['keranjang'][$id_produk]['subtotal'] = 
                        $_SESSION['keranjang'][$id_produk]['qty'] * $produk['harga_jual'];
                } else {
                    $_SESSION['keranjang'][$id_produk] = [
                        'id_produk'     => $produk['id'],
                        'nama_produk'   => $produk['nama_produk'],
                        'harga_satuan'  => $produk['harga_jual'],
                        'qty'           => $qty,
                        'subtotal'      => $produk['harga_jual'] * $qty,
                    ];
                }
            }
        }
    }
    
    // ---------- HAPUS ITEM DARI KERANJANG ----------
    if ($aksi === 'hapus') {
        $id_produk = (int)$_POST['id_produk'];
        unset($_SESSION['keranjang'][$id_produk]);
    }
    
    // ---------- KOSONGKAN KERANJANG ----------
    if ($aksi === 'kosongkan') {
        $_SESSION['keranjang'] = [];
    }
    
    // Redirect untuk cegah re-submit saat refresh
    header('Location: index.php');
    exit();
}

// Hitung total keranjang
$total = 0;
foreach ($_SESSION['keranjang'] as $item) {
    $total += $item['subtotal'];
}

// Ambil semua produk untuk pilihan
$produkList = $pdo->query(
    "SELECT p.*, k.nama_kategori 
     FROM produk p 
     LEFT JOIN kategori k ON p.id_kategori = k.id 
     WHERE p.stok > 0 
     ORDER BY p.nama_produk"
)->fetchAll();

// Ambil pesan error/sukses jika ada
$pesanError = $_SESSION['pesan_error'] ?? '';
unset($_SESSION['pesan_error']);
?>

<div class="row g-4">
    
    <!-- KOLOM KIRI: Daftar Produk -->
    <div class="col-lg-7">
        <div class="card border-0 shadow-sm">
            <div class="card-header bg-primary text-white fw-bold">
                ๐Ÿ” Pilih Produk
            </div>
            <div class="card-body">
                
                <!-- Search produk -->
                <input type="text" id="searchProduk" class="form-control mb-3"
                       placeholder="Cari nama produk...">
                
                <?php if ($pesanError): ?>
                    <div class="alert alert-danger py-2 small">
                        โš ๏ธ <?= htmlspecialchars($pesanError) ?>
                    </div>
                <?php endif; ?>
                
                <div class="row g-2" id="daftarProduk">
                    <?php foreach ($produkList as $p): ?>
                    <div class="col-md-6 produk-item" 
                         data-nama="<?= strtolower($p['nama_produk']) ?>">
                        <form method="POST">
                            <input type="hidden" name="aksi" value="tambah">
                            <input type="hidden" name="id_produk" value="<?= $p['id'] ?>">
                            <input type="hidden" name="qty" value="1">
                            <button type="submit" 
                                    class="btn btn-outline-primary w-100 text-start p-2 h-100">
                                <div class="d-flex justify-content-between align-items-start">
                                    <div>
                                        <div class="fw-semibold small">
                                            <?= htmlspecialchars($p['nama_produk']) ?>
                                        </div>
                                        <div class="text-muted" style="font-size:0.75rem">
                                            Stok: <?= $p['stok'] ?>
                                        </div>
                                    </div>
                                    <div class="text-success fw-bold small">
                                        <?= formatRupiah($p['harga_jual']) ?>
                                    </div>
                                </div>
                            </button>
                        </form>
                    </div>
                    <?php endforeach; ?>
                </div>
            </div>
        </div>
    </div>
    
    <!-- KOLOM KANAN: Keranjang & Bayar -->
    <div class="col-lg-5">
        <div class="card border-0 shadow-sm">
            <div class="card-header bg-success text-white fw-bold d-flex justify-content-between">
                ๐Ÿ›’ Keranjang
                <form method="POST" class="mb-0">
                    <input type="hidden" name="aksi" value="kosongkan">
                    <button type="submit" class="btn btn-sm btn-outline-light"
                            onclick="return confirm('Kosongkan keranjang?')">
                        Kosongkan
                    </button>
                </form>
            </div>
            <div class="card-body p-0">
                
                <!-- Daftar item keranjang -->
                <?php if (empty($_SESSION['keranjang'])): ?>
                    <div class="text-center py-5 text-muted">
                        <i class="fas fa-shopping-cart fa-3x mb-3 opacity-25 d-block"></i>
                        Keranjang kosong<br>
                        <small>Klik produk di sebelah kiri untuk menambahkan</small>
                    </div>
                <?php else: ?>
                    <table class="table table-sm mb-0">
                        <thead class="table-light">
                            <tr><th>Produk</th><th class="text-center">Qty</th>
                                <th class="text-end">Subtotal</th><th></th></tr>
                        </thead>
                        <tbody>
                        <?php foreach ($_SESSION['keranjang'] as $id => $item): ?>
                        <tr>
                            <td>
                                <div class="small fw-semibold">
                                    <?= htmlspecialchars($item['nama_produk']) ?>
                                </div>
                                <div class="text-muted" style="font-size:0.75rem">
                                    <?= formatRupiah($item['harga_satuan']) ?> /pcs
                                </div>
                            </td>
                            <td class="text-center">
                                <span class="badge bg-primary"><?= $item['qty'] ?></span>
                            </td>
                            <td class="text-end small fw-bold">
                                <?= formatRupiah($item['subtotal']) ?>
                            </td>
                            <td>
                                <form method="POST">
                                    <input type="hidden" name="aksi" value="hapus">
                                    <input type="hidden" name="id_produk" value="<?= $id ?>">
                                    <button type="submit" class="btn btn-sm btn-outline-danger">
                                        <i class="fas fa-times"></i>
                                    </button>
                                </form>
                            </td>
                        </tr>
                        <?php endforeach; ?>
                        </tbody>
                    </table>
                <?php endif; ?>
            </div>
            
            <!-- Form Pembayaran -->
            <?php if (!empty($_SESSION['keranjang'])): ?>
            <div class="card-footer bg-light">
                <div class="d-flex justify-content-between mb-2">
                    <span class="fw-bold">TOTAL:</span>
                    <span class="h5 fw-bold text-success mb-0">
                        <?= formatRupiah($total) ?>
                    </span>
                </div>
                
                <form method="POST" action="proses.php">
                    <div class="mb-2">
                        <label class="form-label fw-semibold small">Uang Bayar:</label>
                        <div class="input-group">
                            <span class="input-group-text">Rp</span>
                            <input type="number" name="bayar" id="inputBayar"
                                   class="form-control fw-bold"
                                   min="<?= $total ?>" placeholder="0"
                                   oninput="hitungKembalian(this.value)"
                                   required>
                        </div>
                    </div>
                    <div class="d-flex justify-content-between mb-3">
                        <span class="small">Kembalian:</span>
                        <span class="fw-bold text-primary" id="kembalian">Rp 0</span>
                    </div>
                    <input type="hidden" name="total" value="<?= $total ?>">
                    <button type="submit" class="btn btn-success w-100 py-2 fw-bold">
                        <i class="fas fa-check-circle me-2"></i>BAYAR & SIMPAN
                    </button>
                </form>
            </div>
            <?php endif; ?>
        </div>
    </div>
</div>

<script>
// Hitung kembalian real-time
function hitungKembalian(bayar) {
    const total = <?= $total ?>;
    const kembalian = parseInt(bayar || 0) - total;
    const el = document.getElementById('kembalian');
    if (kembalian >= 0) {
        el.textContent = 'Rp ' + kembalian.toLocaleString('id-ID');
        el.className = 'fw-bold text-success';
    } else {
        el.textContent = 'Uang kurang!';
        el.className = 'fw-bold text-danger';
    }
}

// Filter produk saat mengetik
document.getElementById('searchProduk').addEventListener('input', function() {
    const q = this.value.toLowerCase();
    document.querySelectorAll('.produk-item').forEach(el => {
        el.style.display = el.dataset.nama.includes(q) ? '' : 'none';
    });
});
</script>

<?php require_once '../includes/footer.php'; ?>
3. Proses Simpan Transaksi: transaksi/proses.php

File ini menerima data dari form kasir, menyimpan ke 2 tabel sekaligus, dan mengurangi stok. Kita pakai PDO Transaction agar jika salah satu gagal, semua dibatalkan.

๐Ÿ“„ transaksi/proses.php PDO Transaction
<?php
// ============================================
// FILE: transaksi/proses.php
// Menyimpan transaksi ke database
// ============================================

session_start();
require_once '../config/database.php';
require_once '../includes/functions.php';

// Validasi: keranjang tidak boleh kosong
if (empty($_SESSION['keranjang'])) {
    header('Location: index.php');
    exit();
}

// Ambil data dari form
$total = (float)($_POST['total'] ?? 0);
$bayar = (float)($_POST['bayar'] ?? 0);

// Validasi: bayar tidak boleh kurang dari total
if ($bayar < $total) {
    $_SESSION['pesan_error'] = 'Uang bayar kurang!';
    header('Location: index.php');
    exit();
}

$kembalian = $bayar - $total;
$id_user = $_SESSION['user_id'];
$no_transaksi = generateNoTransaksi($pdo); // Dari functions.php

// ============================================
// SIMPAN DENGAN PDO TRANSACTION
// Transaksi DB = "semua berhasil atau semua dibatalkan"
// ============================================
try {
    // Mulai transaksi database
    $pdo->beginTransaction();
    
    // 1. Simpan ke tabel TRANSAKSI (header)
    $stmt = $pdo->prepare(
        "INSERT INTO transaksi (no_transaksi, total, bayar, kembalian, id_user) 
         VALUES (:no, :total, :bayar, :kembalian, :user)"
    );
    $stmt->execute([
        ':no'        => $no_transaksi,
        ':total'     => $total,
        ':bayar'     => $bayar,
        ':kembalian' => $kembalian,
        ':user'      => $id_user,
    ]);
    
    // Ambil ID transaksi yang baru saja dibuat
    $id_transaksi = $pdo->lastInsertId();
    
    // 2. Simpan setiap item ke DETAIL_TRANSAKSI
    $stmtDetail = $pdo->prepare(
        "INSERT INTO detail_transaksi 
             (id_transaksi, id_produk, nama_produk, harga_satuan, qty, subtotal)
         VALUES 
             (:id_trx, :id_prd, :nama, :harga, :qty, :subtotal)"
    );
    
    // 3. Update stok produk
    $stmtStok = $pdo->prepare(
        "UPDATE produk SET stok = stok - :qty WHERE id = :id AND stok >= :qty"
    );
    
    foreach ($_SESSION['keranjang'] as $item) {
        // Simpan detail transaksi
        $stmtDetail->execute([
            ':id_trx'  => $id_transaksi,
            ':id_prd'  => $item['id_produk'],
            ':nama'    => $item['nama_produk'],
            ':harga'   => $item['harga_satuan'],
            ':qty'     => $item['qty'],
            ':subtotal'=> $item['subtotal'],
        ]);
        
        // Kurangi stok
        $stmtStok->execute([
            ':qty' => $item['qty'],
            ':id'  => $item['id_produk'],
        ]);
        
        // Cek apakah stok berhasil dikurangi
        if ($stmtStok->rowCount() === 0) {
            // Stok tidak mencukupi! Batalkan semua
            throw new Exception(
                "Stok {$item['nama_produk']} tidak mencukupi!"
            );
        }
    }
    
    // Semua berhasil! Commit transaksi
    $pdo->commit();
    
    // Kosongkan keranjang
    $_SESSION['keranjang'] = [];
    
    // Redirect ke nota/detail transaksi
    header("Location: detail.php?id={$id_transaksi}&baru=1");
    exit();
    
} catch (Exception $e) {
    // Ada yang gagal! Batalkan SEMUA perubahan
    $pdo->rollBack();
    
    $_SESSION['pesan_error'] = 'Transaksi gagal: ' . $e->getMessage();
    header('Location: index.php');
    exit();
}
?>
๐Ÿ’ก Apa itu PDO Transaction (beginTransaction)?
  • $pdo->beginTransaction() โ€” Mulai "blok operasi"
  • $pdo->commit() โ€” Konfirmasi semua perubahan ke database
  • $pdo->rollBack() โ€” Batalkan SEMUA perubahan jika ada error
  • Ini penting karena transaksi menyentuh 2 tabel sekaligus. Jika menyimpan header berhasil tapi detail gagal, data akan tidak konsisten tanpa transaction!
4. Nota / Detail Transaksi: transaksi/detail.php
๐Ÿ“„ transaksi/detail.php โ€” Nota Transaksi
<?php
// ============================================
// FILE: transaksi/detail.php
// Menampilkan nota / struk transaksi
// ============================================

$pageTitle = 'Detail Transaksi';
require_once '../config/database.php';
require_once '../includes/functions.php';
require_once '../includes/header.php';

$id = (int)($_GET['id'] ?? 0);
$isBaru = isset($_GET['baru']); // Apakah baru saja selesai transaksi?

// Ambil data transaksi header
$stmt = $pdo->prepare(
    "SELECT t.*, u.nama as nama_kasir 
     FROM transaksi t 
     LEFT JOIN users u ON t.id_user = u.id 
     WHERE t.id = ?"
);
$stmt->execute([$id]);
$trx = $stmt->fetch();

if (!$trx) {
    echo "<div class='alert alert-danger m-4'>Transaksi tidak ditemukan.</div>";
    require_once '../includes/footer.php';
    exit();
}

// Ambil detail item transaksi
$stmtDetail = $pdo->prepare(
    "SELECT * FROM detail_transaksi WHERE id_transaksi = ?"
);
$stmtDetail->execute([$id]);
$items = $stmtDetail->fetchAll();
?>

<div class="row justify-content-center">
    <div class="col-lg-6">
        
        <?php if ($isBaru): ?>
        <div class="alert alert-success text-center">
            <i class="fas fa-check-circle fa-2x mb-2 d-block"></i>
            <strong>Transaksi Berhasil!</strong>
            <div class="mt-2">
                <a href="index.php" class="btn btn-success btn-sm me-2">
                    Transaksi Baru
                </a>
                <button onclick="window.print()" class="btn btn-outline-success btn-sm">
                    <i class="fas fa-print me-1"></i>Cetak Nota
                </button>
            </div>
        </div>
        <?php endif; ?>
        
        <!-- NOTA / STRUK -->
        <div class="card border-0 shadow" id="nota">
            <div class="card-body p-4">
                
                <!-- Header Nota -->
                <div class="text-center mb-3 pb-3 border-bottom">
                    <h5 class="fw-bold mb-1">๐Ÿ›’ TOKO SERBA ADA</h5>
                    <small class="text-muted">Jl. Contoh No. 123, Purwokerto</small>
                    <div class="mt-2">
                        <strong>STRUK PEMBELIAN</strong>
                    </div>
                </div>
                
                <!-- Info Transaksi -->
                <div class="row mb-3 small">
                    <div class="col-6">
                        <div>No: <strong><?= $trx['no_transaksi'] ?></strong></div>
                        <div>Kasir: <?= htmlspecialchars($trx['nama_kasir']) ?></div>
                    </div>
                    <div class="col-6 text-end">
                        <div><?= date('d/m/Y', strtotime($trx['tanggal'])) ?></div>
                        <div><?= date('H:i', strtotime($trx['tanggal'])) ?> WIB</div>
                    </div>
                </div>
                
                <!-- Daftar Item -->
                <table class="table table-sm border-top border-bottom mb-3">
                    <?php foreach ($items as $item): ?>
                    <tr>
                        <td>
                            <div class="small fw-semibold">
                                <?= htmlspecialchars($item['nama_produk']) ?>
                            </div>
                            <div class="text-muted" style="font-size:0.75rem">
                                <?= $item['qty'] ?> x <?= formatRupiah($item['harga_satuan']) ?>
                            </div>
                        </td>
                        <td class="text-end small">
                            <?= formatRupiah($item['subtotal']) ?>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                </table>
                
                <!-- Total & Kembalian -->
                <div class="row mb-1">
                    <div class="col-6">Total</div>
                    <div class="col-6 text-end fw-bold">
                        <?= formatRupiah($trx['total']) ?>
                    </div>
                </div>
                <div class="row mb-1">
                    <div class="col-6">Bayar</div>
                    <div class="col-6 text-end">
                        <?= formatRupiah($trx['bayar']) ?>
                    </div>
                </div>
                <div class="row border-top pt-2">
                    <div class="col-6 fw-bold">Kembalian</div>
                    <div class="col-6 text-end fw-bold text-success h5 mb-0">
                        <?= formatRupiah($trx['kembalian']) ?>
                    </div>
                </div>
                
                <div class="text-center mt-4 text-muted small border-top pt-3">
                    Terima kasih sudah berbelanja! ๐Ÿ™
                </div>
                
            </div>
        </div>
    </div>
</div>

<?php require_once '../includes/footer.php'; ?>
5. Ringkasan PDO Methods yang Dipakai
Method PDOFungsiContoh
$pdo->query()Query sederhana tanpa parameterSELECT * FROM produk
$pdo->prepare()Siapkan query dengan parameter (aman!)SELECT WHERE id = ?
$stmt->execute()Jalankan prepared statementexecute([$id])
$stmt->fetch()Ambil satu baris hasil querySatu produk
$stmt->fetchAll()Ambil semua baris sekaligusSemua produk
$stmt->fetchColumn()Ambil satu nilai sajaCOUNT(*)
$pdo->lastInsertId()ID terakhir yang di-INSERTSetelah INSERT
$pdo->beginTransaction()Mulai transaksi DBSebelum operasi ganda
$pdo->commit()Konfirmasi transaksiSetelah semua berhasil
$pdo->rollBack()Batalkan transaksiDi blok catch