Initial commit
This commit is contained in:
commit
e473c9e5c8
5
backend/config.php
Normal file
5
backend/config.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
define('DB_HOST', 'localhost');
|
||||||
|
define('DB_NAME', 'rms');
|
||||||
|
define('DB_USER', 'rms');
|
||||||
|
define('DB_PASS', 'kelokelo');
|
||||||
88
backend/dashboard.php
Normal file
88
backend/dashboard.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header("Access-Control-Allow-Methods: GET, OPTIONS");
|
||||||
|
header("Access-Control-Allow-Headers: Content-Type");
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4",
|
||||||
|
DB_USER, DB_PASS,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'DB connection failed']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = <<<SQL
|
||||||
|
SELECT
|
||||||
|
-- Sales sums
|
||||||
|
SUM(CASE WHEN i.created_at >= CURDATE() THEN il.quantity * mi.price ELSE 0 END) AS daily_sales,
|
||||||
|
SUM(CASE WHEN i.created_at >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)
|
||||||
|
AND i.created_at < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 7 DAY)
|
||||||
|
THEN il.quantity * mi.price ELSE 0 END) AS weekly_sales,
|
||||||
|
SUM(CASE WHEN i.created_at >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH) THEN il.quantity * mi.price ELSE 0 END) AS monthly_sales,
|
||||||
|
|
||||||
|
-- Most popular dish in each period
|
||||||
|
(SELECT mi2.name
|
||||||
|
FROM invoice_items AS il2
|
||||||
|
JOIN invoices AS i2 ON i2.id = il2.invoice_id
|
||||||
|
JOIN menu_items AS mi2 ON mi2.id = il2.menu_item_id
|
||||||
|
WHERE i2.created_at >= CURDATE()
|
||||||
|
GROUP BY il2.menu_item_id
|
||||||
|
ORDER BY SUM(il2.quantity) DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS daily_top,
|
||||||
|
|
||||||
|
(SELECT mi3.name
|
||||||
|
FROM invoice_items AS il3
|
||||||
|
JOIN invoices AS i3 ON i3.id = il3.invoice_id
|
||||||
|
JOIN menu_items AS mi3 ON mi3.id = il3.menu_item_id
|
||||||
|
WHERE i3.created_at >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)
|
||||||
|
AND i3.created_at < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY), INTERVAL 7 DAY)
|
||||||
|
GROUP BY il3.menu_item_id
|
||||||
|
ORDER BY SUM(il3.quantity) DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS weekly_top,
|
||||||
|
|
||||||
|
(SELECT mi4.name
|
||||||
|
FROM invoice_items AS il4
|
||||||
|
JOIN invoices AS i4 ON i4.id = il4.invoice_id
|
||||||
|
JOIN menu_items AS mi4 ON mi4.id = il4.menu_item_id
|
||||||
|
WHERE i4.created_at >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH)
|
||||||
|
GROUP BY il4.menu_item_id
|
||||||
|
ORDER BY SUM(il4.quantity) DESC
|
||||||
|
LIMIT 1
|
||||||
|
) AS monthly_top
|
||||||
|
|
||||||
|
FROM invoices AS i
|
||||||
|
JOIN invoice_items AS il ON il.invoice_id = i.id
|
||||||
|
JOIN menu_items AS mi ON mi.id = il.menu_item_id;
|
||||||
|
SQL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->query($sql);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'daily_sales' => (float)$result['daily_sales'],
|
||||||
|
'weekly_sales' => (float)$result['weekly_sales'],
|
||||||
|
'monthly_sales' => (float)$result['monthly_sales'],
|
||||||
|
'daily_top' => $result['daily_top'] ?? null,
|
||||||
|
'weekly_top' => $result['weekly_top'] ?? null,
|
||||||
|
'monthly_top' => $result['monthly_top'] ?? null,
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Query failed: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
147
backend/invoice.php
Normal file
147
backend/invoice.php
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
header("Access-Control-Allow-Headers: Content-Type");
|
||||||
|
|
||||||
|
// Handle preflight OPTIONS request
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
||||||
|
DB_USER,
|
||||||
|
DB_PASS,
|
||||||
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
|
);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'DB Connection failed: ' . $e->getMessage()]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$parts = explode('/', trim($_SERVER['PATH_INFO'] ?? '', '/'));
|
||||||
|
$resource = array_shift($parts);
|
||||||
|
$id = array_shift($parts);
|
||||||
|
|
||||||
|
// Ensure resource is 'invoices'
|
||||||
|
if ($resource !== 'invoices') {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Resource not found']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode JSON input
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
// Helper: fetch a single invoice with its lines
|
||||||
|
function fetchInvoice(PDO $pdo, $invoiceId) {
|
||||||
|
// Fetch invoice meta
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM invoices WHERE id = ?");
|
||||||
|
$stmt->execute([$invoiceId]);
|
||||||
|
$invoice = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$invoice) return null;
|
||||||
|
|
||||||
|
// Fetch lines
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT il.id, il.menu_item_id, il.quantity,
|
||||||
|
mi.name, mi.price
|
||||||
|
FROM invoice_items il
|
||||||
|
JOIN menu_items mi ON mi.id = il.menu_item_id
|
||||||
|
WHERE il.invoice_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$invoiceId]);
|
||||||
|
$lines = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$invoice['lines'] = $lines;
|
||||||
|
return $invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($method) {
|
||||||
|
|
||||||
|
case 'GET':
|
||||||
|
if ($id) {
|
||||||
|
// GET /invoices/{id}
|
||||||
|
$invoice = fetchInvoice($pdo, $id);
|
||||||
|
if (!$invoice) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Invoice not found']);
|
||||||
|
} else {
|
||||||
|
echo json_encode($invoice);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// GET /invoices
|
||||||
|
// List all invoices without lines
|
||||||
|
$stmt = $pdo->query("SELECT id, created_at FROM invoices ORDER BY created_at DESC");
|
||||||
|
$invs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo json_encode($invs);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
// POST /invoices
|
||||||
|
// Expect payload { lines: [ { menu_item_id, quantity }, ... ] }
|
||||||
|
if (empty($input['lines']) || !is_array($input['lines'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invoice lines are required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Create invoice record
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO invoices () VALUES ()");
|
||||||
|
$stmt->execute();
|
||||||
|
$invoiceId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Insert each line
|
||||||
|
$stmtLine = $pdo->prepare("
|
||||||
|
INSERT INTO invoice_items (invoice_id, menu_item_id, quantity)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
");
|
||||||
|
foreach ($input['lines'] as $line) {
|
||||||
|
if (empty($line['menu_item_id']) || empty($line['quantity'])) {
|
||||||
|
throw new Exception("menu_item_id and quantity required for each line");
|
||||||
|
}
|
||||||
|
$stmtLine->execute([
|
||||||
|
$invoiceId,
|
||||||
|
$line['menu_item_id'],
|
||||||
|
$line['quantity']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
http_response_code(201);
|
||||||
|
echo json_encode(['message' => 'Invoice created', 'id' => $invoiceId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Failed to create invoice: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DELETE':
|
||||||
|
// DELETE /invoices/{id}
|
||||||
|
if (!$id) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invoice ID is required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM invoices WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['message' => 'Invoice deleted']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Method not allowed']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
118
backend/menu.php
Normal file
118
backend/menu.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json");
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
header("Access-Control-Allow-Headers: Content-Type");
|
||||||
|
|
||||||
|
// Handle preflight OPTIONS request
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||||
|
http_response_code(200);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO(
|
||||||
|
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
||||||
|
DB_USER,
|
||||||
|
DB_PASS
|
||||||
|
);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Handle connection errors
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the HTTP method and request URI
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
$request = explode('/', trim($_SERVER['PATH_INFO'] ?? '', '/'));
|
||||||
|
$resource = array_shift($request);
|
||||||
|
$id = array_shift($request);
|
||||||
|
|
||||||
|
// Ensure the resource is 'menu'
|
||||||
|
if ($resource !== 'menu') {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Resource not found']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the input JSON
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
// Define the SQL queries
|
||||||
|
switch ($method) {
|
||||||
|
case 'GET':
|
||||||
|
if ($id) {
|
||||||
|
// Retrieve a single menu item
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM menu_items WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$item = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($item) {
|
||||||
|
echo json_encode($item);
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Menu item not found']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Retrieve all menu items
|
||||||
|
$stmt = $pdo->query("SELECT * FROM menu_items");
|
||||||
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo json_encode($items);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'POST':
|
||||||
|
// Create a new menu item
|
||||||
|
if (!isset($input['name'], $input['price'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Name and price are required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO menu_items (name, description, price) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$input['name'],
|
||||||
|
$input['description'] ?? null,
|
||||||
|
$input['price']
|
||||||
|
]);
|
||||||
|
$id = $pdo->lastInsertId();
|
||||||
|
http_response_code(201);
|
||||||
|
echo json_encode(['message' => 'Menu item created', 'id' => $id]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PUT':
|
||||||
|
// Update an existing menu item
|
||||||
|
if (!$id) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'ID is required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("UPDATE menu_items SET name = ?, description = ?, price = ? WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
$input['name'] ?? null,
|
||||||
|
$input['description'] ?? null,
|
||||||
|
$input['price'] ?? null,
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
echo json_encode(['message' => 'Menu item updated']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DELETE':
|
||||||
|
// Delete a menu item
|
||||||
|
if (!$id) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'ID is required']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM menu_items WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['message' => 'Menu item deleted']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['error' => 'Method not allowed']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
22
backend/schema.sql
Normal file
22
backend/schema.sql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
CREATE TABLE menu_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Invoices table
|
||||||
|
CREATE TABLE invoices (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Invoice items table
|
||||||
|
CREATE TABLE invoice_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
invoice_id INT NOT NULL,
|
||||||
|
menu_item_id INT NOT NULL,
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
86
frontend/dashboard.html
Normal file
86
frontend/dashboard.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full bg-gray-100">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dashboard | Restaurant Management</title>
|
||||||
|
<script type="module" src="src/main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="h-full flex items-center justify-center p-4">
|
||||||
|
<div
|
||||||
|
x-data="dashboardApp()"
|
||||||
|
class="w-full max-w-2xl bg-white shadow-lg rounded-lg overflow-hidden font-mono"
|
||||||
|
>
|
||||||
|
<!-- Header & Close -->
|
||||||
|
<div class="p-6 flex justify-between items-center">
|
||||||
|
<h2 class="text-2xl font-semibold">Sales Dashboard</h2>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="init()"
|
||||||
|
class="bg-blue-500 text-white px-4 py-2 mr-4 rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openMain()"
|
||||||
|
class="bg-red-400 text-white px-4 py-2 rounded hover:bg-red-500"
|
||||||
|
>
|
||||||
|
Close Manager
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI Cards -->
|
||||||
|
<div class="p-6 space-y-6">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<!-- Daily Sales -->
|
||||||
|
<div class="bg-gray-50 border rounded p-4 flex flex-col">
|
||||||
|
<div class="text-gray-600">Daily Sales</div>
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold mt-1"
|
||||||
|
x-text="formatCurrency(dailySales)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<!-- Weekly Sales -->
|
||||||
|
<div class="bg-gray-50 border rounded p-4 flex flex-col">
|
||||||
|
<div class="text-gray-600">Weekly Sales</div>
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold mt-1"
|
||||||
|
x-text="formatCurrency(weeklySales)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<!-- Monthly Sales -->
|
||||||
|
<div class="bg-gray-50 border rounded p-4 flex flex-col">
|
||||||
|
<div class="text-gray-600">Monthly Sales</div>
|
||||||
|
<div
|
||||||
|
class="text-2xl font-bold mt-1"
|
||||||
|
x-text="formatCurrency(monthlySales)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<!-- Top Dish (Daily) -->
|
||||||
|
<div class="bg-gray-50 border rounded p-4 flex flex-col">
|
||||||
|
<div class="text-gray-600">Top Dish (Daily)</div>
|
||||||
|
<div
|
||||||
|
class="text-xl font-medium mt-1"
|
||||||
|
x-text="dailyTop || '—'"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Dish Weekly & Monthly -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="bg-gray-50 border rounded p-4">
|
||||||
|
<div class="text-gray-600 mb-2">Top Dish (Weekly)</div>
|
||||||
|
<div class="text-xl font-medium" x-text="weeklyTop || '—'"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 border rounded p-4">
|
||||||
|
<div class="text-gray-600 mb-2">Top Dish (Monthly)</div>
|
||||||
|
<div class="text-xl font-medium" x-text="monthlyTop || '—'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
57
frontend/index.html
Normal file
57
frontend/index.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full bg-gray-100">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Restaurant Management</title>
|
||||||
|
|
||||||
|
<script type="module" src="src/main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="h-full flex items-center justify-center">
|
||||||
|
|
||||||
|
<div x-data="mainApp()" class="w-72 bg-white shadow-lg rounded-lg overflow-hidden font-mono">
|
||||||
|
|
||||||
|
<div class="bg-gray-200 text-gray-800 p-4 h-96 flex flex-col justify-between">
|
||||||
|
|
||||||
|
<!-- Menu Options -->
|
||||||
|
<ul id="feature-menu" class="space-y-2">
|
||||||
|
<template x-for="(opt, idx) in options" :key="opt.view">
|
||||||
|
<li
|
||||||
|
:class="idx === selected
|
||||||
|
? 'bg-gray-800 text-gray-100'
|
||||||
|
: 'text-gray-700'"
|
||||||
|
class="px-3 py-2 rounded cursor-pointer"
|
||||||
|
@click="select(idx)"
|
||||||
|
>
|
||||||
|
<span x-text="`${idx + 1}. ${opt.label}`"></span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Soft-Key “Open” -->
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button
|
||||||
|
@click="openSelected()"
|
||||||
|
class="bg-green-600 text-white px-4 py-1 rounded hover:bg-green-700 focus:outline-none"
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Views Container -->
|
||||||
|
<div class="p-4">
|
||||||
|
<template x-if="view === 'dashboard'">
|
||||||
|
<h2 class="text-xl font-semibold">Dashboard</h2>
|
||||||
|
</template>
|
||||||
|
<template x-if="view === 'menu'">
|
||||||
|
<h2 class="text-xl font-semibold">Menu Manager</h2>
|
||||||
|
</template>
|
||||||
|
<template x-if="view === 'invoices'">
|
||||||
|
<h2 class="text-xl font-semibold">Invoices</h2>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
106
frontend/invoices.html
Normal file
106
frontend/invoices.html
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full bg-gray-100">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Invoices | Restaurant Management</title>
|
||||||
|
<script type="module" src="src/main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="h-full flex items-center justify-center p-4">
|
||||||
|
|
||||||
|
<div x-data="invoiceApp()" class="w-full max-w-2xl bg-white shadow-lg rounded-lg overflow-hidden font-mono">
|
||||||
|
|
||||||
|
<!-- Create Invoice -->
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<h2 class="text-2xl font-semibold">Create Invoice</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openMain()"
|
||||||
|
class="bg-red-400 text-white px-4 py-2 rounded hover:bg-red-500"
|
||||||
|
>Close Manager</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<select x-model="selectedId" class="flex-1 border rounded p-2">
|
||||||
|
<option value="" disabled>Select item…</option>
|
||||||
|
<template x-for="item in menu" :key="item.id">
|
||||||
|
<option :value="item.id"
|
||||||
|
x-text="item.name + ' ($' + Number(item.price).toFixed(2) + ')'"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
<input type="number" x-model.number="quantity" min="1" placeholder="Qty"
|
||||||
|
class="w-20 border rounded p-2" />
|
||||||
|
<button @click="addLine()"
|
||||||
|
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Invoice Lines & Total -->
|
||||||
|
<template x-if="lines.length">
|
||||||
|
<div class="space-y-2 border-t pt-4">
|
||||||
|
<template x-for="(line, idx) in lines" :key="idx">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span x-text="line.name + ' × ' + line.qty"></span>
|
||||||
|
<span x-text="'$' + (line.price * line.qty).toFixed(2)"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex justify-between font-semibold">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span x-text="'$' + total.toFixed(2)"></span>
|
||||||
|
</div>
|
||||||
|
<button @click="saveInvoice()"
|
||||||
|
class="w-full bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
|
||||||
|
Save Invoice
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!lines.length">
|
||||||
|
<p class="text-gray-500">No items added.</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Invoice List -->
|
||||||
|
<div class="bg-gray-50 p-6 border-t space-y-4 max-h-[400px] overflow-auto">
|
||||||
|
<h2 class="text-2xl font-semibold">Invoices</h2>
|
||||||
|
|
||||||
|
<template x-if="invoices.length">
|
||||||
|
<template x-for="inv in invoices" :key="inv.id">
|
||||||
|
<div class="border rounded p-4 bg-white space-y-2">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>Invoice #<span x-text="inv.id"></span></span>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<button @click="printInvoice(inv)"
|
||||||
|
class="text-green-600 hover:underline text-sm">
|
||||||
|
Print
|
||||||
|
</button>
|
||||||
|
<button @click="deleteInvoice(inv.id)"
|
||||||
|
class="text-red-500 hover:underline text-sm">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 mb-2">
|
||||||
|
<template x-for="line in inv.lines" :key="line.id">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span x-text="line.name + ' × ' + line.quantity"></span>
|
||||||
|
<span x-text="'$' + (line.price * line.quantity).toFixed(2)"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between font-medium">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span x-text="'$' + inv.total.toFixed(2)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="!invoices.length">
|
||||||
|
<p class="text-gray-500">No invoices yet.</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
82
frontend/menu.html
Normal file
82
frontend/menu.html
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full bg-gray-100">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Menu Manager | Restaurant Management</title>
|
||||||
|
<script type="module" src="src/main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body class="h-full flex items-center justify-center p-4">
|
||||||
|
|
||||||
|
<div x-data="menuApp()" class="w-full max-w-2xl bg-white shadow-lg rounded-lg overflow-hidden font-mono">
|
||||||
|
|
||||||
|
<!-- Header & Close -->
|
||||||
|
<div class="p-6 flex justify-between items-center">
|
||||||
|
<h2 class="text-2xl font-semibold">Menu Management</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openMain()"
|
||||||
|
class="bg-red-400 text-white px-4 py-2 rounded hover:bg-red-500"
|
||||||
|
>Close Manager</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New/Edit Form -->
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
x-model="form.name"
|
||||||
|
placeholder="Item Name"
|
||||||
|
class="flex-1 border rounded p-2"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
x-model.number="form.price"
|
||||||
|
placeholder="Price"
|
||||||
|
class="w-24 border rounded p-2"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="saveItem()"
|
||||||
|
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||||
|
x-text="form.id ? 'Update' : 'Add'"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
x-model="form.description"
|
||||||
|
placeholder="Description"
|
||||||
|
class="w-full border rounded p-2"
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items Grid -->
|
||||||
|
<div class="bg-gray-50 p-6 border-t space-y-4 max-h-[400px] overflow-auto">
|
||||||
|
<template x-if="items.length">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<template x-for="item in items" :key="item.id">
|
||||||
|
<div class="border rounded p-4 bg-white flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold" x-text="item.name"></div>
|
||||||
|
<div class="text-sm text-gray-600" x-text="item.description"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center mt-4">
|
||||||
|
<div class="text-gray-800 font-medium" x-text="`$${Number(item.price).toFixed(2)}`"></div>
|
||||||
|
<div class="space-x-2">
|
||||||
|
<button @click="editItem(item)" class="text-blue-500 hover:underline text-sm">Edit</button>
|
||||||
|
<button @click="deleteItem(item.id)" class="text-red-500 hover:underline text-sm">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!items.length">
|
||||||
|
<p class="text-gray-500 text-center">No menu items found.</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1514
frontend/package-lock.json
generated
Normal file
1514
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
frontend/package.json
Normal file
19
frontend/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "restaurant-management-system",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
|
"alpinejs": "^3.14.9",
|
||||||
|
"tailwindcss": "^4.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
40
frontend/src/dashboardApp.js
Normal file
40
frontend/src/dashboardApp.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const baseUrl = "http://localhost:8080/dashboard.php";
|
||||||
|
|
||||||
|
export function dashboardApp() {
|
||||||
|
return {
|
||||||
|
dailySales: 0,
|
||||||
|
weeklySales: 0,
|
||||||
|
monthlySales: 0,
|
||||||
|
dailyTop: '',
|
||||||
|
weeklyTop: '',
|
||||||
|
monthlyTop: '',
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
this.dailySales = data['daily_sales']
|
||||||
|
this.weeklySales = data['weekly_sales']
|
||||||
|
this.monthlySales = data['monthly_sales']
|
||||||
|
this.dailyTop = data['daily_top']
|
||||||
|
this.weeklyTop = data['weekly_top']
|
||||||
|
this.monthlyTop = data['monthly_top']
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Analytics fetch failed:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Format number as currency
|
||||||
|
formatCurrency(value) {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD'
|
||||||
|
}).format(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
openMain() {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
177
frontend/src/invoiceApp.js
Normal file
177
frontend/src/invoiceApp.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
const baseUrl = "http://localhost:8080/invoice.php";
|
||||||
|
const menuBaseUrl = "http://localhost:8080/menu.php";
|
||||||
|
|
||||||
|
export function invoiceApp() {
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
menu: [],
|
||||||
|
selectedId: "",
|
||||||
|
quantity: 1,
|
||||||
|
lines: [],
|
||||||
|
invoices: [],
|
||||||
|
|
||||||
|
// Computed total for current invoice lines
|
||||||
|
get total() {
|
||||||
|
return this.lines.reduce((sum, l) => sum + l.price * l.qty, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.loadMenu();
|
||||||
|
await this.loadInvoices();
|
||||||
|
},
|
||||||
|
|
||||||
|
openMain() {
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadMenu() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${menuBaseUrl}/menu`);
|
||||||
|
this.menu = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load menu:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadInvoices() {
|
||||||
|
try {
|
||||||
|
const listRes = await fetch(`${baseUrl}/invoices`);
|
||||||
|
const list = await listRes.json();
|
||||||
|
this.invoices = await Promise.all(
|
||||||
|
list.map(async inv => {
|
||||||
|
const linesRes = await fetch(`${baseUrl}/invoices/${inv.id}`);
|
||||||
|
const full = await linesRes.json();
|
||||||
|
// Ensure numeric values
|
||||||
|
full.lines = full.lines.map(line => ({
|
||||||
|
...line,
|
||||||
|
price: Number(line.price),
|
||||||
|
quantity: Number(line.quantity)
|
||||||
|
}));
|
||||||
|
full.total = full.lines.reduce((s, l) => s + l.price * l.quantity, 0);
|
||||||
|
full.date = full.created_at || inv.created_at || new Date().toLocaleString();
|
||||||
|
return full;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load invoices:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addLine() {
|
||||||
|
if (!this.selectedId || this.quantity < 1) return;
|
||||||
|
const item = this.menu.find(i => i.id == this.selectedId);
|
||||||
|
if (!item) return;
|
||||||
|
this.lines.push({
|
||||||
|
id: Date.now(),
|
||||||
|
name: item.name,
|
||||||
|
price: Number(item.price),
|
||||||
|
qty: Number(this.quantity)
|
||||||
|
});
|
||||||
|
this.selectedId = "";
|
||||||
|
this.quantity = 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveInvoice() {
|
||||||
|
if (!this.lines.length) return;
|
||||||
|
const payload = {
|
||||||
|
lines: this.lines.map(l => ({
|
||||||
|
menu_item_id: Number(this.menu.find(i => i.name === l.name).id),
|
||||||
|
quantity: l.qty
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}/invoices`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
this.lines = [];
|
||||||
|
await this.loadInvoices();
|
||||||
|
} else {
|
||||||
|
console.error('Save invoice failed:', res.status);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error saving invoice:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteInvoice(id) {
|
||||||
|
if (!confirm(`Delete invoice #${id}?`)) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}/invoices/${id}`, { method: 'DELETE' });
|
||||||
|
if (res.ok) {
|
||||||
|
this.invoices = this.invoices.filter(i => i.id !== id);
|
||||||
|
} else {
|
||||||
|
console.error('Delete invoice failed:', res.status);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting invoice:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Print a formatted invoice
|
||||||
|
printInvoice(inv) {
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Invoice #${inv.id}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin:20px; }
|
||||||
|
h1 { text-align: center; margin-bottom: 20px; }
|
||||||
|
.meta { margin-bottom: 20px; }
|
||||||
|
.meta div { margin-bottom: 4px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 8px; }
|
||||||
|
th { background: #f3f3f3; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
tfoot td { font-weight: bold; background: #f9f9f9; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Invoice #${inv.id}</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<div>Date: ${inv.date}</div>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="text-right">Qty</th>
|
||||||
|
<th class="text-right">Unit Price</th>
|
||||||
|
<th class="text-right">Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${inv.lines.map(l => `
|
||||||
|
<tr>
|
||||||
|
<td>${l.name}</td>
|
||||||
|
<td class="text-right">${l.quantity}</td>
|
||||||
|
<td class="text-right">$${l.price.toFixed(2)}</td>
|
||||||
|
<td class="text-right">$${(l.price * l.quantity).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-right">Total:</td>
|
||||||
|
<td class="text-right">$${inv.total.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
const win = window.open('', '_blank');
|
||||||
|
win.document.open();
|
||||||
|
win.document.write(html);
|
||||||
|
win.document.close();
|
||||||
|
win.focus();
|
||||||
|
setTimeout(() => win.print(), 300);
|
||||||
|
win.addEventListener('afterprint', () => win.close());
|
||||||
|
setTimeout(() => { if (!win.closed) win.close(); }, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
13
frontend/src/main.js
Normal file
13
frontend/src/main.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import './style.css'
|
||||||
|
import Alpine from 'alpinejs'
|
||||||
|
import { mainApp } from './mainApp.js'
|
||||||
|
import { menuApp } from './menuApp.js'
|
||||||
|
import { invoiceApp } from './invoiceApp.js'
|
||||||
|
import { dashboardApp } from './dashboardApp.js'
|
||||||
|
|
||||||
|
window.mainApp = mainApp
|
||||||
|
window.menuApp = menuApp
|
||||||
|
window.invoiceApp = invoiceApp
|
||||||
|
window.dashboardApp = dashboardApp
|
||||||
|
|
||||||
|
Alpine.start()
|
||||||
43
frontend/src/mainApp.js
Normal file
43
frontend/src/mainApp.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
export function mainApp() {
|
||||||
|
return {
|
||||||
|
options: [
|
||||||
|
{ label: 'Dashboard', view: 'dashboard' },
|
||||||
|
{ label: 'Menu Manager', view: 'menu' },
|
||||||
|
{ label: 'Invoices Manager', view: 'invoices' },
|
||||||
|
],
|
||||||
|
selected: 0,
|
||||||
|
view: 'menu',
|
||||||
|
|
||||||
|
init() {
|
||||||
|
window.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 's') {
|
||||||
|
this.selected = (this.selected + 1) % this.options.length
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowUp' || e.key === 'w') {
|
||||||
|
this.selected = (this.selected - 1 + this.options.length) % this.options.length
|
||||||
|
}
|
||||||
|
if (e.key === 'Enter') this.openSelected()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
select(idx) {
|
||||||
|
this.selected = idx
|
||||||
|
this.view = this.options[idx].view
|
||||||
|
},
|
||||||
|
|
||||||
|
openSelected() {
|
||||||
|
if (this.options[this.selected].view === 'dashboard') {
|
||||||
|
window.location.href = '/dashboard'
|
||||||
|
}
|
||||||
|
if (this.options[this.selected].view === 'menu') {
|
||||||
|
window.location.href = '/menu'
|
||||||
|
}
|
||||||
|
if (this.options[this.selected].view === 'invoices') {
|
||||||
|
window.location.href = '/invoices'
|
||||||
|
} else {
|
||||||
|
this.view = this.options[this.selected].view
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
70
frontend/src/menuApp.js
Normal file
70
frontend/src/menuApp.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const baseUrl = "http://localhost:8080/menu.php";
|
||||||
|
|
||||||
|
export function menuApp() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
form: { id: null, name: "", description: "", price: null },
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${baseUrl}/menu`);
|
||||||
|
if (!res.ok) throw new Error("Fetch error " + res.status);
|
||||||
|
this.items = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error loading items:", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.form = { id: null, name: "", description: "", price: null };
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveItem() {
|
||||||
|
const isEdit = Boolean(this.form.id);
|
||||||
|
const url = isEdit
|
||||||
|
? `${baseUrl}/menu/${this.form.id}`
|
||||||
|
: `${baseUrl}/menu`;
|
||||||
|
const method = isEdit ? "PUT" : "POST";
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Save failed:", res.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await res.json();
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
const idx = this.items.findIndex(i => i.id === this.form.id);
|
||||||
|
if (idx !== -1) this.items.splice(idx, 1, { ...this.form });
|
||||||
|
} else {
|
||||||
|
this.items.push({ id: payload.id ?? Date.now(), ...this.form });
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.init();
|
||||||
|
this.resetForm();
|
||||||
|
},
|
||||||
|
|
||||||
|
editItem(item) {
|
||||||
|
this.form = { ...item };
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteItem(id) {
|
||||||
|
const res = await fetch(`${baseUrl}/menu/${id}`, { method: "DELETE" });
|
||||||
|
if (res.ok) {
|
||||||
|
this.items = this.items.filter(i => i.id !== id);
|
||||||
|
} else {
|
||||||
|
console.error("Delete failed:", res.status);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openMain() {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
1
frontend/src/style.css
Normal file
1
frontend/src/style.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
7
frontend/vite.config.js
Normal file
7
frontend/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user