Compare commits

...

4 Commits

Author SHA1 Message Date
Flatlogic Bot
0cff4dbb3e version 4 2025-11-22 17:31:56 +00:00
Flatlogic Bot
183ad1886c Version 3 2025-11-22 17:28:11 +00:00
Flatlogic Bot
4d1d667390 version 2 2025-11-22 17:22:34 +00:00
Flatlogic Bot
b6296eed55 Version 1 2025-11-22 17:18:03 +00:00
26 changed files with 1686 additions and 149 deletions

88
api/complete_sale.php Normal file
View File

@ -0,0 +1,88 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
session_start();
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
http_response_code(401); // Unauthorized
echo json_encode(['error' => '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()]);
}
?>

32
api/create_product.php Normal file
View File

@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../db/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: /login.php');
exit;
}
$name = $_POST['name'] ?? '';
$description = $_POST['description'] ?? '';
$price = $_POST['price'] ?? 0;
$barcode = $_POST['barcode'] ?? null;
if (empty($name) || !is_numeric($price)) {
$_SESSION['error_message'] = "Product name and a valid price are required.";
header('Location: /dashboard.php?page=admin_products');
exit;
}
try {
$pdo = db();
$stmt = $pdo->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;

37
api/delete_product.php Normal file
View File

@ -0,0 +1,37 @@
<?php
require_once __DIR__ . '/../db/config.php';
session_start();
header('Content-Type: application/json');
$response = ['success' => 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;

57
api/get_sale_details.php Normal file
View File

@ -0,0 +1,57 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
session_start();
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
http_response_code(403); // Forbidden
echo json_encode(['error' => '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()]);
}
?>

42
api/search_products.php Normal file
View File

@ -0,0 +1,42 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$query = $_GET['q'] ?? '';
$exact = isset($_GET['exact']) && $_GET['exact'] === 'true';
if (strlen($query) < 2 && !$exact) {
echo json_encode([]);
exit;
}
try {
$pdo = db();
if ($exact) {
$stmt = $pdo->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()]);
}
?>

33
api/update_product.php Normal file
View File

@ -0,0 +1,33 @@
<?php
require_once __DIR__ . '/../db/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: /login.php');
exit;
}
$id = $_POST['id'] ?? null;
$name = $_POST['name'] ?? '';
$description = $_POST['description'] ?? '';
$price = $_POST['price'] ?? 0;
$barcode = $_POST['barcode'] ?? null;
if (empty($id) || empty($name) || !is_numeric($price)) {
$_SESSION['error_message'] = "Invalid data provided.";
header('Location: /dashboard.php?page=admin_products');
exit;
}
try {
$pdo = db();
$stmt = $pdo->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;

30
api/update_stock.php Normal file
View File

@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../db/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: /login.php');
exit;
}
$product_id = $_POST['product_id'] ?? null;
$quantity = $_POST['quantity'] ?? null;
if (empty($product_id) || !is_numeric($quantity) || $quantity < 0) {
$_SESSION['error_message'] = "Invalid data provided. Please enter a valid quantity.";
header('Location: /dashboard.php?page=admin_inventory');
exit;
}
try {
$pdo = db();
$stmt = $pdo->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;

101
assets/css/custom.css Normal file
View File

@ -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;
}

314
assets/js/main.js Normal file
View File

