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