Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions plugin/src/Branding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace DanMat\Restaurant;

/**
* The shared RAS identity for the staff terminals — the small "Restaurant
* Automation System" eyebrow above each page title, carried over (uplifted) from
* the original app so every terminal reads as one system. The styles ride in a
* nonce-carrying `<style>` block because the admin CSP drops inline `style=`.
*/
final class Branding
{
/** The plugin's shared status palette (uplifted from the original RAS colours). */
public const STATUS_COLORS = [
'open' => '#3d8b40',
'occupied' => '#b8860b',
'dirty' => '#c0392b',
'reserved' => '#2471a3',
];

/** A page header with the RAS eyebrow, a title, and a one-line subtitle. */
public static function head(string $title, string $subtitle, string $nonce): string
{
return '<style nonce="' . self::e($nonce) . '">'
. '.rs-head{margin:0 0 1rem}'
. '.rs-eyebrow{margin:0;font-size:.72rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#b8860b}'
. '.rs-head h1{margin:.15rem 0 .1rem}'
. '.rs-sub{margin:0;font-size:.9rem;color:var(--nb-muted,#6b7280)}'
. '</style>'
. '<div class="nb-page-head rs-head">'
. '<p class="rs-eyebrow">RAS · Restaurant Automation System</p>'
. '<h1>' . self::e($title) . '</h1>'
. '<p class="rs-sub">' . self::e($subtitle) . '</p>'
. '</div>';
}

private static function e(string $v): string
{
return htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}
}
2 changes: 1 addition & 1 deletion plugin/src/KitchenAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function render(string $csrf = '', ?string $notice = null, string $nonce
}