@ -0,0 +1,314 @@
document.addEventListener('DOMContentLoaded', function () {
const barcodeInput = document.getElementById('barcode-scanner-input');
const productSearchInput = document.getElementById('product-search');
const productGrid = document.getElementById('product-grid');
const cartItemsContainer = document.getElementById('cart-items');
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');
const cartPlaceholder = document.getElementById('cart-placeholder');
const productGridPlaceholder = document.getElementById('product-grid-placeholder');
let cart = [];
const TAX_RATE = 0.00;
// =========================================================================
// CORE FUNCTIONS
// =========================================================================
const showToast = (message, type = 'success') => {
// A simple toast notification function (can be replaced with a library)
const toast = document.createElement('div');
toast.className = `toast show align-items-center text-white bg-${type} border-0`;
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
};
const searchProducts = (query, searchBy = 'name') => {
if (query.length < 2) {
productGrid.innerHTML = '';
if (productGridPlaceholder) productGridPlaceholder.style.display = 'block';
return;
}
const url = searchBy === 'barcode'
? `../api/search_products.php?barcode=${encodeURIComponent(query)}`
: `../api/search_products.php?name=${encodeURIComponent(query)}`;
fetch(url)
.then(response => response.json())
.then(products => {
if (productGridPlaceholder) productGridPlaceholder.style.display = 'none';
productGrid.innerHTML = '';
if (products.length > 0) {
if (searchBy === 'barcode' && products.length === 1) {
addToCart(products[0]);
barcodeInput.value = ''; // Clear input after successful scan
barcodeInput.focus();
} else {
products.forEach(product => {
const productCard = `
<div class="col">
<div class="card h-100 product-card" data-product-id="${product.id}">
<div class="card-body text-center">
<h6 class="card-title fs-sm">${product.name}</h6>
<p class="card-text fw-bold">PKR ${parseFloat(product.price).toFixed(2)}</p>
</div>
</div>
</div>`;
productGrid.innerHTML += productCard;
});
}
} else {
if (productGridPlaceholder) productGridPlaceholder.style.display = 'block';
if (searchBy === 'barcode') {
showToast('Product not found for this barcode.', 'danger');
barcodeInput.value = '';
barcodeInput.focus();
}
}
})
.catch(error => {
console.error('Error searching products:', error);
showToast('Failed to search for products.', 'danger');
});
};
const addToCart = (product) => {
const existingItem = cart.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity++;
} else {
cart.push({ ...product, quantity: 1 });
}
updateCartView();
showToast(`${product.name} added to cart.`, 'success');
};
const updateCartView = () => {
if (cart.length === 0) {
if (cartPlaceholder) cartPlaceholder.style.display = 'block';
cartItemsContainer.innerHTML = cartPlaceholder ? cartPlaceholder.outerHTML : '';
} else {
if (cartPlaceholder) cartPlaceholder.style.display = 'none';
const table = `
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Item</th>
<th class="text-center">Qty</th>
<th class="text-end">Price</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
${cart.map(item => `
<tr>
<td class="w-50">${item.name}</td>
<td class="text-center">
<input type="number" class="form-control form-control-sm quantity-input" data-product-id="${item.id}" value="${item.quantity}" min="1">
</td>
<td class="text-end">PKR ${parseFloat(item.price).toFixed(2)}</td>
<td class="text-end">
<button class="btn btn-outline-danger btn-sm remove-item-btn" data-product-id="${item.id}">
&times;
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>`;
cartItemsContainer.innerHTML = table;
}
updateCartTotal();
};
const updateCartTotal = () => {
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const tax = subtotal * TAX_RATE;
const total = subtotal + tax;
cartSubtotal.textContent = `PKR ${subtotal.toFixed(2)}`;
cartTax.textContent = `PKR ${tax.toFixed(2)}`;
cartTotal.textContent = `PKR ${total.toFixed(2)}`;
cartItemCount.textContent = cart.reduce((sum, item) => sum + item.quantity, 0);
completeSaleBtn.disabled = cart.length === 0;
};
const completeSale = () => {
if (cart.length === 0) return;
fetch('../api/complete_sale.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cart: cart })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Sale completed successfully!', 'success');
saveInvoiceForOffline(data.sale_details);
printInvoice(data.sale_details);
cart = [];
updateCartView();
} else {
showToast(`Sale failed: ${data.message}`, 'danger');
}
})
.catch(error => {
console.error('Error completing sale:', error);
showToast('An error occurred while completing the sale.', 'danger');
});
};
const cancelSale = () => {
if (confirm('Are you sure you want to cancel this sale?')) {
cart = [];
updateCartView();
showToast('Sale cancelled.', 'info');
}
};
const saveInvoiceForOffline = (saleDetails) => {
try {
localStorage.setItem('lastInvoice', JSON.stringify(saleDetails));
} catch (e) {
console.error('Could not save invoice to localStorage:', e);
}
};
const printInvoice = (invoiceData) => {
if (!invoiceData) {
showToast('No invoice data to print.', 'warning');
return;
}
document.getElementById('invoice-receipt-number').textContent = invoiceData.sale_id;
document.getElementById('invoice-cashier-name').textContent = invoiceData.cashier_name || 'N/A';
document.getElementById('invoice-date').textContent = new Date(invoiceData.sale_date).toLocaleString();
const itemsHtml = invoiceData.items.map((item, index) => `
<tr>
<td>${index + 1}</td>
<td>${item.name}</td>
<td class="text-center">${item.quantity}</td>
<td class="text-end">PKR ${parseFloat(item.price).toFixed(2)}</td>
<td class="text-end">PKR ${(item.quantity * item.price).toFixed(2)}</td>
</tr>
`).join('');
document.getElementById('invoice-items-table').innerHTML = itemsHtml;
const subtotal = invoiceData.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const tax = subtotal * TAX_RATE;
const total = subtotal + tax;
document.getElementById('invoice-subtotal').textContent = `PKR ${subtotal.toFixed(2)}`;
document.getElementById('invoice-tax').textContent = `PKR ${tax.toFixed(2)}`;
document-getElementById('invoice-total').textContent = `PKR ${total.toFixed(2)}`;
const invoiceHtml = document.getElementById('invoice-container').innerHTML;
const printWindow = window.open('', '_blank');
printWindow.document.write('<html><head><title>Print Invoice</title>');
// Optional: Add bootstrap for styling the print view
printWindow.document.write('<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">');
printWindow.document.write('</head><body>');
printWindow.document.write(invoiceHtml.replace('d-none', '')); // Show the invoice
printWindow.document.write('</body></html>');
printWindow.document.close();
setTimeout(() => { // Wait for content to load
printWindow.print();
printWindow.close();
}, 500);
};
const printLastInvoice = () => {
try {
const lastInvoice = localStorage.getItem('lastInvoice');
if (lastInvoice) {
printInvoice(JSON.parse(lastInvoice));
} else {
showToast('No previously saved invoice found.', 'info');
}
} catch (e) {
console.error('Could not retrieve or print last invoice:', e);
showToast('Failed to print last invoice.', 'danger');
}
};
// =========================================================================
// EVENT LISTENERS
// =========================================================================
if (barcodeInput) {
barcodeInput.addEventListener('change', (e) => { // 'change' is often better for scanners
const barcode = e.target.value.trim();
if (barcode) {
searchProducts(barcode, 'barcode');
}
});
barcodeInput.focus(); // Keep focus on the barcode input
}
if (productSearchInput) {
productSearchInput.addEventListener('keyup', (e) => {
searchProducts(e.target.value.trim(), 'name');
});
}
if (productGrid) {
productGrid.addEventListener('click', (e) => {
const card = e.target.closest('.product-card');
if (card) {
const productId = card.dataset.productId;
// We need to fetch the full product details to add to cart
fetch(`../api/search_products.php?id=${productId}`)
.then(res => res.json())
.then(product => {
if(product) addToCart(product);
});
}
});
}
if (cartItemsContainer) {
cartItemsContainer.addEventListener('change', (e) => {
if (e.target.classList.contains('quantity-input')) {
const productId = parseInt(e.target.dataset.productId);
const newQuantity = parseInt(e.target.value);
const itemInCart = cart.find(item => item.id === productId);
if (itemInCart) {
if (newQuantity > 0) {
itemInCart.quantity = newQuantity;
} else {
// Remove if quantity is 0 or less
cart = cart.filter(item => item.id !== productId);
}
updateCartView();
}
}
});
cartItemsContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-item-btn')) {
const productId = parseInt(e.target.dataset.productId);
cart = cart.filter(item => item.id !== productId);
updateCartView();
}
});
}
if (completeSaleBtn) completeSaleBtn.addEventListener('click', completeSale);
if (cancelSaleBtn) cancelSaleBtn.addEventListener('click', cancelSale);
if (printLastInvoiceBtn) printLastInvoiceBtn.addEventListener('click', printLastInvoice);
// Initial setup
updateCartView();
});

93
dashboard.php Normal file
View File

@ -0,0 +1,93 @@
<?php
session_start();
require_once 'db/config.php';
// If user is not logged in, redirect to login page
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
$role = $_SESSION['role'];
$page = $_GET['page'] ?? ($role === 'admin' ? 'products' : 'checkout'); // Default page based on role
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Opulent POS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<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=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo filemtime('assets/css/custom.css'); ?>">
<link rel="manifest" href="manifest.json">
</head>
<body data-page="<?php echo htmlspecialchars($role . '_' . $page); ?>">
<div class="sidebar">
<h1 class="sidebar-header">Opulent POS</h1>
<div class="px-3 mb-3 text-light">
Welcome, <strong><?php echo htmlspecialchars($_SESSION['username']); ?></strong>!
</div>
<ul class="nav flex-column px-3">
<?php if ($role === 'admin'): ?>
<li class="nav-item">
<a class="nav-link <?php echo ($page === 'products') ? 'active' : ''; ?>" href="?page=products">
<i class="bi bi-box-seam"></i> Products
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo ($page === 'inventory') ? 'active' : ''; ?>" href="?page=inventory">
<i class="bi bi-clipboard-data"></i> Inventory
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo ($page === 'sales') ? 'active' : ''; ?>" href="?page=sales">
<i class="bi bi-graph-up"></i> Sales & Analytics
</a>
</li>
<?php endif; ?>
<?php if ($role === 'cashier'): ?>
<li class="nav-item">
<a class="nav-link <?php echo ($page === 'checkout') ? 'active' : ''; ?>" href="?page=checkout">
<i class="bi bi-cart"></i> Checkout
</a>
</li>
<?php endif; ?>
<li class="nav-item mt-auto mb-3">
<a class="nav-link" href="logout.php">
<i class="bi bi-box-arrow-left"></i> Logout
</a>
</li>
</ul>
</div>
<div class="top-header d-flex justify-content-end align-items-center">
<div class="status-indicator">
<span class="badge bg-success">Online</span>
<span class="badge bg-secondary">Scanner: Disconnected</span>
<span class="badge bg-secondary">Printer: Disconnected</span>
</div>
</div>
<main class="main-content">
<?php
$page_path = 'views/' . $role . '_' . $page . '.php';
if (file_exists($page_path)) {
include $page_path;
} else {
echo "<h1>Page not found</h1><p>Looking for: {$page_path}</p>";
}
?>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo filemtime('assets/js/main.js'); ?>"></script>
</body>
</html>

67
db/migrate.php Normal file
View File

@ -0,0 +1,67 @@
<?php
require_once __DIR__ . '/config.php';
echo "Starting database migration...\n";
try {
$pdo = db();
// 1. Create migrations table if it doesn't exist
$pdo->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");
}
?>

View File

@ -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

View File

@ -0,0 +1,7 @@
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'),
('Gemini Test Product', 'A special product for testing purposes.', 1.00, '9999999999999');

View File

@ -0,0 +1,2 @@
INSERT INTO `products` (`name`, `description`, `price`, `barcode`) VALUES
('Gemini Test Product', 'A special product for testing purposes.', 1.00, '9999999999999');

View File

@ -0,0 +1,2 @@
-- Add new product: TOKIO PINEAPLE PEACH 1 ML BOTTLE
INSERT INTO products (name, price, barcode) VALUES ('TOKIO PINEAPLE PEACH 1 ML BOTTLE', 2500, '23445677');

152
index.php
View File

@ -1,150 +1,4 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en">
<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>
</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>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
// Redirect to the login page
header('Location: login.php');
exit;

105
login.php Normal file
View File

@ -0,0 +1,105 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
// If user is already logged in, redirect to dashboard
if (isset($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
$error_message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error_message = 'Please enter both username and password.';
} else {
try {
$pdo = db();
$stmt = $pdo->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());
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Opulent POS</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<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=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
<link rel="manifest" href="manifest.json">
</head>
<body class="login-body">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card login-card shadow-lg">
<div class="card-body">
<div class="text-center mb-4">
<h1 class="login-title">Opulent POS</h1>
<p class="text-muted">Please sign in to continue</p>
</div>
<?php if ($error_message): ?>
<div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($error_message); ?>
</div>
<?php endif; ?>
<form action="login.php" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Enter username" required>
</div>
<div class="mb-4">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Login</button>
</div>
</form>
<div class="text-center mt-4">
<small class="text-muted">
Default users seeded:<br>
<strong>Admin:</strong> admin / password<br>
<strong>Cashier:</strong> cashier / password
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

23
logout.php Normal file
View File

@ -0,0 +1,23 @@
<?php
session_start();
// Unset all of the session variables.
$_SESSION = array();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Finally, destroy the session.
session_destroy();
// Redirect to login page
header("Location: login.php");
exit;
?>

21
manifest.json Normal file
View File

@ -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"
}
]
}

80
service-worker.js Normal file
View File

@ -0,0 +1,80 @@
const CACHE_NAME = 'opulent-pos-cache-v2';
const urlsToCache = [
'/',
'/index.php',
'/dashboard.php',
'/dashboard.php?view=cashier_checkout',
'/manifest.json',
'/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 => {
self.skipWaiting(); // Activate worker immediately
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache and caching assets');
return cache.addAll(urlsToCache);
})
);
});
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) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => self.clients.claim()) // Take control of all clients
);
});
self.addEventListener('fetch', event => {
// We only want to cache GET requests
if (event.request.method !== 'GET') {
return;
}
event.respondWith(
caches.open(CACHE_NAME).then(async (cache) => {
// Try to get the response from the cache
const cachedResponse = await cache.match(event.request);
// Return the cached response if found
if (cachedResponse) {
// Re-fetch in the background to update the cache for next time
fetch(event.request).then(response => {
if (response.ok) {
cache.put(event.request, response.clone());
}
});
return cachedResponse;
}
// If not in cache, fetch from the network
try {
const networkResponse = await fetch(event.request);
// If the fetch is successful, clone it and store it in the cache
if (networkResponse.ok) {
cache.put(event.request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// If the fetch fails (e.g., offline), return a fallback or an error
console.log('Fetch failed; returning offline page instead.', error);
// You could return a generic offline page here if you had one
// For now, just let the browser handle the error
}
})
);
});

View File

@ -0,0 +1,38 @@
<?php
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
echo "<div class='alert alert-danger'>Invalid product ID.</div>";
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 "<div class='alert alert-danger'>Database error: " . htmlspecialchars($e->getMessage()) . "</div>";
return;
}
if (!$product) {
echo "<div class='alert alert-danger'>Product not found.</div>";
return;
}
?>
<h1 class="h3 mb-4">Edit Product</h1>
<?php
if (isset($_SESSION['error_message'])) {
echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">' . htmlspecialchars($_SESSION['error_message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error_message']);
}
?>
<div class="card shadow-sm">
<div class="card-body">
<?php include __DIR__ . '/partials/product_form.php'; // The form is included here ?>
</div>
</div>

65
views/admin_inventory.php Normal file
View File

@ -0,0 +1,65 @@
<?php
try {
$pdo = db();
$stmt = $pdo->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 "<div class='alert alert-danger'>Database error: " . htmlspecialchars($e->getMessage()) . "</div>";
$inventory_items = [];
}
?>
<h1 class="h3 mb-4">Inventory Management</h1>
<?php
if (isset($_SESSION['success_message'])) {
echo '<div class="alert alert-success alert-dismissible fade show" role="alert">' . htmlspecialchars($_SESSION['success_message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['success_message']);
}
if (isset($_SESSION['error_message'])) {
echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">' . htmlspecialchars($_SESSION['error_message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error_message']);
}
?>
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Product Name</th>
<th>Barcode</th>
<th class="text-center">Current Stock</th>
<th style="width: 250px;">Update Stock</th>
</tr>
</thead>
<tbody>
<?php if (empty($inventory_items)): ?>
<tr>
<td colspan="4" class="text-center">No products found in inventory.</td>
</tr>
<?php else: ?>
<?php foreach ($inventory_items as $item): ?>
<tr>
<td><?= htmlspecialchars($item['name']) ?></td>
<td><?= htmlspecialchars($item['barcode'] ?? 'N/A') ?></td>
<td class="text-center fs-5"><?= htmlspecialchars($item['quantity']) ?></td>
<td>
<form action="/api/update_stock.php" method="post" class="d-flex">
<input type="hidden" name="product_id" value="<?= $item['id'] ?>">
<input type="number" name="quantity" class="form-control me-2" value="<?= htmlspecialchars($item['quantity']) ?>" required min="0">
<button type="submit" class="btn btn-primary">Save</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>

119
views/admin_products.php Normal file
View File

@ -0,0 +1,119 @@
<?php
// Fetch products from the database
try {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM products ORDER BY created_at DESC");
$products = $stmt->fetchAll();
} catch (PDOException $e) {
echo "<div class='alert alert-danger'>Database error: " . htmlspecialchars($e->getMessage()) . "</div>";
$products = [];
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Manage Products</h1>
</div>
<?php
if (isset($_SESSION['success_message'])) {
echo '<div class="alert alert-success alert-dismissible fade show" role="alert">' . htmlspecialchars($_SESSION['success_message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['success_message']);
}
if (isset($_SESSION['error_message'])) {
echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">' . htmlspecialchars($_SESSION['error_message']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error_message']);
}
?>
<!-- Add Product Form -->
<div class="card shadow-sm mb-4">
<div class="card-header">
<h2 class="h5 mb-0">Add New Product</h2>
</div>
<div class="card-body">
<?php include __DIR__ . '/partials/product_form.php'; ?>
</div>
</div>
<!-- Product List -->
<div class="card shadow-sm">
<div class="card-header">
<h2 class="h5 mb-0">Existing Products</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Price</th>
<th scope="col">Barcode</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($products)): ?>
<tr>
<td colspan="5" class="text-center">No products found.</td>
</tr>
<?php else: ?>
<?php foreach ($products as $product): ?>
<tr id="product-row-<?= $product['id'] ?>">
<td><?= htmlspecialchars($product['id']) ?></td>
<td><?= htmlspecialchars($product['name']) ?></td>
<td><?= htmlspecialchars(number_format($product['price'], 2)) ?></td>
<td><?= htmlspecialchars($product['barcode'] ?? 'N/A') ?></td>
<td>
<a href="dashboard.php?page=admin_edit_product&id=<?= $product['id'] ?>" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil-square"></i> Edit
</a>
<button class="btn btn-sm btn-outline-danger delete-product-btn" data-id="<?= $product['id'] ?>">
<i class="bi bi-trash"></i> Delete
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const deleteButtons = document.querySelectorAll('.delete-product-btn');
deleteButtons.forEach(button => {
button.addEventListener('click', function () {
const productId = this.getAttribute('data-id');
if (confirm('Are you sure you want to delete this product?')) {
fetch('/api/delete_product.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'id=' + encodeURIComponent(productId)
})
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById('product-row-' + productId);
row.remove();
// Optionally, show a success message
const alertContainer = document.querySelector('.d-flex.justify-content-between');
const successAlert = `<div class="alert alert-success alert-dismissible fade show" role="alert">${data.message}<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>`;
alertContainer.insertAdjacentHTML('afterend', successAlert);
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the product.');
});
}
});
});
});
</script>

119
views/admin_sales.php Normal file
View File

@ -0,0 +1,119 @@
<?php
try {
$pdo = db();
$stmt = $pdo->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 "<div class='alert alert-danger'>Database error: " . htmlspecialchars($e->getMessage()) . "</div>";
$sales = [];
}
?>
<h1 class="h3 mb-4">Sales Reports</h1>
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Receipt Number</th>
<th>Cashier</th>
<th>Total Amount</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($sales)): ?>
<tr>
<td colspan="5" class="text-center">No sales recorded yet.</td>
</tr>
<?php else: ?>
<?php foreach ($sales as $sale): ?>
<tr>
<td><?= htmlspecialchars($sale['receipt_number']) ?></td>
<td><?= htmlspecialchars($sale['username']) ?></td>
<td>$<?= htmlspecialchars(number_format($sale['total_amount'], 2)) ?></td>
<td><?= htmlspecialchars(date('M d, Y h:i A', strtotime($sale['created_at']))) ?></td>
<td>
<button class="btn btn-sm btn-primary view-sale-details-btn" data-sale-id="<?= $sale['id'] ?>" data-bs-toggle="modal" data-bs-target="#saleDetailsModal">
View Details
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Sale Details Modal -->
<div class="modal fade" id="saleDetailsModal" tabindex="-1" aria-labelledby="saleDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saleDetailsModalLabel">Sale Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="saleDetailsBody">
<!-- Sale details will be loaded here -->
<p class="text-center">Loading...</p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const saleDetailsModal = document.getElementById('saleDetailsModal');
const saleDetailsBody = document.getElementById('saleDetailsBody');
saleDetailsModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const saleId = button.getAttribute('data-sale-id');
saleDetailsBody.innerHTML = '<p class="text-center">Loading...</p>';
fetch(`/api/get_sale_details.php?id=${saleId}`)
.then(response => response.json())
.then(data => {
if (data.error) {
saleDetailsBody.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
return;
}
let itemsHtml = '<ul class="list-group">';
data.items.forEach(item => {
itemsHtml += `<li class="list-group-item d-flex justify-content-between align-items-center">
${item.product_name} (x${item.quantity})
<span>$${parseFloat(item.price_at_sale).toFixed(2)} each</span>
</li>`;
});
itemsHtml += '</ul>';
saleDetailsBody.innerHTML = `
<p><strong>Receipt Number:</strong> ${data.receipt_number}</p>
<p><strong>Cashier:</strong> ${data.cashier_name}</p>
<p><strong>Date:</strong> ${new Date(data.created_at).toLocaleString()}</p>
<hr>
<h5>Items Sold</h5>
${itemsHtml}
<hr>
<div class="text-end fs-5 fw-bold">
Total: $${parseFloat(data.total_amount).toFixed(2)}
</div>
`;
})
.catch(err => {
saleDetailsBody.innerHTML = `<div class="alert alert-danger">An error occurred while fetching sale details.</div>`;
console.error('Fetch error:', err);
});
});
});
</script>

126
views/cashier_checkout.php Normal file
View File

@ -0,0 +1,126 @@
<?php
// Main view for the cashier checkout process
// This interface is designed for rapid entry via barcode scanner and manual search.
?>
<div class="row g-4">
<!-- Left side: Cart and Checkout -->
<div class="col-lg-5 col-md-12">
<div class="card shadow-sm sticky-top" style="top: 1rem;">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h5 mb-0">Current Sale</h2>
<span class="badge bg-primary rounded-pill" id="cart-item-count">0</span>
</div>
<div class="card-body">
<div id="cart-items" class="mb-3" style="max-height: 40vh; overflow-y: auto;">
<!-- Cart items will be injected here as a table -->
<div id="cart-placeholder" class="text-center text-muted p-4">
<p class="mb-0">Scan a barcode or search for a product to begin.</p>
</div>
</div>
<div class="d-grid gap-2">
<div class="row fs-5">
<div class="col">Subtotal:</div>
<div class="col text-end" id="cart-subtotal">PKR 0.00</div>
</div>
<div class="row text-muted">
<div class="col">Tax (0%):</div>
<div class="col text-end" id="cart-tax">PKR 0.00</div>
</div>
<hr class="my-1">
<div class="d-flex justify-content-between align-items-center fs-4 fw-bold">
<span>Total:</span>
<span id="cart-total">PKR 0.00</span>
</div>
<button id="complete-sale-btn" class="btn btn-success btn-lg" disabled>Complete Sale</button>
<div class="btn-group">
<button id="cancel-sale-btn" class="btn btn-outline-danger">Cancel Sale</button>
<button id="print-last-invoice-btn" class="btn btn-outline-secondary">Print Last Invoice</button>
</div>
</div>
</div>
</div>
</div>
<!-- Right side: Product Search and Barcode Input -->
<div class="col-lg-7 col-md-12">
<div class="card shadow-sm mb-4">
<div class="card-header">
<h2 class="h5 mb-0">Barcode Scan</h2>
</div>
<div class="card-body">
<div class="mb-3">
<label for="barcode-scanner-input" class="form-label">Scan Product Barcode</label>
<input type="text" id="barcode-scanner-input" class="form-control form-control-lg" placeholder="Ready for barcode scan..." autofocus>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">
<h2 class="h5 mb-0">Manual Product Search</h2>
</div>
<div class="card-body">
<div class="mb-3">
<input type="text" id="product-search" class="form-control" placeholder="Start typing product name...">
</div>
<div id="product-grid" class="row row-cols-2 row-cols-md-3 row-cols-lg-4 g-3" style="max-height: 400px; overflow-y: auto;">
<!-- Search results will be injected here by JavaScript -->
<div id="product-grid-placeholder" class="col-12 text-center text-muted p-4">
<p>Search results will appear here.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Hidden Printable Invoice -->
<div id="invoice-container" class="d-none">
<div id="printable-invoice" class="p-4">
<div class="text-center mb-4">
<h2 class="mb-1">Opulent POS</h2>
<p class="mb-0">123 Business Rd, Cityville</p>
<p>Sale Invoice</p>
</div>
<div class="row mb-3">
<div class="col">
<strong>Receipt No:</strong> <span id="invoice-receipt-number"></span><br>
<strong>Cashier:</strong> <span id="invoice-cashier-name"></span>
</div>
<div class="col text-end">
<strong>Date:</strong> <span id="invoice-date"></span>
</div>
</div>
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Item</th>
<th class="text-center">Qty</th>
<th class="text-end">Price</th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody id="invoice-items-table">
<!-- Items here -->
</tbody>
<tfoot>
<tr>
<th colspan="4" class="text-end">Subtotal:</th>
<td class="text-end" id="invoice-subtotal"></td>
</tr>
<tr>
<th colspan="4" class="text-end">Tax:</th>
<td class="text-end" id="invoice-tax"></td>
</tr>
<tr>
<th colspan="4" class="text-end fs-5">Total:</th>
<td class="text-end fs-5" id="invoice-total"></td>
</tr>
</tfoot>
</table>
<div class="text-center mt-4">
<p class="small">Thank you for your business!</p>
</div>
</div>
</div>

View File

@ -0,0 +1,29 @@
<?php
$product = $product ?? null; // Use existing product data or null for a new one
$is_edit = isset($product) && $product['id'];
?>
<form action="api/<?= $is_edit ? 'update_product.php' : 'create_product.php' ?>" method="post">
<?php if ($is_edit): ?>
<input type="hidden" name="id" value="<?= htmlspecialchars($product['id']) ?>">
<?php endif; ?>
<div class="mb-3">
<label for="name" class="form-label">Product Name</label>
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($product['name'] ?? '') ?>" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3"><?= htmlspecialchars($product['description'] ?? '') ?></textarea>
</div>
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<input type="number" class="form-control" id="price" name="price" step="0.01" value="<?= htmlspecialchars($product['price'] ?? '') ?>" required>
</div>
<div class="mb-3">
<label for="barcode" class="form-label">Barcode</label>
<input type="text" class="form-control" id="barcode" name="barcode" value="<?= htmlspecialchars($product['barcode'] ?? '') ?>">
</div>
<button type="submit" class="btn btn-primary"><?= $is_edit ? 'Update' : 'Create' ?> Product</button>
<?php if ($is_edit): ?>
<a href="dashboard.php?page=admin_products" class="btn btn-secondary">Cancel</a>
<?php endif; ?>
</form>