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…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
Ringkasan Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NO. INV
+ PELANGGAN
+ TOTAL
+ STATUS
+
+
+
+
+ #INV-1024
+ Pelanggan Umum
+ Rp 45.000
+ Lunas
+
+
+ #INV-1023
+ John Doe
+ Rp 120.000
+ Lunas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Kopi Susu Gula Aren
+
Minuman
+
+
156 terjual
+
+
+
+
+
Nasi Goreng Spesial
+
Makanan
+
+
92 terjual
+
+
+
+
+
+
+
-
-
- Page updated: = htmlspecialchars($now) ?> (UTC)
-
+
+
+
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
+
+ Add Member
+
+
+
+
+
+ Member added successfully!
+
+
+
+
+
+
+
+
+
+
+ CODE
+ NAME
+ PHONE
+ EMAIL
+ POINTS
+ ACTIONS
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Semua Kategori
+ query("SELECT * FROM categories")->fetchAll();
+ foreach($categories as $cat): ?>
+
+
+
+
+
+
+
+
+
+
+
Produk tidak tersedia. Silakan tambah produk terlebih dahulu.
+
+
+
+
+
+
+
+
+
+
+
+
+
Keranjang Belanja
+ Bersihkan
+
+
+
+
+
Pilih Member (Opsional)
+
+ -- Pelanggan Umum --
+
+ ()
+
+
+
+
+
Poin: 0
+
+
+ Tukar Poin
+
+
+
+
+ 1 Poin = Rp 1
+
+
+
+
+
+
Voucher Promo
+
+
+ Gunakan
+
+
+
+
+
+
+
+
+
+
+
+ Subtotal
+ Rp 0
+
+
+ Hemat Member
+ - Rp 0
+
+
+ Tukar Poin
+ - Rp 0
+
+
+ Diskon Voucher
+ - Rp 0
+
+
+ Poin Didapat
+ 0 Pts
+
+
+
+
Total Akhir
+ Rp 0
+
+
+ PROSES PEMBAYARAN
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Total yang Harus Dibayar
+ 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
+
+ Add Product
+
+
+
+
+
+ Product added successfully!
+
+
+
+
+
+
+
+
+
+
+ PRODUCT NAME
+ CATEGORY
+ REGULAR PRICE
+ MEMBER PRICE
+ STOCK
+ ACTIONS
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manajemen Voucher
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Kode
+ Diskon
+ Status
+ Kedaluwarsa
+ Aksi
+
+
+
+
+
+
+ Rp
+
+
+ Terpakai
+
+ Tersedia
+
+
+
+
+
+ Cetak
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ VOUCHER RESMI
+
+
+
+
+
+
+
+
+ KODE VOUCHER
+
+
+
+ PINDAI UNTUK MENUKAR
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file