Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
0488c2eb9c Autosave: 20260207-113906 2026-02-07 11:39:06 +00:00
15 changed files with 2079 additions and 524 deletions

22
api/check_voucher.php Normal file
View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require_once '../db/config.php';
$code = $_GET['code'] ?? '';
if (empty($code)) {
echo json_encode(['success' => false, 'error' => 'Kode voucher harus diisi']);
exit;
}
$db = db();
$stmt = $db->prepare("SELECT * FROM vouchers WHERE code = ? AND is_used = 0 AND (expires_at IS NULL OR expires_at > NOW())");
$stmt->execute([$code]);
$voucher = $stmt->fetch();
if ($voucher) {
echo json_encode(['success' => true, 'voucher' => $voucher]);
} else {
echo json_encode(['success' => false, 'error' => 'Voucher tidak valid atau sudah kedaluwarsa']);
}

62
api/process_sale.php Normal file
View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require_once '../db/config.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || empty($data['items'])) {
echo json_encode(['success' => false, 'error' => 'Data permintaan tidak valid']);
exit;
}
$db = db();
$db->beginTransaction();
try {
$invoice_no = 'INV-' . strtoupper(bin2hex(random_bytes(4)));
$member_id = $data['member_id'] ? (int)$data['member_id'] : null;
$total_amount = (float)$data['total'];
$payment_method = $data['payment_method'] ?? 'cash';
$cash_received = (float)($data['cash_received'] ?? 0);
$change_amount = (float)($data['change_amount'] ?? 0);
$points_redeemed = (int)($data['points_redeemed'] ?? 0);
$voucher_id = $data['voucher_id'] ? (int)$data['voucher_id'] : null;
// Points calculation (1 point for every 10k spent)
$points_earned = $member_id ? (int)floor($total_amount / 10000) : 0;
// Insert sale
$stmt = $db->prepare("INSERT INTO sales (invoice_no, member_id, total_amount, payment_method, cash_received, change_amount, points_earned, points_redeemed, voucher_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$invoice_no, $member_id, $total_amount, $payment_method, $cash_received, $change_amount, $points_earned, $points_redeemed, $voucher_id]);
$sale_id = $db->lastInsertId();
// Process items
$stmtItem = $db->prepare("INSERT INTO sale_items (sale_id, product_id, quantity, unit_price, subtotal) VALUES (?, ?, ?, ?, ?)");
$stmtStock = $db->prepare("UPDATE products SET stock = stock - ? WHERE id = ?");
foreach ($data['items'] as $item) {
$subtotal = $item['qty'] * $item['price'];
$stmtItem->execute([$sale_id, $item['id'], $item['qty'], $item['price'], $subtotal]);
$stmtStock->execute([$item['qty'], $item['id']]);
}
// Update member points
if ($member_id) {
$stmtMember = $db->prepare("UPDATE members SET points = points + ? - ? WHERE id = ?");
$stmtMember->execute([$points_earned, $points_redeemed, $member_id]);
}
// Mark voucher as used
if ($voucher_id) {
$stmtVoucher = $db->prepare("UPDATE vouchers SET is_used = 1 WHERE id = ?");
$stmtVoucher->execute([$voucher_id]);
}
$db->commit();
echo json_encode(['success' => true, 'invoice_no' => $invoice_no]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -1,346 +1,105 @@
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-primary: #2563EB; /* Vibrant Blue */
--color-secondary: #000000;
--color-accent: #A3E635; /* Lime Green */
--color-surface: #f8f9fa;
--font-heading: 'Space Grotesk', sans-serif;
--font-body: 'Inter', sans-serif;
--border-width: 2px;
--shadow-hard: 5px 5px 0px #000;
--shadow-hover: 8px 8px 0px #000;
--radius-pill: 50rem;
--radius-card: 1rem;
--primary-color: #0f172a;
--accent-color: #10b981;
--bg-light: #f8fafc;
--sidebar-width: 260px;
}
body {
font-family: var(--font-body);
background-color: var(--color-bg);
color: var(--color-text);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-light);
color: #334155;
overflow-x: hidden;
}
h1, h2, h3, h4, h5, h6, .navbar-brand {
font-family: var(--font-heading);
letter-spacing: -0.03em;
}
/* Utilities */
.text-primary { color: var(--color-primary) !important; }
.bg-black { background-color: #000 !important; }
.text-white { color: #fff !important; }
.shadow-hard { box-shadow: var(--shadow-hard); }
.border-2-black { border: var(--border-width) solid #000; }
.py-section { padding-top: 5rem; padding-bottom: 5rem; }
/* Navbar */
.navbar {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-bottom: var(--border-width) solid transparent;
transition: all 0.3s;
padding-top: 1rem;
padding-bottom: 1rem;
}
.navbar.scrolled {
border-bottom-color: #000;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.brand-text {
font-size: 1.5rem;
font-weight: 800;
}
.nav-link {
font-weight: 500;
color: var(--color-text);
margin-left: 1rem;
position: relative;
}
.nav-link:hover, .nav-link.active {
color: var(--color-primary);
}
/* Buttons */
.btn {
font-weight: 700;
font-family: var(--font-heading);
padding: 0.8rem 2rem;
border-radius: var(--radius-pill);
border: var(--border-width) solid #000;
transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1);
box-shadow: var(--shadow-hard);
}
.btn:hover {
transform: translate(-2px, -2px);
box-shadow: var(--shadow-hover);
}
.btn:active {
transform: translate(2px, 2px);
box-shadow: 0 0 0 #000;
}
.btn-primary {
background-color: var(--color-primary);
border-color: #000;
color: #fff;
}
.btn-primary:hover {
background-color: #1d4ed8;
border-color: #000;
color: #fff;
}
.btn-outline-dark {
background-color: #fff;
color: #000;
}
.btn-cta {
background-color: var(--color-accent);
color: #000;
}
.btn-cta:hover {
background-color: #8cc629;
color: #000;
}
/* Hero Section */
.hero-section {
min-height: 100vh;
padding-top: 80px;
}
.background-blob {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.6;
z-index: 1;
}
.blob-1 {
top: -10%;
right: -10%;
width: 600px;
height: 600px;
background: radial-gradient(circle, var(--color-accent), transparent);
}
.blob-2 {
bottom: 10%;
left: -10%;
width: 500px;
height: 500px;
background: radial-gradient(circle, var(--color-primary), transparent);
}
.highlight-text {
background: linear-gradient(120deg, transparent 0%, transparent 40%, var(--color-accent) 40%, var(--color-accent) 100%);
background-repeat: no-repeat;
background-size: 100% 40%;
background-position: 0 88%;
padding: 0 5px;
}
.dot { color: var(--color-primary); }
.badge-pill {
display: inline-block;
padding: 0.5rem 1rem;
border: 2px solid #000;
border-radius: 50px;
font-weight: 700;
background: #fff;
box-shadow: 4px 4px 0 #000;
font-family: var(--font-heading);
font-size: 0.9rem;
}
/* Marquee */
.marquee-container {
overflow: hidden;
white-space: nowrap;
border-top: 2px solid #000;
border-bottom: 2px solid #000;
}
.rotate-divider {
transform: rotate(-2deg) scale(1.05);
z-index: 10;
position: relative;
margin-top: -50px;
margin-bottom: 30px;
}
.marquee-content {
display: inline-block;
animation: marquee 20s linear infinite;
font-family: var(--font-heading);
font-weight: 700;
font-size: 1.5rem;
letter-spacing: 2px;
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
/* Portfolio Cards */
.project-card {
border: 2px solid #000;
border-radius: var(--radius-card);
overflow: hidden;
background: #fff;
transition: transform 0.3s ease;
box-shadow: var(--shadow-hard);
height: 100%;
.wrapper {
display: flex;
flex-direction: column;
width: 100%;
align-items: stretch;
}
.project-card:hover {
transform: translateY(-10px);
box-shadow: 8px 8px 0 #000;
#sidebar {
min-width: var(--sidebar-width);
max-width: var(--sidebar-width);
min-height: 100vh;
transition: all 0.3s;
background-color: var(--primary-color) !important;
}
.card-img-holder {
height: 250px;
#sidebar.active {
margin-left: calc(-1 * var(--sidebar-width));
}
#sidebar .nav-link {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7) !important;
margin-bottom: 5px;
transition: 0.2s;
}
#sidebar .nav-link:hover, #sidebar .active .nav-link {
color: #fff !important;
background-color: rgba(255, 255, 255, 0.1);
}
#content {
width: 100%;
min-height: 100vh;
transition: all 0.3s;
}
.navbar {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stats-card {
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-5px);
}
.icon-box {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 2px solid #000;
position: relative;
font-size: 4rem;
font-size: 1.5rem;
}
.placeholder-art {
transition: transform 0.3s ease;
}
.bg-soft-success { background-color: #ecfdf5; }
.bg-soft-primary { background-color: #eff6ff; }
.bg-soft-info { background-color: #ecfeff; }
.bg-soft-warning { background-color: #fffbeb; }
.project-card:hover .placeholder-art {
transform: scale(1.2) rotate(10deg);
}
.bg-soft-blue { background-color: #e0f2fe; }
.bg-soft-green { background-color: #dcfce7; }
.bg-soft-purple { background-color: #f3e8ff; }
.bg-soft-yellow { background-color: #fef9c3; }
.category-tag {
position: absolute;
top: 15px;
right: 15px;
background: #000;
color: #fff;
padding: 5px 12px;
border-radius: 20px;
.table thead th {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.025em;
font-weight: 600;
color: #64748b;
border-bottom-width: 1px;
}
.card-body { padding: 1.5rem; }
.link-arrow {
text-decoration: none;
color: #000;
font-weight: 700;
display: inline-flex;
align-items: center;
margin-top: auto;
}
.link-arrow i { transition: transform 0.2s; margin-left: 5px; }
.link-arrow:hover i { transform: translateX(5px); }
/* About */
.about-image-stack {
position: relative;
height: 400px;
width: 100%;
}
.stack-card {
position: absolute;
width: 80%;
height: 100%;
border-radius: var(--radius-card);
border: 2px solid #000;
box-shadow: var(--shadow-hard);
left: 10%;
transform: rotate(-3deg);
background-size: cover;
}
/* Forms */
.form-control {
border: 2px solid #000;
border-radius: 0.5rem;
padding: 1rem;
.btn {
font-weight: 500;
background: #f8f9fa;
padding: 0.5rem 1rem;
border-radius: 8px;
}
.form-control:focus {
box-shadow: 4px 4px 0 var(--color-primary);
border-color: #000;
background: #fff;
.card {
border-radius: 12px;
}
/* Animations */
.animate-up {
opacity: 0;
transform: translateY(30px);
animation: fadeUp 0.8s ease forwards;
@media (max-width: 768px) {
#sidebar {
margin-left: calc(-1 * var(--sidebar-width));
}
.delay-100 { animation-delay: 0.1s; }
.delay-200 { animation-delay: 0.2s; }
@keyframes fadeUp {
to {
opacity: 1;
transform: translateY(0);
#sidebar.active {
margin-left: 0;
}
}
/* Social */
.social-links a {
transition: transform 0.2s;
display: inline-block;
}
.social-links a:hover {
transform: scale(1.2) rotate(10deg);
color: var(--color-accent) !important;
}
/* Responsive */
@media (max-width: 991px) {
.rotate-divider {
transform: rotate(0);
margin-top: 0;
margin-bottom: 2rem;
}
.hero-section {
padding-top: 120px;
text-align: center;
min-height: auto;
padding-bottom: 100px;
}
.display-1 { font-size: 3.5rem; }
.blob-1 { width: 300px; height: 300px; right: -20%; }
.blob-2 { width: 300px; height: 300px; left: -20%; }
}

View File

@ -1,73 +1,129 @@
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', function () {
const sidebarCollapse = document.getElementById('sidebarCollapse');
const sidebar = document.getElementById('sidebar');
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
// Close mobile menu if open
const navbarToggler = document.querySelector('.navbar-toggler');
const navbarCollapse = document.querySelector('.navbar-collapse');
if (navbarCollapse.classList.contains('show')) {
navbarToggler.click();
}
// Scroll with offset
const offset = 80;
const elementPosition = targetElement.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
if (sidebarCollapse) {
sidebarCollapse.addEventListener('click', function () {
sidebar.classList.toggle('active');
});
}
});
// Auto hide alerts after 5 seconds
const alerts = document.querySelectorAll('.alert');
alerts.forEach(function (alert) {
setTimeout(function () {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
}, 5000);
});
// Navbar scroll effect
const navbar = document.querySelector('.navbar');
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
navbar.classList.add('scrolled', 'shadow-sm', 'bg-white');
navbar.classList.remove('bg-transparent');
} else {
navbar.classList.remove('scrolled', 'shadow-sm', 'bg-white');
navbar.classList.add('bg-transparent');
// --- Modern Charts Initialization ---
// 1. Sales Analytics Chart
const salesOptions = {
series: [{
name: 'Sales',
data: [3100000, 4000000, 2800000, 5100000, 4200000, 10900000, 12500000]
}],
chart: {
height: 350,
type: 'area',
toolbar: {
show: false
},
fontFamily: 'Inter, sans-serif'
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth',
width: 3,
colors: ['#10b981']
},
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
opacityFrom: 0.45,
opacityTo: 0.05,
stops: [20, 100, 100, 100]
}
});
// Intersection Observer for fade-up animations
const observerOptions = {
threshold: 0.1,
rootMargin: "0px 0px -50px 0px"
},
xaxis: {
categories: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
labels: {
formatter: function (val) {
return "Rp " + (val / 1000000).toFixed(1) + "M";
}
}
},
grid: {
borderColor: '#f1f5f9',
strokeDashArray: 4
},
tooltip: {
y: {
formatter: function (val) {
return "Rp " + val.toLocaleString('id-ID');
}
}
},
colors: ['#10b981']
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-up');
entry.target.style.opacity = "1";
observer.unobserve(entry.target); // Only animate once
const salesChartEl = document.querySelector("#salesChart");
if (salesChartEl) {
const salesChart = new ApexCharts(salesChartEl, salesOptions);
salesChart.render();
}
// 2. Category Distribution Chart
const categoryOptions = {
series: [44, 55, 13, 33],
chart: {
height: 350,
type: 'donut',
fontFamily: 'Inter, sans-serif'
},
labels: ['Electronics', 'Groceries', 'Apparel', 'Others'],
colors: ['#0f172a', '#10b981', '#3b82f6', '#f59e0b'],
plotOptions: {
pie: {
donut: {
size: '75%',
labels: {
show: true,
total: {
show: true,
label: 'Total',
formatter: function (w) {
return w.globals.seriesTotals.reduce((a, b) => a + b, 0) + "%";
}
}
}
}
}
},
legend: {
position: 'bottom'
},
dataLabels: {
enabled: false
}
};
const categoryChartEl = document.querySelector("#categoryChart");
if (categoryChartEl) {
const categoryChart = new ApexCharts(categoryChartEl, categoryOptions);
categoryChart.render();
}
});
}, observerOptions);
// Select elements to animate (add a class 'reveal' to them in HTML if not already handled by CSS animation)
// For now, let's just make sure the hero animations run.
// If we want scroll animations, we'd add opacity: 0 to elements in CSS and reveal them here.
// Given the request, the CSS animation I added runs on load for Hero.
// Let's make the project cards animate in.
const projectCards = document.querySelectorAll('.project-card');
projectCards.forEach((card, index) => {
card.style.opacity = "0";
card.style.animationDelay = `${index * 0.1}s`;
observer.observe(card);
});
});

View File

@ -1,9 +1,9 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_38220');
define('DB_USER', 'app_38220');
define('DB_PASS', '5f905595-f08d-48bc-9b00-3e1a868017ea');
define('DB_NAME', 'app_38244');
define('DB_USER', 'app_38244');
define('DB_PASS', '8f62dd8d-49c1-4ea1-94d3-45dab7c8adcb');
function db() {
static $pdo;

42
db/init.php Normal file
View File

@ -0,0 +1,42 @@
<?php
require 'db/config.php';
$db = db();
try {
$db->exec("CREATE TABLE IF NOT EXISTS branches (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
location VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;");
$db->exec("CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;");
$db->exec("CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
branch_id INT NOT NULL,
category_id INT,
name VARCHAR(255) NOT NULL,
sku VARCHAR(50) UNIQUE,
cost_price DECIMAL(15, 2) NOT NULL DEFAULT 0,
selling_price DECIMAL(15, 2) NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;");
$count = $db->query("SELECT COUNT(*) FROM branches")->fetchColumn();
if ($count == 0) {
$db->exec("INSERT INTO branches (name, location) VALUES ('Pusat', 'Jakarta'), ('Cabang Bandung', 'Bandung')");
}
$countCat = $db->query("SELECT COUNT(*) FROM categories")->fetchColumn();
if ($countCat == 0) {
$db->exec("INSERT INTO categories (name) VALUES ('General'), ('Food'), ('Beverage')");
}
echo "DB Init Success";
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}

51
db/init_members.php Normal file
View File

@ -0,0 +1,51 @@
<?php
require_once 'db/config.php';
$db = db();
try {
// 1. Add member_price to products
$columns = $db->query("SHOW COLUMNS FROM products LIKE 'member_price'")->fetchAll();
if (empty($columns)) {
$db->exec("ALTER TABLE products ADD COLUMN member_price DECIMAL(15, 2) NOT NULL DEFAULT 0 AFTER selling_price");
// Update member_price to be slightly lower than selling_price for existing products
$db->exec("UPDATE products SET member_price = selling_price * 0.9 WHERE member_price = 0");
}
// 2. Create members table
$db->exec("CREATE TABLE IF NOT EXISTS members (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
phone VARCHAR(20),
email VARCHAR(100),
points INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;");
// 3. Create sales table
$db->exec("CREATE TABLE IF NOT EXISTS sales (
id INT AUTO_INCREMENT PRIMARY KEY,
invoice_no VARCHAR(50) UNIQUE NOT NULL,
member_id INT NULL,
total_amount DECIMAL(15, 2) NOT NULL,
discount DECIMAL(15, 2) DEFAULT 0,
points_earned INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (member_id) REFERENCES members(id)
) ENGINE=InnoDB;");
// 4. Create sale_items table
$db->exec("CREATE TABLE IF NOT EXISTS sale_items (
id INT AUTO_INCREMENT PRIMARY KEY,
sale_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(15, 2) NOT NULL,
subtotal DECIMAL(15, 2) NOT NULL,
FOREIGN KEY (sale_id) REFERENCES sales(id),
FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB;");
echo "Member and Points Tables Initialized";
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}

View File

@ -0,0 +1 @@
ALTER TABLE sales ADD COLUMN payment_method VARCHAR(20) DEFAULT 'cash'; ALTER TABLE sales ADD COLUMN cash_received DECIMAL(15,2) DEFAULT 0; ALTER TABLE sales ADD COLUMN change_amount DECIMAL(15,2) DEFAULT 0;

View File

@ -0,0 +1,15 @@
-- Create vouchers table
CREATE TABLE IF NOT EXISTS vouchers (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
discount_amount DECIMAL(15, 2) NOT NULL,
is_used TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL
);
-- Update sales table to track point redemption and voucher usage
ALTER TABLE sales ADD COLUMN IF NOT EXISTS points_redeemed INT DEFAULT 0;
ALTER TABLE sales ADD COLUMN IF NOT EXISTS voucher_id INT NULL;
ALTER TABLE sales ADD CONSTRAINT fk_sale_voucher FOREIGN KEY (voucher_id) REFERENCES vouchers(id);

14
db/seed_products.php Normal file
View File

@ -0,0 +1,14 @@
<?php
require 'db/config.php';
$db = db();
$count = $db->query('SELECT COUNT(*) FROM products')->fetchColumn();
if ($count == 0) {
$db->exec("INSERT INTO products (branch_id, category_id, name, sku, cost_price, selling_price, stock) VALUES
(1, 1, 'Americano', 'COF001', 15000, 25000, 100),
(1, 1, 'Caffe Latte', 'COF002', 20000, 35000, 50),
(1, 2, 'Chocolate Cake', 'FOD001', 25000, 45000, 20),
(2, 1, 'Ice Tea', 'BEV001', 5000, 12000, 200)");
echo "Seed products success";
} else {
echo "Products already exist";
}

404
index.php
View File

@ -1,150 +1,276 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once 'db/config.php';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$current_branch_id = (int)($_GET['branch_id'] ?? 1);
$db = db();
// Fetch branches for the switcher
$branches = $db->query("SELECT * FROM branches")->fetchAll();
$current_branch = array_filter($branches, fn($b) => $b['id'] == $current_branch_id);
$current_branch = reset($current_branch);
// Mock data for dashboard stats (to be replaced with real queries later)
$stats = [
'total_sales' => 'Rp 12.450.000',
'total_orders' => 128,
'total_products' => 45,
'total_members' => 12
];
?>
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POS Pro - Dashboard Modern</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">
<!-- ApexCharts -->
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<div class="wrapper">
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark text-white">
<div class="sidebar-header p-3">
<h4 class="mb-0">POS<span class="text-success">PRO</span></h4>
<small class="text-muted">Sistem Multi-Cabang</small>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<ul class="list-unstyled components p-2">
<li class="active">
<a href="index.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
</a>
</li>
<li>
<a href="pos.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-cart3 me-2"></i> Kasir (POS)
</a>
</li>
<li>
<a href="products.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-box-seam me-2"></i> Produk
</a>
</li>
<li>
<a href="members.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-people me-2"></i> Member
</a>
</li>
<li>
<a href="vouchers.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-ticket-perforated me-2"></i> Voucher
</a>
</li>
<li>
<a href="#" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-graph-up me-2"></i> Laporan
</a>
</li>
<li>
<a href="#" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-gear me-2"></i> Pengaturan
</a>
</li>
</ul>
</nav>
<!-- Page Content -->
<div id="content">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom p-3">
<div class="container-fluid">
<button type="button" id="sidebarCollapse" class="btn btn-outline-dark me-3">
<i class="bi bi-list"></i>
</button>
<div class="ms-auto d-flex align-items-center">
<div class="dropdown me-3">
<button class="btn btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-geo-alt me-1"></i> <?php echo htmlspecialchars($current_branch['name']); ?>
</button>
<ul class="dropdown-menu shadow border-0">
<?php foreach ($branches as $branch): ?>
<li><a class="dropdown-item" href="?branch_id=<?php echo $branch['id']; ?>"><?php echo htmlspecialchars($branch['name']); ?></a></li>
<?php endforeach; ?>
</ul>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<div class="dropdown">
<button class="btn btn-light rounded-circle p-2" type="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle fs-5"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li><a class="dropdown-item" href="#">Profil</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#">Keluar</a></li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Ringkasan Dashboard</h2>
<div class="text-muted small"><?php
setlocale(LC_TIME, 'id_ID.utf8', 'id_ID', 'id');
echo date('l, d F Y');
?></div>
</div>
<!-- Stats Cards -->
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between mb-3">
<div class="bg-soft-primary p-3 rounded-4">
<i class="bi bi-currency-dollar text-primary fs-4"></i>
</div>
<span class="text-success small fw-bold">+12% <i class="bi bi-arrow-up"></i></span>
</div>
<h4 class="mb-1"><?php echo $stats['total_sales']; ?></h4>
<div class="text-muted small">Total Pendapatan</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between mb-3">
<div class="bg-soft-success p-3 rounded-4">
<i class="bi bi-bag-check text-success fs-4"></i>
</div>
<span class="text-success small fw-bold">+8% <i class="bi bi-arrow-up"></i></span>
</div>
<h4 class="mb-1"><?php echo $stats['total_orders']; ?></h4>
<div class="text-muted small">Total Pesanan</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between mb-3">
<div class="bg-soft-warning p-3 rounded-4">
<i class="bi bi-box-seam text-warning fs-4"></i>
</div>
<span class="text-muted small fw-bold">Aktif</span>
</div>
<h4 class="mb-1"><?php echo $stats['total_products']; ?></h4>
<div class="text-muted small">Stok Produk</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<div class="d-flex justify-content-between mb-3">
<div class="bg-soft-info p-3 rounded-4">
<i class="bi bi-people text-info fs-4"></i>
</div>
<span class="text-info small fw-bold">+5 <i class="bi bi-plus"></i></span>
</div>
<h4 class="mb-1"><?php echo $stats['total_members']; ?></h4>
<div class="text-muted small">Member Loyal</div>
</div>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="row g-4 mb-4">
<div class="col-md-8">
<div class="card border-0 shadow-sm rounded-4 h-100">
<div class="card-header bg-white border-0 p-4 pb-0">
<h5 class="mb-0">Analisis Penjualan</h5>
<small class="text-muted">Tren performa mingguan</small>
</div>
<div class="card-body p-4">
<div id="salesChart"></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 h-100">
<div class="card-header bg-white border-0 p-4 pb-0">
<h5 class="mb-0">Penjualan per Kategori</h5>
<small class="text-muted">Distribusi produk</small>
</div>
<div class="card-body p-4">
<div id="categoryChart"></div>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-header bg-white border-0 p-4 pb-0">
<h5 class="mb-0">Transaksi Terakhir</h5>
</div>
<div class="card-body p-4">
<div class="table-responsive">
<table class="table table-borderless align-middle">
<thead>
<tr class="text-muted small">
<th>NO. INV</th>
<th>PELANGGAN</th>
<th>TOTAL</th>
<th>STATUS</th>
</tr>
</thead>
<tbody>
<tr>
<td class="fw-bold">#INV-1024</td>
<td>Pelanggan Umum</td>
<td>Rp 45.000</td>
<td><span class="badge bg-soft-success text-success">Lunas</span></td>
</tr>
<tr>
<td class="fw-bold">#INV-1023</td>
<td>John Doe</td>
<td>Rp 120.000</td>
<td><span class="badge bg-soft-success text-success">Lunas</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-header bg-white border-0 p-4 pb-0">
<h5 class="mb-0">Produk Terlaris</h5>
</div>
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light p-2 rounded-3 me-3"><i class="bi bi-cup-hot text-primary"></i></div>
<div class="flex-grow-1">
<div class="fw-bold">Kopi Susu Gula Aren</div>
<small class="text-muted">Minuman</small>
</div>
<div class="fw-bold text-success">156 terjual</div>
</div>
<div class="d-flex align-items-center">
<div class="bg-light p-2 rounded-3 me-3"><i class="bi bi-egg-fried text-primary"></i></div>
<div class="flex-grow-1">
<div class="fw-bold">Nasi Goreng Spesial</div>
<small class="text-muted">Makanan</small>
</div>
<div class="fw-bold text-success">92 terjual</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>

202
members.php Normal file
View File

@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
require_once 'db/config.php';
$current_branch_id = (int)($_GET['branch_id'] ?? 1);
$db = db();
// Handle Add Member
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add') {
$name = $_POST['name'];
$code = $_POST['code'];
$phone = $_POST['phone'];
$email = $_POST['email'];
$stmt = $db->prepare("INSERT INTO members (code, name, phone, email) VALUES (?, ?, ?, ?)");
try {
$stmt->execute([$code, $name, $phone, $email]);
header("Location: members.php?branch_id=$current_branch_id&success=1");
exit;
} catch (Exception $e) {
$error = "Error adding member: " . $e->getMessage();
}
}
// Fetch Data
$branches = $db->query("SELECT * FROM branches")->fetchAll();
$current_branch = array_filter($branches, fn($b) => $b['id'] == $current_branch_id);
$current_branch = reset($current_branch);
$members = $db->query("SELECT * FROM members ORDER BY id DESC")->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Members - POS Pro</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">
</head>
<body>
<div class="wrapper">
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark text-white">
<div class="sidebar-header p-3">
<h4 class="mb-0">POS<span class="text-success">PRO</span></h4>
<small class="text-muted">Multi-Branch System</small>
</div>
<ul class="list-unstyled components p-2">
<li>
<a href="index.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
</a>
</li>
<li>
<a href="pos.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-cart3 me-2"></i> Cashier (POS)
</a>
</li>
<li>
<a href="products.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-box-seam me-2"></i> Products
</a>
</li>
<li class="active">
<a href="members.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-people me-2"></i> Members
</a>
</li>
<li>
<a href="vouchers.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-ticket-perforated me-2"></i> Vouchers
</a>
</li>
<li>
<a href="#" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-graph-up me-2"></i> Reports
</a>
</li>
</ul>
</nav>
<!-- Page Content -->
<div id="content">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom p-3">
<div class="container-fluid">
<button type="button" id="sidebarCollapse" class="btn btn-outline-dark me-3">
<i class="bi bi-list"></i>
</button>
<div class="ms-auto d-flex align-items-center">
<div class="dropdown me-3">
<button class="btn btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-geo-alt me-1"></i> <?php echo htmlspecialchars($current_branch['name']); ?>
</button>
<ul class="dropdown-menu shadow border-0">
<?php foreach ($branches as $branch): ?>
<li><a class="dropdown-item" href="?branch_id=<?php echo $branch['id']; ?>"><?php echo htmlspecialchars($branch['name']); ?></a></li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
</nav>
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Member Management</h2>
<button class="btn btn-primary rounded-pill" data-bs-toggle="modal" data-bs-target="#addMemberModal">
<i class="bi bi-person-plus me-1"></i> Add Member
</button>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
Member added successfully!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr class="text-muted small">
<th class="ps-4">CODE</th>
<th>NAME</th>
<th>PHONE</th>
<th>EMAIL</th>
<th>POINTS</th>
<th class="pe-4 text-end">ACTIONS</th>
</tr>
</thead>
<tbody>
<?php foreach ($members as $m): ?>
<tr>
<td class="ps-4 fw-bold"><?php echo htmlspecialchars($m['code']); ?></td>
<td><?php echo htmlspecialchars($m['name']); ?></td>
<td><?php echo htmlspecialchars($m['phone']); ?></td>
<td><?php echo htmlspecialchars($m['email']); ?></td>
<td><span class="badge bg-soft-info text-info"><?php echo number_format($m['points']); ?> Pts</span></td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-light rounded-circle text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Member Modal -->
<div class="modal fade" id="addMemberModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<form method="POST">
<input type="hidden" name="action" value="add">
<div class="modal-header border-0 p-4">
<h5 class="modal-title">New Member</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 pt-0">
<div class="mb-3">
<label class="form-label">Member Code</label>
<input type="text" name="code" class="form-control" value="MEM-<?php echo strtoupper(bin2hex(random_bytes(2))); ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Full Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Phone Number</label>
<input type="text" name="phone" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Email Address</label>
<input type="email" name="email" class="form-control">
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4">Save Member</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('sidebarCollapse').addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('active');
});
</script>
</body>
</html>

707
pos.php Normal file
View File

@ -0,0 +1,707 @@
<?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>

220
products.php Normal file
View File

@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
require_once 'db/config.php';
$current_branch_id = (int)($_GET['branch_id'] ?? 1);
$db = db();
// Handle Add Product
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add') {
$name = $_POST['name'];
$category_id = (int)$_POST['category_id'];
$selling_price = (float)$_POST['selling_price'];
$member_price = (float)$_POST['member_price'];
$stock = (int)$_POST['stock'];
$stmt = $db->prepare("INSERT INTO products (name, category_id, selling_price, member_price, stock, branch_id) VALUES (?, ?, ?, ?, ?, ?)");
try {
$stmt->execute([$name, $category_id, $selling_price, $member_price, $stock, $current_branch_id]);
header("Location: products.php?branch_id=$current_branch_id&success=1");
exit;
} catch (Exception $e) {
$error = "Error adding product: " . $e->getMessage();
}
}
// Fetch Data
$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 ORDER BY p.id DESC")->fetchAll();
$categories = $db->query("SELECT * FROM categories")->fetchAll();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Products - POS Pro</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">
</head>
<body>
<div class="wrapper">
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark text-white">
<div class="sidebar-header p-3">
<h4 class="mb-0">POS<span class="text-success">PRO</span></h4>
<small class="text-muted">Multi-Branch System</small>
</div>
<ul class="list-unstyled components p-2">
<li>
<a href="index.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
</a>
</li>
<li>
<a href="pos.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-cart3 me-2"></i> Cashier (POS)
</a>
</li>
<li class="active">
<a href="products.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-box-seam me-2"></i> Products
</a>
</li>
<li>
<a href="members.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-people me-2"></i> Members
</a>
</li>
<li>
<a href="vouchers.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-ticket-perforated me-2"></i> Vouchers
</a>
</li>
<li>
<a href="#" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-graph-up me-2"></i> Reports
</a>
</li>
</ul>
</nav>
<!-- Page Content -->
<div id="content">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom p-3">
<div class="container-fluid">
<button type="button" id="sidebarCollapse" class="btn btn-outline-dark me-3">
<i class="bi bi-list"></i>
</button>
<div class="ms-auto d-flex align-items-center">
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-geo-alt me-1"></i> <?php echo htmlspecialchars($current_branch['name']); ?>
</button>
<ul class="dropdown-menu shadow border-0">
<?php foreach ($branches as $branch): ?>
<li><a class="dropdown-item" href="?branch_id=<?php echo $branch['id']; ?>"><?php echo htmlspecialchars($branch['name']); ?></a></li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
</nav>
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Product Inventory</h2>
<button class="btn btn-primary rounded-pill" data-bs-toggle="modal" data-bs-target="#addProductModal">
<i class="bi bi-plus-lg me-1"></i> Add Product
</button>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
Product added successfully!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr class="text-muted small">
<th class="ps-4">PRODUCT NAME</th>
<th>CATEGORY</th>
<th>REGULAR PRICE</th>
<th>MEMBER PRICE</th>
<th>STOCK</th>
<th class="pe-4 text-end">ACTIONS</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $p): ?>
<tr>
<td class="ps-4 fw-bold"><?php echo htmlspecialchars($p['name']); ?></td>
<td><span class="badge bg-light text-dark border"><?php echo htmlspecialchars($p['category_name'] ?? 'General'); ?></span></td>
<td class="text-muted">Rp <?php echo number_format((float)$p['selling_price'], 0, ',', '.'); ?></td>
<td class="fw-bold text-success">Rp <?php echo number_format((float)$p['member_price'], 0, ',', '.'); ?></td>
<td>
<?php if ($p['stock'] <= 5): ?>
<span class="badge bg-soft-danger text-danger"><?php echo $p['stock']; ?> Low</span>
<?php else: ?>
<span class="badge bg-soft-success text-success"><?php echo $p['stock']; ?> In Stock</span>
<?php endif; ?>
</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light rounded-circle"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-light rounded-circle text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Product Modal -->
<div class="modal fade" id="addProductModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<form method="POST">
<input type="hidden" name="action" value="add">
<div class="modal-header border-0 p-4">
<h5 class="modal-title">Add New Product</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 pt-0">
<div class="mb-3">
<label class="form-label small fw-bold">Product Name</label>
<input type="text" name="name" class="form-control" placeholder="e.g. Kopi Susu" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Category</label>
<select name="category_id" class="form-select" required>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat['id']; ?>"><?php echo htmlspecialchars($cat['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row mb-3">
<div class="col">
<label class="form-label small fw-bold">Regular Price (Rp)</label>
<input type="number" name="selling_price" class="form-control" placeholder="25000" required>
</div>
<div class="col">
<label class="form-label small fw-bold">Member Price (Rp)</label>
<input type="number" name="member_price" class="form-control" placeholder="22000" required>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">Initial Stock</label>
<input type="number" name="stock" class="form-control" placeholder="100" required>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4">Save Product</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('sidebarCollapse').addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('active');
});
</script>
</body>
</html>

278
vouchers.php Normal file
View File

@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
require_once 'db/config.php';
$db = db();
$current_branch_id = (int)($_GET['branch_id'] ?? 1);
// Handle voucher creation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create') {
$code = strtoupper(bin2hex(random_bytes(4))); // Generate a random 8-character code
if (!empty($_POST['custom_code'])) {
$code = strtoupper(htmlspecialchars($_POST['custom_code']));
}
$discount = (float)$_POST['discount_amount'];
$expires_at = !empty($_POST['expires_at']) ? $_POST['expires_at'] : null;
try {
$stmt = $db->prepare("INSERT INTO vouchers (code, discount_amount, expires_at) VALUES (?, ?, ?)");
$stmt->execute([$code, $discount, $expires_at]);
$success_msg = "Voucher berhasil dibuat!";
} catch (PDOException $e) {
$error_msg = "Gagal membuat voucher: " . $e->getMessage();
}
}
// Fetch vouchers
$vouchers = $db->query("SELECT * FROM vouchers ORDER BY created_at DESC")->fetchAll();
// Fetch branches for sidebar
$branches = $db->query("SELECT * FROM branches")->fetchAll();
$current_branch = array_filter($branches, fn($b) => $b['id'] == $current_branch_id);
$current_branch = reset($current_branch);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manajemen Voucher - POS Pro</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>
.voucher-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px;
padding: 20px;
position: relative;
overflow: hidden;
width: 350px;
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.voucher-card::before {
content: '';
position: absolute;
top: 0;
right: -20px;
bottom: 0;
width: 40px;
background: radial-gradient(circle at center, #f8f9fa 10px, transparent 11px);
background-size: 40px 40px;
}
.voucher-card::after {
content: '';
position: absolute;
top: 0;
left: -20px;
bottom: 0;
width: 40px;
background: radial-gradient(circle at center, #f8f9fa 10px, transparent 11px);
background-size: 40px 40px;
}
.qr-code {
background: white;
padding: 5px;
border-radius: 8px;
width: 80px;
height: 80px;
}
@media print {
body * { visibility: hidden; }
#printArea, #printArea * { visibility: visible; }
#printArea { position: absolute; left: 0; top: 0; width: 100%; }
.no-print { display: none !important; }
}
</style>
</head>
<body>
<div class="wrapper text-white">
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark text-white">
<div class="sidebar-header p-3">
<h4 class="mb-0">POS<span class="text-success">PRO</span></h4>
<small class="text-muted">Sistem Multi-Cabang</small>
</div>
<ul class="list-unstyled components p-2">
<li>
<a href="index.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-speedometer2 me-2"></i> Dashboard
</a>
</li>
<li>
<a href="pos.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-cart3 me-2"></i> Kasir (POS)
</a>
</li>
<li>
<a href="products.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-box-seam me-2"></i> Produk
</a>
</li>
<li>
<a href="members.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-people me-2"></i> Member
</a>
</li>
<li class="active">
<a href="vouchers.php?branch_id=<?php echo $current_branch_id; ?>" class="nav-link text-white p-2 d-block rounded bg-primary">
<i class="bi bi-ticket-perforated me-2"></i> Voucher
</a>
</li>
<li>
<a href="#" class="nav-link text-white p-2 d-block rounded">
<i class="bi bi-graph-up me-2"></i> Laporan
</a>
</li>
</ul>
</nav>
<!-- Page Content -->
<div id="content" class="text-dark">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom p-3">
<div class="container-fluid">
<button type="button" id="sidebarCollapse" class="btn btn-outline-dark me-3">
<i class="bi bi-list"></i>
</button>
<h4 class="mb-0">Manajemen Voucher</h4>
</div>
</nav>
<div class="container-fluid p-4">
<?php if (isset($success_msg)): ?>
<div class="alert alert-success"><?php echo $success_msg; ?></div>
<?php endif; ?>
<?php if (isset($error_msg)): ?>
<div class="alert alert-danger"><?php echo $error_msg; ?></div>
<?php endif; ?>
<div class="row g-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-header bg-white border-0 p-4 pb-0">
<h5 class="mb-0">Buat Voucher Baru</h5>
</div>
<div class="card-body p-4">
<form method="POST">
<input type="hidden" name="action" value="create">
<div class="mb-3">
<label class="form-label">Kode Kustom (Opsional)</label>
<input type="text" name="custom_code" class="form-control" placeholder="cth: PROMO2024">
<small class="text-muted">Kosongkan untuk pembuatan otomatis</small>
</div>
<div class="mb-3">
<label class="form-label">Jumlah Diskon (Rp)</label>
<input type="number" name="discount_amount" class="form-control" required placeholder="5000">
</div>
<div class="mb-3">
<label class="form-label">Tanggal Kedaluwarsa (Opsional)</label>
<input type="date" name="expires_at" class="form-control">
</div>
<button type="submit" class="btn btn-primary w-100 rounded-pill">Buat Voucher</button>
</form>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card border-0 shadow-sm rounded-4">
<div class="card-header bg-white border-0 p-4 pb-0">
<h5 class="mb-0">Voucher Aktif</h5>
</div>
<div class="card-body p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Kode</th>
<th>Diskon</th>
<th>Status</th>
<th>Kedaluwarsa</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($vouchers as $v): ?>
<tr>
<td><span class="badge bg-light text-dark border p-2"><?php echo htmlspecialchars($v['code']); ?></span></td>
<td class="fw-bold text-success">Rp <?php echo number_format((float)$v['discount_amount'], 0, ',', '.'); ?></td>
<td>
<?php if ($v['is_used']): ?>
<span class="badge bg-soft-danger text-danger">Terpakai</span>
<?php else: ?>
<span class="badge bg-soft-success text-success">Tersedia</span>
<?php endif; ?>
</td>
<td class="small text-muted"><?php echo $v['expires_at'] ?? 'Selamanya'; ?></td>
<td>
<button class="btn btn-sm btn-outline-primary rounded-pill" onclick="printVoucher('<?php echo $v['code']; ?>', '<?php echo number_format((float)$v['discount_amount'], 0, ',', '.'); ?>', '<?php echo htmlspecialchars($current_branch['name']); ?>')">
<i class="bi bi-printer"></i> Cetak
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Hidden Print Area -->
<div id="printArea" class="d-none">
<div class="d-flex justify-content-center p-5">
<div class="voucher-card">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h4 class="mb-0 fw-bold" id="printBranch"></h4>
<small>VOUCHER RESMI</small>
</div>
<div class="qr-code">
<img id="printQR" src="" alt="QR Code" style="width: 100%; height: 100%;">
</div>
</div>
<div class="text-center my-4">
<div class="display-6 fw-bold">Rp <span id="printDiscount"></span></div>
<div class="text-uppercase tracking-widest small">Voucher Diskon</div>
</div>
<div class="border-top border-white border-opacity-25 pt-3 d-flex justify-content-between align-items-center">
<div>
<small class="d-block opacity-75">KODE VOUCHER</small>
<h5 class="mb-0 fw-bold tracking-widest" id="printCode"></h5>
</div>
<div class="text-end">
<small class="d-block opacity-75">PINDAI UNTUK MENUKAR</small>
<i class="bi bi-stars"></i>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
function printVoucher(code, discount, branch) {
document.getElementById('printCode').innerText = code;
document.getElementById('printDiscount').innerText = discount;
document.getElementById('printBranch').innerText = branch;
// Generate QR Code using Google Charts API
const qrUrl = `https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=${encodeURIComponent(code)}&choe=UTF-8`;
document.getElementById('printQR').src = qrUrl;
// Wait for image to load before printing
document.getElementById('printQR').onload = function() {
window.print();
};
}
document.getElementById('sidebarCollapse').addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('active');
});
</script>
</body>
</html>