corentin_wakdo/src/app/Views/admin/layout.php
Imugiii eed2daffb0
All checks were successful
CI / secret-scan (push) Successful in 16s
CI / secret-scan (pull_request) Successful in 17s
CI / php-lint (pull_request) Successful in 40s
CI / php-lint (push) Successful in 41s
CI / static-tests (push) Successful in 1m14s
CI / js-tests (push) Successful in 46s
CI / static-tests (pull_request) Successful in 1m15s
CI / js-tests (pull_request) Successful in 42s
feat(back-office): refonte saisie commande comptoir/drive (prix, verrou, nav, file)
7 ameliorations de la page de saisie comptoir/drive, inutilisable au quotidien :
- prix par ligne + total + bouton "Encaisser X,XX EUR" (l'equipier encaissait
  sans voir aucun montant) ;
- verrou drive reel (affichage fige + input hidden ; readonly sur un select est
  sans effet HTML) ;
- lien de nav "Saisie commande" route selon le canal du role (un equipier drive
  atterrissait au comptoir) ;
- champ quantite desactive pour un produit personnalisable (sa saisie etait
  ignoree en silence) ;
- file "En cours" (commandes payees du canal, plus ancienne d'abord) au-dessus
  de l'historique ;
- feedback prix Normal/Maxi dans la liste et le total de ligne ;
- numero de table (dine_in comptoir), groupage par categorie, modale a overlay
  + fermeture Echap + message requis inline.

Serveur autoritatif inchange (les prix cote client sont indicatifs).
availableForCatalogue expose category_name et trie par categorie ; la borne
regroupe deja par categorie (ordre intra-categorie preserve) donc son rendu ne
bouge pas. Tests : JS 104, PHP unit 398, PHPStan L6.
2026-06-24 09:59:18 +00:00

173 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
/**
* Shell du back-office (topbar + sidebar + zone de contenu), reutilise par toutes
* les pages admin rendues serveur. Recoit le contenu de la page et le contexte
* commun injecte par AdminController::adminView().
*
* Chemins d'assets ABSOLUS (/assets/...) : les pages sont servies sous des routes
* /admin/... alors que les fichiers vivent a la racine du docroot du vhost admin ;
* un chemin relatif resoudrait vers /admin/assets/... (404).
*
* @var string $title
* @var string $content
* @var string $currentUserName
* @var string $currentUserRole
* @var list<string> $permissions
* @var string $csrfToken
* @var string $activeNav
* @var string $orderChannel 'counter' | 'drive' : canal de saisie du role courant
* @var string|null $flash
*/
$pageTitle = htmlspecialchars($title ?? 'Wakdo Admin', ENT_QUOTES, 'UTF-8');
$userName = htmlspecialchars($currentUserName ?? 'Utilisateur', ENT_QUOTES, 'UTF-8');
$userRole = htmlspecialchars($currentUserRole ?? '', ENT_QUOTES, 'UTF-8');
$csrf = htmlspecialchars($csrfToken ?? '', ENT_QUOTES, 'UTF-8');
$active = is_string($activeNav ?? null) ? $activeNav : '';
// Canal de saisie du role courant : un equipier drive est route vers /drive/orders,
// les autres roles vers /counter/orders. CounterOrderController pose activeNav a
// 'counter' ou 'drive' selon le chemin ; on marque le lien actif sur l'un OU l'autre.
$channel = ($orderChannel ?? '') === 'drive' ? 'drive' : 'counter';
$orderHref = $channel === 'drive' ? '/drive/orders' : '/counter/orders';
$orderActive = ($active === 'counter' || $active === 'drive') ? 'sidebar-item active' : 'sidebar-item';
/** @var list<string> $perms */
$perms = isset($permissions) && is_array($permissions) ? $permissions : [];
$can = static fn (string $code): bool => in_array($code, $perms, true);
// Initiales pour l'avatar (2 lettres max), derivees du nom affiche. Fonctions
// multibyte (UTF-8) : un prenom a initiale accentuee (frequent en francais) doit
// produire une lettre valide, pas un octet de tete isole qui viderait l'echappement.
$initials = '';
foreach (preg_split('/\s+/', trim((string) ($currentUserName ?? ''))) ?: [] as $word) {
if ($word !== '' && mb_strlen($initials, 'UTF-8') < 2) {
$initials .= mb_strtoupper(mb_substr($word, 0, 1, 'UTF-8'), 'UTF-8');
}
}
$initials = $initials !== '' ? $initials : 'U';
/**
* @param string $code cle de nav active
* @param string $current
*/
$navClass = static function (string $code, string $current): string {
return $code === $current ? 'sidebar-item active' : 'sidebar-item';
};
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title><?= $pageTitle ?></title>
<link rel="stylesheet" href="/assets/css/admin.css">
</head>
<body data-user-email="<?= htmlspecialchars($currentUserEmail ?? '', ENT_QUOTES, 'UTF-8') ?>">
<div class="admin-layout">
<header class="topbar">
<div class="topbar-actions">
<div class="topbar-user">
<button class="topbar-user-btn" id="userMenuBtn" type="button" aria-haspopup="true" aria-expanded="false">
<div class="topbar-user-avatar"><?= htmlspecialchars($initials, ENT_QUOTES, 'UTF-8') ?></div>
<div>
<div class="topbar-user-name"><?= $userName ?></div>
<div class="topbar-user-role"><?= $userRole ?></div>
</div>
</button>
<div class="dropdown-menu" id="userMenu">
<a href="/admin/profile/pin">Mon PIN d'action sensible</a>
<a href="/admin/privacy">Traitement de mes donnees</a>
<div class="divider"></div>
<form method="post" action="/logout">
<input type="hidden" name="_csrf" value="<?= $csrf ?>">
<button class="danger" type="submit">Se deconnecter</button>
</form>
</div>
</div>
</div>
</header>
<nav class="sidebar">
<div class="sidebar-brand">
<img class="sidebar-brand-logo" src="/assets/images/logo.png" alt="Wakdo">
<span class="sidebar-brand-name">Wak<span>do</span></span>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Vue d'ensemble</div>
<a href="/admin/dashboard" class="<?= $navClass('dashboard', $active) ?>">Tableau de bord</a>
</div>
<?php if ($can('category.manage') || $can('product.read') || $can('menu.read')): ?>
<div class="sidebar-section">
<div class="sidebar-section-label">Catalogue</div>
<?php if ($can('category.manage')): ?>
<a href="/admin/categories" class="<?= $navClass('categories', $active) ?>">Categories</a>
<?php endif; ?>
<?php if ($can('product.read')): ?>
<a href="/admin/products" class="<?= $navClass('products', $active) ?>">Produits</a>
<?php endif; ?>
<?php if ($can('menu.read')): ?>
<a href="/admin/menus" class="<?= $navClass('menus', $active) ?>">Menus</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($can('stock.read')): ?>
<div class="sidebar-section">
<div class="sidebar-section-label">Stock</div>
<a href="/admin/ingredients" class="<?= $navClass('stock', $active) ?>">Ingredients</a>
</div>
<?php endif; ?>
<?php if ($can('stats.read') || $can('order.read') || $can('order.create')): ?>
<div class="sidebar-section">
<div class="sidebar-section-label">Pilotage</div>
<?php if ($can('order.create')): ?>
<?php /* Le canal (counter/drive) vient de role.order_source : un equipier
drive est route vers /drive/orders, les autres vers /counter/orders. */ ?>
<a href="<?= htmlspecialchars($orderHref, ENT_QUOTES, 'UTF-8') ?>" class="<?= $orderActive ?>">Saisie commande</a>
<?php endif; ?>
<?php if ($can('stats.read')): ?>
<a href="/admin/stats" class="<?= $navClass('stats', $active) ?>">Statistiques</a>
<?php endif; ?>
<?php if ($can('order.read')): ?>
<a href="/admin/orders" class="<?= $navClass('orders', $active) ?>">Commandes</a>
<a href="/kitchen/display" class="<?= $navClass('kitchen', $active) ?>">Cuisine (KDS)</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($can('user.read') || $can('role.manage')): ?>
<div class="sidebar-section">
<div class="sidebar-section-label">Administration</div>
<?php if ($can('user.read')): ?>
<a href="/admin/users" class="<?= $navClass('users', $active) ?>">Utilisateurs</a>
<?php endif; ?>
<?php if ($can('role.manage')): ?>
<a href="/admin/roles" class="<?= $navClass('roles', $active) ?>">Roles</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php /*
Items de nav volontairement absents tant que leur page n'existe pas
(un lien vers une route non enregistree renvoie un 404).
*/ ?>
</nav>
<main class="content">
<?php $flashMessage = isset($flash) && is_string($flash) ? $flash : null; ?>
<?php if ($flashMessage !== null && $flashMessage !== ''): ?>
<div class="flash" role="status"><?= htmlspecialchars($flashMessage, ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?= $content ?? '' ?>
</main>
</div>
<script src="/assets/js/admin.js"></script>
<script src="/assets/js/pin-modal.js"></script>
</body>
</html>