Version 1
This commit is contained in:
parent
578f1811d7
commit
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;
|
||||||
|
}
|
||||||
364
assets/js/main.js
Normal file
364
assets/js/main.js
Normal 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
92
dashboard.php
Normal 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
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
|
||||||
6
db/migrations/002_add_sample_products.sql
Normal file
6
db/migrations/002_add_sample_products.sql
Normal 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
152
index.php
@ -1,150 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
// Redirect to the login page
|
||||||
@ini_set('display_errors', '1');
|
header('Location: login.php');
|
||||||
@error_reporting(E_ALL);
|
exit;
|
||||||
@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>
|
|
||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
70
service-worker.js
Normal file
70
service-worker.js
Normal 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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
'''
|
||||||
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