$_SESSION['keranjang']. Data ini sementara dan hilang saat browser ditutup atau session dihancurkan. Setelah transaksi selesai, data dipindahkan ke database.
<?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'; ?>
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.
<?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();
}
?>
$pdo->beginTransaction() โ Mulai "blok operasi"$pdo->commit() โ Konfirmasi semua perubahan ke database$pdo->rollBack() โ Batalkan SEMUA perubahan jika ada error<?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'; ?>
| Method PDO | Fungsi | Contoh |
|---|---|---|
$pdo->query() | Query sederhana tanpa parameter | SELECT * FROM produk |
$pdo->prepare() | Siapkan query dengan parameter (aman!) | SELECT WHERE id = ? |
$stmt->execute() | Jalankan prepared statement | execute([$id]) |
$stmt->fetch() | Ambil satu baris hasil query | Satu produk |
$stmt->fetchAll() | Ambil semua baris sekaligus | Semua produk |
$stmt->fetchColumn() | Ambil satu nilai saja | COUNT(*) |
$pdo->lastInsertId() | ID terakhir yang di-INSERT | Setelah INSERT |
$pdo->beginTransaction() | Mulai transaksi DB | Sebelum operasi ganda |
$pdo->commit() | Konfirmasi transaksi | Setelah semua berhasil |
$pdo->rollBack() | Batalkan transaksi | Di blok catch |