Version 1

This commit is contained in:
Flatlogic Bot 2025-11-22 17:18:03 +00:00
parent 578f1811d7
commit b6296eed55
24 changed files with 1720 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;
}

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

@ -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 = '<p class="text-danger col-12">Could not fetch products.</p>';
}
};
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 = '<p>No products found.</p>';
productGridPlaceholder.style.display = 'block';
return;
}
productGridPlaceholder.style.display = 'none';
products.forEach(product => {
const productCard = document.createElement('div');
productCard.className = 'col';
productCard.innerHTML = `
<div class="card h-100 product-card" role="button" data-product-id="${product.id}" data-product-name="${product.name}" data-product-price="${product.price}" data-product-barcode="${product.barcode}">
<div class="card-body text-center">
<h6 class="card-title fs-sm">${product.name}</h6>
<p class="card-text text-muted small">${formatCurrency(product.price)}</p>
</div>
</div>
`;
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 = `
<thead>
<tr>
<th>Item</th>
<th class="text-center">Qty</th>
<th class="text-end">Price</th>
<th></th>
</tr>
</thead>
<tbody></tbody>`;
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 = `
<td>
<div class="fs-sm">${item.name}</div>
<small class="text-muted">${formatCurrency(item.price)}</small>
</td>
<td class="text-center">
<div class="input-group input-group-sm" style="width: 90px; margin: auto;">
<button class="btn btn-outline-secondary btn-sm cart-quantity-change" data-product-id="${productId}" data-change="-1">-</button>
<input type="text" class="form-control text-center" value="${item.quantity}" readonly>
<button class="btn btn-outline-secondary btn-sm cart-quantity-change" data-product-id="${productId}" data-change="1">+</button>
</div>
</td>
<td class="text-end fw-bold">${formatCurrency(item.price * item.quantity)}</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-danger cart-remove-item" data-product-id="${productId}"><i class="bi bi-trash"></i></button>
</td>
`;
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 = '<span class="spinner-border spinner-border-sm"></span> 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 = `
<td>${++i}</td>
<td>${item.name}</td>
<td class="text-center">${item.quantity}</td>
<td class="text-end">${formatCurrency(item.price)}</td>
<td class="text-end">${formatCurrency(total)}</td>
`;
}
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('<html><head><title>Print Invoice</title>');
// Include bootstrap for styling
printWindow.document.write('<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">');
printWindow.document.write('<style>body { -webkit-print-color-adjust: exact; } @media print { .d-none { display: block !important; } }</style>');
printWindow.document.write('</head><body>');
printWindow.document.write(invoiceContent);
printWindow.document.write('</body></html>');
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 = '<p>Loading...</p>';
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 = '<ul class="list-group list-group-flush">';
sale.items.forEach(item => {
itemsHtml += `<li class="list-group-item d-flex justify-content-between align-items-center">
${item.product_name} (x${item.quantity})
<span>PKR ${parseFloat(item.price_at_sale * item.quantity).toFixed(2)}</span>
</li>`;
});
itemsHtml += '</ul>';
saleDetailsContent.innerHTML = `
<h5>Receipt: ${sale.receipt_number}</h5>
<p><strong>Cashier:</strong> ${sale.cashier_name || 'N/A'}</p>
<p><strong>Date:</strong> ${new Date(sale.created_at).toLocaleString()}</p>
<hr>
<h6>Items Sold</h6>
${itemsHtml}
<hr>
<div class="text-end">
<p class="fs-5"><strong>Total: PKR ${parseFloat(sale.total_amount).toFixed(2)}</strong></p>
</div>
`;
} catch (error) {
saleDetailsContent.innerHTML = `<p class="text-danger">Error: ${error.message}</p>`;
}
}
});
}
});
''

92
dashboard.php Normal file
View File

@ -0,0 +1,92 @@
<?php
session_start();
// 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">
<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"></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,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');

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

70
service-worker.js Normal file
View File

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

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>