feat: initial foundation for collaborative canteen ordering system in vanilla PHP

This commit is contained in:
2026-08-27 17:29:13 +07:00
commit 5effce30cb
52 changed files with 4915 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class Customer
{
public static function all(): array
{
$db = Database::getConnection();
$stmt = $db->query("SELECT * FROM customers ORDER BY name ASC");
return $stmt->fetchAll();
}
public static function find(int $id): ?array
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT * FROM customers WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
return $row ?: null;
}
public static function count(): int
{
$db = Database::getConnection();
return (int)$db->query("SELECT COUNT(*) FROM customers")->fetchColumn();
}
public static function create(array $data): int
{
$db = Database::getConnection();
$stmt = $db->prepare("
INSERT INTO customers (name, email, phone, address)
VALUES (:name, :email, :phone, :address)
");
$stmt->execute([
':name' => $data['name'],
':email' => $data['email'],
':phone' => $data['phone'] ?? '',
':address' => $data['address'] ?? '',
]);
return (int)$db->lastInsertId();
}
public static function update(int $id, array $data): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("
UPDATE customers
SET name = :name, email = :email, phone = :phone, address = :address
WHERE id = :id
");
return $stmt->execute([
':id' => $id,
':name' => $data['name'],
':email' => $data['email'],
':phone' => $data['phone'] ?? '',
':address' => $data['address'] ?? '',
]);
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class DailySession
{
public static function create(int $creatorId, string $title, string $canteenName, string $sessionDate, ?string $notes): int
{
$db = Database::getConnection();
$stmt = $db->prepare("
INSERT INTO daily_sessions (creator_id, session_date, title, canteen_name, status, notes)
VALUES (?, ?, ?, ?, 'open', ?)
");
$stmt->execute([
$creatorId,
$sessionDate,
trim($title),
trim($canteenName),
trim((string)$notes)
]);
return (int)$db->lastInsertId();
}
public static function find(int $id): ?array
{
$db = Database::getConnection();
$stmt = $db->prepare("
SELECT s.*, u.name as creator_name, u.username as creator_username
FROM daily_sessions s
JOIN users u ON s.creator_id = u.id
WHERE s.id = ?
");
$stmt->execute([$id]);
$session = $stmt->fetch();
if (!$session) {
return null;
}
$session['menus'] = SessionMenu::getBySessionId($id);
$session['orders'] = EmployeeOrder::getBySessionId($id);
$session['recap'] = self::getCanteenRecap($id);
return $session;
}
public static function all(): array
{
$db = Database::getConnection();
$stmt = $db->query("
SELECT s.*, u.name as creator_name, u.username as creator_username,
(SELECT COUNT(*) FROM employee_orders WHERE session_id = s.id) as order_count,
(SELECT COUNT(*) FROM session_menus WHERE session_id = s.id) as menu_count,
(SELECT COALESCE(SUM(total_bill), 0) FROM employee_orders WHERE session_id = s.id) as total_session_amount
FROM daily_sessions s
JOIN users u ON s.creator_id = u.id
ORDER BY s.id DESC
");
return $stmt->fetchAll();
}
public static function getActiveSessions(): array
{
$db = Database::getConnection();
$stmt = $db->query("
SELECT s.*, u.name as creator_name, u.username as creator_username,
(SELECT COUNT(*) FROM employee_orders WHERE session_id = s.id) as order_count,
(SELECT COUNT(*) FROM session_menus WHERE session_id = s.id) as menu_count,
(SELECT COALESCE(SUM(total_bill), 0) FROM employee_orders WHERE session_id = s.id) as total_session_amount
FROM daily_sessions s
JOIN users u ON s.creator_id = u.id
WHERE s.status IN ('open', 'closed', 'arrived')
ORDER BY s.id DESC
");
return $stmt->fetchAll();
}
public static function updateStatus(int $id, string $status): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("UPDATE daily_sessions SET status = ?, updated_at = datetime('now', 'localtime') WHERE id = ?");
return $stmt->execute([$status, $id]);
}
public static function updateCanteenInfo(int $id, string $canteenName, ?string $notes): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("UPDATE daily_sessions SET canteen_name = ?, notes = ?, updated_at = datetime('now', 'localtime') WHERE id = ?");
return $stmt->execute([$canteenName, $notes, $id]);
}
public static function getCanteenRecap(int $sessionId): array
{
$db = Database::getConnection();
$stmt = $db->prepare("
SELECT eoi.item_name,
SUM(eoi.quantity) as total_quantity,
GROUP_CONCAT(DISTINCT eoi.notes) as combined_notes
FROM employee_order_items eoi
JOIN employee_orders eo ON eoi.employee_order_id = eo.id
WHERE eo.session_id = ?
GROUP BY eoi.item_name
ORDER BY total_quantity DESC
");
$stmt->execute([$sessionId]);
return $stmt->fetchAll();
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class Employee
{
public static function all(): array
{
$db = Database::getConnection();
$stmt = $db->query("
SELECT e.*,
(SELECT COUNT(*) FROM employee_orders WHERE employee_id = e.id) as total_orders,
(SELECT COALESCE(SUM(total_bill), 0) FROM employee_orders WHERE employee_id = e.id AND status = 'unpaid') as unpaid_balance
FROM employees e
ORDER BY e.name ASC
");
return $stmt->fetchAll();
}
public static function find(int $id): ?array
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT * FROM employees WHERE id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
return $res ?: null;
}
public static function findOrCreateByName(string $name, ?string $mattermostUsername = null): int
{
$db = Database::getConnection();
$name = trim($name);
$stmt = $db->prepare("SELECT id FROM employees WHERE LOWER(name) = LOWER(?)");
$stmt->execute([$name]);
$id = $stmt->fetchColumn();
if ($id) {
return (int)$id;
}
$stmt = $db->prepare("INSERT INTO employees (name, mattermost_username) VALUES (?, ?)");
$stmt->execute([$name, $mattermostUsername ?: strtolower(preg_replace('/[^a-zA-Z0-9]/', '', $name))]);
return (int)$db->lastInsertId();
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class EmployeeOrder
{
public static function getBySessionId(int $sessionId): array
{
$db = Database::getConnection();
$stmt = $db->prepare("
SELECT eo.*, u.name as user_name, u.username as mattermost_username, u.email as user_email
FROM employee_orders eo
JOIN users u ON eo.user_id = u.id
WHERE eo.session_id = ?
ORDER BY eo.id ASC
");
$stmt->execute([$sessionId]);
$orders = $stmt->fetchAll();
foreach ($orders as &$order) {
$order['items'] = self::getItemsByOrderId((int)$order['id']);
}
return $orders;
}
public static function getItemsByOrderId(int $orderId): array
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT * FROM employee_order_items WHERE employee_order_id = ? ORDER BY id ASC");
$stmt->execute([$orderId]);
return $stmt->fetchAll();
}
public static function findUserOrderBySession(int $sessionId, int $userId): ?array
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT * FROM employee_orders WHERE session_id = ? AND user_id = ?");
$stmt->execute([$sessionId, $userId]);
$order = $stmt->fetch();
if (!$order) {
return null;
}
$order['items'] = self::getItemsByOrderId((int)$order['id']);
return $order;
}
public static function createOrder(int $sessionId, int $userId, ?string $notes, array $items): int
{
$db = Database::getConnection();
$db->beginTransaction();
try {
$stmtOrder = $db->prepare("
INSERT INTO employee_orders (session_id, user_id, status, notes, total_bill)
VALUES (?, ?, 'unpaid', ?, 0.00)
");
$stmtOrder->execute([$sessionId, $userId, $notes]);
$orderId = (int)$db->lastInsertId();
$stmtItem = $db->prepare("
INSERT INTO employee_order_items (employee_order_id, session_menu_id, item_name, quantity, unit_price, total_price, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$totalBill = 0.0;
foreach ($items as $item) {
$menuId = !empty($item['session_menu_id']) ? (int)$item['session_menu_id'] : null;
$name = trim((string)($item['item_name'] ?? ''));
$qty = max(1, (int)($item['quantity'] ?? 1));
$unitPrice = (float)($item['unit_price'] ?? 0.0);
$lineTotal = $qty * $unitPrice;
$itemNotes = trim((string)($item['notes'] ?? ''));
if (empty($name)) {
continue;
}
$stmtItem->execute([
$orderId,
$menuId,
$name,
$qty,
$unitPrice,
$lineTotal,
$itemNotes
]);
$totalBill += $lineTotal;
}
// Update total bill
$stmtUpdateTotal = $db->prepare("UPDATE employee_orders SET total_bill = ? WHERE id = ?");
$stmtUpdateTotal->execute([$totalBill, $orderId]);
$db->commit();
return $orderId;
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
}
public static function togglePaid(int $orderId): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("
UPDATE employee_orders
SET status = CASE WHEN status = 'paid' THEN 'unpaid' ELSE 'paid' END,
updated_at = datetime('now', 'localtime')
WHERE id = ?
");
return $stmt->execute([$orderId]);
}
public static function delete(int $orderId): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("DELETE FROM employee_orders WHERE id = ?");
return $stmt->execute([$orderId]);
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class Order
{
public static function all(?string $status = null, ?string $search = null): array
{
$db = Database::getConnection();
$query = "
SELECT o.*, c.name as customer_name, c.email as customer_email,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as item_count
FROM orders o
JOIN customers c ON o.customer_id = c.id
";
$conditions = [];
$params = [];
if ($status && $status !== 'all') {
$conditions[] = "o.status = :status";
$params[':status'] = $status;
}
if ($search) {
$conditions[] = "(o.order_number LIKE :search OR c.name LIKE :search OR c.email LIKE :search)";
$params[':search'] = "%{$search}%";
}
if (!empty($conditions)) {
$query .= " WHERE " . implode(" AND ", $conditions);
}
$query .= " ORDER BY o.id DESC";
$stmt = $db->prepare($query);
$stmt->execute($params);
return $stmt->fetchAll();
}
public static function find(int $id): ?array
{
$db = Database::getConnection();
$stmt = $db->prepare("
SELECT o.*, c.name as customer_name, c.email as customer_email,
c.phone as customer_phone, c.address as customer_address
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.id = ?
");
$stmt->execute([$id]);
$order = $stmt->fetch();
if (!$order) {
return null;
}
$order['items'] = OrderItem::getByOrderId($id);
return $order;
}
public static function count(?string $status = null): int
{
$db = Database::getConnection();
if ($status) {
$stmt = $db->prepare("SELECT COUNT(*) FROM orders WHERE status = ?");
$stmt->execute([$status]);
return (int)$stmt->fetchColumn();
}
return (int)$db->query("SELECT COUNT(*) FROM orders")->fetchColumn();
}
public static function totalRevenue(): float
{
$db = Database::getConnection();
return (float)$db->query("SELECT COALESCE(SUM(total), 0) FROM orders WHERE status != 'Cancelled'")->fetchColumn();
}
public static function recent(int $limit = 5): array
{
$db = Database::getConnection();
$stmt = $db->prepare("
SELECT o.*, c.name as customer_name,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as item_count
FROM orders o
JOIN customers c ON o.customer_id = c.id
ORDER BY o.id DESC
LIMIT :limit
");
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll();
}
public static function createOrderWithItems(array $orderData, array $items): int
{
$db = Database::getConnection();
$db->beginTransaction();
try {
$orderNumber = 'ORD-' . date('Ymd') . '-' . strtoupper(substr(uniqid(), -4));
$stmt = $db->prepare("
INSERT INTO orders (order_number, customer_id, status, subtotal, tax, discount, total, notes)
VALUES (:order_number, :customer_id, :status, :subtotal, :tax, :discount, :total, :notes)
");
$stmt->execute([
':order_number' => $orderNumber,
':customer_id' => $orderData['customer_id'],
':status' => $orderData['status'] ?? 'Pending',
':subtotal' => $orderData['subtotal'],
':tax' => $orderData['tax'] ?? 0.00,
':discount' => $orderData['discount'] ?? 0.00,
':total' => $orderData['total'],
':notes' => $orderData['notes'] ?? '',
]);
$orderId = (int)$db->lastInsertId();
foreach ($items as $item) {
if (empty($item['product_id']) || empty($item['quantity'])) {
continue;
}
$productId = (int)$item['product_id'];
$quantity = (int)$item['quantity'];
$unitPrice = (float)$item['unit_price'];
$totalPrice = $quantity * $unitPrice;
$productName = $item['product_name'] ?? 'Item';
OrderItem::create([
'order_id' => $orderId,
'product_id' => $productId,
'product_name' => $productName,
'quantity' => $quantity,
'unit_price' => $unitPrice,
'total_price' => $totalPrice,
]);
// Deduct stock inventory
Product::deductStock($productId, $quantity);
}
$db->commit();
return $orderId;
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
}
public static function updateStatus(int $id, string $status): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("UPDATE orders SET status = ?, updated_at = datetime('now', 'localtime') WHERE id = ?");
return $stmt->execute([$status, $id]);
}
public static function delete(int $id): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("DELETE FROM orders WHERE id = ?");
return $stmt->execute([$id]);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class OrderItem
{
public static function getByOrderId(int $orderId): array
{
$db = Database::getConnection();
$stmt = $db->prepare("
SELECT oi.*, p.sku, p.category
FROM order_items oi
LEFT JOIN products p ON oi.product_id = p.id
WHERE oi.order_id = ?
ORDER BY oi.id ASC
");
$stmt->execute([$orderId]);
return $stmt->fetchAll();
}
public static function create(array $data): int
{
$db = Database::getConnection();
$stmt = $db->prepare("
INSERT INTO order_items (order_id, product_id, product_name, quantity, unit_price, total_price)
VALUES (:order_id, :product_id, :product_name, :quantity, :unit_price, :total_price)
");
$stmt->execute([
':order_id' => $data['order_id'],
':product_id' => $data['product_id'],
':product_name' => $data['product_name'],
':quantity' => $data['quantity'],
':unit_price' => $data['unit_price'],
':total_price' => $data['total_price'],
]);
return (int)$db->lastInsertId();
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class Product
{
public static function all(): array
{
$db = Database::getConnection();
$stmt = $db->query("SELECT * FROM products ORDER BY name ASC");
return $stmt->fetchAll();
}
public static function find(int $id): ?array
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch();
return $row ?: null;
}
public static function count(): int
{
$db = Database::getConnection();
return (int)$db->query("SELECT COUNT(*) FROM products")->fetchColumn();
}
public static function lowStockCount(int $threshold = 25): int
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT COUNT(*) FROM products WHERE stock <= ?");
$stmt->execute([$threshold]);
return (int)$stmt->fetchColumn();
}
public static function create(array $data): int
{
$db = Database::getConnection();
$stmt = $db->prepare("
INSERT INTO products (sku, name, description, price, stock, category)
VALUES (:sku, :name, :description, :price, :stock, :category)
");
$stmt->execute([
':sku' => $data['sku'],
':name' => $data['name'],
':description' => $data['description'] ?? '',
':price' => $data['price'],
':stock' => (int)($data['stock'] ?? 0),
':category' => $data['category'] ?? 'General',
]);
return (int)$db->lastInsertId();
}
public static function update(int $id, array $data): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("
UPDATE products
SET sku = :sku, name = :name, description = :description,
price = :price, stock = :stock, category = :category
WHERE id = :id
");
return $stmt->execute([
':id' => $id,
':sku' => $data['sku'],
':name' => $data['name'],
':description' => $data['description'] ?? '',
':price' => $data['price'],
':stock' => (int)($data['stock'] ?? 0),
':category' => $data['category'] ?? 'General',
]);
}
public static function deductStock(int $id, int $quantity): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("UPDATE products SET stock = MAX(0, stock - ?) WHERE id = ?");
return $stmt->execute([$quantity, $id]);
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class SessionMenu
{
public static function getBySessionId(int $sessionId): array
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT * FROM session_menus WHERE session_id = ? ORDER BY id ASC");
$stmt->execute([$sessionId]);
return $stmt->fetchAll();
}
public static function add(int $sessionId, string $name, float $price = 0.00): int
{
$db = Database::getConnection();
$stmt = $db->prepare("INSERT INTO session_menus (session_id, name, final_price) VALUES (?, ?, ?)");
$stmt->execute([$sessionId, trim($name), $price]);
return (int)$db->lastInsertId();
}
public static function delete(int $id): bool
{
$db = Database::getConnection();
$stmt = $db->prepare("DELETE FROM session_menus WHERE id = ?");
return $stmt->execute([$id]);
}
/**
* Update final prices from canteen bill and recalculate all employee order line items
*/
public static function updatePricesAndRecalculate(int $sessionId, array $menuPrices): void
{
$db = Database::getConnection();
$db->beginTransaction();
try {
$stmtUpdateMenu = $db->prepare("UPDATE session_menus SET final_price = ? WHERE id = ? AND session_id = ?");
$stmtUpdateItems = $db->prepare("
UPDATE employee_order_items
SET unit_price = ?, total_price = quantity * ?
WHERE session_menu_id = ?
");
foreach ($menuPrices as $menuId => $price) {
$menuId = (int)$menuId;
$price = (float)$price;
$stmtUpdateMenu->execute([$price, $menuId, $sessionId]);
$stmtUpdateItems->execute([$price, $price, $menuId]);
}
// Recalculate total_bill on employee_orders
$stmtOrders = $db->prepare("SELECT id FROM employee_orders WHERE session_id = ?");
$stmtOrders->execute([$sessionId]);
$orders = $stmtOrders->fetchAll();
$stmtUpdateOrderTotal = $db->prepare("
UPDATE employee_orders
SET total_bill = (SELECT COALESCE(SUM(total_price), 0) FROM employee_order_items WHERE employee_order_id = ?)
WHERE id = ?
");
foreach ($orders as $order) {
$stmtUpdateOrderTotal->execute([$order['id'], $order['id']]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Core\Database\Database;
use PDO;
class User
{
public static function all(): array
{
$db = Database::getConnection();
$stmt = $db->query("
SELECT u.id, u.name, u.username, u.email, u.created_at,
(SELECT COUNT(*) FROM employee_orders WHERE user_id = u.id) as total_orders,
(SELECT COALESCE(SUM(total_bill), 0) FROM employee_orders WHERE user_id = u.id AND status = 'unpaid') as unpaid_balance
FROM users u
ORDER BY u.name ASC
");
return $stmt->fetchAll();
}
public static function find(int $id): ?array
{
$db = Database::getConnection();
$stmt = $db->prepare("SELECT id, name, username, email, created_at FROM users WHERE id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
return $res ?: null;
}
public static function findByIdentifier(string $identifier): ?array
{
$db = Database::getConnection();
$identifier = trim($identifier);
$stmt = $db->prepare("SELECT * FROM users WHERE LOWER(username) = LOWER(?) OR LOWER(email) = LOWER(?)");
$stmt->execute([$identifier, $identifier]);
$res = $stmt->fetch();
return $res ?: null;
}
public static function create(string $name, string $username, string $email, string $password): int
{
$db = Database::getConnection();
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO users (name, username, email, password_hash) VALUES (?, ?, ?, ?)");
$stmt->execute([
trim($name),
strtolower(trim($username)),
strtolower(trim($email)),
$hash
]);
return (int)$db->lastInsertId();
}
}