Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cff4dbb3e | ||
|
|
183ad1886c | ||
|
|
4d1d667390 | ||
|
|
b6296eed55 |
88
api/complete_sale.php
Normal file
88
api/complete_sale.php
Normal 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
32
api/create_product.php
Normal 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
37
api/delete_product.php
Normal 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
57
api/get_sale_details.php
Normal 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
42
api/search_products.php
Normal 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
33
api/update_product.php
Normal 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
30
api/update_stock.php
Normal 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
101
assets/css/custom.css
Normal 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
314
assets/js/main.js
Normal 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}">
|
||||
×
|
||||
</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
93
dashboard.php
Normal 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
67
db/migrate.php
Normal 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");
|
||||
}
|
||||
?>
|
||||
53
db/migrations/001_initial_schema.sql
Normal file
53
db/migrations/001_initial_schema.sql
Normal 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
|
||||
7
db/migrations/002_add_sample_products.sql
Normal file
7
db/migrations/002_add_sample_products.sql
Normal 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');
|
||||
2
db/migrations/003_add_gemini_product.sql
Normal file
2
db/migrations/003_add_gemini_product.sql
Normal 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');
|
||||
2
db/migrations/004_add_tokio_product.sql
Normal file
2
db/migrations/004_add_tokio_product.sql
Normal 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
152
index.php
@ -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
105
login.php
Normal 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
23
logout.php
Normal 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
21
manifest.json
Normal 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
80
service-worker.js
Normal 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
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
38
views/admin_edit_product.php
Normal file
38
views/admin_edit_product.php
Normal 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
65
views/admin_inventory.php
Normal 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
119
views/admin_products.php
Normal 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
119
views/admin_sales.php
Normal 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
126
views/cashier_checkout.php
Normal 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>
|
||||
29
views/partials/product_form.php
Normal file
29
views/partials/product_form.php
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user