return $this->styles($nonce)
. '<div class="nb-page-head"><h1>Kitchen</h1></div>'
. Branding::head('Kitchen', 'Tickets on the line, oldest first.', $nonce)
. $this->notice($notice)
. '<p class="nb-muted kx-intro">Tickets on the line. Start a ticket when you begin it, mark it ready when it is up for the pass.</p>'
. '<div class="kx-board">' . $cols . '</div>'
Expand Down
2 changes: 1 addition & 1 deletion plugin/src/OrdersAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function render(string $csrf = '', ?string $notice = null, ?string $view
$viewId = ($view !== null && preg_match('/^\d+$/', trim($view)) === 1) ? (int) trim($view) : null;
$viewOrder = $viewId !== null ? $this->orders->get($viewId) : null;

$html = $this->styles($nonce) . '<div class="nb-page-head"><h1>Orders</h1></div>' . $this->notice($notice);
$html = $this->styles($nonce) . Branding::head('Orders', 'Open a table, build the order, settle up.', $nonce) . $this->notice($notice);

if ($viewOrder !== null) {
return $html . $this->orderScreen($csrf, $viewOrder);
Expand Down
2 changes: 1 addition & 1 deletion plugin/src/ReportsAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function render(string $csrf = '', ?string $notice = null, string $nonce
$active = $this->reports->activeOrders();

return $this->styles($nonce)
. '<div class="nb-page-head"><h1>Reports</h1></div>'
. Branding::head('Reports', 'Revenue and what is selling.', $nonce)
. '<p class="nb-muted rz-intro">How service is going — revenue and what is selling. Figures are from settled (paid) orders.</p>'
. '<div class="rz-cards">'
. $this->card('Revenue today', $today['revenue'], $today['orders'] . ' paid')
Expand Down
2 changes: 1 addition & 1 deletion plugin/src/ReservationsAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function render(string $csrf = '', ?string $notice = null, ?string $edit
$filter = ($status !== null && in_array(trim($status), Reservations::STATUSES, true)) ? trim($status) : null;

return $this->styles($nonce)
. '<div class="nb-page-head"><h1>Reservations</h1></div>'
. Branding::head('Reservations', 'The book — upcoming bookings.', $nonce)
. $this->notice($notice)
. '<p class="nb-muted rz-intro">Upcoming bookings. Linking a guest connects the booking to their record in the CRM — opening it needs CRM access.</p>'
. $this->form($csrf, $editRes)
Expand Down
64 changes: 36 additions & 28 deletions plugin/src/TablesAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ public function render(string $csrf = '', ?string $notice = null, ?string $edit
$filter = ($status !== null && in_array(trim($status), Tables::STATUSES, true)) ? trim($status) : null;

return $this->styles($nonce)
. '<div class="nb-page-head"><h1>Floor</h1></div>'
. Branding::head('Floor', 'The room at a glance — seat, clear and turn tables.', $nonce)
. $this->notice($notice)
. '<p class="nb-muted rz-intro">Your tables and their status. Seat guests, send tables for cleaning, and turn them for the next party.</p>'
. $this->form($csrf, $editTable)
. $this->legend()
. $this->filterBar($filter)
. $this->board($csrf, $this->tables->all($filter), $filter);
}
Expand Down Expand Up @@ -85,7 +85,21 @@ private function filterBar(?string $active): string
return '<div class="rz-filter" role="group" aria-label="Filter by status">' . $chips . '</div>';
}

/** A key to the status colours — the circular tokens read at a glance. */
private function legend(): string
{
$items = '';
foreach (self::STATUS_LABELS as $s => $label) {
$items .= '<span class="rz-key rz-status-' . self::e($s) . '"><i class="rz-dot"></i>' . self::e($label) . '</span>';
}
return '<div class="rz-legend">' . $items . '</div>';
}

/**
* The floor as a grid of circular table tokens, coloured by status — the RAS
* signature, uplifted. The circle links to the table; the seats and the
* contextual quick actions sit beneath it.
*
* @param list<array<string,mixed>> $tables
*/
private function board(string $csrf, array $tables, ?string $filter): string
Expand All @@ -95,20 +109,18 @@ private function board(string $csrf, array $tables, ?string $filter): string
return '<p class="nb-muted">' . $msg . '</p>';
}

$cards = '';
$tokens = '';
foreach ($tables as $t) {
$status = (string) $t['status'];
$cards .= '<li class="rz-table rz-status-' . self::e($status) . '">'
. '<div class="rz-table-top">'
. '<a class="rz-table-label" href="/admin/restaurant?edit=' . self::e((string) $t['id']) . '">' . self::e((string) $t['label']) . '</a>'
. '<span class="rz-badge rz-badge-' . self::e($status) . '">' . self::e(self::STATUS_LABELS[$status] ?? $status) . '</span>'
. '</div>'
. '<div class="rz-table-seats">' . self::e((string) $t['seats']) . ' seats</div>'
. '<div class="rz-table-acts">' . $this->actions($csrf, (int) $t['id'], $status) . '</div>'
$tokens .= '<li class="rz-token rz-status-' . self::e($status) . '">'
. '<a class="rz-circle" href="/admin/restaurant?edit=' . self::e((string) $t['id']) . '" title="' . self::e(self::STATUS_LABELS[$status] ?? $status) . '">'
. '<span class="rz-circle-label">' . self::e((string) $t['label']) . '</span></a>'
. '<div class="rz-token-seats">' . self::e((string) $t['seats']) . ' seats · ' . self::e(self::STATUS_LABELS[$status] ?? $status) . '</div>'
. '<div class="rz-token-acts">' . $this->actions($csrf, (int) $t['id'], $status) . '</div>'
. '</li>';
}

return '<ul class="rz-board">' . $cards . '</ul>';
return '<ul class="rz-board">' . $tokens . '</ul>';
}

/** The status quick-actions relevant to a table's current state. */
Expand Down Expand Up @@ -166,24 +178,20 @@ private function styles(string $nonce): string
. '.rz-filter{display:flex;flex-wrap:wrap;gap:.4rem;margin:0 0 1rem}'
. '.rz-chip{text-decoration:none;background:rgba(128,128,128,.12);border-radius:999px;padding:.3rem .8rem;font-size:.82rem;color:inherit;min-height:32px;display:inline-flex;align-items:center}'
. '.rz-chip.is-active{background:rgba(52,152,219,.22);color:#2471a3;font-weight:700}'
. '.rz-board{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(9.5rem,1fr));gap:.75rem}'
. '.rz-table{border:1px solid rgba(128,128,128,.2);border-left:4px solid rgba(128,128,128,.4);border-radius:10px;padding:.6rem .7rem;display:flex;flex-direction:column;gap:.4rem}'
. '.rz-status-open{border-left-color:#27ae60}'
. '.rz-status-occupied{border-left-color:#2980b9}'
. '.rz-status-dirty{border-left-color:#c0392b}'
. '.rz-status-reserved{border-left-color:#b8860b}'
. '.rz-table-top{display:flex;justify-content:space-between;align-items:center;gap:.4rem}'
. '.rz-table-label{font-weight:800;font-size:1.15rem;text-decoration:none}'
. '.rz-table-seats{font-size:.8rem;opacity:.7}'
. '.rz-badge{font-size:.65rem;font-weight:700;padding:.1rem .4rem;border-radius:999px;text-transform:uppercase;letter-spacing:.03em}'
. '.rz-badge-open{background:rgba(39,174,96,.18);color:#1e8449}'
. '.rz-badge-occupied{background:rgba(41,128,185,.18);color:#2471a3}'
. '.rz-badge-dirty{background:rgba(192,57,43,.15);color:#c0392b}'
. '.rz-badge-reserved{background:rgba(184,134,11,.18);color:#9a7d0a}'
. '.rz-table-acts{display:flex;flex-wrap:wrap;gap:.35rem;align-items:center;margin-top:.1rem}'
. '.rz-status-open{--rs:#3d8b40}.rz-status-occupied{--rs:#b8860b}.rz-status-dirty{--rs:#c0392b}.rz-status-reserved{--rs:#2471a3}'
. '.rz-legend{display:flex;flex-wrap:wrap;gap:.9rem;margin:0 0 1rem;font-size:.8rem;color:var(--nb-muted,#6b7280)}'
. '.rz-key{display:inline-flex;align-items:center;gap:.4rem}'
. '.rz-dot{width:.75rem;height:.75rem;border-radius:50%;background:var(--rs,#888);display:inline-block}'
. '.rz-board{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(6.5rem,1fr));gap:1rem .75rem}'
. '.rz-token{display:flex;flex-direction:column;align-items:center;gap:.4rem;text-align:center}'
. '.rz-circle{width:76px;height:76px;border-radius:50%;background:var(--rs,#888);color:#fff;display:flex;align-items:center;justify-content:center;text-decoration:none;padding:.35rem;box-sizing:border-box;transition:transform .08s ease}'
. '.rz-circle:hover{transform:scale(1.05)}'
. '.rz-circle-label{font-weight:700;font-size:1.05rem;line-height:1.1;overflow-wrap:anywhere}'
. '.rz-token-seats{font-size:.72rem;color:var(--nb-muted,#6b7280);line-height:1.2}'
. '.rz-token-acts{display:flex;flex-wrap:wrap;gap:.3rem;align-items:center;justify-content:center}'
. '.rz-act{display:inline}'
. '.rz-act-btn{min-height:36px;padding:.25rem .6rem;font-size:.8rem}'
. '.rz-link-danger{background:none;border:0;color:#c0392b;font:inherit;cursor:pointer;text-decoration:underline;padding:.25rem 0;min-height:36px}'
. '.rz-act-btn{min-height:32px;padding:.2rem .55rem;font-size:.75rem}'
. '.rz-link-danger{background:none;border:0;color:#c0392b;font:inherit;cursor:pointer;text-decoration:underline;padding:.2rem 0;min-height:32px;font-size:.75rem}'
. '</style>';
}

Expand Down
3 changes: 2 additions & 1 deletion plugin/tests/TablesAdminTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public function test_the_board_shows_status_and_quick_actions(): void

$html = $this->admin->render('CSRF123', null, null, null, 'n');

self::assertStringContainsString('rz-status-occupied', $html, 'the card is marked with its status');
self::assertStringContainsString('rz-status-occupied', $html, 'the token is marked with its status');
self::assertStringContainsString('rz-circle', $html, 'tables render as circular tokens (the RAS signature)');
self::assertStringContainsString('action="/admin/restaurant/table-status"', $html, 'quick-action posts to the status action');
// An occupied table offers "Clear" (→ dirty), not "Seat".
self::assertStringContainsString('value="dirty"', $html);
Expand Down
Loading