diff --git a/api/complete_sale.php b/api/complete_sale.php
new file mode 100644
index 0000000..4d4198c
--- /dev/null
+++ b/api/complete_sale.php
@@ -0,0 +1,88 @@
+ 'You must be logged in to complete a sale.']);
+ exit;
+}
+
+// Get cart data from POST body
+$json_data = file_get_contents('php://input');
+$cart = json_decode($json_data, true);
+
+if (empty($cart) || !is_array($cart)) {
+ http_response_code(400); // Bad Request
+ echo json_encode(['error' => 'Invalid or empty cart data provided.']);
+ exit;
+}
+
+$pdo = db();
+
+try {
+ $pdo->beginTransaction();
+
+ // 1. Calculate total and generate receipt number
+ $total_amount = 0;
+ foreach ($cart as $item) {
+ $total_amount += ($item['price'] * $item['quantity']);
+ }
+ $tax_amount = 0; // Assuming 0% tax for now
+ $receipt_number = 'SALE-' . date('Ymd-His') . '-' . strtoupper(uniqid());
+ $user_id = $_SESSION['user_id'];
+
+ // 2. Insert into `sales` table
+ $stmt = $pdo->prepare(
+ "INSERT INTO sales (receipt_number, total_amount, tax_amount, user_id) VALUES (?, ?, ?, ?)"
+ );
+ $stmt->execute([$receipt_number, $total_amount, $tax_amount, $user_id]);
+ $sale_id = $pdo->lastInsertId();
+
+ // 3. Insert into `sale_items` and update `inventory`
+ $sale_item_stmt = $pdo->prepare(
+ "INSERT INTO sale_items (sale_id, product_id, quantity, price_at_sale) VALUES (?, ?, ?, ?)"
+ );
+ $update_inventory_stmt = $pdo->prepare(
+ "UPDATE inventory SET quantity = quantity - ? WHERE product_id = ?"
+ );
+
+ // Check stock and lock rows before proceeding
+ foreach ($cart as $product_id => $item) {
+ $stmt = $pdo->prepare("SELECT quantity FROM inventory WHERE product_id = ? FOR UPDATE");
+ $stmt->execute([$product_id]);
+ $current_stock = $stmt->fetchColumn();
+ if ($current_stock === false || $current_stock < $item['quantity']) {
+ throw new Exception("Not enough stock for product: " . htmlspecialchars($item['name']));
+ }
+ }
+
+ // If all checks pass, insert items and update inventory
+ foreach ($cart as $product_id => $item) {
+ $sale_item_stmt->execute([$sale_id, $product_id, $item['quantity'], $item['price']]);
+ $update_inventory_stmt->execute([$item['quantity'], $product_id]);
+ }
+
+ // If we got here, everything is fine. Commit the transaction.
+ $pdo->commit();
+
+ // 4. Return success response
+ echo json_encode([
+ 'success' => true,
+ 'message' => 'Sale completed successfully!',
+ 'sale_id' => $sale_id,
+ 'receipt_number' => $receipt_number
+ ]);
+
+} catch (Exception $e) {
+ // An error occurred, roll back the transaction
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+
+ http_response_code(500); // Internal Server Error
+ echo json_encode(['error' => 'Failed to complete sale: ' . $e->getMessage()]);
+}
+?>
\ No newline at end of file
diff --git a/api/create_product.php b/api/create_product.php
new file mode 100644
index 0000000..7ba4257
--- /dev/null
+++ b/api/create_product.php
@@ -0,0 +1,32 @@
+prepare("INSERT INTO products (name, description, price, barcode) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$name, $description, $price, $barcode]);
+ $_SESSION['success_message'] = "Product created successfully!";
+} catch (PDOException $e) {
+ error_log("Product creation failed: " . $e->getMessage());
+ $_SESSION['error_message'] = "Failed to create product. Please try again.";
+}
+
+header('Location: /dashboard.php?page=admin_products');
+exit;
diff --git a/api/delete_product.php b/api/delete_product.php
new file mode 100644
index 0000000..7dc26d4
--- /dev/null
+++ b/api/delete_product.php
@@ -0,0 +1,37 @@
+ false, 'message' => 'Permission denied.'];
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
+ echo json_encode($response);
+ exit;
+}
+
+$id = $_POST['id'] ?? null;
+
+if (empty($id)) {
+ $response['message'] = 'Invalid product ID.';
+ echo json_encode($response);
+ exit;
+}
+
+try {
+ $pdo = db();
+ $stmt = $pdo->prepare("DELETE FROM products WHERE id = ?");
+ $stmt->execute([$id]);
+ if ($stmt->rowCount() > 0) {
+ $response['success'] = true;
+ $response['message'] = 'Product deleted successfully!';
+ } else {
+ $response['message'] = 'Product not found or already deleted.';
+ }
+} catch (PDOException $e) {
+ error_log("Product deletion failed: " . $e->getMessage());
+ $response['message'] = 'Failed to delete product.';
+}
+
+echo json_encode($response);
+exit;
diff --git a/api/get_sale_details.php b/api/get_sale_details.php
new file mode 100644
index 0000000..7f5823c
--- /dev/null
+++ b/api/get_sale_details.php
@@ -0,0 +1,57 @@
+ 'You do not have permission to view this content.']);
+ exit;
+}
+
+$sale_id = $_GET['id'] ?? 0;
+
+if (empty($sale_id)) {
+ http_response_code(400); // Bad Request
+ echo json_encode(['error' => 'Invalid Sale ID.']);
+ exit;
+}
+
+try {
+ $pdo = db();
+
+ // Fetch main sale info
+ $stmt = $pdo->prepare(
+ "SELECT s.id, s.receipt_number, s.total_amount, s.tax_amount, s.created_at, u.username as cashier_name
+ FROM sales s
+ LEFT JOIN users u ON s.user_id = u.id
+ WHERE s.id = ?"
+ );
+ $stmt->execute([$sale_id]);
+ $sale = $stmt->fetch();
+
+ if (!$sale) {
+ http_response_code(404); // Not Found
+ echo json_encode(['error' => 'Sale not found.']);
+ exit;
+ }
+
+ // Fetch sale items
+ $items_stmt = $pdo->prepare(
+ "SELECT si.quantity, si.price_at_sale, p.name as product_name
+ FROM sale_items si
+ JOIN products p ON si.product_id = p.id
+ WHERE si.sale_id = ?"
+ );
+ $items_stmt->execute([$sale_id]);
+ $items = $items_stmt->fetchAll();
+
+ $sale['items'] = $items;
+
+ echo json_encode($sale);
+
+} catch (PDOException $e) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
+}
+?>
\ No newline at end of file
diff --git a/api/search_products.php b/api/search_products.php
new file mode 100644
index 0000000..491bc94
--- /dev/null
+++ b/api/search_products.php
@@ -0,0 +1,42 @@
+prepare(
+ "SELECT id, name, barcode, price, description
+ FROM products
+ WHERE barcode = ?
+ LIMIT 1"
+ );
+ $stmt->execute([$query]);
+ } else {
+ $stmt = $pdo->prepare(
+ "SELECT id, name, barcode, price, description
+ FROM products
+ WHERE name LIKE ? OR barcode LIKE ?
+ LIMIT 10"
+ );
+ $stmt->execute(['%' . $query . '%', $query . '%']);
+ }
+
+ $products = $stmt->fetchAll();
+
+ echo json_encode($products);
+
+} catch (PDOException $e) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
+}
+?>
\ No newline at end of file
diff --git a/api/update_product.php b/api/update_product.php
new file mode 100644
index 0000000..06f060f
--- /dev/null
+++ b/api/update_product.php
@@ -0,0 +1,33 @@
+prepare("UPDATE products SET name = ?, description = ?, price = ?, barcode = ? WHERE id = ?");
+ $stmt->execute([$name, $description, $price, $barcode, $id]);
+ $_SESSION['success_message'] = "Product updated successfully!";
+} catch (PDOException $e) {
+ error_log("Product update failed: " . $e->getMessage());
+ $_SESSION['error_message'] = "Failed to update product. Please try again.";
+}
+
+header('Location: /dashboard.php?page=admin_products');
+exit;
diff --git a/api/update_stock.php b/api/update_stock.php
new file mode 100644
index 0000000..1453938
--- /dev/null
+++ b/api/update_stock.php
@@ -0,0 +1,30 @@
+prepare("UPDATE inventory SET quantity = ? WHERE product_id = ?");
+ $stmt->execute([$quantity, $product_id]);
+ $_SESSION['success_message'] = "Inventory updated successfully!";
+} catch (PDOException $e) {
+ error_log("Inventory update failed: " . $e->getMessage());
+ $_SESSION['error_message'] = "Failed to update inventory. Please try again.";
+}
+
+header('Location: /dashboard.php?page=admin_inventory');
+exit;
diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..7c2843d
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,101 @@
+:root {
+ --primary-color: #B8860B; /* DarkGoldenRod */
+ --secondary-color: #008080; /* Teal */
+ --background-color: #FDFDFD;
+ --surface-color: #FFFFFF;
+ --text-color: #343A40;
+ --font-family: 'Poppins', sans-serif;
+}
+
+body {
+ font-family: var(--font-family);
+ background-color: var(--background-color);
+}
+
+.login-body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ background-color: #f8f9fa;
+}
+
+.login-card {
+ border: none;
+ border-radius: 1rem;
+}
+
+.login-title {
+ font-weight: 600;
+ color: var(--primary-color);
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.btn-primary:hover {
+ background-color: #a3750a;
+ border-color: #a3750a;
+}
+
+.btn-secondary {
+ background-color: var(--secondary-color);
+ border-color: var(--secondary-color);
+}
+
+.btn-secondary:hover {
+ background-color: #006666;
+ border-color: #006666;
+}
+
+/* Dashboard Styles */
+.sidebar {
+ background-color: var(--surface-color);
+ border-right: 1px solid #dee2e6;
+ height: 100vh;
+ position: fixed;
+ width: 250px;
+ padding-top: 1rem;
+}
+
+.sidebar .nav-link {
+ color: var(--text-color);
+ font-weight: 500;
+ margin-bottom: 0.5rem;
+}
+
+.sidebar .nav-link.active,
+.sidebar .nav-link:hover {
+ color: var(--primary-color);
+ background-color: #f0e6d2;
+ border-radius: 0.5rem;
+}
+
+.sidebar .nav-link .bi {
+ margin-right: 10px;
+}
+
+.sidebar-header {
+ padding: 0 1rem 1rem;
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: var(--primary-color);
+}
+
+.main-content {
+ margin-left: 250px;
+ padding: 2rem;
+}
+
+.top-header {
+ background-color: var(--surface-color);
+ border-bottom: 1px solid #dee2e6;
+ padding: 1rem 2rem;
+ margin-left: 250px;
+}
+
+.status-indicator .badge {
+ font-size: 0.9rem;
+}
diff --git a/assets/js/main.js b/assets/js/main.js
new file mode 100644
index 0000000..752d989
--- /dev/null
+++ b/assets/js/main.js
@@ -0,0 +1,364 @@
+'''
+// Main javascript file for Opulent POS
+
+document.addEventListener('DOMContentLoaded', () => {
+ const page = document.body.dataset.page;
+
+ // --- Logic for Cashier Checkout Page ---
+ if (page === 'cashier_checkout') {
+ // --- Element Selectors ---
+ const barcodeInput = document.getElementById('barcode-scanner-input');
+ const productSearchInput = document.getElementById('product-search');
+ const productGrid = document.getElementById('product-grid');
+ const productGridPlaceholder = document.getElementById('product-grid-placeholder');
+ const cartItemsContainer = document.getElementById('cart-items');
+ const cartPlaceholder = document.getElementById('cart-placeholder');
+ const cartItemCount = document.getElementById('cart-item-count');
+ const cartSubtotal = document.getElementById('cart-subtotal');
+ const cartTax = document.getElementById('cart-tax');
+ const cartTotal = document.getElementById('cart-total');
+ const completeSaleBtn = document.getElementById('complete-sale-btn');
+ const cancelSaleBtn = document.getElementById('cancel-sale-btn');
+ const printLastInvoiceBtn = document.getElementById('print-last-invoice-btn');
+
+ // --- State Management ---
+ let cart = JSON.parse(localStorage.getItem('cart')) || {};
+
+ // --- Utility Functions ---
+ const debounce = (func, delay) => {
+ let timeout;
+ return function(...args) {
+ const context = this;
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func.apply(context, args), delay);
+ };
+ };
+
+ const formatCurrency = (amount) => `PKR ${parseFloat(amount).toFixed(2)}`;
+
+ // --- API Communication ---
+ const searchProducts = async (query) => {
+ if (query.length < 2) {
+ productGrid.innerHTML = '';
+ productGridPlaceholder.style.display = 'block';
+ return;
+ }
+ try {
+ const response = await fetch(`api/search_products.php?q=${encodeURIComponent(query)}`);
+ if (!response.ok) throw new Error('Network response was not ok');
+ const products = await response.json();
+ renderProductGrid(products);
+ } catch (error) {
+ console.error('Error fetching products:', error);
+ productGrid.innerHTML = '
Could not fetch products.
';
+ }
+ };
+
+ const findProductByBarcode = async (barcode) => {
+ try {
+ const response = await fetch(`api/search_products.php?q=${encodeURIComponent(barcode)}&exact=true`);
+ if (!response.ok) throw new Error('Network response was not ok');
+ const products = await response.json();
+ if (products.length > 0) {
+ addToCart(products[0]);
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('Error fetching product by barcode:', error);
+ return false;
+ }
+ };
+
+ // --- Rendering Functions ---
+ const renderProductGrid = (products) => {
+ productGrid.innerHTML = '';
+ if (products.length === 0) {
+ productGridPlaceholder.innerHTML = 'No products found.
';
+ productGridPlaceholder.style.display = 'block';
+ return;
+ }
+ productGridPlaceholder.style.display = 'none';
+ products.forEach(product => {
+ const productCard = document.createElement('div');
+ productCard.className = 'col';
+ productCard.innerHTML = `
+
+
+
${product.name}
+
${formatCurrency(product.price)}
+
+
+ `;
+ productGrid.appendChild(productCard);
+ });
+ };
+
+ const renderCart = () => {
+ cartItemsContainer.innerHTML = '';
+ let subtotal = 0;
+ let itemCount = 0;
+
+ if (Object.keys(cart).length === 0) {
+ cartItemsContainer.appendChild(cartPlaceholder);
+ } else {
+ const table = document.createElement('table');
+ table.className = 'table table-sm';
+ table.innerHTML = `
+
+
+ Item
+ Qty
+ Price
+
+
+
+ `;
+ const tbody = table.querySelector('tbody');
+
+ for (const productId in cart) {
+ const item = cart[productId];
+ subtotal += item.price * item.quantity;
+ itemCount += item.quantity;
+ const row = document.createElement('tr');
+ row.innerHTML = `
+
+ ${item.name}
+ ${formatCurrency(item.price)}
+
+
+
+ -
+
+ +
+
+
+ ${formatCurrency(item.price * item.quantity)}
+
+
+
+ `;
+ tbody.appendChild(row);
+ }
+ cartItemsContainer.appendChild(table);
+ }
+
+ const tax = subtotal * 0;
+ const total = subtotal + tax;
+ cartSubtotal.textContent = formatCurrency(subtotal);
+ cartTax.textContent = formatCurrency(tax);
+ cartTotal.textContent = formatCurrency(total);
+ cartItemCount.textContent = itemCount;
+ completeSaleBtn.disabled = itemCount === 0;
+ };
+
+ // --- Cart Logic ---
+ const saveCart = () => localStorage.setItem('cart', JSON.stringify(cart));
+ const addToCart = (product) => {
+ const id = product.id;
+ if (cart[id]) {
+ cart[id].quantity++;
+ } else {
+ cart[id] = { id: id, name: product.name, price: parseFloat(product.price), quantity: 1, barcode: product.barcode };
+ }
+ saveCart();
+ renderCart();
+ };
+ const updateCartQuantity = (productId, change) => {
+ if (cart[productId]) {
+ cart[productId].quantity += change;
+ if (cart[productId].quantity <= 0) delete cart[productId];
+ saveCart();
+ renderCart();
+ }
+ };
+ const removeFromCart = (productId) => {
+ if (cart[productId]) {
+ delete cart[productId];
+ saveCart();
+ renderCart();
+ }
+ };
+ const clearCart = () => { cart = {}; saveCart(); renderCart(); };
+
+ // --- Sale Completion & Invoicing ---
+ const completeSale = async () => {
+ if (Object.keys(cart).length === 0) return alert('Cannot complete sale with an empty cart.');
+ if (!confirm('Are you sure you want to complete this sale?')) return;
+
+ completeSaleBtn.disabled = true;
+ completeSaleBtn.innerHTML = ' Processing...';
+ try {
+ const response = await fetch('api/complete_sale.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(cart) });
+ const result = await response.json();
+ if (response.ok && result.success) {
+ alert(`Sale Completed Successfully!
+Receipt Number: ${result.receipt_number}`);
+ localStorage.setItem('lastSale', JSON.stringify({ cart: { ...cart }, ...result }));
+ clearCart();
+ } else {
+ throw new Error(result.error || 'An unknown error occurred.');
+ }
+ } catch (error) {
+ alert(`Failed to complete sale: ${error.message}`);
+ } finally {
+ completeSaleBtn.disabled = false;
+ completeSaleBtn.innerHTML = 'Complete Sale';
+ }
+ };
+
+ const printInvoice = () => {
+ const lastSale = JSON.parse(localStorage.getItem('lastSale'));
+ if (!lastSale) {
+ alert('No last sale found to print.');
+ return;
+ }
+
+ // Populate invoice details
+ document.getElementById('invoice-receipt-number').textContent = lastSale.receipt_number;
+ document.getElementById('invoice-cashier-name').textContent = lastSale.cashier_name || 'N/A';
+ document.getElementById('invoice-date').textContent = new Date(lastSale.created_at).toLocaleString();
+
+ const itemsTable = document.getElementById('invoice-items-table');
+ itemsTable.innerHTML = '';
+ let subtotal = 0;
+ let i = 0;
+ for (const productId in lastSale.cart) {
+ const item = lastSale.cart[productId];
+ const total = item.price * item.quantity;
+ subtotal += total;
+ const row = itemsTable.insertRow();
+ row.innerHTML = `
+ ${++i}
+ ${item.name}
+ ${item.quantity}
+ ${formatCurrency(item.price)}
+ ${formatCurrency(total)}
+ `;
+ }
+ const tax = subtotal * 0;
+ document.getElementById('invoice-subtotal').textContent = formatCurrency(subtotal);
+ document.getElementById('invoice-tax').textContent = formatCurrency(tax);
+ document.getElementById('invoice-total').textContent = formatCurrency(subtotal + tax);
+
+ // Print logic
+ const invoiceContent = document.getElementById('invoice-container').innerHTML;
+ const printWindow = window.open('', '_blank', 'height=600,width=800');
+ printWindow.document.write('Print Invoice ');
+ // Include bootstrap for styling
+ printWindow.document.write(' ');
+ printWindow.document.write('');
+ printWindow.document.write('');
+ printWindow.document.write(invoiceContent);
+ printWindow.document.write('');
+ printWindow.document.close();
+ setTimeout(() => { // Wait for content to load
+ printWindow.print();
+ }, 500);
+ };
+
+ // --- Event Listeners ---
+ barcodeInput.addEventListener('keyup', async (e) => {
+ if (e.key === 'Enter') {
+ const barcode = barcodeInput.value.trim();
+ if (barcode) {
+ barcodeInput.disabled = true;
+ const found = await findProductByBarcode(barcode);
+ if (!found) {
+ alert(`Product with barcode "${barcode}" not found.`);
+ }
+ barcodeInput.value = '';
+ barcodeInput.disabled = false;
+ barcodeInput.focus();
+ }
+ }
+ });
+
+ productSearchInput.addEventListener('keyup', debounce((e) => searchProducts(e.target.value.trim()), 300));
+
+ productGrid.addEventListener('click', (e) => {
+ const card = e.target.closest('.product-card');
+ if (card) {
+ addToCart({
+ id: card.dataset.productId,
+ name: card.dataset.productName,
+ price: card.dataset.productPrice,
+ barcode: card.dataset.productBarcode
+ });
+ }
+ });
+
+ cartItemsContainer.addEventListener('click', (e) => {
+ const quantityChangeBtn = e.target.closest('.cart-quantity-change');
+ const removeItemBtn = e.target.closest('.cart-remove-item');
+ if (quantityChangeBtn) {
+ updateCartQuantity(quantityChangeBtn.dataset.productId, parseInt(quantityChangeBtn.dataset.change, 10));
+ }
+ if (removeItemBtn) {
+ removeFromCart(removeItemBtn.dataset.productId);
+ }
+ });
+
+ cancelSaleBtn.addEventListener('click', () => {
+ if (confirm('Are you sure you want to cancel this sale and clear the cart?')) {
+ clearCart();
+ }
+ });
+
+ completeSaleBtn.addEventListener('click', completeSale);
+ printLastInvoiceBtn.addEventListener('click', printInvoice);
+
+ // --- Initial Load ---
+ renderCart();
+ barcodeInput.focus();
+ }
+
+ // --- Logic for Admin Sales Page ---
+ if (page === 'admin_sales') {
+ const saleDetailsModal = new bootstrap.Modal(document.getElementById('saleDetailsModal'));
+ const saleDetailsContent = document.getElementById('saleDetailsContent');
+
+ document.body.addEventListener('click', async (e) => {
+ const detailsButton = e.target.closest('a.btn-outline-info[href*="action=details"]');
+ if (detailsButton) {
+ e.preventDefault();
+ const url = new URL(detailsButton.href);
+ const saleId = url.searchParams.get('id');
+
+ saleDetailsContent.innerHTML = 'Loading...
';
+ saleDetailsModal.show();
+
+ try {
+ const response = await fetch(`api/get_sale_details.php?id=${saleId}`);
+ if (!response.ok) throw new Error('Failed to fetch details.');
+ const sale = await response.json();
+
+ let itemsHtml = '';
+ sale.items.forEach(item => {
+ itemsHtml += `
+ ${item.product_name} (x${item.quantity})
+ PKR ${parseFloat(item.price_at_sale * item.quantity).toFixed(2)}
+ `;
+ });
+ itemsHtml += ' ';
+
+ saleDetailsContent.innerHTML = `
+ Receipt: ${sale.receipt_number}
+ Cashier: ${sale.cashier_name || 'N/A'}
+ Date: ${new Date(sale.created_at).toLocaleString()}
+
+ Items Sold
+ ${itemsHtml}
+
+
+
Total: PKR ${parseFloat(sale.total_amount).toFixed(2)}
+
+ `;
+ } catch (error) {
+ saleDetailsContent.innerHTML = `Error: ${error.message}
`;
+ }
+ }
+ });
+ }
+});
+''
\ No newline at end of file
diff --git a/dashboard.php b/dashboard.php
new file mode 100644
index 0000000..8406f43
--- /dev/null
+++ b/dashboard.php
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+ Dashboard - Opulent POS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Page not foundLooking for: {$page_path}
";
+ }
+ ?>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/db/migrate.php b/db/migrate.php
new file mode 100644
index 0000000..c94d22b
--- /dev/null
+++ b/db/migrate.php
@@ -0,0 +1,67 @@
+exec("CREATE TABLE IF NOT EXISTS `migrations` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `migration_file` VARCHAR(255) NOT NULL UNIQUE,
+ `applied_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
+
+ // 2. Get applied migrations
+ $appliedMigrations = $pdo->query("SELECT `migration_file` FROM `migrations`")->fetchAll(PDO::FETCH_COLUMN);
+
+ // 3. Find migration files
+ $migrationFiles = glob(__DIR__ . '/migrations/*.sql');
+ if (empty($migrationFiles)) {
+ echo "No migration files found.\n";
+ }
+
+ // 4. Apply pending migrations
+ $migrationsApplied = 0;
+ foreach ($migrationFiles as $file) {
+ $basename = basename($file);
+ if (!in_array($basename, $appliedMigrations)) {
+ echo "Applying migration: {$basename}...\n";
+
+ $sql = file_get_contents($file);
+ $pdo->exec($sql);
+
+ $stmt = $pdo->prepare("INSERT INTO `migrations` (`migration_file`) VALUES (?)");
+ $stmt->execute([$basename]);
+
+ echo " -> Applied successfully.\n";
+ $migrationsApplied++;
+ } else {
+ echo "Skipping already applied migration: {$basename}\n";
+ }
+ }
+
+ if ($migrationsApplied > 0) {
+ echo "\nMigration complete. Applied {$migrationsApplied} new migration(s).\n";
+ } else {
+ echo "\nDatabase is already up to date.\n";
+ }
+
+ // 5. Synchronize inventory table
+ echo "\nSynchronizing inventory...\n";
+ $stmt = $pdo->query("INSERT INTO inventory (product_id, quantity)
+ SELECT p.id, 0 FROM products p
+ LEFT JOIN inventory i ON p.id = i.product_id
+ WHERE i.product_id IS NULL");
+ $newInventoryCount = $stmt->rowCount();
+ if ($newInventoryCount > 0) {
+ echo "Added {$newInventoryCount} new products to the inventory with a default stock of 0.\n";
+ } else {
+ echo "Inventory is already in sync with products.\n";
+ }
+
+} catch (PDOException $e) {
+ die("Database migration failed: " . $e->getMessage() . "\n");
+}
+?>
\ No newline at end of file
diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql
new file mode 100644
index 0000000..4c358d0
--- /dev/null
+++ b/db/migrations/001_initial_schema.sql
@@ -0,0 +1,53 @@
+
+CREATE TABLE IF NOT EXISTS `users` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `username` VARCHAR(255) NOT NULL UNIQUE,
+ `password_hash` VARCHAR(255) NOT NULL,
+ `role` ENUM('admin', 'cashier') NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `products` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `name` VARCHAR(255) NOT NULL,
+ `barcode` VARCHAR(255) NULL UNIQUE,
+ `price` DECIMAL(10, 2) NOT NULL,
+ `description` TEXT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `inventory` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `product_id` INT NOT NULL,
+ `quantity` INT NOT NULL DEFAULT 0,
+ `low_stock_threshold` INT NOT NULL DEFAULT 10,
+ `last_updated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `sales` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `receipt_number` VARCHAR(255) NOT NULL UNIQUE,
+ `total_amount` DECIMAL(10, 2) NOT NULL,
+ `tax_amount` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
+ `user_id` INT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+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_at_sale` DECIMAL(10, 2) NOT NULL,
+ FOREIGN KEY (`sale_id`) REFERENCES `sales`(`id`) ON DELETE CASCADE,
+ FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON DELETE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- Seed default users: admin/password, cashier/password
+-- IMPORTANT: These are default passwords and should be changed immediately.
+INSERT IGNORE INTO `users` (`username`, `password_hash`, `role`) VALUES
+('admin', '$2y$10$lZrH00Q3UgsdZWnEWU0EROOCaOtGdeSVcQ1pdYg4ft77jR6zW4.UG', 'admin'), -- password
+('cashier', '$2y$10$lZrH00Q3UgsdZWnEWU0EROOCaOtGdeSVcQ1pdYg4ft77jR6zW4.UG', 'cashier'); -- password
diff --git a/db/migrations/002_add_sample_products.sql b/db/migrations/002_add_sample_products.sql
new file mode 100644
index 0000000..521acb3
--- /dev/null
+++ b/db/migrations/002_add_sample_products.sql
@@ -0,0 +1,6 @@
+INSERT INTO `products` (`name`, `description`, `price`, `barcode`) VALUES
+('Vintage Leather Journal', 'A beautiful leather-bound journal for your thoughts and sketches.', 25.00, '1234567890123'),
+('Stainless Steel Water Bottle', 'Keeps your drinks cold for 24 hours or hot for 12.', 18.50, '2345678901234'),
+('Organic Blend Coffee Beans', 'A rich, full-bodied coffee blend, ethically sourced.', 15.75, '3456789012345'),
+('Wireless Noise-Cancelling Headphones', 'Immerse yourself in sound with these high-fidelity headphones.', 120.00, '4567890123456'),
+('Handcrafted Wooden Pen', 'A unique, handcrafted pen made from reclaimed oak.', 35.00, '5678901234567');
\ No newline at end of file
diff --git a/index.php b/index.php
index 7205f3d..f3829c9 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,4 @@
-
-
-
-
-
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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) ?>
-
-
-
- Page updated: = htmlspecialchars($now) ?> (UTC)
-
-
-
+// Redirect to the login page
+header('Location: login.php');
+exit;
\ No newline at end of file
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..66d444c
--- /dev/null
+++ b/login.php
@@ -0,0 +1,105 @@
+prepare("SELECT * FROM `users` WHERE `username` = ?");
+ $stmt->execute([$username]);
+ $user = $stmt->fetch();
+
+ if ($user && password_verify($password, $user['password_hash'])) {
+ // Password is correct, start session
+ $_SESSION['user_id'] = $user['id'];
+ $_SESSION['username'] = $user['username'];
+ $_SESSION['role'] = $user['role'];
+
+ header("Location: dashboard.php");
+ exit;
+ } else {
+ $error_message = 'Invalid username or password.';
+ }
+ } catch (PDOException $e) {
+ $error_message = 'Database error. Please try again later.';
+ // In a real production environment, you would log this error.
+ // error_log($e->getMessage());
+ }
+ }
+}
+?>
+
+
+
+
+
+ Login - Opulent POS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Opulent POS
+
Please sign in to continue
+
+
+
+
+
+
+
+
+
+
+
+ Default users seeded:
+ Admin: admin / password
+ Cashier: cashier / password
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..2370a5c
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..afec5ac
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "Opulent POS",
+ "short_name": "OpulentPOS",
+ "start_url": "index.php",
+ "display": "standalone",
+ "background_color": "#FFFFFF",
+ "theme_color": "#B8860B",
+ "description": "A simple offline PWA point of sale system.",
+ "icons": [
+ {
+ "src": "assets/images/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "assets/images/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/service-worker.js b/service-worker.js
new file mode 100644
index 0000000..bdc3288
--- /dev/null
+++ b/service-worker.js
@@ -0,0 +1,70 @@
+'''
+const CACHE_NAME = 'opulent-pos-cache-v1';
+const urlsToCache = [
+ '/dashboard.php?view=cashier_checkout',
+ '/assets/css/custom.css',
+ '/assets/js/main.js',
+ 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
+ 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css'
+];
+
+self.addEventListener('install', event => {
+ event.waitUntil(
+ caches.open(CACHE_NAME)
+ .then(cache => {
+ console.log('Opened cache');
+ // Add all the assets to the cache
+ return cache.addAll(urlsToCache);
+ })
+ );
+});
+
+self.addEventListener('fetch', event => {
+ event.respondWith(
+ caches.match(event.request)
+ .then(response => {
+ // Cache hit - return response
+ if (response) {
+ return response;
+ }
+
+ // Clone the request because it's a stream and can only be consumed once.
+ const fetchRequest = event.request.clone();
+
+ return fetch(fetchRequest).then(
+ response => {
+ // Check if we received a valid response
+ if (!response || response.status !== 200 || response.type !== 'basic') {
+ return response;
+ }
+
+ // Clone the response because it also is a stream
+ const responseToCache = response.clone();
+
+ caches.open(CACHE_NAME)
+ .then(cache => {
+ cache.put(event.request, responseToCache);
+ });
+
+ return response;
+ }
+ );
+ })
+ );
+});
+
+self.addEventListener('activate', event => {
+ const cacheWhitelist = [CACHE_NAME];
+ event.waitUntil(
+ caches.keys().then(cacheNames => {
+ return Promise.all(
+ cacheNames.map(cacheName => {
+ if (cacheWhitelist.indexOf(cacheName) === -1) {
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ })
+ );
+});
+'''
\ No newline at end of file
diff --git a/views/admin_edit_product.php b/views/admin_edit_product.php
new file mode 100644
index 0000000..3050cbd
--- /dev/null
+++ b/views/admin_edit_product.php
@@ -0,0 +1,38 @@
+Invalid product ID.";
+ return;
+}
+
+$product_id = $_GET['id'];
+
+try {
+ $pdo = db();
+ $stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
+ $stmt->execute([$product_id]);
+ $product = $stmt->fetch();
+} catch (PDOException $e) {
+ echo "Database error: " . htmlspecialchars($e->getMessage()) . "
";
+ return;
+}
+
+if (!$product) {
+ echo "Product not found.
";
+ return;
+}
+?>
+
+Edit Product
+
+' . htmlspecialchars($_SESSION['error_message']) . ' ';
+ unset($_SESSION['error_message']);
+}
+?>
+
+
diff --git a/views/admin_inventory.php b/views/admin_inventory.php
new file mode 100644
index 0000000..4274916
--- /dev/null
+++ b/views/admin_inventory.php
@@ -0,0 +1,65 @@
+query("SELECT p.id, p.name, p.barcode, i.quantity
+ FROM products p
+ JOIN inventory i ON p.id = i.product_id
+ ORDER BY p.name");
+ $inventory_items = $stmt->fetchAll();
+} catch (PDOException $e) {
+ echo "Database error: " . htmlspecialchars($e->getMessage()) . "
";
+ $inventory_items = [];
+}
+?>
+
+Inventory Management
+
+' . htmlspecialchars($_SESSION['success_message']) . ' ';
+ unset($_SESSION['success_message']);
+}
+if (isset($_SESSION['error_message'])) {
+ echo '' . htmlspecialchars($_SESSION['error_message']) . '
';
+ unset($_SESSION['error_message']);
+}
+?>
+
+
+
+
+
+
+
+ Product Name
+ Barcode
+ Current Stock
+ Update Stock
+
+
+
+
+
+ No products found in inventory.
+
+
+
+
+ = htmlspecialchars($item['name']) ?>
+ = htmlspecialchars($item['barcode'] ?? 'N/A') ?>
+ = htmlspecialchars($item['quantity']) ?>
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/admin_products.php b/views/admin_products.php
new file mode 100644
index 0000000..d2dcc0b
--- /dev/null
+++ b/views/admin_products.php
@@ -0,0 +1,119 @@
+query("SELECT * FROM products ORDER BY created_at DESC");
+ $products = $stmt->fetchAll();
+} catch (PDOException $e) {
+ echo "Database error: " . htmlspecialchars($e->getMessage()) . "
";
+ $products = [];
+}
+?>
+
+
+
Manage Products
+
+
+' . htmlspecialchars($_SESSION['success_message']) . ' ';
+ unset($_SESSION['success_message']);
+}
+if (isset($_SESSION['error_message'])) {
+ echo '' . htmlspecialchars($_SESSION['error_message']) . '
';
+ unset($_SESSION['error_message']);
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Name
+ Price
+ Barcode
+ Actions
+
+
+
+
+
+ No products found.
+
+
+
+
+ = htmlspecialchars($product['id']) ?>
+ = htmlspecialchars($product['name']) ?>
+ = htmlspecialchars(number_format($product['price'], 2)) ?>
+ = htmlspecialchars($product['barcode'] ?? 'N/A') ?>
+
+
+ Edit
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/admin_sales.php b/views/admin_sales.php
new file mode 100644
index 0000000..b454125
--- /dev/null
+++ b/views/admin_sales.php
@@ -0,0 +1,119 @@
+query("SELECT s.id, s.receipt_number, s.total_amount, s.created_at, u.username
+ FROM sales s
+ JOIN users u ON s.user_id = u.id
+ ORDER BY s.created_at DESC");
+ $sales = $stmt->fetchAll();
+} catch (PDOException $e) {
+ echo "Database error: " . htmlspecialchars($e->getMessage()) . "
";
+ $sales = [];
+}
+?>
+
+Sales Reports
+
+
+
+
+
+
+
+ Receipt Number
+ Cashier
+ Total Amount
+ Date
+ Actions
+
+
+
+
+
+ No sales recorded yet.
+
+
+
+
+ = htmlspecialchars($sale['receipt_number']) ?>
+ = htmlspecialchars($sale['username']) ?>
+ $= htmlspecialchars(number_format($sale['total_amount'], 2)) ?>
+ = htmlspecialchars(date('M d, Y h:i A', strtotime($sale['created_at']))) ?>
+
+
+ View Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/cashier_checkout.php b/views/cashier_checkout.php
new file mode 100644
index 0000000..b60a76c
--- /dev/null
+++ b/views/cashier_checkout.php
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
Scan a barcode or search for a product to begin.
+
+
+
+
+
+
+
+ Total:
+ PKR 0.00
+
+
Complete Sale
+
+ Cancel Sale
+ Print Last Invoice
+
+
+
+
+
+
+
+
+
+
+
+
+ Scan Product Barcode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Search results will appear here.
+
+
+
+
+
+
+
+
+
+
+
+
Opulent POS
+
123 Business Rd, Cityville
+
Sale Invoice
+
+
+
+ Receipt No:
+ Cashier:
+
+
+ Date:
+
+
+
+
+
+ #
+ Item
+ Qty
+ Price
+ Total
+
+
+
+
+
+
+
+ Subtotal:
+
+
+
+ Tax:
+
+
+
+ Total:
+
+
+
+
+
+
Thank you for your business!
+
+
+
diff --git a/views/partials/product_form.php b/views/partials/product_form.php
new file mode 100644
index 0000000..d2f9399
--- /dev/null
+++ b/views/partials/product_form.php
@@ -0,0 +1,29 @@
+
+
\ No newline at end of file