feat: initial foundation for collaborative canteen ordering system in vanilla PHP
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Create New Order</h1>
|
||||
<p class="page-subtitle">Select customer, add products, and calculate order totals</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/orders" class="btn btn-secondary">← Back to Orders</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/orders" x-data="orderForm()">
|
||||
<?= \App\Core\View::csrfField() ?>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; align-items: start;">
|
||||
|
||||
<!-- Left Column: Customer & Order Items -->
|
||||
<div>
|
||||
<!-- Customer Information Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">1. Customer Selection</h2>
|
||||
<a href="/customers" target="_blank" style="font-size: 0.85rem; color: var(--primary); text-decoration: none;">+ Add Customer</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Customer <span style="color: var(--danger);">*</span></label>
|
||||
<select name="customer_id" class="form-select" required>
|
||||
<option value="">-- Choose Customer --</option>
|
||||
<?php foreach ($customers as $customer): ?>
|
||||
<option value="<?= e($customer['id']) ?>">
|
||||
<?= e($customer['name']) ?> (<?= e($customer['email']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products / Items Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">2. Order Line Items</h2>
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="addItem()">+ Add Product</button>
|
||||
</div>
|
||||
<div class="card-body" style="padding: 0;">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 45%;">Product</th>
|
||||
<th style="width: 18%;">Price ($)</th>
|
||||
<th style="width: 15%;">Qty</th>
|
||||
<th style="width: 17%; text-align: right;">Subtotal ($)</th>
|
||||
<th style="width: 5%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(item, index) in items" :key="index">
|
||||
<tr>
|
||||
<td>
|
||||
<select :name="`items[${index}][product_id]`"
|
||||
class="form-select"
|
||||
x-model="item.product_id"
|
||||
@change="onProductSelect(index)"
|
||||
required>
|
||||
<option value="">-- Select Product --</option>
|
||||
<?php foreach ($products as $prod): ?>
|
||||
<option value="<?= e($prod['id']) ?>"
|
||||
data-price="<?= e($prod['price']) ?>"
|
||||
data-name="<?= e($prod['name']) ?>"
|
||||
data-stock="<?= e($prod['stock']) ?>">
|
||||
<?= e($prod['name']) ?> ($<?= number_format((float)$prod['price'], 2) ?> | Stock: <?= e($prod['stock']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input type="hidden" :name="`items[${index}][product_name]`" :value="item.product_name">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
:name="`items[${index}][unit_price]`"
|
||||
class="form-control"
|
||||
x-model.number="item.unit_price"
|
||||
readonly
|
||||
style="background-color: #f8fafc;">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number"
|
||||
min="1"
|
||||
:name="`items[${index}][quantity]`"
|
||||
class="form-control"
|
||||
x-model.number="item.quantity"
|
||||
required>
|
||||
</td>
|
||||
<td style="text-align: right; font-weight: 600;" x-text="'$' + (item.quantity * item.unit_price).toFixed(2)">
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<button type="button"
|
||||
@click="removeItem(index)"
|
||||
class="btn btn-sm"
|
||||
style="color: var(--danger); background: none; border: none; font-size: 1.25rem;"
|
||||
:disabled="items.length <= 1">×</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer" style="display: flex; justify-content: flex-end;">
|
||||
<button type="button" class="btn btn-secondary btn-sm" @click="addItem()">+ Add Another Line</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">3. Order Notes / Special Instructions</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<textarea name="notes" class="form-textarea" rows="3" placeholder="Optional notes for delivery, customer requests, or reference..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Summary & Actions -->
|
||||
<div>
|
||||
<div class="card" style="position: sticky; top: 5.5rem;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Order Summary</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Initial Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="Processing">Processing</option>
|
||||
<option value="Completed">Completed</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="border-top: 1px solid var(--border-color); padding-top: 1rem; margin-top: 1rem;">
|
||||
<div class="summary-row">
|
||||
<span style="color: var(--text-muted);">Items Subtotal:</span>
|
||||
<span style="font-weight: 600;" x-text="'$' + calculateSubtotal().toFixed(2)">$0.00</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-row" style="align-items: center; margin-top: 0.5rem;">
|
||||
<span style="color: var(--text-muted);">Tax Amount ($):</span>
|
||||
<input type="number" step="0.01" min="0" name="tax" class="form-control" style="width: 100px; padding: 0.35rem 0.5rem; text-align: right;" x-model.number="tax">
|
||||
</div>
|
||||
|
||||
<div class="summary-row" style="align-items: center; margin-top: 0.5rem;">
|
||||
<span style="color: var(--text-muted);">Discount ($):</span>
|
||||
<input type="number" step="0.01" min="0" name="discount" class="form-control" style="width: 100px; padding: 0.35rem 0.5rem; text-align: right; color: var(--danger);" x-model.number="discount">
|
||||
</div>
|
||||
|
||||
<div class="summary-row summary-total">
|
||||
<span>Grand Total:</span>
|
||||
<span x-text="'$' + calculateGrandTotal().toFixed(2)">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%; padding: 0.85rem; font-size: 1rem;">
|
||||
Place Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function orderForm() {
|
||||
return {
|
||||
tax: 0.00,
|
||||
discount: 0.00,
|
||||
items: [
|
||||
{ product_id: '', product_name: '', unit_price: 0, quantity: 1 }
|
||||
],
|
||||
addItem() {
|
||||
this.items.push({ product_id: '', product_name: '', unit_price: 0, quantity: 1 });
|
||||
},
|
||||
removeItem(index) {
|
||||
if (this.items.length > 1) {
|
||||
this.items.splice(index, 1);
|
||||
}
|
||||
},
|
||||
onProductSelect(index) {
|
||||
const item = this.items[index];
|
||||
if (!item.product_id) {
|
||||
item.unit_price = 0;
|
||||
item.product_name = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Find option in DOM to grab data attributes
|
||||
const selectEl = document.querySelectorAll('select[name^="items"]')[index];
|
||||
if (selectEl) {
|
||||
const selectedOption = selectEl.options[selectEl.selectedIndex];
|
||||
if (selectedOption) {
|
||||
item.unit_price = parseFloat(selectedOption.getAttribute('data-price') || 0);
|
||||
item.product_name = selectedOption.getAttribute('data-name') || '';
|
||||
}
|
||||
}
|
||||
},
|
||||
calculateSubtotal() {
|
||||
return this.items.reduce((sum, item) => {
|
||||
return sum + ((item.unit_price || 0) * (item.quantity || 0));
|
||||
}, 0);
|
||||
},
|
||||
calculateGrandTotal() {
|
||||
const subtotal = this.calculateSubtotal();
|
||||
const total = subtotal + (Number(this.tax) || 0) - (Number(this.discount) || 0);
|
||||
return Math.max(0, total);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,92 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Orders</h1>
|
||||
<p class="page-subtitle">Track, filter, and manage customer orders</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/orders/create" class="btn btn-primary">+ Create New Order</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Search -->
|
||||
<div class="card" style="margin-bottom: 1.25rem;">
|
||||
<div class="card-body" style="padding: 1rem 1.25rem;">
|
||||
<form method="GET" action="/orders" class="filter-bar" style="margin-bottom: 0;">
|
||||
<div style="flex: 1; min-width: 240px;">
|
||||
<input type="text" name="search" class="form-control" placeholder="Search by Order #, Customer name or email..." value="<?= e($search ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div style="min-width: 180px;">
|
||||
<select name="status" class="form-select" onchange="this.form.submit()">
|
||||
<option value="all" <?= ($currentStatus ?? 'all') === 'all' ? 'selected' : '' ?>>All Statuses</option>
|
||||
<option value="Pending" <?= ($currentStatus ?? '') === 'Pending' ? 'selected' : '' ?>>Pending</option>
|
||||
<option value="Processing" <?= ($currentStatus ?? '') === 'Processing' ? 'selected' : '' ?>>Processing</option>
|
||||
<option value="Completed" <?= ($currentStatus ?? '') === 'Completed' ? 'selected' : '' ?>>Completed</option>
|
||||
<option value="Cancelled" <?= ($currentStatus ?? '') === 'Cancelled' ? 'selected' : '' ?>>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-secondary">Search</button>
|
||||
<?php if (!empty($search) || ($currentStatus ?? 'all') !== 'all'): ?>
|
||||
<a href="/orders" class="btn btn-secondary" style="color: var(--danger);">Reset</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div class="card">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order Number</th>
|
||||
<th>Customer</th>
|
||||
<th>Subtotal</th>
|
||||
<th>Tax</th>
|
||||
<th>Discount</th>
|
||||
<th>Total</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
<th style="text-align: right;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($orders)): ?>
|
||||
<tr>
|
||||
<td colspan="9" style="text-align: center; color: var(--text-muted); padding: 3rem;">
|
||||
No orders found matching your filter criteria.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/orders/<?= e($order['id']) ?>" style="font-weight: 600; color: var(--primary); text-decoration: none;">
|
||||
<?= e($order['order_number']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div style="font-weight: 500;"><?= e($order['customer_name']) ?></div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);"><?= e($order['customer_email']) ?></div>
|
||||
</td>
|
||||
<td><?= money($order['subtotal']) ?></td>
|
||||
<td><?= money($order['tax']) ?></td>
|
||||
<td style="color: var(--danger);">-<?= money($order['discount']) ?></td>
|
||||
<td style="font-weight: 700; color: var(--primary); font-size: 1rem;"><?= money($order['total']) ?></td>
|
||||
<td>
|
||||
<span class="badge badge-<?= strtolower(e($order['status'])) ?>">
|
||||
<?= e($order['status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="color: var(--text-muted); font-size: 0.85rem;"><?= date('Y-m-d H:i', strtotime($order['created_at'])) ?></td>
|
||||
<td style="text-align: right;">
|
||||
<a href="/orders/<?= e($order['id']) ?>" class="btn btn-secondary btn-sm">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Order <?= e($order['order_number']) ?></h1>
|
||||
<p class="page-subtitle">Created on <?= date('F j, Y, g:i a', strtotime($order['created_at'])) ?></p>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="button" class="btn btn-secondary" onclick="window.print()">🖨️ Print Invoice</button>
|
||||
<a href="/orders" class="btn btn-secondary">← Back to Orders</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; align-items: start;">
|
||||
|
||||
<!-- Left Column: Detailed Invoice -->
|
||||
<div class="invoice-card">
|
||||
<div class="invoice-header">
|
||||
<div>
|
||||
<h2 style="font-size: 1.5rem; font-weight: 700; color: var(--primary);">SimpleOrder</h2>
|
||||
<p style="color: var(--text-muted); font-size: 0.875rem;">Official Order Invoice</p>
|
||||
</div>
|
||||
<div class="invoice-meta">
|
||||
<div style="font-size: 1.1rem; font-weight: 700;"><?= e($order['order_number']) ?></div>
|
||||
<div style="margin-top: 0.25rem;">
|
||||
<span class="badge badge-<?= strtolower(e($order['status'])) ?>">
|
||||
Status: <?= e($order['status']) ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<h4 style="font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem;">Billed To:</h4>
|
||||
<div style="font-weight: 600; font-size: 1.05rem;"><?= e($order['customer_name']) ?></div>
|
||||
<div style="color: var(--text-muted); font-size: 0.9rem;"><?= e($order['customer_email']) ?></div>
|
||||
<?php if (!empty($order['customer_phone'])): ?>
|
||||
<div style="color: var(--text-muted); font-size: 0.9rem;"><?= e($order['customer_phone']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($order['customer_address'])): ?>
|
||||
<div style="color: var(--text-muted); font-size: 0.9rem; margin-top: 0.25rem;"><?= nl2br(e($order['customer_address'])) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style="font-size: 0.8rem; text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem;">Order Details:</h4>
|
||||
<div style="font-size: 0.9rem; margin-bottom: 0.25rem;"><strong>Date:</strong> <?= date('M d, Y', strtotime($order['created_at'])) ?></div>
|
||||
<div style="font-size: 0.9rem; margin-bottom: 0.25rem;"><strong>Payment Status:</strong> Paid / Pending</div>
|
||||
<div style="font-size: 0.9rem;"><strong>Last Updated:</strong> <?= date('M d, Y H:i', strtotime($order['updated_at'] ?? $order['created_at'])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Table -->
|
||||
<table class="table" style="margin-bottom: 1.5rem;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<th>SKU</th>
|
||||
<th style="text-align: right;">Unit Price</th>
|
||||
<th style="text-align: center;">Qty</th>
|
||||
<th style="text-align: right;">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($order['items'] as $item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-weight: 600;"><?= e($item['product_name']) ?></div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);"><?= e($item['category'] ?? 'Product') ?></div>
|
||||
</td>
|
||||
<td style="font-family: monospace; font-size: 0.85rem;"><?= e($item['sku'] ?? 'N/A') ?></td>
|
||||
<td style="text-align: right;"><?= money($item['unit_price']) ?></td>
|
||||
<td style="text-align: center; font-weight: 600;"><?= e($item['quantity']) ?></td>
|
||||
<td style="text-align: right; font-weight: 600;"><?= money($item['total_price']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals Breakdown -->
|
||||
<div class="order-summary-box">
|
||||
<div class="summary-row">
|
||||
<span style="color: var(--text-muted);">Subtotal:</span>
|
||||
<span><?= money($order['subtotal']) ?></span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span style="color: var(--text-muted);">Tax:</span>
|
||||
<span><?= money($order['tax']) ?></span>
|
||||
</div>
|
||||
<?php if ((float)$order['discount'] > 0): ?>
|
||||
<div class="summary-row" style="color: var(--danger);">
|
||||
<span>Discount:</span>
|
||||
<span>-<?= money($order['discount']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="summary-row summary-total">
|
||||
<span>Grand Total:</span>
|
||||
<span><?= money($order['total']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($order['notes'])): ?>
|
||||
<div style="margin-top: 2rem; padding: 1rem; background-color: #f8fafc; border-radius: var(--radius-sm); border-left: 3px solid var(--primary);">
|
||||
<div style="font-weight: 600; font-size: 0.85rem; margin-bottom: 0.25rem;">Order Notes:</div>
|
||||
<div style="font-size: 0.9rem; color: var(--text-muted);"><?= nl2br(e($order['notes'])) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Status Controls & Actions -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Update Status</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/orders/<?= e($order['id']) ?>/status">
|
||||
<?= \App\Core\View::csrfField() ?>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Current Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="Pending" <?= $order['status'] === 'Pending' ? 'selected' : '' ?>>Pending</option>
|
||||
<option value="Processing" <?= $order['status'] === 'Processing' ? 'selected' : '' ?>>Processing</option>
|
||||
<option value="Completed" <?= $order['status'] === 'Completed' ? 'selected' : '' ?>>Completed</option>
|
||||
<option value="Cancelled" <?= $order['status'] === 'Cancelled' ? 'selected' : '' ?>>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%;">Update Order Status</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title" style="color: var(--danger);">Danger Zone</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 1rem;">Permanently remove this order and its associated records.</p>
|
||||
<form method="POST" action="/orders/<?= e($order['id']) ?>/delete" onsubmit="return confirm('Are you sure you want to permanently delete this order?');">
|
||||
<?= \App\Core\View::csrfField() ?>
|
||||
<button type="submit" class="btn btn-danger" style="width: 100%;">Delete Order</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user