From ab07326894ff4dfa41bd969728a58e807b56d64c Mon Sep 17 00:00:00 2001 From: DanMat Date: Sat, 5 Sep 2026 21:51:41 -0400 Subject: [PATCH] Slice 8a: RAS design uplift for the staff terminals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retain the feel of the original Restaurant Automation System, uplifted. - Branding::head — a shared "RAS · Restaurant Automation System" eyebrow + title + subtitle on every terminal (Floor, Orders, Kitchen, Reservations, Reports), so they read as one system. Nonce'd styles (admin CSP drops inline style=). - Floor: the signature circular table tokens are back — a grid of status- coloured circles (open green / occupied amber / dirty red / reserved blue, modernized from the harsh originals), each linking to the table with seats + contextual quick actions beneath, plus a colour legend. Replaces the rectangular cards; keeps the same status classes, actions and behaviour. - Shared status palette via a --rs custom property (legend dots + circles from one source). No behaviour change; markup/CSS only. Tests updated to assert the circular tokens; all terminal tests still green. cs-fixer + PHPStan green locally. Co-Authored-By: Claude Opus 4.8 --- plugin/src/Branding.php | 43 +++++++++++++++++++++ plugin/src/KitchenAdmin.php | 2 +- plugin/src/OrdersAdmin.php | 2 +- plugin/src/ReportsAdmin.php | 2 +- plugin/src/ReservationsAdmin.php | 2 +- plugin/src/TablesAdmin.php | 64 ++++++++++++++++++-------------- plugin/tests/TablesAdminTest.php | 3 +- 7 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 plugin/src/Branding.php diff --git a/plugin/src/Branding.php b/plugin/src/Branding.php new file mode 100644 index 0000000..85b134d --- /dev/null +++ b/plugin/src/Branding.php @@ -0,0 +1,43 @@ +` 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 '' + . '
' + . '

RAS · Restaurant Automation System

' + . '

' . self::e($title) . '

' + . '

' . self::e($subtitle) . '

' + . '
'; + } + + private static function e(string $v): string + { + return htmlspecialchars($v, ENT_QUOTES, 'UTF-8'); + } +} diff --git a/plugin/src/KitchenAdmin.php b/plugin/src/KitchenAdmin.php index 104a6a7..043ad03 100644 --- a/plugin/src/KitchenAdmin.php +++ b/plugin/src/KitchenAdmin.php @@ -48,7 +48,7 @@ public function render(string $csrf = '', ?string $notice = null, string $nonce } return $this->styles($nonce) - . '

Kitchen

' + . Branding::head('Kitchen', 'Tickets on the line, oldest first.', $nonce) . $this->notice($notice) . '

Tickets on the line. Start a ticket when you begin it, mark it ready when it is up for the pass.

' . '
' . $cols . '
' diff --git a/plugin/src/OrdersAdmin.php b/plugin/src/OrdersAdmin.php index 853de82..c523e75 100644 --- a/plugin/src/OrdersAdmin.php +++ b/plugin/src/OrdersAdmin.php @@ -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) . '

Orders

' . $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); diff --git a/plugin/src/ReportsAdmin.php b/plugin/src/ReportsAdmin.php index fa88396..711910e 100644 --- a/plugin/src/ReportsAdmin.php +++ b/plugin/src/ReportsAdmin.php @@ -31,7 +31,7 @@ public function render(string $csrf = '', ?string $notice = null, string $nonce $active = $this->reports->activeOrders(); return $this->styles($nonce) - . '

Reports

' + . Branding::head('Reports', 'Revenue and what is selling.', $nonce) . '

How service is going — revenue and what is selling. Figures are from settled (paid) orders.

' . '
' . $this->card('Revenue today', $today['revenue'], $today['orders'] . ' paid') diff --git a/plugin/src/ReservationsAdmin.php b/plugin/src/ReservationsAdmin.php index 9694db3..c28673c 100644 --- a/plugin/src/ReservationsAdmin.php +++ b/plugin/src/ReservationsAdmin.php @@ -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) - . '

Reservations

' + . Branding::head('Reservations', 'The book — upcoming bookings.', $nonce) . $this->notice($notice) . '

Upcoming bookings. Linking a guest connects the booking to their record in the CRM — opening it needs CRM access.

' . $this->form($csrf, $editRes) diff --git a/plugin/src/TablesAdmin.php b/plugin/src/TablesAdmin.php index da21965..ac55f9e 100644 --- a/plugin/src/TablesAdmin.php +++ b/plugin/src/TablesAdmin.php @@ -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) - . '

Floor

' + . Branding::head('Floor', 'The room at a glance — seat, clear and turn tables.', $nonce) . $this->notice($notice) - . '

Your tables and their status. Seat guests, send tables for cleaning, and turn them for the next party.

' . $this->form($csrf, $editTable) + . $this->legend() . $this->filterBar($filter) . $this->board($csrf, $this->tables->all($filter), $filter); } @@ -85,7 +85,21 @@ private function filterBar(?string $active): string return '
' . $chips . '
'; } + /** 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 .= '' . self::e($label) . ''; + } + return '
' . $items . '
'; + } + /** + * 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> $tables */ private function board(string $csrf, array $tables, ?string $filter): string @@ -95,20 +109,18 @@ private function board(string $csrf, array $tables, ?string $filter): string return '

' . $msg . '

'; } - $cards = ''; + $tokens = ''; foreach ($tables as $t) { $status = (string) $t['status']; - $cards .= '
  • ' - . '
    ' - . '' . self::e((string) $t['label']) . '' - . '' . self::e(self::STATUS_LABELS[$status] ?? $status) . '' - . '
    ' - . '
    ' . self::e((string) $t['seats']) . ' seats
    ' - . '
    ' . $this->actions($csrf, (int) $t['id'], $status) . '
    ' + $tokens .= '
  • ' + . '' + . '' . self::e((string) $t['label']) . '' + . '
    ' . self::e((string) $t['seats']) . ' seats · ' . self::e(self::STATUS_LABELS[$status] ?? $status) . '
    ' + . '
    ' . $this->actions($csrf, (int) $t['id'], $status) . '
    ' . '
  • '; } - return '
      ' . $cards . '
    '; + return '
      ' . $tokens . '
    '; } /** The status quick-actions relevant to a table's current state. */ @@ -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}' . ''; } diff --git a/plugin/tests/TablesAdminTest.php b/plugin/tests/TablesAdminTest.php index 9897a33..b63fb61 100644 --- a/plugin/tests/TablesAdminTest.php +++ b/plugin/tests/TablesAdminTest.php @@ -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);