diff --git a/api/check_voucher.php b/api/check_voucher.php new file mode 100644 index 0000000..60654a5 --- /dev/null +++ b/api/check_voucher.php @@ -0,0 +1,22 @@ + 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']); +} \ No newline at end of file diff --git a/api/process_sale.php b/api/process_sale.php new file mode 100644 index 0000000..b760ac6 --- /dev/null +++ b/api/process_sale.php @@ -0,0 +1,62 @@ + 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()]); +} \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 65a1626..d1d558e 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -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%; } -} +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index fdf2cfd..f58c91a 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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(); + } }); \ No newline at end of file diff --git a/db/config.php b/db/config.php index 6250722..021f332 100644 --- a/db/config.php +++ b/db/config.php @@ -1,9 +1,9 @@ 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(); +} diff --git a/db/init_members.php b/db/init_members.php new file mode 100644 index 0000000..fccb0f3 --- /dev/null +++ b/db/init_members.php @@ -0,0 +1,51 @@ +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(); +} diff --git a/db/migrations/20260207_add_payment_fields_to_sales.sql b/db/migrations/20260207_add_payment_fields_to_sales.sql new file mode 100644 index 0000000..1c73d42 --- /dev/null +++ b/db/migrations/20260207_add_payment_fields_to_sales.sql @@ -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; diff --git a/db/migrations/20260207_vouchers_and_points.sql b/db/migrations/20260207_vouchers_and_points.sql new file mode 100644 index 0000000..84764b6 --- /dev/null +++ b/db/migrations/20260207_vouchers_and_points.sql @@ -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); diff --git a/db/seed_products.php b/db/seed_products.php new file mode 100644 index 0000000..bf4bd69 --- /dev/null +++ b/db/seed_products.php @@ -0,0 +1,14 @@ +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"; +} diff --git a/index.php b/index.php index 7205f3d..3a62e01 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,276 @@ 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 +]; ?> - - + + - - - New Style - - - - - - - - - - - - - - - - - - - + + + POS Pro - Dashboard Modern + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+ + + + +
+ + +
+
+

Ringkasan Dashboard

+
+
+ + +
+
+
+
+
+
+ +
+ +12% +
+

+
Total Pendapatan
+
+
+
+
+
+
+
+
+ +
+ +8% +
+

+
Total Pesanan
+
+
+
+
+
+
+
+
+ +
+ Aktif +
+

+
Stok Produk
+
+
+
+
+
+
+
+
+ +
+ +5 +
+

+
Member Loyal
+
+
+
+
+ + +
+
+
+
+
Analisis Penjualan
+ Tren performa mingguan +
+
+
+
+
+
+
+
+
+
Penjualan per Kategori
+ Distribusi produk +
+
+
+
+
+
+
+ +
+
+
+
+
Transaksi Terakhir
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
NO. INVPELANGGANTOTALSTATUS
#INV-1024Pelanggan UmumRp 45.000Lunas
#INV-1023John DoeRp 120.000Lunas
+
+
+
+
+
+
+
+
Produk Terlaris
+
+
+
+
+
+
Kopi Susu Gula Aren
+ Minuman +
+
156 terjual
+
+
+
+
+
Nasi Goreng Spesial
+ Makanan +
+
92 terjual
+
+
+
+
+
+
+
-
- + + + diff --git a/members.php b/members.php new file mode 100644 index 0000000..b6a1808 --- /dev/null +++ b/members.php @@ -0,0 +1,202 @@ +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(); +?> + + + + + + Members - POS Pro + + + + + +
+ + + + +
+ + +
+
+

Member Management

+ +
+ + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
CODENAMEPHONEEMAILPOINTSACTIONS
Pts + + +
+
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/pos.php b/pos.php new file mode 100644 index 0000000..25282f6 --- /dev/null +++ b/pos.php @@ -0,0 +1,707 @@ +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(); +?> + + + + + + POS - Kasir + + + + + + + + +
+
+ +
+
+
+
+ + +
+
+
+ +
+
+ +
+ +
+ +

Produk tidak tersedia. Silakan tambah produk terlebih dahulu.

+
+ + +
+
+
+
+
+
+
Rp
+
Rp MEMBER
+
+
Stok:
+
+
+
+ + +
+
+ + +
+
+
Keranjang Belanja
+ +
+ + +
+ + +
+
+ Poin: 0 +
+ + +
+
+
+ + 1 Poin = Rp 1 +
+
+ + +
+ +
+ + +
+
+
+
+ +
+ +
+ +
+
+ Subtotal + Rp 0 +
+
+ Hemat Member + - Rp 0 +
+
+ Tukar Poin + - Rp 0 +
+
+ Diskon Voucher + - Rp 0 +
+
+ Poin Didapat + 0 Pts +
+
+
+

Total Akhir

+

Rp 0

+
+ +
+
+
+
+ + + + + +
+ + + + + \ No newline at end of file diff --git a/products.php b/products.php new file mode 100644 index 0000000..f61c18b --- /dev/null +++ b/products.php @@ -0,0 +1,220 @@ +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(); +?> + + + + + + Products - POS Pro + + + + + +
+ + + + +
+ + +
+
+

Product Inventory

+ +
+ + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
PRODUCT NAMECATEGORYREGULAR PRICEMEMBER PRICESTOCKACTIONS
Rp Rp + + Low + + In Stock + + + + +
+
+
+
+
+
+
+ + + + + + + + diff --git a/vouchers.php b/vouchers.php new file mode 100644 index 0000000..0b35309 --- /dev/null +++ b/vouchers.php @@ -0,0 +1,278 @@ +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); +?> + + + + + + Manajemen Voucher - POS Pro + + + + + + +
+ + + + +
+ + +
+ +
+ + +
+ + +
+
+
+
+
Buat Voucher Baru
+
+
+
+ +
+ + + Kosongkan untuk pembuatan otomatis +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+
+
Voucher Aktif
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
KodeDiskonStatusKedaluwarsaAksi
Rp + + Terpakai + + Tersedia + + + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+

+ VOUCHER RESMI +
+
+ QR Code +
+
+
+
Rp
+
Voucher Diskon
+
+
+
+ KODE VOUCHER +
+
+
+ PINDAI UNTUK MENUKAR + +
+
+
+
+
+ + + + + \ No newline at end of file