Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0488c2eb9c |
22
api/check_voucher.php
Normal file
22
api/check_voucher.php
Normal 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
62
api/process_sale.php
Normal 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()]);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
.delay-100 { animation-delay: 0.1s; }
|
||||
.delay-200 { animation-delay: 0.2s; }
|
||||
|
||||
@keyframes fadeUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
margin-left: calc(-1 * var(--sidebar-width));
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
#sidebar.active {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.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%; }
|
||||
}
|
||||
}
|
||||
@ -1,73 +1,129 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// 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();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sidebarCollapse = document.getElementById('sidebarCollapse');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
// 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 ---
|
||||
|
||||
// Intersection Observer for fade-up animations
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: "0px 0px -50px 0px"
|
||||
// 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]
|
||||
}
|
||||
},
|
||||
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) + "%";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 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);
|
||||
});
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
|
||||
const categoryChartEl = document.querySelector("#categoryChart");
|
||||
if (categoryChartEl) {
|
||||
const categoryChart = new ApexCharts(categoryChartEl, categoryOptions);
|
||||
categoryChart.render();
|
||||
}
|
||||
});
|
||||
@ -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
42
db/init.php
Normal 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
51
db/init_members.php
Normal 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();
|
||||
}
|
||||
1
db/migrations/20260207_add_payment_fields_to_sales.sql
Normal file
1
db/migrations/20260207_add_payment_fields_to_sales.sql
Normal 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;
|
||||
15
db/migrations/20260207_vouchers_and_points.sql
Normal file
15
db/migrations/20260207_vouchers_and_points.sql
Normal 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
14
db/seed_products.php
Normal 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";
|
||||
}
|
||||
406
index.php
406
index.php
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
|
||||
<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
202
members.php
Normal 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
707
pos.php
Normal 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
220
products.php
Normal 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
278
vouchers.php
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user