Compare commits

...

24 commits

Author SHA1 Message Date
Imugiii
be53b7e5e0 docs(ci): add security-by-design checklist to PR template
All checks were successful
CI / secret-scan (pull_request) Successful in 10s
CI / php-lint (pull_request) Successful in 17s
CI / static-tests (pull_request) Successful in 4s
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 18s
CI / static-tests (push) Successful in 5s
2026-06-15 10:12:23 +00:00
Imugiii
d555988b81 Merge remote-tracking branch 'origin/dev' into feat/p1-conception
# Conflicts:
#	.forgejo/workflows/ci.yml
2026-06-15 10:11:37 +00:00
822fdc1bc4 ci: add Forgejo Actions pipeline + gitleaks secret-scan (#2)
All checks were successful
CI / secret-scan (push) Successful in 7s
CI / php-lint (push) Successful in 15s
CI / static-tests (push) Successful in 6s
2026-06-15 12:01:31 +02:00
Imugiii
971ce0c7d0 chore: add Forgejo PR template (BYAN conventions) 2026-06-04 15:31:38 +00:00
Imugiii
a3eae01906
Merge pull request #6 from AcadeNice/feat/p3-admin-shell
feat(admin): admin back-office visual shell (P3 scaffold)
2026-06-04 17:28:31 +02:00
Imugiii
803b840536
Merge pull request #4 from AcadeNice/feat/p5-front-landing
feat(front): P5 kiosk complete flow with vanilla JS and JSON fallback
2026-06-04 17:28:13 +02:00
Imugiii
a9938b6e5c
Merge pull request #5 from AcadeNice/feat/p1-conception
docs(merise): MCD diagrams in drawio XML (4 files)
2026-06-04 17:27:52 +02:00
Imugiii
17b792acfa feat(admin): vanilla JS for dropdowns, sortable tables, inline search 2026-05-09 09:27:51 +00:00
Imugiii
199d926903 feat(admin): orders, kitchen view, users pages with realistic placeholders 2026-05-09 09:27:48 +00:00
Imugiii
447cc598f6 feat(admin): catalogue page with tabs categories/products/menus 2026-05-09 09:27:45 +00:00
Imugiii
b9a5414c37 feat(admin): dashboard with KPI cards and recent orders table 2026-05-09 09:27:42 +00:00
Imugiii
0b028e534b feat(admin): admin design system CSS (palette, typography, components) 2026-05-09 09:27:40 +00:00
Imugiii
6f07238569 feat(admin): scaffold login screen and admin layout shell (sidebar, topbar) 2026-05-09 09:27:38 +00:00
Imugiii
c9fafd1c78 feat(front): render menu composition breakdown in cart lines with supplement total 2026-05-09 09:18:56 +00:00
Imugiii
e64adb60d3 feat(front): add menu composer multi-step logic and burger pre-selection 2026-05-09 09:18:49 +00:00
Imugiii
ad0b59a668 feat(front): extend cart state for menu composition with size supplements 2026-05-09 09:18:47 +00:00
Imugiii
6db68da0f9 feat(front): add menu composer modal HTML structure and CSS 2026-05-09 09:18:42 +00:00
Imugiii
6a7e772646 feat(front): extend CSS design system for P5 new screens
Adds components (sections 7-13 in style.css):
- Shared: .btn, .mode-badge, .cart-badge, .site-header__cart
- products page: .products-grid, .product-card (3-col grid)
- product detail: .product-detail, skeleton animation, composition block
- cart: .cart-line, .qty-btn, .cart-summary
- payment: .payment-choice with inline SVG icons
- confirmation: .confirmation-banner with check animation
All new components reuse existing design tokens; no new palette entries.
2026-05-09 07:59:50 +00:00
Imugiii
0d83512a4f feat(front): payment selection and order confirmation pages
payment.html     - card / cash choice with inline SVG icons; both simulate payment (MVP)
confirmation.html - order number WK-<base36 timestamp>, cart cleared on load,
                    new-order button resets flow to index.html
2026-05-09 07:59:45 +00:00
Imugiii
c517b16569 feat(front): cart page with quantity controls and TVA breakdown
Displays line items with - / + controls and delete button.
TVA 10% (restauration FR 2024, simplified).
TODO in P3: verify rate with accountant (sur-place vs a-emporter + product type).
Abandon button clears cart and returns to categories.
2026-05-09 07:59:40 +00:00
Imugiii
cd6e05c353 feat(front): products list and product detail pages
products.html - dynamic grid from ?category=<id>, JS fetch from data/produits.json
product.html  - detail view; menus show fixed composition note (MVP: no selection)
Both pages: cart badge, mode badge, keyboard/RGAA accessible cards
2026-05-09 07:59:35 +00:00
Imugiii
43b6e7a309 feat(front): vanilla JS state management, data loader, and nav helpers
state.js  - cart (localStorage) + mode + price formatting in centimes
data.js   - fetch wrapper over static JSON with in-memory cache; P4 swap points marked
nav.js    - mode badge injection and cart count badge across pages
2026-05-09 07:59:31 +00:00
Imugiii
6f5daca679 feat(front): copy school JSON sources to public/data for static fetch fallback
Normalizes produits.json:
- Prix converted from float EUR to integer centimes
- Image paths rewritten to match actual filesystem (lowercase, dashes)
- Added type field ('produit'|'menu') on each entry
- Added slug field to categories.json

In P4, swap fetch URLs in assets/js/data.js (marked with TODO comments).
2026-05-09 07:59:26 +00:00
Imugiii
71c863d2b2 feat(front): borne welcome screen and category list scaffold using school assets
- Welcome screen (index.html): background photo, white card, Sur Place / A Emporter
  choice buttons with verified school illustrations; pure HTML <a> navigation, no JS
- Category grid (categories.html): 9 categories from categories.json rendered as 3-col
  card grid with verified category images; stub links to products.html?category=<id>
- Design system CSS (assets/css/style.css): CSS custom properties for brand yellow
  #FFC72C, spacing scale, border-radius, shadows extracted from maquette PDF;
  BEM-style component classes; WCAG AA focus-visible rings; kiosk portrait 1080px primary
2026-05-09 07:12:55 +00:00
28 changed files with 6950 additions and 10 deletions

View file

@ -0,0 +1,44 @@
<!--
Modele de PR Wakdo (Forgejo). Conventions BYAN : Merise Agile + TDD + 64 mantras.
Remplis les sections, coche ce qui s'applique, supprime ce qui ne sert pas.
-->
## Description
<!-- Quoi et pourquoi. Lier la decision / le journal / l'issue si pertinent. -->
## Type
- [ ] feat
- [ ] fix
- [ ] docs
- [ ] refactor
- [ ] test
- [ ] chore
## Checklist conventions BYAN
- [ ] Pas d'emoji dans le code, les commits ou les specs (mantra IA-23)
- [ ] Commits au format `type: description` en anglais, sans trailer `Co-Authored-By`
- [ ] Claims techniques sources si applicable (protocole fact-check)
- [ ] Docs Merise / dictionnaire a jour si le modele de donnees change
- [ ] Tests ajoutes et passants si du code est touche (unit > integration > e2e)
## Checklist securite (security-by-design)
<!-- Cocher ce qui s'applique ; voir SECURITY.md et PROJECT_CONTEXT section 19. -->
- [ ] Aucun secret commite (CI gitleaks verte) ; `.env` reste gitignore
- [ ] Entrees utilisateur validees ; requetes SQL en prepared statements (anti-injection)
- [ ] Mots de passe / PIN en argon2id ; pas de donnee sensible en clair ni dans les logs
- [ ] Sorties HTML echappees (anti-XSS) ; CSRF gere sur les formulaires d'etat
- [ ] Permissions RBAC verifiees cote serveur pour toute action sensible
- [ ] Impact RGPD evalue si nouvelles donnees personnelles (retention, droit a l'effacement)
## Bloc RNCP impacte
<!-- ex : Bloc 2 Cr 3.b (modelisation), Bloc 1 (accessibilite), Bloc 5 (infra/CI)... -->
## Base de la PR
- [ ] La base de cette PR est `dev` (et non `main`)

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

View file

@ -0,0 +1,232 @@
/* Wakdo Admin Vanilla JS
* No framework dependency. Handles:
* - User dropdown toggle (topbar)
* - Action menu (kebab) open/close
* - Sortable table columns (client-side)
* - Inline table search
* - Tab switching (catalogue)
* - Clock display (cuisine)
*/
(function () {
'use strict';
/* ---- Utility ---- */
function qs(selector, root) {
return (root || document).querySelector(selector);
}
function qsa(selector, root) {
return Array.from((root || document).querySelectorAll(selector));
}
/* ---- User dropdown (topbar) ---- */
function initUserMenu() {
var btn = qs('#userMenuBtn');
var menu = qs('#userMenu');
if (!btn || !menu) return;
btn.addEventListener('click', function (e) {
e.stopPropagation();
var isOpen = menu.classList.contains('open');
closeAllDropdowns();
if (!isOpen) {
menu.classList.add('open');
btn.setAttribute('aria-expanded', 'true');
}
});
}
/* ---- Action menus (kebab per table row) ---- */
function initActionMenus() {
qsa('.action-menu-btn').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var dropdown = btn.nextElementSibling;
if (!dropdown) return;
var isOpen = dropdown.classList.contains('open');
closeAllDropdowns();
if (!isOpen) {
dropdown.classList.add('open');
btn.classList.add('open');
}
});
});
}
function closeAllDropdowns() {
qsa('.dropdown-menu.open, .action-menu-dropdown.open').forEach(function (el) {
el.classList.remove('open');
});
qsa('.action-menu-btn.open').forEach(function (el) {
el.classList.remove('open');
});
var userBtn = qs('#userMenuBtn');
if (userBtn) userBtn.setAttribute('aria-expanded', 'false');
}
document.addEventListener('click', closeAllDropdowns);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeAllDropdowns();
});
/* ---- Sortable tables ---- */
function initSortableTables() {
qsa('table').forEach(function (table) {
var headers = qsa('th.sortable', table);
if (!headers.length) return;
headers.forEach(function (th) {
th.addEventListener('click', function () {
var colIndex = parseInt(th.getAttribute('data-col'), 10);
var currentDir = th.getAttribute('data-dir') || 'none';
var newDir = currentDir === 'asc' ? 'desc' : 'asc';
/* reset other headers */
headers.forEach(function (h) {
h.removeAttribute('data-dir');
h.classList.remove('sort-asc', 'sort-desc');
});
th.setAttribute('data-dir', newDir);
th.classList.add('sort-' + newDir);
sortTableByCol(table, colIndex, newDir);
});
});
});
}
function sortTableByCol(table, colIndex, dir) {
var tbody = table.querySelector('tbody');
if (!tbody) return;
var rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort(function (a, b) {
var cellA = getCellText(a, colIndex);
var cellB = getCellText(b, colIndex);
/* detect numeric (strip currency, spaces) */
var numA = parseFloat(cellA.replace(/[^0-9,.-]/g, '').replace(',', '.'));
var numB = parseFloat(cellB.replace(/[^0-9,.-]/g, '').replace(',', '.'));
var cmp;
if (!isNaN(numA) && !isNaN(numB)) {
cmp = numA - numB;
} else {
cmp = cellA.localeCompare(cellB, 'fr');
}
return dir === 'asc' ? cmp : -cmp;
});
rows.forEach(function (row) {
tbody.appendChild(row);
});
}
function getCellText(row, index) {
var cell = row.cells[index];
if (!cell) return '';
return cell.textContent.trim();
}
/* ---- Inline table search ---- */
function initTableSearch() {
var searchInputs = [
{ inputId: 'orderSearch', tableId: 'ordersTable' },
{ inputId: 'productSearch', tableId: 'productTable' },
{ inputId: 'cmdSearch', tableId: 'cmdTable' },
{ inputId: 'userSearch', tableId: 'userTable' }
];
searchInputs.forEach(function (pair) {
var input = qs('#' + pair.inputId);
var table = qs('#' + pair.tableId);
if (!input || !table) return;
input.addEventListener('input', function () {
var term = input.value.trim().toLowerCase();
var rows = qsa('tbody tr', table);
rows.forEach(function (row) {
var text = row.textContent.toLowerCase();
row.style.display = term === '' || text.includes(term) ? '' : 'none';
});
});
});
}
/* ---- Tabs (catalogue) ---- */
function initTabs() {
var tabDefs = [
{ btnId: 'tabCategories', panelId: 'panelCategories' },
{ btnId: 'tabProduits', panelId: 'panelProduits' },
{ btnId: 'tabMenus', panelId: 'panelMenus' }
];
var btns = tabDefs.map(function (d) { return qs('#' + d.btnId); }).filter(Boolean);
var panels = tabDefs.map(function (d) { return qs('#' + d.panelId); }).filter(Boolean);
if (!btns.length) return;
btns.forEach(function (btn, i) {
btn.addEventListener('click', function () {
btns.forEach(function (b) { b.classList.remove('active'); });
panels.forEach(function (p) { p.classList.remove('active'); });
btn.classList.add('active');
if (panels[i]) panels[i].classList.add('active');
});
});
}
/* ---- Kitchen clock ---- */
function initKitchenClock() {
var clockEl = qs('#kitchenTime');
if (!clockEl) return;
function tick() {
var now = new Date();
var h = String(now.getHours()).padStart(2, '0');
var m = String(now.getMinutes()).padStart(2, '0');
var s = String(now.getSeconds()).padStart(2, '0');
clockEl.textContent = h + ':' + m + ':' + s;
}
tick();
setInterval(tick, 1000);
}
/* ---- Refresh button (visual feedback only — no real fetch) ---- */
function initRefreshButtons() {
qsa('#refreshBtn, #kitchenRefresh').forEach(function (btn) {
btn.addEventListener('click', function () {
var svg = btn.querySelector('svg');
if (svg) {
svg.style.transition = 'transform 0.6s';
svg.style.transform = 'rotate(360deg)';
setTimeout(function () {
svg.style.transition = '';
svg.style.transform = '';
}, 650);
}
});
});
}
/* ---- Bootstrap ---- */
function init() {
initUserMenu();
initActionMenus();
initSortableTables();
initTableSearch();
initTabs();
initKitchenClock();
initRefreshButtons();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
}());

View file

@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catalogue — Wakdo Admin</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<div class="admin-layout">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-logo">
<img src="assets/images/logo.png" alt="Wakdo">
<div>
<span class="topbar-logo-text">Wakdo</span>
<span class="topbar-logo-sub">Administration</span>
</div>
</div>
<div class="topbar-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="Rechercher un produit, une categorie...">
</div>
<div class="topbar-actions">
<div class="topbar-user">
<button class="topbar-user-btn" id="userMenuBtn" type="button">
<div class="topbar-user-avatar">CJ</div>
<div>
<div class="topbar-user-name">Corentin J.</div>
<div class="topbar-user-role">Administrateur</div>
</div>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="dropdown-menu" id="userMenu">
<a href="#">Mon profil</a>
<a href="#">Parametres</a>
<div class="divider"></div>
<button class="danger" type="button">Se deconnecter</button>
</div>
</div>
</div>
</header>
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-section">
<div class="sidebar-section-label">Vue d'ensemble</div>
<a href="dashboard.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect></svg>
Tableau de bord
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Catalogue</div>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub active">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
Categories
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
Produits
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect x="9" y="3" width="6" height="4" rx="2"></rect></svg>
Menus
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Operations</div>
<a href="commandes.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
Commandes
</a>
<a href="cuisine.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>
Cuisine
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Administration</div>
<a href="users.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
Utilisateurs
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Roles
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"></path></svg>
Parametres
</a>
</div>
</nav>
<!-- Content -->
<main class="content">
<div class="page-header">
<div>
<h1 class="page-title">Catalogue</h1>
<p class="page-subtitle">Categories, produits et menus disponibles sur la borne</p>
</div>
<div class="page-actions">
<button class="btn btn-secondary" type="button">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
Importer
</button>
<button class="btn btn-primary" type="button">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Nouveau produit
</button>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn" id="tabCategories" type="button">Categories (9)</button>
<button class="tab-btn active" id="tabProduits" type="button">Produits (53)</button>
<button class="tab-btn" id="tabMenus" type="button">Menus (13)</button>
</div>
<!-- Panel: Produits (default) -->
<div id="panelProduits" class="tab-panel active">
<div class="toolbar">
<div class="toolbar-left">
<div class="search-field">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="Rechercher un produit..." id="productSearch">
</div>
<select class="filter-select" id="categoryFilter">
<option value="">Toutes les categories</option>
<option value="burgers">Burgers</option>
<option value="wraps">Wraps</option>
<option value="salades">Salades</option>
<option value="frites">Frites</option>
<option value="boissons">Boissons</option>
<option value="sauces">Sauces</option>
<option value="desserts">Desserts</option>
<option value="encas">Encas</option>
</select>
<select class="filter-select">
<option value="">Tous les statuts</option>
<option value="active">Disponible</option>
<option value="inactive">Indisponible</option>
</select>
</div>
</div>
<div class="table-container">
<div class="table-wrapper">
<table id="productTable">
<thead>
<tr>
<th style="width:52px;">Image</th>
<th class="sortable" data-col="1">Libelle <span class="sort-icon">&#8597;</span></th>
<th class="sortable" data-col="2">Categorie <span class="sort-icon">&#8597;</span></th>
<th class="sortable" data-col="3" style="text-align:right;">Prix <span class="sort-icon">&#8597;</span></th>
<th>Stock</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">Big Mac</td>
<td class="muted">Burgers</td>
<td class="text-right fw-600">6,00 &euro;</td>
<td><span class="pill pill-success">Disponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">Royal Bacon</td>
<td class="muted">Burgers</td>
<td class="text-right fw-600">5,10 &euro;</td>
<td><span class="pill pill-success">Disponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">CBO</td>
<td class="muted">Burgers</td>
<td class="text-right fw-600">8,90 &euro;</td>
<td><span class="pill pill-success">Disponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">MC Crispy</td>
<td class="muted">Burgers</td>
<td class="text-right fw-600">5,30 &euro;</td>
<td><span class="pill pill-danger">Indisponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Activer</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">Coca Cola</td>
<td class="muted">Boissons</td>
<td class="text-right fw-600">1,90 &euro;</td>
<td><span class="pill pill-success">Disponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">Moyenne Frite</td>
<td class="muted">Frites</td>
<td class="text-right fw-600">2,75 &euro;</td>
<td><span class="pill pill-success">Disponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">Ketchup</td>
<td class="muted">Sauces</td>
<td class="text-right fw-600">0,70 &euro;</td>
<td><span class="pill pill-success">Disponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
<tr>
<td><div class="thumb-placeholder"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div></td>
<td class="fw-600">Nuggets x4</td>
<td class="muted">Encas</td>
<td class="text-right fw-600">4,20 &euro;</td>
<td><span class="pill pill-success">Disponible</span></td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button><div class="divider"></div><button class="danger" type="button">Supprimer</button></div></div></td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="pagination-info">Affichage de 8 sur 53 produits</span>
<div class="pagination-controls">
<button class="pagination-btn" disabled type="button">&laquo;</button>
<button class="pagination-btn active" type="button">1</button>
<button class="pagination-btn" type="button">2</button>
<button class="pagination-btn" type="button">3</button>
<button class="pagination-btn" type="button">4</button>
<button class="pagination-btn" type="button">&raquo;</button>
</div>
</div>
</div>
</div>
<!-- Panel: Categories -->
<div id="panelCategories" class="tab-panel">
<div class="table-container">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Libelle</th>
<th>Produits</th>
<th>Ordre affichage</th>
<th>Statut</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody>
<tr><td class="fw-600">Menus</td><td class="muted">13</td><td class="muted">1</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Burgers</td><td class="muted">13</td><td class="muted">2</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Wraps</td><td class="muted">4</td><td class="muted">3</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Salades</td><td class="muted">3</td><td class="muted">4</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Frites</td><td class="muted">5</td><td class="muted">5</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Boissons</td><td class="muted">8</td><td class="muted">6</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Desserts</td><td class="muted">9</td><td class="muted">7</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Encas</td><td class="muted">4</td><td class="muted">8</td><td><span class="pill pill-success">Visible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Masquer</button></div></div></td></tr>
<tr><td class="fw-600">Sauces</td><td class="muted">7</td><td class="muted">9</td><td><span class="pill pill-neutral">Masquee</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Afficher</button></div></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Panel: Menus -->
<div id="panelMenus" class="tab-panel">
<div class="table-container">
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Libelle</th>
<th style="text-align:right;">Prix</th>
<th>Contenu</th>
<th>Statut</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody>
<tr><td class="fw-600">Menu Le 280</td><td class="text-right fw-600">8,80 &euro;</td><td class="muted text-sm">Burger + Frites + Boisson + Sauce</td><td><span class="pill pill-success">Disponible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button></div></div></td></tr>
<tr><td class="fw-600">Menu Big Tasty</td><td class="text-right fw-600">10,60 &euro;</td><td class="muted text-sm">Burger + Frites + Boisson + Sauce</td><td><span class="pill pill-success">Disponible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button></div></div></td></tr>
<tr><td class="fw-600">Menu Big Mac</td><td class="text-right fw-600">8,00 &euro;</td><td class="muted text-sm">Burger + Frites + Boisson + Sauce</td><td><span class="pill pill-success">Disponible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button></div></div></td></tr>
<tr><td class="fw-600">Menu CBO</td><td class="text-right fw-600">10,90 &euro;</td><td class="muted text-sm">Burger + Frites + Boisson + Sauce</td><td><span class="pill pill-success">Disponible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button></div></div></td></tr>
<tr><td class="fw-600">Menu Royal Cheese</td><td class="text-right fw-600">6,40 &euro;</td><td class="muted text-sm">Burger + Frites + Boisson + Sauce</td><td><span class="pill pill-success">Disponible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Desactiver</button></div></div></td></tr>
<tr><td class="fw-600">Menu Royal Bacon</td><td class="text-right fw-600">7,05 &euro;</td><td class="muted text-sm">Burger + Frites + Boisson + Sauce</td><td><span class="pill pill-danger">Indisponible</span></td><td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Modifier</a><button type="button">Activer</button></div></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
<script src="assets/js/admin.js"></script>
</body>
</html>

View file

@ -0,0 +1,254 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Commandes — Wakdo Admin</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<div class="admin-layout">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-logo">
<img src="assets/images/logo.png" alt="Wakdo">
<div>
<span class="topbar-logo-text">Wakdo</span>
<span class="topbar-logo-sub">Administration</span>
</div>
</div>
<div class="topbar-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="N° commande, statut...">
</div>
<div class="topbar-actions">
<div class="topbar-user">
<button class="topbar-user-btn" id="userMenuBtn" type="button">
<div class="topbar-user-avatar">CJ</div>
<div>
<div class="topbar-user-name">Corentin J.</div>
<div class="topbar-user-role">Administrateur</div>
</div>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="dropdown-menu" id="userMenu">
<a href="#">Mon profil</a>
<a href="#">Parametres</a>
<div class="divider"></div>
<button class="danger" type="button">Se deconnecter</button>
</div>
</div>
</div>
</header>
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-section">
<div class="sidebar-section-label">Vue d'ensemble</div>
<a href="dashboard.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect></svg>
Tableau de bord
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Catalogue</div>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
Categories
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
Produits
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect x="9" y="3" width="6" height="4" rx="2"></rect></svg>
Menus
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Operations</div>
<a href="commandes.html" class="sidebar-item active">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
Commandes
</a>
<a href="cuisine.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>
Cuisine
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Administration</div>
<a href="users.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
Utilisateurs
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Roles
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"></path></svg>
Parametres
</a>
</div>
</nav>
<!-- Content -->
<main class="content">
<div class="page-header">
<div>
<h1 class="page-title">Commandes</h1>
<p class="page-subtitle">Historique et suivi de toutes les commandes</p>
</div>
</div>
<!-- Filters -->
<div class="toolbar">
<div class="toolbar-left">
<div class="search-field">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="N° commande..." id="cmdSearch">
</div>
<select class="filter-select" id="statusFilter">
<option value="">Tous les statuts</option>
<option value="pending">En attente</option>
<option value="paid">Payee</option>
<option value="preparing">En preparation</option>
<option value="ready">Prete</option>
<option value="delivered">Livree</option>
<option value="cancelled">Annulee</option>
</select>
<select class="filter-select">
<option value="">Tous les modes</option>
<option value="kiosk">Borne</option>
<option value="counter">Comptoir</option>
<option value="drive">Drive</option>
</select>
<select class="filter-select">
<option value="today">Aujourd'hui</option>
<option value="7d">7 derniers jours</option>
<option value="30d">30 derniers jours</option>
<option value="custom">Personnalise</option>
</select>
</div>
</div>
<div class="table-container">
<div class="table-wrapper">
<table id="cmdTable">
<thead>
<tr>
<th class="sortable" data-col="0"><span class="sort-icon">&#8597;</span></th>
<th class="sortable" data-col="1">Date / Heure <span class="sort-icon">&#8597;</span></th>
<th>Mode</th>
<th>Source</th>
<th>Statut</th>
<th style="text-align:center;">Lignes</th>
<th class="sortable" data-col="6" style="text-align:right;">Total <span class="sort-icon">&#8597;</span></th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono fw-600">#1087</td>
<td class="muted">09/05/2026 13:42</td>
<td><span class="pill pill-info">Sur place</span></td>
<td class="muted text-sm">Borne</td>
<td><span class="pill pill-success">Livree</span></td>
<td style="text-align:center;" class="muted">3</td>
<td class="text-right fw-600">18,70 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a><div class="divider"></div><button class="danger" type="button">Annuler</button></div></div></td>
</tr>
<tr>
<td class="mono fw-600">#1086</td>
<td class="muted">09/05/2026 13:38</td>
<td><span class="pill pill-neutral">A emporter</span></td>
<td class="muted text-sm">Comptoir</td>
<td><span class="pill pill-warning">En preparation</span></td>
<td style="text-align:center;" class="muted">5</td>
<td class="text-right fw-600">24,30 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a><div class="divider"></div><button class="danger" type="button">Annuler</button></div></div></td>
</tr>
<tr>
<td class="mono fw-600">#1085</td>
<td class="muted">09/05/2026 13:31</td>
<td><span class="pill pill-info">Sur place</span></td>
<td class="muted text-sm">Borne</td>
<td><span class="pill pill-success">Livree</span></td>
<td style="text-align:center;" class="muted">2</td>
<td class="text-right fw-600">11,40 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a><div class="divider"></div><button class="danger" type="button">Annuler</button></div></div></td>
</tr>
<tr>
<td class="mono fw-600">#1084</td>
<td class="muted">09/05/2026 13:27</td>
<td><span class="pill pill-neutral">A emporter</span></td>
<td class="muted text-sm">Drive</td>
<td><span class="pill pill-success">Livree</span></td>
<td style="text-align:center;" class="muted">2</td>
<td class="text-right fw-600">8,80 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a><div class="divider"></div><button class="danger" type="button">Annuler</button></div></div></td>
</tr>
<tr>
<td class="mono fw-600">#1083</td>
<td class="muted">09/05/2026 13:19</td>
<td><span class="pill pill-info">Sur place</span></td>
<td class="muted text-sm">Borne</td>
<td><span class="pill pill-neutral">Annulee</span></td>
<td style="text-align:center;" class="muted">1</td>
<td class="text-right muted">6,40 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a></div></div></td>
</tr>
<tr>
<td class="mono fw-600">#1082</td>
<td class="muted">09/05/2026 13:14</td>
<td><span class="pill pill-info">Sur place</span></td>
<td class="muted text-sm">Borne</td>
<td><span class="pill pill-success">Livree</span></td>
<td style="text-align:center;" class="muted">7</td>
<td class="text-right fw-600">32,10 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a><div class="divider"></div><button class="danger" type="button">Annuler</button></div></div></td>
</tr>
<tr>
<td class="mono fw-600">#1081</td>
<td class="muted">09/05/2026 13:08</td>
<td><span class="pill pill-neutral">A emporter</span></td>
<td class="muted text-sm">Drive</td>
<td><span class="pill pill-success">Livree</span></td>
<td style="text-align:center;" class="muted">2</td>
<td class="text-right fw-600">10,90 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a><div class="divider"></div><button class="danger" type="button">Annuler</button></div></div></td>
</tr>
<tr>
<td class="mono fw-600">#1080</td>
<td class="muted">09/05/2026 12:58</td>
<td><span class="pill pill-info">Sur place</span></td>
<td class="muted text-sm">Comptoir</td>
<td><span class="pill pill-success">Livree</span></td>
<td style="text-align:center;" class="muted">4</td>
<td class="text-right fw-600">15,60 &euro;</td>
<td><div class="action-menu"><button class="action-menu-btn" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg></button><div class="action-menu-dropdown"><a href="#">Voir detail</a><div class="divider"></div><button class="danger" type="button">Annuler</button></div></div></td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="pagination-info">Affichage de 8 sur 231 commandes</span>
<div class="pagination-controls">
<button class="pagination-btn" disabled type="button">&laquo;</button>
<button class="pagination-btn active" type="button">1</button>
<button class="pagination-btn" type="button">2</button>
<button class="pagination-btn" type="button">3</button>
<span style="padding:0 4px;color:var(--color-text-muted);font-size:12px;">...</span>
<button class="pagination-btn" type="button">29</button>
<button class="pagination-btn" type="button">&raquo;</button>
</div>
</div>
</div>
</main>
</div>
<script src="assets/js/admin.js"></script>
</body>
</html>

View file

@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cuisine — Wakdo Admin</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<div class="admin-layout">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-logo">
<img src="assets/images/logo.png" alt="Wakdo">
<div>
<span class="topbar-logo-text">Wakdo</span>
<span class="topbar-logo-sub">Administration</span>
</div>
</div>
<div class="topbar-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="Rechercher une commande...">
</div>
<div class="topbar-actions">
<div class="topbar-user">
<button class="topbar-user-btn" id="userMenuBtn" type="button">
<div class="topbar-user-avatar">CJ</div>
<div>
<div class="topbar-user-name">Corentin J.</div>
<div class="topbar-user-role">Administrateur</div>
</div>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="dropdown-menu" id="userMenu">
<a href="#">Mon profil</a>
<a href="#">Parametres</a>
<div class="divider"></div>
<button class="danger" type="button">Se deconnecter</button>
</div>
</div>
</div>
</header>
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-section">
<div class="sidebar-section-label">Vue d'ensemble</div>
<a href="dashboard.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect></svg>
Tableau de bord
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Catalogue</div>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
Categories
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
Produits
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect x="9" y="3" width="6" height="4" rx="2"></rect></svg>
Menus
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Operations</div>
<a href="commandes.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
Commandes
</a>
<a href="cuisine.html" class="sidebar-item active">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>
Cuisine
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Administration</div>
<a href="users.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
Utilisateurs
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Roles
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"></path></svg>
Parametres
</a>
</div>
</nav>
<!-- Content -->
<main class="content">
<div class="page-header">
<div>
<h1 class="page-title">Cuisine</h1>
<p class="page-subtitle">Commandes en attente de preparation — tries par heure croissante</p>
</div>
<div class="page-actions">
<span class="text-sm text-muted" id="kitchenTime">13:42:17</span>
<button class="btn btn-secondary" type="button" id="kitchenRefresh">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
Actualiser
</button>
</div>
</div>
<!-- Kitchen cards grid -->
<div class="kitchen-grid" id="kitchenGrid">
<div class="kitchen-card">
<div class="kitchen-card-header">
<div>
<div class="kitchen-order-num">#1086</div>
<div class="kitchen-order-time">13:38 &mdash; 4 min</div>
</div>
<span class="pill pill-neutral">A emporter</span>
</div>
<div class="kitchen-card-body">
<div class="kitchen-line">
<span><span class="kitchen-qty">x2</span>Menu Big Mac</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Grande Frite</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x2</span>Coca Cola</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Nuggets x4</span>
</div>
</div>
<div class="kitchen-card-footer">
<span class="text-sm text-muted">24,30 &euro;</span>
<button class="btn btn-primary btn-sm" type="button">Marquer prete</button>
</div>
</div>
<div class="kitchen-card">
<div class="kitchen-card-header">
<div>
<div class="kitchen-order-num">#1088</div>
<div class="kitchen-order-time">13:44 &mdash; 2 min</div>
</div>
<span class="pill pill-info">Sur place</span>
</div>
<div class="kitchen-card-body">
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Menu CBO</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Fanta Orange</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Classic Barbecue</span>
</div>
</div>
<div class="kitchen-card-footer">
<span class="text-sm text-muted">13,50 &euro;</span>
<button class="btn btn-primary btn-sm" type="button">Marquer prete</button>
</div>
</div>
<div class="kitchen-card">
<div class="kitchen-card-header">
<div>
<div class="kitchen-order-num">#1089</div>
<div class="kitchen-order-time">13:45 &mdash; 1 min</div>
</div>
<span class="pill pill-neutral">A emporter</span>
</div>
<div class="kitchen-card-body">
<div class="kitchen-line">
<span><span class="kitchen-qty">x3</span>Menu Royal Cheese</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Petite Salade</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x3</span>Eau</span>
</div>
</div>
<div class="kitchen-card-footer">
<span class="text-sm text-muted">25,50 &euro;</span>
<button class="btn btn-primary btn-sm" type="button">Marquer prete</button>
</div>
</div>
<div class="kitchen-card">
<div class="kitchen-card-header">
<div>
<div class="kitchen-order-num">#1090</div>
<div class="kitchen-order-time">13:46 &mdash; maintenant</div>
</div>
<span class="pill pill-info">Sur place</span>
</div>
<div class="kitchen-card-body">
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Big Tasty Bacon</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Grande Frite</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x1</span>Ice Tea Peche</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x2</span>Ketchup</span>
</div>
</div>
<div class="kitchen-card-footer">
<span class="text-sm text-muted">12,60 &euro;</span>
<button class="btn btn-primary btn-sm" type="button">Marquer prete</button>
</div>
</div>
<div class="kitchen-card">
<div class="kitchen-card-header">
<div>
<div class="kitchen-order-num">#1091</div>
<div class="kitchen-order-time">13:46 &mdash; maintenant</div>
</div>
<span class="pill pill-neutral">A emporter</span>
</div>
<div class="kitchen-card-body">
<div class="kitchen-line">
<span><span class="kitchen-qty">x4</span>Cheeseburger</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x2</span>Moyenne Frite</span>
</div>
<div class="kitchen-line">
<span><span class="kitchen-qty">x4</span>Coca Cola</span>
</div>
</div>
<div class="kitchen-card-footer">
<span class="text-sm text-muted">22,10 &euro;</span>
<button class="btn btn-primary btn-sm" type="button">Marquer prete</button>
</div>
</div>
</div>
</main>
</div>
<script src="assets/js/admin.js"></script>
</body>
</html>

View file

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tableau de bord — Wakdo Admin</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<div class="admin-layout">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-logo">
<img src="assets/images/logo.png" alt="Wakdo">
<div>
<span class="topbar-logo-text">Wakdo</span>
<span class="topbar-logo-sub">Administration</span>
</div>
</div>
<div class="topbar-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" placeholder="Rechercher une commande, un produit...">
</div>
<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">CJ</div>
<div>
<div class="topbar-user-name">Corentin J.</div>
<div class="topbar-user-role">Administrateur</div>
</div>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div class="dropdown-menu" id="userMenu">
<a href="#">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"></circle><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"></path></svg>
Mon profil
</a>
<a href="#">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"></path></svg>
Parametres
</a>
<div class="divider"></div>
<button class="danger" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
Se deconnecter
</button>
</div>
</div>
</div>
</header>
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-section">
<div class="sidebar-section-label">Vue d'ensemble</div>
<a href="dashboard.html" class="sidebar-item active">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect></svg>
Tableau de bord
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Catalogue</div>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
Categories
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
Produits
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect x="9" y="3" width="6" height="4" rx="2"></rect></svg>
Menus
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Operations</div>
<a href="commandes.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
Commandes
</a>
<a href="cuisine.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>
Cuisine
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Administration</div>
<a href="users.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
Utilisateurs
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Roles
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"></path></svg>
Parametres
</a>
</div>
</nav>
<!-- Content -->
<main class="content">
<div class="page-header">
<div>
<h1 class="page-title">Tableau de bord</h1>
<p class="page-subtitle">Samedi 9 mai 2026 — Service en cours</p>
</div>
<div class="page-actions">
<select class="filter-select" id="periodFilter">
<option value="today" selected>Aujourd'hui</option>
<option value="7d">7 derniers jours</option>
<option value="30d">30 derniers jours</option>
<option value="custom">Personnalise</option>
</select>
<button class="btn btn-secondary" type="button" id="refreshBtn">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
Actualiser
</button>
</div>
</div>
<!-- KPI Cards -->
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Ventes du jour</div>
<div class="kpi-value">2 847,50 &euro;</div>
<div>
<span class="kpi-delta up">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>
+12,4 %
</span>
<span class="kpi-delta-sub">vs hier</span>
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Commandes du jour</div>
<div class="kpi-value">231</div>
<div>
<span class="kpi-delta up">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>
+8,2 %
</span>
<span class="kpi-delta-sub">vs hier</span>
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Panier moyen</div>
<div class="kpi-value">12,33 &euro;</div>
<div>
<span class="kpi-delta down">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
-1,8 %
</span>
<span class="kpi-delta-sub">vs hier</span>
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Produits actifs</div>
<div class="kpi-value">53</div>
<div>
<span class="kpi-delta neutral">
&mdash;
</span>
<span class="kpi-delta-sub">inchange</span>
</div>
</div>
</div>
<!-- Recent Orders -->
<div class="section-block">
<div class="card-header">
<span class="card-title">Dernieres commandes</span>
<a href="commandes.html" class="btn btn-ghost btn-sm">
Voir tout
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
</a>
</div>
<div class="toolbar" style="padding: 12px 16px 0; margin-bottom: 0;">
<div class="toolbar-left">
<div class="search-field">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="N° de commande..." id="orderSearch">
</div>
<select class="filter-select">
<option value="">Tous les statuts</option>
<option value="pending">En attente</option>
<option value="preparing">En preparation</option>
<option value="ready">Prete</option>
<option value="delivered">Livree</option>
<option value="cancelled">Annulee</option>
</select>
</div>
</div>
<div class="table-wrapper">
<table id="ordersTable">
<thead>
<tr>
<th class="sortable" data-col="0"><span class="sort-icon">&#8597;</span></th>
<th class="sortable" data-col="1">Heure <span class="sort-icon">&#8597;</span></th>
<th>Mode</th>
<th>Statut</th>
<th class="sortable" data-col="4">Total <span class="sort-icon">&#8597;</span></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono fw-600">#1087</td>
<td class="muted">13:42</td>
<td><span class="pill pill-info">Sur place</span></td>
<td><span class="pill pill-success">Livree</span></td>
<td class="fw-600">18,70 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
Voir detail
</a>
<div class="divider"></div>
<button class="danger" type="button">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path></svg>
Annuler
</button>
</div>
</div>
</td>
</tr>
<tr>
<td class="mono fw-600">#1086</td>
<td class="muted">13:38</td>
<td><span class="pill pill-neutral">A emporter</span></td>
<td><span class="pill pill-warning">En preparation</span></td>
<td class="fw-600">24,30 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
Voir detail
</a>
<div class="divider"></div>
<button class="danger" type="button">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path></svg>
Annuler
</button>
</div>
</div>
</td>
</tr>
<tr>
<td class="mono fw-600">#1085</td>
<td class="muted">13:31</td>
<td><span class="pill pill-info">Sur place</span></td>
<td><span class="pill pill-success">Livree</span></td>
<td class="fw-600">11,40 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>Voir detail</a>
<div class="divider"></div>
<button class="danger" type="button"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path></svg>Annuler</button>
</div>
</div>
</td>
</tr>
<tr>
<td class="mono fw-600">#1084</td>
<td class="muted">13:27</td>
<td><span class="pill pill-neutral">A emporter</span></td>
<td><span class="pill pill-success">Livree</span></td>
<td class="fw-600">8,80 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>Voir detail</a>
<div class="divider"></div>
<button class="danger" type="button"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path></svg>Annuler</button>
</div>
</div>
</td>
</tr>
<tr>
<td class="mono fw-600">#1083</td>
<td class="muted">13:19</td>
<td><span class="pill pill-info">Sur place</span></td>
<td><span class="pill pill-neutral">Annulee</span></td>
<td class="fw-600 text-muted">6,40 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>Voir detail</a>
</div>
</div>
</td>
</tr>
<tr>
<td class="mono fw-600">#1082</td>
<td class="muted">13:14</td>
<td><span class="pill pill-info">Sur place</span></td>
<td><span class="pill pill-success">Livree</span></td>
<td class="fw-600">32,10 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>Voir detail</a>
<div class="divider"></div>
<button class="danger" type="button"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path></svg>Annuler</button>
</div>
</div>
</td>
</tr>
<tr>
<td class="mono fw-600">#1081</td>
<td class="muted">13:08</td>
<td><span class="pill pill-neutral">A emporter</span></td>
<td><span class="pill pill-success">Livree</span></td>
<td class="fw-600">10,90 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>Voir detail</a>
<div class="divider"></div>
<button class="danger" type="button"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path></svg>Annuler</button>
</div>
</div>
</td>
</tr>
<tr>
<td class="mono fw-600">#1080</td>
<td class="muted">12:58</td>
<td><span class="pill pill-info">Sur place</span></td>
<td><span class="pill pill-success">Livree</span></td>
<td class="fw-600">15,60 &euro;</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button" aria-label="Actions">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>Voir detail</a>
<div class="divider"></div>
<button class="danger" type="button"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path></svg>Annuler</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="pagination-info">Affichage de 8 sur 231 commandes</span>
<div class="pagination-controls">
<button class="pagination-btn" disabled type="button">&laquo;</button>
<button class="pagination-btn active" type="button">1</button>
<button class="pagination-btn" type="button">2</button>
<button class="pagination-btn" type="button">3</button>
<span style="padding: 0 4px; color: var(--color-text-muted); font-size: 12px;">...</span>
<button class="pagination-btn" type="button">29</button>
<button class="pagination-btn" type="button">&raquo;</button>
</div>
</div>
</div>
</main>
</div>
<script src="assets/js/admin.js"></script>
</body>
</html>

View file

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connexion — Wakdo Admin</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<div class="login-page">
<div class="login-card">
<div class="login-logo">
<img src="assets/images/logo.png" alt="Wakdo">
<span class="login-logo-title">Wakdo Admin</span>
<span class="login-logo-sub">Back-office de gestion</span>
</div>
<form action="dashboard.html" method="get">
<div class="form-group">
<label class="form-label" for="email">Adresse e-mail</label>
<input
class="form-input"
type="email"
id="email"
name="email"
placeholder="prenom.nom@wakdo.fr"
autocomplete="email"
required
>
</div>
<div class="form-group">
<label class="form-label" for="password">Mot de passe</label>
<input
class="form-input"
type="password"
id="password"
name="password"
placeholder="Votre mot de passe"
autocomplete="current-password"
required
>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;height:40px;justify-content:center;font-size:14px;font-weight:600;margin-top:4px;">
Se connecter
</button>
</form>
<div class="login-footer">
<a href="#">Mot de passe oublie ?</a>
</div>
</div>
</div>
</body>
</html>

296
src/public/admin/users.html Normal file
View file

@ -0,0 +1,296 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Utilisateurs — Wakdo Admin</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<div class="admin-layout">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-logo">
<img src="assets/images/logo.png" alt="Wakdo">
<div>
<span class="topbar-logo-text">Wakdo</span>
<span class="topbar-logo-sub">Administration</span>
</div>
</div>
<div class="topbar-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="Rechercher un utilisateur...">
</div>
<div class="topbar-actions">
<div class="topbar-user">
<button class="topbar-user-btn" id="userMenuBtn" type="button">
<div class="topbar-user-avatar">CJ</div>
<div>
<div class="topbar-user-name">Corentin J.</div>
<div class="topbar-user-role">Administrateur</div>
</div>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="dropdown-menu" id="userMenu">
<a href="#">Mon profil</a>
<a href="#">Parametres</a>
<div class="divider"></div>
<button class="danger" type="button">Se deconnecter</button>
</div>
</div>
</div>
</header>
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-section">
<div class="sidebar-section-label">Vue d'ensemble</div>
<a href="dashboard.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect></svg>
Tableau de bord
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Catalogue</div>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
Categories
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
Produits
</a>
<a href="catalogue.html" class="sidebar-item sidebar-item-sub">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"></path><rect x="9" y="3" width="6" height="4" rx="2"></rect></svg>
Menus
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Operations</div>
<a href="commandes.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
Commandes
</a>
<a href="cuisine.html" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>
Cuisine
</a>
</div>
<div class="sidebar-section">
<div class="sidebar-section-label">Administration</div>
<a href="users.html" class="sidebar-item active">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
Utilisateurs
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Roles
</a>
<a href="#" class="sidebar-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"></path></svg>
Parametres
</a>
</div>
</nav>
<!-- Content -->
<main class="content">
<div class="page-header">
<div>
<h1 class="page-title">Utilisateurs</h1>
<p class="page-subtitle">Comptes et droits d'acces au back-office</p>
</div>
<div class="page-actions">
<button class="btn btn-primary" type="button">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Nouvel utilisateur
</button>
</div>
</div>
<div class="toolbar">
<div class="toolbar-left">
<div class="search-field">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" placeholder="Nom, email..." id="userSearch">
</div>
<select class="filter-select">
<option value="">Tous les roles</option>
<option value="admin">Administrateur</option>
<option value="manager">Manager</option>
<option value="preparation">Preparation</option>
<option value="accueil">Accueil</option>
</select>
<select class="filter-select">
<option value="">Tous les statuts</option>
<option value="active">Actif</option>
<option value="inactive">Inactif</option>
</select>
</div>
</div>
<div class="table-container">
<div class="table-wrapper">
<table id="userTable">
<thead>
<tr>
<th class="sortable" data-col="0">Nom / Email <span class="sort-icon">&#8597;</span></th>
<th class="sortable" data-col="1">Role <span class="sort-icon">&#8597;</span></th>
<th>Statut</th>
<th class="sortable" data-col="3">Derniere connexion <span class="sort-icon">&#8597;</span></th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-center gap-8">
<div class="topbar-user-avatar" style="width:30px;height:30px;font-size:12px;">CJ</div>
<div>
<div class="fw-600">Corentin Jog</div>
<div class="text-sm text-muted">corentin@wakdo.fr</div>
</div>
</div>
</td>
<td><span class="pill pill-info">Administrateur</span></td>
<td><span class="pill pill-success">Actif</span></td>
<td class="muted">09/05/2026 13:42</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#">Modifier</a>
<button type="button">Reinitialiser mdp</button>
<div class="divider"></div>
<button class="danger" type="button">Desactiver</button>
</div>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-center gap-8">
<div class="topbar-user-avatar" style="width:30px;height:30px;font-size:12px;background:#E5E7EB;color:#4A4A4A;">ML</div>
<div>
<div class="fw-600">Marie Laurent</div>
<div class="text-sm text-muted">marie.laurent@wakdo.fr</div>
</div>
</div>
</td>
<td><span class="pill pill-warning">Manager</span></td>
<td><span class="pill pill-success">Actif</span></td>
<td class="muted">09/05/2026 10:15</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#">Modifier</a>
<button type="button">Reinitialiser mdp</button>
<div class="divider"></div>
<button class="danger" type="button">Desactiver</button>
</div>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-center gap-8">
<div class="topbar-user-avatar" style="width:30px;height:30px;font-size:12px;background:#E5E7EB;color:#4A4A4A;">AD</div>
<div>
<div class="fw-600">Ahmed Diallo</div>
<div class="text-sm text-muted">ahmed.diallo@wakdo.fr</div>
</div>
</div>
</td>
<td><span class="pill pill-neutral">Preparation</span></td>
<td><span class="pill pill-success">Actif</span></td>
<td class="muted">09/05/2026 11:00</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#">Modifier</a>
<button type="button">Reinitialiser mdp</button>
<div class="divider"></div>
<button class="danger" type="button">Desactiver</button>
</div>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-center gap-8">
<div class="topbar-user-avatar" style="width:30px;height:30px;font-size:12px;background:#E5E7EB;color:#4A4A4A;">SP</div>
<div>
<div class="fw-600">Sophie Petit</div>
<div class="text-sm text-muted">sophie.petit@wakdo.fr</div>
</div>
</div>
</td>
<td><span class="pill pill-neutral">Accueil</span></td>
<td><span class="pill pill-success">Actif</span></td>
<td class="muted">09/05/2026 09:58</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#">Modifier</a>
<button type="button">Reinitialiser mdp</button>
<div class="divider"></div>
<button class="danger" type="button">Desactiver</button>
</div>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-center gap-8">
<div class="topbar-user-avatar" style="width:30px;height:30px;font-size:12px;background:#F3F4F6;color:#9CA3AF;">TM</div>
<div>
<div class="fw-600" style="color:var(--color-text-muted);">Thomas Martin</div>
<div class="text-sm text-muted">thomas.martin@wakdo.fr</div>
</div>
</div>
</td>
<td><span class="pill pill-neutral">Preparation</span></td>
<td><span class="pill pill-neutral">Inactif</span></td>
<td class="muted">02/04/2026 17:30</td>
<td>
<div class="action-menu">
<button class="action-menu-btn" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg>
</button>
<div class="action-menu-dropdown">
<a href="#">Modifier</a>
<button type="button">Reactiver</button>
<div class="divider"></div>
<button class="danger" type="button">Supprimer</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<span class="pagination-info">5 utilisateurs</span>
<div class="pagination-controls">
<button class="pagination-btn active" type="button">1</button>
</div>
</div>
</div>
</main>
</div>
<script src="assets/js/admin.js"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,111 @@
/*
* data.js Data loading layer for the Wakdo kiosk.
*
* P5 reads static JSON copies in /data/ (same origin).
* In P4, swap the BASE_URL constants to point to REST API endpoints.
* The function signatures and return shapes remain unchanged so that
* page scripts need no modification when the data source changes.
*
* Category-to-slug mapping (mirrors data/categories.json id field):
* 1=menus 2=boissons 3=burgers 4=frites 5=encas
* 6=wraps 7=salades 8=desserts 9=sauces
*/
/* --- P4 swap point -------------------------------------------------------
* TODO(P4): replace these two paths with API endpoints, e.g.:
* const CATEGORIES_URL = '/api/categories';
* const PRODUCTS_URL = '/api/products';
* The rest of this file is API-agnostic.
* ----------------------------------------------------------------------- */
const CATEGORIES_URL = 'data/categories.json';
const PRODUCTS_URL = 'data/produits.json';
/** @type {Array|null} — in-memory cache to avoid repeated fetches */
let _categoriesCache = null;
/** @type {Object|null} */
let _productsCache = null;
/**
* Fetches and caches the categories list.
* @returns {Promise<Array>}
*/
export async function loadCategories() {
if (_categoriesCache) return _categoriesCache;
const res = await fetch(CATEGORIES_URL);
if (!res.ok) throw new Error(`Failed to load categories: HTTP ${res.status}`);
_categoriesCache = await res.json();
return _categoriesCache;
}
/**
* Fetches and caches the full products object keyed by category slug.
* @returns {Promise<Object>}
*/
export async function loadProducts() {
if (_productsCache) return _productsCache;
const res = await fetch(PRODUCTS_URL);
if (!res.ok) throw new Error(`Failed to load products: HTTP ${res.status}`);
_productsCache = await res.json();
return _productsCache;
}
/**
* Returns the array of products for a given category slug.
* Returns [] if the slug is not found.
* @param {string} slug e.g. "burgers", "menus"
* @returns {Promise<Array>}
*/
export async function getProductsByCategory(slug) {
const data = await loadProducts();
return data[slug] ?? [];
}
/**
* Returns the category object for the given id.
* @param {number} id
* @returns {Promise<Object|null>}
*/
export async function getCategoryById(id) {
const cats = await loadCategories();
return cats.find(c => c.id === id) ?? null;
}
/**
* Finds a product by its numeric id, searching all category slates.
* Returns null if not found.
* @param {number} id
* @returns {Promise<Object|null>}
*/
export async function findProduct(id) {
const data = await loadProducts();
for (const slug of Object.keys(data)) {
const found = data[slug].find(p => p.id === id);
if (found) return { ...found, categorie: slug };
}
return null;
}
/**
* Maps a category id integer to its slug string.
* Derived from data/categories.json kept here as a convenience
* so page scripts can convert query-string ids without an extra fetch.
*/
export const CATEGORY_ID_TO_SLUG = {
1: 'menus',
2: 'boissons',
3: 'burgers',
4: 'frites',
5: 'encas',
6: 'wraps',
7: 'salades',
8: 'desserts',
9: 'sauces'
};
/**
* Inverse of the above: slug -> id.
*/
export const CATEGORY_SLUG_TO_ID = Object.fromEntries(
Object.entries(CATEGORY_ID_TO_SLUG).map(([id, slug]) => [slug, Number(id)])
);

View file

@ -0,0 +1,56 @@
/*
* nav.js Shared navigation helpers loaded on every page.
*
* Responsibilities:
* - Inject the mode badge ("Sur place" / "A emporter") into any
* element with [data-mode-badge] on the page.
* - Sync the cart item count into any element with [data-cart-count].
* - Handle the mode query-string on page load (welcome -> categories handoff).
*
* Import this module in every page that has a header.
*/
import { getMode, setMode, getCartCount } from './state.js';
/**
* Reads ?mode= from the current URL and persists it if present.
* Called once on DOMContentLoaded so that the welcome -> categories
* navigation stores the chosen mode before any render.
*/
function syncModeFromURL() {
const params = new URLSearchParams(window.location.search);
const modeParam = params.get('mode');
if (modeParam === 'sur-place' || modeParam === 'a-emporter') {
setMode(modeParam);
}
}
/**
* Renders the human-readable mode label into every [data-mode-badge] element.
*/
function renderModeBadge() {
const mode = getMode();
const label = mode === 'a-emporter' ? 'A emporter' : 'Sur place';
document.querySelectorAll('[data-mode-badge]').forEach(el => {
el.textContent = label;
});
}
/**
* Updates the cart item count badge in every [data-cart-count] element.
* Called on load and after any cart mutation.
*/
export function refreshCartBadge() {
const count = getCartCount();
document.querySelectorAll('[data-cart-count]').forEach(el => {
el.textContent = count > 0 ? String(count) : '';
el.hidden = count === 0;
});
}
/* Initialise on DOM ready */
document.addEventListener('DOMContentLoaded', () => {
syncModeFromURL();
renderModeBadge();
refreshCartBadge();
});

View file

@ -0,0 +1,180 @@
/*
* page-cart.js Shopping cart screen.
*
* Displays all cart lines with quantity controls and totals.
* Handles two item shapes:
* - Simple product: { id, type, libelle, prix_cents, quantite, image }
* - Composed menu: { ...above, composition: {...}, supplement_cents: number }
*
* Menu lines render a composition breakdown beneath the product name.
* Simple product lines render as before (no composition block).
*
* TVA: 10% (taux normal restauration, France 2024 simplification MVP).
* TODO: verify exact applicable TVA rate with an accountant in P3.
* The real rate depends on sur-place vs a-emporter, alcohol content, etc.
*
* The total displayed is TTC (tax inclusive) because French consumer law
* requires prices shown to end-consumers to include all taxes.
*/
import { getCart, removeFromCart, updateQuantity, getTotalCents, computeMenuLineCents, clearCart, formatPrice } from './state.js';
import { refreshCartBadge } from './nav.js';
/* TVA rate used for display breakdown only — stored prices are already TTC */
const TVA_RATE = 0.10;
const cartList = document.getElementById('cart-list');
const emptyBlock = document.getElementById('cart-empty');
const summaryBlock= document.getElementById('cart-summary');
const totalTTC = document.getElementById('total-ttc');
const totalHT = document.getElementById('total-ht');
const totalTVA = document.getElementById('total-tva');
const payBtn = document.getElementById('pay-btn');
const abandonBtn = document.getElementById('abandon-btn');
function renderCart() {
const items = getCart();
refreshCartBadge();
if (!items.length) {
cartList.innerHTML = '';
emptyBlock.hidden = false;
summaryBlock.hidden = true;
if (payBtn) payBtn.disabled = true;
return;
}
emptyBlock.hidden = true;
summaryBlock.hidden = false;
if (payBtn) payBtn.disabled = false;
cartList.innerHTML = '';
items.forEach((item, index) => {
const isMenu = item.type === 'menu';
const lineTotalCents = isMenu
? computeMenuLineCents(item)
: item.prix_cents * item.quantite;
const row = document.createElement('li');
row.className = 'cart-line';
row.setAttribute('aria-label', `${item.libelle}, quantite ${item.quantite}`);
row.innerHTML = `
<img
class="cart-line__image"
src="${item.image}"
alt="${item.libelle}"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
<div class="cart-line__info">
<span class="cart-line__name">${item.libelle}</span>
<span class="cart-line__unit-price">${formatPrice(item.prix_cents)} / unite${isMenu && (item.supplement_cents ?? 0) > 0 ? ` + ${formatPrice(item.supplement_cents)} suppl.` : ''}</span>
${isMenu && item.composition ? renderCompositionBlock(item) : ''}
</div>
<div class="cart-line__qty" role="group" aria-label="Quantite de ${item.libelle}">
<button
class="qty-btn qty-btn--minus"
data-index="${index}"
aria-label="Diminuer la quantite de ${item.libelle}"
type="button"
>-</button>
<span class="qty-value" aria-live="polite">${item.quantite}</span>
<button
class="qty-btn qty-btn--plus"
data-index="${index}"
aria-label="Augmenter la quantite de ${item.libelle}"
type="button"
>+</button>
</div>
<span class="cart-line__total">${formatPrice(lineTotalCents)}</span>
<button
class="cart-line__remove"
data-index="${index}"
aria-label="Supprimer ${item.libelle} du panier"
type="button"
>
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="24" height="24">
</button>
`;
cartList.appendChild(row);
});
/* Attach event listeners after render */
cartList.querySelectorAll('.qty-btn--minus').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.index, 10);
const cart = getCart();
updateQuantity(idx, cart[idx].quantite - 1);
renderCart();
});
});
cartList.querySelectorAll('.qty-btn--plus').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.index, 10);
const cart = getCart();
updateQuantity(idx, cart[idx].quantite + 1);
renderCart();
});
});
cartList.querySelectorAll('.cart-line__remove').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.index, 10);
removeFromCart(idx);
renderCart();
});
});
/* Update totals */
const ttcCents = getTotalCents();
/* Back-calculate HT from TTC (prices assumed to be TTC already) */
const htCents = Math.round(ttcCents / (1 + TVA_RATE));
const tvaCents = ttcCents - htCents;
if (totalTTC) totalTTC.textContent = formatPrice(ttcCents);
if (totalHT) totalHT.textContent = formatPrice(htCents);
if (totalTVA) totalTVA.textContent = formatPrice(tvaCents);
}
/**
* Builds the composition breakdown HTML for a menu cart line.
* Renders burger (with personalisation options), accompagnement with taille,
* boisson with taille, sauce, and the supplement summary if applicable.
*
* @param {Object} item cart item with type === 'menu' and composition object
* @returns {string} HTML string
*/
function renderCompositionBlock(item) {
const c = item.composition;
if (!c) return '';
const burgerOpts = c.burger.options && c.burger.options.length
? ` (${c.burger.options.map(o => o === 'sans-oignon' ? 'sans oignon' : 'avec fromage').join(', ')})`
: '';
const accompTailleLabel = c.accompagnement.taille === 'G' ? ' grande' : ' normale';
const boissonTailleLabel = c.boisson.taille === 'G' ? ' grande' : ' normale';
const nbGrandes = (c.accompagnement.taille === 'G' ? 1 : 0) + (c.boisson.taille === 'G' ? 1 : 0);
const supplTotal = item.supplement_cents ?? 0;
return `
<ul class="cart-line__composition" aria-label="Composition du menu">
<li class="cart-line__comp-item">+ ${c.burger.libelle}${burgerOpts}</li>
<li class="cart-line__comp-item">+ ${c.accompagnement.libelle}${accompTailleLabel}</li>
<li class="cart-line__comp-item">+ ${c.boisson.libelle}${boissonTailleLabel}</li>
<li class="cart-line__comp-item">+ ${c.sauce.libelle}</li>
${supplTotal > 0 ? `<li class="cart-line__comp-suppl">Supplement ${nbGrandes} grande(s) : +${formatPrice(supplTotal)}</li>` : ''}
</ul>
`;
}
if (abandonBtn) {
abandonBtn.addEventListener('click', () => {
clearCart();
window.location.href = 'categories.html';
});
}
document.addEventListener('DOMContentLoaded', renderCart);

View file

@ -0,0 +1,42 @@
/*
* page-confirmation.js Order confirmation screen.
*
* Generates a short order number: "WK-" + Date.now() encoded in base 36.
* This is session-unique and human-readable at the counter.
*
* Clears the cart on load so that "Nouvelle commande" starts fresh.
*/
import { clearCart, getTotalCents, formatPrice } from './state.js';
const orderNumberEl = document.getElementById('order-number');
const orderTotalEl = document.getElementById('order-total');
const newOrderBtn = document.getElementById('new-order-btn');
function generateOrderNumber() {
return 'WK-' + Date.now().toString(36).toUpperCase();
}
document.addEventListener('DOMContentLoaded', () => {
/* Capture total before clearing */
const totalCents = getTotalCents();
if (orderTotalEl) {
orderTotalEl.textContent = formatPrice(totalCents);
}
if (orderNumberEl) {
orderNumberEl.textContent = generateOrderNumber();
}
/* Clear cart immediately — order is confirmed */
clearCart();
});
if (newOrderBtn) {
newOrderBtn.addEventListener('click', () => {
/* clearCart() already called on DOMContentLoaded, but guard anyway */
clearCart();
window.location.href = 'index.html';
});
}

View file

@ -0,0 +1,702 @@
/*
* page-product-menu.js Multi-step menu composer for the Wakdo kiosk.
*
* Imported by page-product.js only when the loaded product has type === 'menu'.
* Keeping the composer in its own module avoids bloating page-product.js and
* makes future unit-testing of the composition logic straightforward.
*
* Steps:
* 1 Burger selection + personalisation options (sans oignon / avec fromage)
* 2 Accompagnement (frites or salades) + taille toggle
* 3 Boisson + taille toggle
* 4 Sauce
* 5 Recap + "Ajouter au panier"
*
* Price rule: grande taille = +50 centimes per sized item (accompagnement + boisson).
*
* A11y: role=dialog, aria-modal=true, focus-trap (Tab cycles inside the modal),
* ESC closes/cancels, focus is moved to the first interactive element on each step.
*/
import { getProductsByCategory } from './data.js';
import { addToCart, computeMenuLineCents, formatPrice } from './state.js';
import { refreshCartBadge } from './nav.js';
const SUPPLEMENT_GRANDE_CENTS = 50;
const TOTAL_STEPS = 5;
/* ------------------------------------------------------------------ */
/* Public entry-point — called from page-product.js */
/* ------------------------------------------------------------------ */
/**
* Initialises and opens the menu composer modal.
* Fetches required category products, builds the initial state, then renders.
*
* @param {Object} menu product object with type === 'menu'
* @param {string} returnCategory category slug to redirect to after add/cancel
*/
export async function openMenuComposer(menu, returnCategory) {
let burgers, frites, salades, boissons, sauces;
try {
[burgers, frites, salades, boissons, sauces] = await Promise.all([
getProductsByCategory('burgers'),
getProductsByCategory('frites'),
getProductsByCategory('salades'),
getProductsByCategory('boissons'),
getProductsByCategory('sauces')
]);
} catch (err) {
console.error('Menu composer: failed to load category products', err);
return;
}
const accompagnements = [...frites, ...salades];
/* Heuristic pre-selection: if the menu name contains a burger name, pre-select it.
* "Menu CBO" -> first burger whose nom equals "CBO".
* Fallback: first burger in the list. */
const menuNameUpper = menu.nom.toUpperCase();
const preselectedBurger =
burgers.find(b => menuNameUpper.includes(b.nom.toUpperCase())) ?? burgers[0] ?? null;
/* Composer internal state — single mutable object, re-read on each render. */
const state = {
currentStep: 1,
menu,
returnCategory,
burgers,
accompagnements,
boissons,
sauces,
/* Selections */
burger: preselectedBurger,
burgerOptions: [], // subset of ['sans-oignon', 'avec-fromage']
accompagnement: accompagnements[0] ?? null,
accompTaille: 'N', // 'N' or 'G'
boisson: boissons[0] ?? null,
boissonTaille: 'N',
sauce: sauces[0] ?? null
};
const modal = buildModalShell(menu);
document.body.appendChild(modal);
modal.removeAttribute('hidden');
/* Prevent background scroll while composer is open. */
document.body.style.overflow = 'hidden';
renderStep(modal, state);
trapFocus(modal);
/* ESC closes the modal and returns to product list. */
const escHandler = (e) => {
if (e.key === 'Escape') {
cancelComposer(modal, returnCategory, escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
/* ------------------------------------------------------------------ */
/* Modal shell builder */
/* ------------------------------------------------------------------ */
function buildModalShell(menu) {
const overlay = document.createElement('div');
overlay.className = 'composer-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-labelledby', 'composer-title');
overlay.hidden = true;
overlay.innerHTML = `
<div class="composer-container" role="document">
<div class="composer-header">
<h2 class="composer-title" id="composer-title">${escHtml(menu.nom)}</h2>
<div class="composer-progress" aria-label="Progression">
<span class="composer-progress__text" id="composer-step-indicator" aria-live="polite">Etape 1 / ${TOTAL_STEPS}</span>
<div class="composer-progress__bar">
<div class="composer-progress__fill" id="composer-progress-fill" style="width: 20%"></div>
</div>
</div>
</div>
<div class="composer-body" id="composer-body">
<!-- step content injected here -->
</div>
<div class="composer-footer" id="composer-footer">
<!-- navigation buttons injected here -->
</div>
</div>
`;
return overlay;
}
/* ------------------------------------------------------------------ */
/* Step renderer — decides which step to paint */
/* ------------------------------------------------------------------ */
function renderStep(modal, state) {
const body = modal.querySelector('#composer-body');
const footer = modal.querySelector('#composer-footer');
const stepEl = modal.querySelector('#composer-step-indicator');
const fillEl = modal.querySelector('#composer-progress-fill');
stepEl.textContent = `Etape ${state.currentStep} / ${TOTAL_STEPS}`;
fillEl.style.width = `${(state.currentStep / TOTAL_STEPS) * 100}%`;
/* Each step renderer returns {bodyHTML, canAdvance()} and may attach
* its own event listeners after DOM insertion. */
switch (state.currentStep) {
case 1: renderStep1(body, footer, modal, state); break;
case 2: renderStep2(body, footer, modal, state); break;
case 3: renderStep3(body, footer, modal, state); break;
case 4: renderStep4(body, footer, modal, state); break;
case 5: renderStep5(body, footer, modal, state); break;
}
/* Move focus to the first interactive element so keyboard users and
* screen readers start at the right place after each step transition. */
requestAnimationFrame(() => {
const first = modal.querySelector(
'button:not([disabled]), input:not([disabled]), [tabindex="0"]'
);
if (first) first.focus();
});
}
/* ------------------------------------------------------------------ */
/* Step 1 — Burger + personalisation options */
/* ------------------------------------------------------------------ */
function renderStep1(body, footer, modal, state) {
body.innerHTML = `
<p class="composer-step__subtitle">Choisissez votre burger</p>
<ul class="composer-grid" role="list" id="burger-grid">
${state.burgers.map(b => `
<li>
<button
class="composer-card ${state.burger && state.burger.id === b.id ? 'composer-card--selected' : ''}"
type="button"
data-id="${b.id}"
aria-pressed="${state.burger && state.burger.id === b.id ? 'true' : 'false'}"
aria-label="${escHtml(b.nom)}, ${formatPrice(b.prix)}"
>
<img
class="composer-card__image"
src="${escHtml(b.image)}"
alt="${escHtml(b.nom)}"
onerror="this.src='assets/images/ui/logo.png';"
>
<span class="composer-card__name">${escHtml(b.nom)}</span>
<span class="composer-card__price">${formatPrice(b.prix)}</span>
</button>
</li>
`).join('')}
</ul>
<fieldset class="composer-options" id="burger-options">
<legend class="composer-options__legend">Personnalisation</legend>
<label class="composer-option-label">
<input type="checkbox" name="burger-opt" value="sans-oignon"
${state.burgerOptions.includes('sans-oignon') ? 'checked' : ''}>
Sans oignon
</label>
<label class="composer-option-label">
<input type="checkbox" name="burger-opt" value="avec-fromage"
${state.burgerOptions.includes('avec-fromage') ? 'checked' : ''}>
Avec fromage
</label>
</fieldset>
`;
/* Burger card selection */
body.querySelectorAll('#burger-grid .composer-card').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id, 10);
state.burger = state.burgers.find(b => b.id === id) ?? state.burger;
/* Update pressed states without full re-render to preserve scroll position */
body.querySelectorAll('#burger-grid .composer-card').forEach(b => {
const active = parseInt(b.dataset.id, 10) === state.burger.id;
b.classList.toggle('composer-card--selected', active);
b.setAttribute('aria-pressed', active ? 'true' : 'false');
});
});
});
/* Personalisation checkboxes */
body.querySelectorAll('input[name="burger-opt"]').forEach(cb => {
cb.addEventListener('change', () => {
state.burgerOptions = Array.from(
body.querySelectorAll('input[name="burger-opt"]:checked')
).map(el => el.value);
});
});
renderFooter(footer, modal, state, {
canAdvance: () => state.burger !== null
});
}
/* ------------------------------------------------------------------ */
/* Step 2 — Accompagnement + taille toggle */
/* ------------------------------------------------------------------ */
function renderStep2(body, footer, modal, state) {
body.innerHTML = `
<p class="composer-step__subtitle">Choisissez votre accompagnement</p>
<ul class="composer-grid" role="list" id="accomp-grid">
${state.accompagnements.map(a => `
<li>
<button
class="composer-card ${state.accompagnement && state.accompagnement.id === a.id ? 'composer-card--selected' : ''}"
type="button"
data-id="${a.id}"
aria-pressed="${state.accompagnement && state.accompagnement.id === a.id ? 'true' : 'false'}"
aria-label="${escHtml(a.nom)}"
>
<img
class="composer-card__image"
src="${escHtml(a.image)}"
alt="${escHtml(a.nom)}"
onerror="this.src='assets/images/ui/logo.png';"
>
<span class="composer-card__name">${escHtml(a.nom)}</span>
</button>
</li>
`).join('')}
</ul>
${renderTailleToggle('accomp', state.accompTaille)}
`;
body.querySelectorAll('#accomp-grid .composer-card').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id, 10);
state.accompagnement = state.accompagnements.find(a => a.id === id) ?? state.accompagnement;
body.querySelectorAll('#accomp-grid .composer-card').forEach(b => {
const active = parseInt(b.dataset.id, 10) === state.accompagnement.id;
b.classList.toggle('composer-card--selected', active);
b.setAttribute('aria-pressed', active ? 'true' : 'false');
});
});
});
attachTailleToggle(body, 'accomp', state, 'accompTaille');
renderFooter(footer, modal, state, {
canAdvance: () => state.accompagnement !== null
});
}
/* ------------------------------------------------------------------ */
/* Step 3 — Boisson + taille toggle */
/* ------------------------------------------------------------------ */
function renderStep3(body, footer, modal, state) {
body.innerHTML = `
<p class="composer-step__subtitle">Choisissez votre boisson</p>
<ul class="composer-grid" role="list" id="boisson-grid">
${state.boissons.map(b => `
<li>
<button
class="composer-card ${state.boisson && state.boisson.id === b.id ? 'composer-card--selected' : ''}"
type="button"
data-id="${b.id}"
aria-pressed="${state.boisson && state.boisson.id === b.id ? 'true' : 'false'}"
aria-label="${escHtml(b.nom)}"
>
<img
class="composer-card__image"
src="${escHtml(b.image)}"
alt="${escHtml(b.nom)}"
onerror="this.src='assets/images/ui/logo.png';"
>
<span class="composer-card__name">${escHtml(b.nom)}</span>
</button>
</li>
`).join('')}
</ul>
${renderTailleToggle('boisson', state.boissonTaille)}
`;
body.querySelectorAll('#boisson-grid .composer-card').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id, 10);
state.boisson = state.boissons.find(b => b.id === id) ?? state.boisson;
body.querySelectorAll('#boisson-grid .composer-card').forEach(b => {
const active = parseInt(b.dataset.id, 10) === state.boisson.id;
b.classList.toggle('composer-card--selected', active);
b.setAttribute('aria-pressed', active ? 'true' : 'false');
});
});
});
attachTailleToggle(body, 'boisson', state, 'boissonTaille');
renderFooter(footer, modal, state, {
canAdvance: () => state.boisson !== null
});
}
/* ------------------------------------------------------------------ */
/* Step 4 — Sauce */
/* ------------------------------------------------------------------ */
function renderStep4(body, footer, modal, state) {
body.innerHTML = `
<p class="composer-step__subtitle">Choisissez votre sauce</p>
<ul class="composer-grid" role="list" id="sauce-grid">
${state.sauces.map(s => `
<li>
<button
class="composer-card ${state.sauce && state.sauce.id === s.id ? 'composer-card--selected' : ''}"
type="button"
data-id="${s.id}"
aria-pressed="${state.sauce && state.sauce.id === s.id ? 'true' : 'false'}"
aria-label="${escHtml(s.nom)}"
>
<img
class="composer-card__image"
src="${escHtml(s.image)}"
alt="${escHtml(s.nom)}"
onerror="this.src='assets/images/ui/logo.png';"
>
<span class="composer-card__name">${escHtml(s.nom)}</span>
</button>
</li>
`).join('')}
</ul>
`;
body.querySelectorAll('#sauce-grid .composer-card').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id, 10);
state.sauce = state.sauces.find(s => s.id === id) ?? state.sauce;
body.querySelectorAll('#sauce-grid .composer-card').forEach(b => {
const active = parseInt(b.dataset.id, 10) === state.sauce.id;
b.classList.toggle('composer-card--selected', active);
b.setAttribute('aria-pressed', active ? 'true' : 'false');
});
});
});
renderFooter(footer, modal, state, {
canAdvance: () => state.sauce !== null
});
}
/* ------------------------------------------------------------------ */
/* Step 5 — Recap + add to cart */
/* ------------------------------------------------------------------ */
function renderStep5(body, footer, modal, state) {
const supplement = computeSupplement(state);
const baseItem = buildCartItem(state, supplement);
const totalLine = computeMenuLineCents(baseItem);
const optionsText = state.burgerOptions.length
? state.burgerOptions.map(o => o === 'sans-oignon' ? 'sans oignon' : 'avec fromage').join(', ')
: null;
body.innerHTML = `
<p class="composer-step__subtitle">Recapitulatif de votre menu</p>
<ul class="composer-recap" aria-label="Composition du menu">
<li class="composer-recap__line">
<span class="composer-recap__icon" aria-hidden="true">&#9632;</span>
<span class="composer-recap__label">
${escHtml(state.burger.nom)}
${optionsText ? `<span class="composer-recap__opts">(${escHtml(optionsText)})</span>` : ''}
</span>
</li>
<li class="composer-recap__line">
<span class="composer-recap__icon" aria-hidden="true">&#9632;</span>
<span class="composer-recap__label">
${escHtml(state.accompagnement.nom)}
<span class="composer-recap__taille">${state.accompTaille === 'G' ? 'grande' : 'normale'}</span>
${state.accompTaille === 'G' ? '<span class="composer-recap__suppl">+0,50 EUR</span>' : ''}
</span>
</li>
<li class="composer-recap__line">
<span class="composer-recap__icon" aria-hidden="true">&#9632;</span>
<span class="composer-recap__label">
${escHtml(state.boisson.nom)}
<span class="composer-recap__taille">${state.boissonTaille === 'G' ? 'grande' : 'normale'}</span>
${state.boissonTaille === 'G' ? '<span class="composer-recap__suppl">+0,50 EUR</span>' : ''}
</span>
</li>
<li class="composer-recap__line">
<span class="composer-recap__icon" aria-hidden="true">&#9632;</span>
<span class="composer-recap__label">${escHtml(state.sauce.nom)}</span>
</li>
</ul>
<div class="composer-recap__totals">
<span class="composer-recap__base">Menu de base : ${formatPrice(state.menu.prix_cents ?? state.menu.prix)}</span>
${supplement > 0 ? `<span class="composer-recap__suppl-total">Supplement grande(s) taille(s) : +${formatPrice(supplement)}</span>` : ''}
<span class="composer-recap__total-line">Total : <strong>${formatPrice(totalLine)}</strong></span>
</div>
`;
footer.innerHTML = `
<div class="composer-footer__row">
<button class="btn btn--secondary composer-footer__cancel" type="button" id="composer-cancel">
Annuler
</button>
<button class="btn btn--secondary composer-footer__prev" type="button" id="composer-prev">
Precedent
</button>
<button class="btn btn--primary composer-footer__add" type="button" id="composer-add">
Ajouter au panier
</button>
</div>
`;
footer.querySelector('#composer-cancel').addEventListener('click', () => {
cancelComposer(modal, state.returnCategory, null);
});
footer.querySelector('#composer-prev').addEventListener('click', () => {
state.currentStep--;
renderStep(modal, state);
});
footer.querySelector('#composer-add').addEventListener('click', () => {
addToCart(baseItem);
refreshCartBadge();
closeComposer(modal);
window.location.href = `products.html?category=${state.returnCategory}`;
});
}
/* ------------------------------------------------------------------ */
/* Footer renderer (steps 1-4) */
/* ------------------------------------------------------------------ */
/**
* Renders the navigation footer for steps 1 through 4.
* @param {HTMLElement} footer
* @param {HTMLElement} modal
* @param {Object} state
* @param {{ canAdvance: () => boolean }} opts
*/
function renderFooter(footer, modal, state, opts) {
const isFirst = state.currentStep === 1;
footer.innerHTML = `
<div class="composer-footer__row">
<button class="btn btn--secondary composer-footer__cancel" type="button" id="composer-cancel">
Annuler
</button>
${!isFirst ? `
<button class="btn btn--secondary composer-footer__prev" type="button" id="composer-prev">
Precedent
</button>` : ''}
<button class="btn btn--primary composer-footer__next" type="button" id="composer-next">
Suivant
</button>
</div>
`;
footer.querySelector('#composer-cancel').addEventListener('click', () => {
cancelComposer(modal, state.returnCategory, null);
});
if (!isFirst) {
footer.querySelector('#composer-prev').addEventListener('click', () => {
state.currentStep--;
renderStep(modal, state);
});
}
footer.querySelector('#composer-next').addEventListener('click', () => {
if (!opts.canAdvance()) return;
state.currentStep++;
renderStep(modal, state);
});
}
/* ------------------------------------------------------------------ */
/* Taille toggle — shared between accompagnement and boisson steps */
/* ------------------------------------------------------------------ */
/**
* Generates the HTML for the Normale/Grande toggle.
* @param {string} prefix 'accomp' or 'boisson', used for IDs
* @param {'N'|'G'} currentTaille
* @returns {string}
*/
function renderTailleToggle(prefix, currentTaille) {
return `
<div class="composer-taille" role="group" aria-label="Taille">
<button
class="composer-taille__btn ${currentTaille === 'N' ? 'composer-taille__btn--active' : ''}"
type="button"
data-taille="N"
id="${prefix}-taille-n"
aria-pressed="${currentTaille === 'N' ? 'true' : 'false'}"
>
Normale
</button>
<button
class="composer-taille__btn ${currentTaille === 'G' ? 'composer-taille__btn--active' : ''}"
type="button"
data-taille="G"
id="${prefix}-taille-g"
aria-pressed="${currentTaille === 'G' ? 'true' : 'false'}"
>
Grande <span class="composer-taille__price-hint">+0,50 EUR</span>
</button>
</div>
`;
}
/**
* Attaches click handlers to the taille toggle buttons and keeps state in sync.
* @param {HTMLElement} body
* @param {string} prefix
* @param {Object} state
* @param {'accompTaille'|'boissonTaille'} stateKey
*/
function attachTailleToggle(body, prefix, state, stateKey) {
body.querySelectorAll('.composer-taille__btn').forEach(btn => {
btn.addEventListener('click', () => {
state[stateKey] = btn.dataset.taille;
body.querySelectorAll('.composer-taille__btn').forEach(b => {
const active = b.dataset.taille === state[stateKey];
b.classList.toggle('composer-taille__btn--active', active);
b.setAttribute('aria-pressed', active ? 'true' : 'false');
});
});
});
}
/* ------------------------------------------------------------------ */
/* Cart item assembly + supplement calculation */
/* ------------------------------------------------------------------ */
/**
* Counts how many grande-taille choices were made (0, 1, or 2).
* @param {Object} state
* @returns {number} centimes
*/
function computeSupplement(state) {
let suppl = 0;
if (state.accompTaille === 'G') suppl += SUPPLEMENT_GRANDE_CENTS;
if (state.boissonTaille === 'G') suppl += SUPPLEMENT_GRANDE_CENTS;
return suppl;
}
/**
* Builds the cart item object from the current composer state.
* prix_cents is the base menu price; supplement_cents accumulates size upgrades.
*
* @param {Object} state
* @param {number} supplement
* @returns {Object}
*/
function buildCartItem(state, supplement) {
/* Support both raw produits.json field (prix) and normalised (prix_cents) */
const prixCents = state.menu.prix_cents ?? state.menu.prix;
return {
id: state.menu.id,
type: 'menu',
categorie: 'menus',
libelle: state.menu.nom,
prix_cents: prixCents,
quantite: 1,
image: state.menu.image,
supplement_cents: supplement,
composition: {
burger: {
id: state.burger.id,
libelle: state.burger.nom,
options: [...state.burgerOptions]
},
accompagnement: {
id: state.accompagnement.id,
libelle: state.accompagnement.nom,
categorie: state.accompagnement.categorie ?? 'frites',
taille: state.accompTaille
},
boisson: {
id: state.boisson.id,
libelle: state.boisson.nom,
taille: state.boissonTaille
},
sauce: {
id: state.sauce.id,
libelle: state.sauce.nom
}
}
};
}
/* ------------------------------------------------------------------ */
/* Focus trap */
/* ------------------------------------------------------------------ */
/**
* Traps Tab / Shift+Tab inside the modal container.
* The handler is attached to the modal element itself; it is removed
* automatically when the modal is removed from the DOM.
*/
function trapFocus(modal) {
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
const focusable = Array.from(modal.querySelectorAll(
'button:not([disabled]), input:not([disabled]), [tabindex="0"]'
)).filter(el => !el.closest('[hidden]'));
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
}
/* ------------------------------------------------------------------ */
/* Close helpers */
/* ------------------------------------------------------------------ */
function closeComposer(modal) {
modal.remove();
document.body.style.overflow = '';
}
function cancelComposer(modal, returnCategory, escHandler) {
if (escHandler) {
document.removeEventListener('keydown', escHandler);
}
closeComposer(modal);
window.location.href = `products.html?category=${returnCategory}`;
}
/* ------------------------------------------------------------------ */
/* Utilities */
/* ------------------------------------------------------------------ */
/**
* Minimal HTML escaping to prevent XSS when injecting product names/paths
* into innerHTML. Applied to all data-derived strings.
*/
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

View file

@ -0,0 +1,119 @@
/*
* page-product.js Product detail screen.
*
* Reads ?id=<int>&category=<slug> from the query string.
*
* Branch on product type:
* - type === 'menu' open the multi-step composer modal (page-product-menu.js).
* The standard detail layout is bypassed because a menu
* cannot be added to the cart without composition choices.
* - type === 'produit' render the standard detail card with "Ajouter au panier".
*
* After "Ajouter au panier" (simple product):
* 1. Item added to cart via state.addToCart()
* 2. Button changes to "Ajoute !" for 1 second (visual feedback)
* 3. Redirect to products.html?category=<slug>
*/
import { findProduct } from './data.js';
import { addToCart, formatPrice } from './state.js';
import { refreshCartBadge } from './nav.js';
import { openMenuComposer } from './page-product-menu.js';
const params = new URLSearchParams(window.location.search);
const productId = parseInt(params.get('id'), 10);
const categorySlug = params.get('category') ?? 'menus';
const container = document.getElementById('product-detail');
const errorBlock = document.getElementById('product-error');
const backBtn = document.getElementById('back-to-products');
if (backBtn) {
backBtn.href = `products.html?category=${categorySlug}`;
}
async function renderProduct() {
if (!productId) {
showError('Produit introuvable.');
return;
}
try {
const product = await findProduct(productId);
if (!product) {
showError('Ce produit n\'existe pas.');
return;
}
document.title = `Wakdo - ${product.nom}`;
if (product.type === 'menu') {
/* Hide the standard product detail area; the composer will overlay the page.
* The container stays in the DOM so the skeleton does not flash. */
container.hidden = true;
await openMenuComposer(product, categorySlug);
return;
}
container.innerHTML = `
<div class="product-detail__image-wrap">
<img
class="product-detail__image"
src="${product.image}"
alt="${product.nom}"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
</div>
<div class="product-detail__info">
<h1 class="product-detail__name">${product.nom}</h1>
<p class="product-detail__price">${formatPrice(product.prix)}</p>
<button
class="btn btn--primary btn--large product-detail__add"
id="add-to-cart-btn"
aria-label="Ajouter ${product.nom} au panier"
type="button"
>
Ajouter au panier
</button>
</div>
`;
document.getElementById('add-to-cart-btn').addEventListener('click', () => {
addToCart({
id: product.id,
type: product.type,
categorie: product.categorie ?? categorySlug,
libelle: product.nom,
prix_cents: product.prix,
quantite: 1,
image: product.image
});
refreshCartBadge();
const btn = document.getElementById('add-to-cart-btn');
btn.textContent = 'Ajoute !';
btn.disabled = true;
/* Redirect after brief confirmation pause */
setTimeout(() => {
window.location.href = `products.html?category=${categorySlug}`;
}, 1000);
});
} catch (err) {
showError('Erreur lors du chargement du produit.');
console.error('renderProduct error:', err);
}
}
function showError(msg) {
if (errorBlock) {
errorBlock.hidden = false;
errorBlock.textContent = msg;
}
if (container) {
container.hidden = true;
}
}
document.addEventListener('DOMContentLoaded', renderProduct);

View file

@ -0,0 +1,86 @@
/*
* page-products.js Products list screen.
*
* Reads ?category=<id> from the query string, maps to a slug via
* CATEGORY_ID_TO_SLUG, then fetches the matching product array.
* On product card click, navigates to product.html?id=<id>&category=<slug>.
*/
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG } from './data.js';
import { formatPrice } from './state.js';
const params = new URLSearchParams(window.location.search);
const categoryId = parseInt(params.get('category'), 10) || 1;
const categorySlug = CATEGORY_ID_TO_SLUG[categoryId] ?? 'menus';
const grid = document.getElementById('products-grid');
const heading = document.getElementById('products-heading');
const backBtn = document.getElementById('back-to-categories');
const errorBlock = document.getElementById('products-error');
/* Build back URL preserving mode query param if present */
const modeParam = params.get('mode');
function buildBackURL() {
const base = 'categories.html';
return modeParam ? `${base}?mode=${modeParam}` : base;
}
if (backBtn) {
backBtn.href = buildBackURL();
}
async function renderProducts() {
try {
const [products, category] = await Promise.all([
getProductsByCategory(categorySlug),
getCategoryById(categoryId)
]);
if (heading && category) {
/* Capitalize first letter of the category title */
const title = category.title.charAt(0).toUpperCase() + category.title.slice(1);
heading.textContent = `Nos ${title}`;
document.title = `Wakdo - ${title}`;
}
if (!products.length) {
grid.innerHTML = '<p class="products-empty">Aucun produit disponible dans cette categorie.</p>';
return;
}
grid.innerHTML = '';
products.forEach(product => {
const card = document.createElement('a');
card.className = 'product-card';
card.href = `product.html?id=${product.id}&category=${categorySlug}`;
card.setAttribute('aria-label', `${product.nom} - ${formatPrice(product.prix)}`);
card.innerHTML = `
<div class="product-card__image-wrap">
<img
class="product-card__image"
src="${product.image}"
alt="${product.nom}"
loading="lazy"
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
>
</div>
<div class="product-card__body">
<span class="product-card__name">${product.nom}</span>
<span class="product-card__price">${formatPrice(product.prix)}</span>
</div>
`;
grid.appendChild(card);
});
} catch (err) {
if (errorBlock) {
errorBlock.hidden = false;
errorBlock.textContent = 'Impossible de charger les produits. Veuillez reessayer.';
}
console.error('renderProducts error:', err);
}
}
document.addEventListener('DOMContentLoaded', renderProducts);

View file

@ -0,0 +1,174 @@
/*
* state.js Global client-side state for the Wakdo kiosk.
*
* Persists via localStorage so that navigation between pages does not
* lose the cart or the consumption mode.
*
* Price convention: all values stored and computed in INTEGER CENTIMES.
* Formatting for display is handled by formatPrice().
*
* TVA note: 10% applied at display time in cart/payment pages only.
* This is a simplified rate for restaurant consumption (France 2024).
* TODO: verify exact applicable rate with an accountant in P3 the real
* rate depends on sur-place vs a-emporter, alcohol content, etc.
*/
const STORAGE_KEY_MODE = 'wakdo_mode';
const STORAGE_KEY_CART = 'wakdo_cart';
/* --- Consumption mode ---------------------------------------------------- */
/**
* Returns the stored consumption mode string or null if not yet chosen.
* @returns {'sur-place'|'a-emporter'|null}
*/
export function getMode() {
return localStorage.getItem(STORAGE_KEY_MODE);
}
/**
* Persists the consumption mode chosen on the welcome screen.
* @param {'sur-place'|'a-emporter'} mode
*/
export function setMode(mode) {
localStorage.setItem(STORAGE_KEY_MODE, mode);
}
/* --- Cart state ---------------------------------------------------------- */
/**
* Returns the current cart array.
* Each item shape:
* { id, type: 'produit'|'menu', categorie, libelle, prix_cents, quantite, image }
* @returns {Array}
*/
export function getCart() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY_CART)) || [];
} catch {
return [];
}
}
/**
* Replaces the entire cart.
* @param {Array} items
*/
export function setCart(items) {
localStorage.setItem(STORAGE_KEY_CART, JSON.stringify(items));
}
/**
* Appends a product or menu to the cart.
*
* For simple products (type !== 'menu'), merges with an existing line
* of the same id matching real kiosk behavior where two identical
* sandwiches become one line with qty 2.
*
* For composed menus, each call always creates a new line because two
* menus with identical base id may have different compositions (different
* burger options, sizes, sauces). This prevents silent composition loss.
*
* Item shapes:
* Simple: { id, type, categorie, libelle, prix_cents, quantite, image }
* Menu: { ...above, composition: {...}, supplement_cents: number }
*
* @param {Object} item
*/
export function addToCart(item) {
const cart = getCart();
if (item.type !== 'menu') {
const existing = cart.find(c => c.id === item.id && c.type === item.type);
if (existing) {
existing.quantite += item.quantite ?? 1;
setCart(cart);
return;
}
}
cart.push({ quantite: 1, ...item });
setCart(cart);
}
/**
* Removes the item at the given index from the cart.
* @param {number} index
*/
export function removeFromCart(index) {
const cart = getCart();
cart.splice(index, 1);
setCart(cart);
}
/**
* Sets the quantity for the item at the given index.
* If qty reaches 0, the item is removed.
* @param {number} index
* @param {number} qty
*/
export function updateQuantity(index, qty) {
const cart = getCart();
if (qty <= 0) {
cart.splice(index, 1);
} else {
cart[index].quantite = qty;
}
setCart(cart);
}
/**
* Empties the cart completely.
*/
export function clearCart() {
localStorage.removeItem(STORAGE_KEY_CART);
}
/* --- Totals -------------------------------------------------------------- */
/**
* Computes the line total in centimes for a menu item including size supplements.
* For simple product items the caller should use (prix_cents * quantite) directly.
* @param {{ prix_cents: number, supplement_cents: number, quantite: number }} item
* @returns {number}
*/
export function computeMenuLineCents(item) {
return (item.prix_cents + (item.supplement_cents ?? 0)) * item.quantite;
}
/**
* Returns the sum of all line totals in centimes.
* Menu items include their size supplements; simple items do not carry supplements.
* @returns {number}
*/
export function getTotalCents() {
return getCart().reduce((sum, item) => {
if (item.type === 'menu') {
return sum + computeMenuLineCents(item);
}
return sum + item.prix_cents * item.quantite;
}, 0);
}
/* --- Formatting helpers -------------------------------------------------- */
/**
* Formats a centimes integer into a French locale price string.
* Example: 490 -> "4,90 EUR"
* @param {number} cents
* @returns {string}
*/
export function formatPrice(cents) {
const euros = cents / 100;
return euros.toLocaleString('fr-FR', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}) + ' EUR';
}
/**
* Returns the item count (sum of all quantities) in the cart.
* Used to show a badge on the cart button.
* @returns {number}
*/
export function getCartCount() {
return getCart().reduce((sum, item) => sum + item.quantite, 0);
}

View file

@ -0,0 +1,99 @@
<!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">
<meta name="description" content="Wakdo - Votre panier">
<title>Wakdo - Panier</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="cart-page">
<!--
cart.html — Shopping cart.
page-cart.js renders all cart lines, handles qty controls and removal.
TVA: 10% (restauration France 2024 — simplified rate).
TODO: verify exact rate with accountant in P3 — actual rate depends
on sur-place vs a-emporter and product type (alcohol, etc.).
The stored prices are TTC. HT is back-calculated at display time only.
-->
<header class="site-header">
<a
class="site-header__back"
href="categories.html"
aria-label="Continuer mes achats"
>
&#8592; Continuer
</a>
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
<span class="mode-badge site-header__mode" data-mode-badge aria-label="Mode de consommation">Sur place</span>
</header>
<main class="cart-main" aria-label="Votre panier">
<h1 class="cart-main__heading">Votre panier</h1>
<!-- Empty cart state -->
<div id="cart-empty" class="cart-empty" hidden>
<p class="cart-empty__message">Votre panier est vide.</p>
<a class="btn btn--secondary" href="categories.html">Decouvrir nos produits</a>
</div>
<!-- Cart lines -->
<ul id="cart-list" class="cart-list" aria-label="Lignes du panier">
<!-- Filled by page-cart.js -->
</ul>
<!-- Order summary -->
<aside id="cart-summary" class="cart-summary" hidden aria-label="Recapitulatif de commande">
<div class="cart-summary__line">
<span>Total HT</span>
<span id="total-ht"></span>
</div>
<div class="cart-summary__line">
<!-- TVA 10% — taux restauration FR 2024 (simplifie, voir commentaire ci-dessus) -->
<span>TVA (10%)</span>
<span id="total-tva"></span>
</div>
<div class="cart-summary__line cart-summary__line--total">
<span>Total TTC</span>
<strong id="total-ttc"></strong>
</div>
</aside>
<!-- Actions -->
<div class="cart-actions">
<button
id="abandon-btn"
class="btn btn--secondary"
type="button"
aria-label="Abandonner la commande et retourner aux categories"
>
Abandonner
</button>
<a
id="pay-btn"
class="btn btn--primary"
href="payment.html"
role="button"
aria-label="Passer au paiement"
aria-disabled="true"
>
Valider ma commande
</a>
</div>
</main>
<script type="module" src="assets/js/nav.js"></script>
<script type="module" src="assets/js/page-cart.js"></script>
</body>
</html>

View file

@ -0,0 +1,179 @@
<!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">
<meta name="description" content="Wakdo - Choisissez une categorie de produits">
<title>Wakdo - Categories</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="categories-page">
<!--
Categories screen.
Data source: docs/merise/_sources/categories.json (9 categories).
Image paths: assets/images/categories/{title}.png — verified against filesystem.
In P4 this page will be generated dynamically from GET /api/categories.
For now it is a static scaffold that matches the data contract exactly.
-->
<header class="site-header">
<a href="index.html" class="site-header__back" aria-label="Retour a l'accueil">
&#8592; Retour
</a>
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
<!--
Empty div keeps the logo centered via space-between.
When the cart icon is added in P5, it replaces this placeholder.
-->
<div aria-hidden="true" style="width: 80px;"></div>
</header>
<main class="categories-main" aria-label="Categories de produits">
<h1 class="categories-main__heading">Que souhaitez-vous commander&nbsp;?</h1>
<p class="categories-main__sub">Choisissez une categorie pour decouvrir nos produits</p>
<!--
9 categories from categories.json, in the same order as the source.
Each card links to a product page (products.html?category=<id>) — stub URL
for future P5 implementation. The link is functional HTML; no JS needed.
title field from JSON used as alt text and visible label.
-->
<nav class="category-grid" aria-label="Navigation par categorie">
<!-- id: 1 | title: menus -->
<a
class="category-card"
href="products.html?category=1"
aria-label="Voir les menus"
>
<img
class="category-card__image"
src="assets/images/categories/menus.png"
alt="Menus"
>
<span class="category-card__label">Menus</span>
</a>
<!-- id: 2 | title: boissons -->
<a
class="category-card"
href="products.html?category=2"
aria-label="Voir les boissons"
>
<img
class="category-card__image"
src="assets/images/categories/boissons.png"
alt="Boissons"
>
<span class="category-card__label">Boissons</span>
</a>
<!-- id: 3 | title: burgers -->
<a
class="category-card"
href="products.html?category=3"
aria-label="Voir les burgers"
>
<img
class="category-card__image"
src="assets/images/categories/burgers.png"
alt="Burgers"
>
<span class="category-card__label">Burgers</span>
</a>
<!-- id: 4 | title: frites -->
<a
class="category-card"
href="products.html?category=4"
aria-label="Voir les frites"
>
<img
class="category-card__image"
src="assets/images/categories/frites.png"
alt="Frites"
>
<span class="category-card__label">Frites</span>
</a>
<!-- id: 5 | title: encas -->
<a
class="category-card"
href="products.html?category=5"
aria-label="Voir les encas"
>
<img
class="category-card__image"
src="assets/images/categories/encas.png"
alt="Encas"
>
<span class="category-card__label">Encas</span>
</a>
<!-- id: 6 | title: wraps -->
<a
class="category-card"
href="products.html?category=6"
aria-label="Voir les wraps"
>
<img
class="category-card__image"
src="assets/images/categories/wraps.png"
alt="Wraps"
>
<span class="category-card__label">Wraps</span>
</a>
<!-- id: 7 | title: salades -->
<a
class="category-card"
href="products.html?category=7"
aria-label="Voir les salades"
>
<img
class="category-card__image"
src="assets/images/categories/salades.png"
alt="Salades"
>
<span class="category-card__label">Salades</span>
</a>
<!-- id: 8 | title: desserts -->
<a
class="category-card"
href="products.html?category=8"
aria-label="Voir les desserts"
>
<img
class="category-card__image"
src="assets/images/categories/desserts.png"
alt="Desserts"
>
<span class="category-card__label">Desserts</span>
</a>
<!-- id: 9 | title: sauces -->
<a
class="category-card"
href="products.html?category=9"
aria-label="Voir les sauces"
>
<img
class="category-card__image"
src="assets/images/categories/sauces.png"
alt="Sauces"
>
<span class="category-card__label">Sauces</span>
</a>
</nav>
</main>
</body>
</html>

View file

@ -0,0 +1,69 @@
<!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">
<meta name="description" content="Wakdo - Commande confirmee">
<title>Wakdo - Confirmation</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="confirmation-page">
<!--
confirmation.html — Order confirmed screen.
Order number: "WK-" + Date.now().toString(36).toUpperCase()
Cart is cleared on page load by page-confirmation.js.
-->
<header class="site-header site-header--minimal">
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
</header>
<main class="confirmation-main" aria-label="Confirmation de commande">
<div class="confirmation-banner" role="status" aria-live="polite">
<!-- Checkmark SVG (inline, no external dependency) -->
<svg class="confirmation-banner__check" viewBox="0 0 80 80" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
<circle cx="40" cy="40" r="38" fill="#FFC72C"/>
<polyline points="22,40 34,54 58,28" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h1 class="confirmation-banner__title">Commande confirmee !</h1>
<p class="confirmation-banner__sub">Votre commande est en preparation</p>
<div class="confirmation-banner__number-block">
<span class="confirmation-banner__number-label">Votre numero de commande</span>
<strong class="confirmation-banner__number" id="order-number" aria-live="polite"></strong>
</div>
<p class="confirmation-banner__total">
Montant regle : <strong id="order-total"></strong>
</p>
<p class="confirmation-banner__delay">
Temps d'attente estime : <strong>5 - 10 minutes</strong>
</p>
</div>
<button
id="new-order-btn"
class="btn btn--primary btn--large confirmation-new-order"
type="button"
aria-label="Demarrer une nouvelle commande"
>
Nouvelle commande
</button>
</main>
<script type="module" src="assets/js/page-confirmation.js"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
[
{ "id": 1, "title": "menus", "slug": "menus", "image": "assets/images/categories/menus.png" },
{ "id": 2, "title": "boissons", "slug": "boissons", "image": "assets/images/categories/boissons.png" },
{ "id": 3, "title": "burgers", "slug": "burgers", "image": "assets/images/categories/burgers.png" },
{ "id": 4, "title": "frites", "slug": "frites", "image": "assets/images/categories/frites.png" },
{ "id": 5, "title": "encas", "slug": "encas", "image": "assets/images/categories/encas.png" },
{ "id": 6, "title": "wraps", "slug": "wraps", "image": "assets/images/categories/wraps.png" },
{ "id": 7, "title": "salades", "slug": "salades", "image": "assets/images/categories/salades.png" },
{ "id": 8, "title": "desserts", "slug": "desserts", "image": "assets/images/categories/desserts.png" },
{ "id": 9, "title": "sauces", "slug": "sauces", "image": "assets/images/categories/sauces.png" }
]

View file

@ -0,0 +1,86 @@
{
"menus": [
{ "id": 1, "nom": "Menu Le 280", "prix": 880, "image": "assets/images/produits/burgers/280.png", "type": "menu" },
{ "id": 2, "nom": "Menu Big Tasty", "prix": 1060, "image": "assets/images/produits/burgers/big-tasty-1-viande.png", "type": "menu" },
{ "id": 3, "nom": "Menu Big Tasty Bacon", "prix": 1090, "image": "assets/images/produits/burgers/big-tasty-bacon-1-viande.png", "type": "menu" },
{ "id": 4, "nom": "Menu Big Mac", "prix": 800, "image": "assets/images/produits/burgers/bigmac.png", "type": "menu" },
{ "id": 5, "nom": "Menu CBO", "prix": 1090, "image": "assets/images/produits/burgers/cbo.png", "type": "menu" },
{ "id": 6, "nom": "Menu MC Chicken", "prix": 930, "image": "assets/images/produits/burgers/mcchicken.png", "type": "menu" },
{ "id": 7, "nom": "Menu MC Crispy", "prix": 720, "image": "assets/images/produits/burgers/mccrispy.png", "type": "menu" },
{ "id": 8, "nom": "Menu MC Fish", "prix": 720, "image": "assets/images/produits/burgers/mcfish.png", "type": "menu" },
{ "id": 9, "nom": "Menu Royal Bacon", "prix": 705, "image": "assets/images/produits/burgers/royalbacon.png", "type": "menu" },
{ "id": 10, "nom": "Menu Royal Cheese", "prix": 640, "image": "assets/images/produits/burgers/royalcheese.png", "type": "menu" },
{ "id": 11, "nom": "Menu Royal Deluxe", "prix": 740, "image": "assets/images/produits/burgers/royaldeluxe.png", "type": "menu" },
{ "id": 12, "nom": "Menu Signature BBQ Beef 2 viandes","prix": 1350,"image": "assets/images/produits/burgers/signature-bbq-beef-2-viandes.png", "type": "menu" },
{ "id": 13, "nom": "Menu Signature Beef BBQ", "prix": 1190, "image": "assets/images/produits/burgers/signature-beef-bbq-burger-1-viande.png", "type": "menu" }
],
"burgers": [
{ "id": 14, "nom": "Le 280", "prix": 680, "image": "assets/images/produits/burgers/280.png", "type": "produit" },
{ "id": 15, "nom": "Big Tasty", "prix": 860, "image": "assets/images/produits/burgers/big-tasty-1-viande.png", "type": "produit" },
{ "id": 16, "nom": "Big Tasty Bacon", "prix": 890, "image": "assets/images/produits/burgers/big-tasty-bacon-1-viande.png", "type": "produit" },
{ "id": 17, "nom": "Big Mac", "prix": 600, "image": "assets/images/produits/burgers/bigmac.png", "type": "produit" },
{ "id": 18, "nom": "CBO", "prix": 890, "image": "assets/images/produits/burgers/cbo.png", "type": "produit" },
{ "id": 19, "nom": "MC Chicken", "prix": 730, "image": "assets/images/produits/burgers/mcchicken.png", "type": "produit" },
{ "id": 20, "nom": "MC Crispy", "prix": 530, "image": "assets/images/produits/burgers/mccrispy.png", "type": "produit" },
{ "id": 21, "nom": "MC Fish", "prix": 485, "image": "assets/images/produits/burgers/mcfish.png", "type": "produit" },
{ "id": 22, "nom": "Royal Bacon", "prix": 510, "image": "assets/images/produits/burgers/royalbacon.png", "type": "produit" },
{ "id": 23, "nom": "Royal Cheese", "prix": 440, "image": "assets/images/produits/burgers/royalcheese.png", "type": "produit" },
{ "id": 24, "nom": "Royal Deluxe", "prix": 540, "image": "assets/images/produits/burgers/royaldeluxe.png", "type": "produit" },
{ "id": 25, "nom": "Signature BBQ Beef 2 viandes","prix": 1140,"image": "assets/images/produits/burgers/signature-bbq-beef-2-viandes.png","type": "produit" },
{ "id": 26, "nom": "Signature Beef BBQ", "prix": 1030, "image": "assets/images/produits/burgers/signature-beef-bbq-burger-1-viande.png","type": "produit" }
],
"boissons": [
{ "id": 27, "nom": "Coca Cola", "prix": 190, "image": "assets/images/produits/boissons/coca-cola.png", "type": "produit" },
{ "id": 28, "nom": "Coca Sans Sucres", "prix": 190, "image": "assets/images/produits/boissons/coca-sans-sucres.png", "type": "produit" },
{ "id": 29, "nom": "Eau", "prix": 100, "image": "assets/images/produits/boissons/eau.png", "type": "produit" },
{ "id": 30, "nom": "Fanta Orange", "prix": 190, "image": "assets/images/produits/boissons/fanta.png", "type": "produit" },
{ "id": 31, "nom": "Ice Tea Peche", "prix": 190, "image": "assets/images/produits/boissons/ice-tea-peche.png", "type": "produit" },
{ "id": 32, "nom": "Ice Tea Citron", "prix": 190, "image": "assets/images/produits/boissons/the-vert-citron-sans-sucres.png", "type": "produit" },
{ "id": 33, "nom": "Jus d'Orange", "prix": 210, "image": "assets/images/produits/boissons/jus-orange.png", "type": "produit" },
{ "id": 34, "nom": "Jus de Pommes Bio", "prix": 230, "image": "assets/images/produits/boissons/jus-pomme-bio.png", "type": "produit" }
],
"frites": [
{ "id": 35, "nom": "Petite Frite", "prix": 145, "image": "assets/images/produits/frites/petite-frite.png", "type": "produit" },
{ "id": 36, "nom": "Moyenne Frite", "prix": 275, "image": "assets/images/produits/frites/moyenne-frite.png", "type": "produit" },
{ "id": 37, "nom": "Grande Frite", "prix": 350, "image": "assets/images/produits/frites/grande-frite.png", "type": "produit" },
{ "id": 38, "nom": "Potatoes", "prix": 215, "image": "assets/images/produits/frites/potatoes.png", "type": "produit" },
{ "id": 39, "nom": "Grande Potatoes", "prix": 340, "image": "assets/images/produits/frites/grande-potatoes.png", "type": "produit" }
],
"encas": [
{ "id": 40, "nom": "Cheeseburger", "prix": 260, "image": "assets/images/produits/encas/cheeseburger.png", "type": "produit" },
{ "id": 41, "nom": "Croc MCdo", "prix": 320, "image": "assets/images/produits/encas/croc-mc-do.png", "type": "produit" },
{ "id": 42, "nom": "Nuggets x4", "prix": 420, "image": "assets/images/produits/encas/nuggets-4.png", "type": "produit" },
{ "id": 43, "nom": "Nuggets x20", "prix": 1300, "image": "assets/images/produits/encas/nuggets-20.png", "type": "produit" }
],
"desserts": [
{ "id": 44, "nom": "Brownie", "prix": 260, "image": "assets/images/produits/desserts/brownies.png", "type": "produit" },
{ "id": 45, "nom": "Cheesecake Chocolat M&M's","prix": 310, "image": "assets/images/produits/desserts/cheesecake-choconuts-m&m-s.png", "type": "produit" },
{ "id": 46, "nom": "Cheesecake Fraise", "prix": 310, "image": "assets/images/produits/desserts/cheesecake-fraise.png", "type": "produit" },
{ "id": 47, "nom": "Cookie", "prix": 320, "image": "assets/images/produits/desserts/cookie.png", "type": "produit" },
{ "id": 48, "nom": "Donut", "prix": 260, "image": "assets/images/produits/desserts/doghnut.png", "type": "produit" },
{ "id": 49, "nom": "Macarons", "prix": 270, "image": "assets/images/produits/desserts/macarons.png", "type": "produit" },
{ "id": 50, "nom": "MC Fleury", "prix": 440, "image": "assets/images/produits/desserts/mcfleury.png", "type": "produit" },
{ "id": 51, "nom": "Muffin", "prix": 360, "image": "assets/images/produits/desserts/muffin.png", "type": "produit" },
{ "id": 52, "nom": "Sunday", "prix": 100, "image": "assets/images/produits/desserts/sunday.png", "type": "produit" }
],
"sauces": [
{ "id": 53, "nom": "Classic Barbecue", "prix": 70, "image": "assets/images/produits/sauces/classic-barbecue.png", "type": "produit" },
{ "id": 54, "nom": "Classic Moutarde", "prix": 70, "image": "assets/images/produits/sauces/classic-moutarde.png", "type": "produit" },
{ "id": 55, "nom": "Creamy Deluxe", "prix": 70, "image": "assets/images/produits/sauces/cremy-deluxe.png", "type": "produit" },
{ "id": 56, "nom": "Ketchup", "prix": 70, "image": "assets/images/produits/sauces/ketchup.png", "type": "produit" },
{ "id": 57, "nom": "Chinoise", "prix": 70, "image": "assets/images/produits/sauces/sauce-chinoise.png", "type": "produit" },
{ "id": 58, "nom": "Curry", "prix": 70, "image": "assets/images/produits/sauces/sauce-curry.png", "type": "produit" },
{ "id": 59, "nom": "Pommes Frites", "prix": 70, "image": "assets/images/produits/sauces/sauce-pommes-frite.png", "type": "produit" }
],
"salades": [
{ "id": 60, "nom": "Petite Salade", "prix": 330, "image": "assets/images/produits/salades/petite-salade.png", "type": "produit" },
{ "id": 61, "nom": "Cesar Classic", "prix": 880, "image": "assets/images/produits/salades/salade-classic-caesar.png","type": "produit" },
{ "id": 62, "nom": "Italienne Mozza", "prix": 880, "image": "assets/images/produits/salades/salade-italian-mozza.png", "type": "produit" }
],
"wraps": [
{ "id": 63, "nom": "MC Wrap Chevre", "prix": 310, "image": "assets/images/produits/wraps/mcwrap-chevre.png", "type": "produit" },
{ "id": 64, "nom": "MC Wrap Poulet Bacon", "prix": 330, "image": "assets/images/produits/wraps/mcwrap-poulet-bacon.png","type": "produit" },
{ "id": 65, "nom": "Ptit Wrap Chevre", "prix": 260, "image": "assets/images/produits/wraps/ptit-wrap-chevre.png", "type": "produit" },
{ "id": 66, "nom": "Ptit Wrap Ranch", "prix": 260, "image": "assets/images/produits/wraps/ptit-wrap-ranch.png", "type": "produit" }
]
}

View file

@ -3,18 +3,79 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--
noindex: this kiosk is not a public website; no SEO indexing needed.
nofollow: prevents link-following by bots reaching the kiosk URL.
-->
<meta name="robots" content="noindex, nofollow">
<title>Wakdo - borne client</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; color: #222; }
img { max-height: 80px; }
small { color: #666; }
</style>
<meta name="description" content="Borne de commande Wakdo - choisissez votre mode de consommation">
<title>Wakdo - Bienvenue</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<img src="/assets/images/ui/logo.png" alt="Wakdo">
<h1>Wakdo - borne client</h1>
<p>En construction.</p>
<p><small>Phase P1 - conception Merise en cours. Le front borne sera implemente en phase P5.</small></p>
<!--
Welcome / landing screen.
Matches maquette page 1: background photo, white card, two choice buttons.
Navigation is purely HTML <a> links — no JS required for this screen.
-->
<main class="welcome" aria-label="Ecran d'accueil Wakdo">
<!--
mc-landing-banner.png is the background photo (M arches + food).
alt="" because it is purely decorative — the content lives in the card.
-->
<img
class="welcome__bg"
src="assets/images/ui/mc-landing-banner.png"
alt=""
aria-hidden="true"
>
<section class="welcome__card" aria-labelledby="welcome-heading">
<h1 class="welcome__greeting" id="welcome-heading">Bonjour,</h1>
<p class="welcome__question">
Souhaitez-vous consommer votre menu sur place<br>
ou preferez-vous l'emporter&nbsp;?
</p>
<nav class="welcome__choices" aria-label="Mode de consommation">
<!--
href passes the choice via query string.
The categories page reads it to display the correct mode label.
No JS required for the navigation itself.
-->
<a
class="choice-btn"
href="categories.html?mode=sur-place"
role="button"
aria-label="Commander sur place"
>
<img
class="choice-btn__image"
src="assets/images/ui/illustration-sur-place.png"
alt="Table et chaises - Sur place"
>
<span class="choice-btn__label">Sur Place</span>
</a>
<a
class="choice-btn"
href="categories.html?mode=a-emporter"
role="button"
aria-label="Commander a emporter"
>
<img
class="choice-btn__image"
src="assets/images/ui/illustration-a-emporter.png"
alt="Sac a emporter"
>
<span class="choice-btn__label">A Emporter</span>
</a>
</nav>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,120 @@
<!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">
<meta name="description" content="Wakdo - Choisissez votre mode de paiement">
<title>Wakdo - Paiement</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="payment-page">
<!--
payment.html — Payment method selection.
MVP: both buttons simulate payment and redirect to confirmation.html.
No real payment integration (kiosk demo — out of scope permanently).
-->
<header class="site-header">
<a
class="site-header__back"
href="cart.html"
aria-label="Retour au panier"
>
&#8592; Panier
</a>
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
<span class="mode-badge site-header__mode" data-mode-badge aria-label="Mode de consommation">Sur place</span>
</header>
<main class="payment-main" aria-label="Choix du mode de paiement">
<h1 class="payment-main__heading">Comment souhaitez-vous payer ?</h1>
<!-- Mini order recap injected by inline script using localStorage -->
<div class="payment-recap" id="payment-recap" aria-label="Recapitulatif de commande">
<!-- Filled by inline module below -->
</div>
<div class="payment-methods" role="group" aria-label="Modes de paiement">
<!--
Carte bancaire — simulates payment for MVP.
Both methods redirect to confirmation.html.
-->
<button
class="payment-choice"
id="pay-card"
type="button"
aria-label="Payer par carte bancaire"
>
<!-- Card SVG icon (inline, no external dependency) -->
<svg class="payment-choice__icon" viewBox="0 0 64 64" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="14" width="56" height="36" rx="6" ry="6" fill="#FFC72C"/>
<rect x="4" y="24" width="56" height="8" fill="#1A1A1A"/>
<rect x="12" y="36" width="16" height="6" rx="2" fill="#fff"/>
</svg>
<span class="payment-choice__label">Carte bancaire</span>
</button>
<!-- Especes -->
<button
class="payment-choice"
id="pay-cash"
type="button"
aria-label="Payer en especes"
>
<!-- Cash SVG icon (inline) -->
<svg class="payment-choice__icon" viewBox="0 0 64 64" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="20" width="56" height="32" rx="4" ry="4" fill="#FFC72C"/>
<circle cx="32" cy="36" r="8" fill="#fff"/>
<circle cx="32" cy="36" r="4" fill="#FFC72C"/>
<rect x="8" y="12" width="48" height="6" rx="2" fill="#E6A800"/>
</svg>
<span class="payment-choice__label">Especes</span>
</button>
</div>
</main>
<script type="module">
import { getTotalCents, formatPrice, getCart } from './assets/js/state.js';
import { getMode } from './assets/js/state.js';
/* Show mini recap */
const recap = document.getElementById('payment-recap');
const total = getTotalCents();
const items = getCart();
const mode = getMode() === 'a-emporter' ? 'A emporter' : 'Sur place';
if (recap) {
if (!items.length) {
/* Guard: redirect back to cart if somehow empty */
window.location.href = 'cart.html';
} else {
recap.innerHTML = `
<p class="payment-recap__mode">${mode}</p>
<p class="payment-recap__items">${items.length} article${items.length > 1 ? 's' : ''}</p>
<p class="payment-recap__total">Total : <strong>${formatPrice(total)}</strong></p>
`;
}
}
/* Both payment methods redirect to confirmation — MVP simulation */
function simulatePay() {
window.location.href = 'confirmation.html';
}
document.getElementById('pay-card').addEventListener('click', simulatePay);
document.getElementById('pay-cash').addEventListener('click', simulatePay);
</script>
<script type="module" src="assets/js/nav.js"></script>
</body>
</html>

View file

@ -0,0 +1,65 @@
<!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">
<meta name="description" content="Wakdo - Detail du produit">
<title>Wakdo - Produit</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="product-page">
<!--
product.html — Product detail screen.
Reads ?id=<int>&category=<slug>.
JS (page-product.js) fetches the product, renders detail and handles
the "Ajouter au panier" action.
Menu composition is shown as a fixed note (MVP: no composition selection).
-->
<header class="site-header">
<a
id="back-to-products"
class="site-header__back"
href="products.html"
aria-label="Retour a la liste des produits"
>
&#8592; Retour
</a>
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
<a
class="site-header__cart"
href="cart.html"
aria-label="Voir le panier"
>
<span class="cart-icon" aria-hidden="true">&#128722;</span>
<span class="cart-badge" data-cart-count hidden aria-live="polite">0</span>
<span class="sr-only">Panier</span>
</a>
</header>
<main class="product-main" aria-label="Detail du produit">
<!-- Error block: hidden unless fetch fails or id invalid -->
<p id="product-error" class="product-error" hidden role="alert"></p>
<!--
Container filled by page-product.js.
The JS replaces innerHTML once data is ready.
-->
<div id="product-detail" class="product-detail" aria-live="polite">
<!-- Skeleton placeholder visible during fetch -->
<div class="product-detail__skeleton" aria-hidden="true"></div>
</div>
</main>
<script type="module" src="assets/js/nav.js"></script>
<script type="module" src="assets/js/page-product.js"></script>
</body>
</html>

View file

@ -0,0 +1,68 @@
<!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">
<meta name="description" content="Wakdo - Produits de la categorie selectionnee">
<title>Wakdo - Produits</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="products-page">
<!--
products.html — List of products in a category.
Category is determined at runtime from ?category=<id>.
JS (page-products.js) fetches data/produits.json and renders cards.
In P4: swap fetch URL in data.js to point to GET /api/products?category=<slug>.
-->
<header class="site-header">
<a
id="back-to-categories"
class="site-header__back"
href="categories.html"
aria-label="Retour aux categories"
>
&#8592; Categories
</a>
<img
class="site-header__logo"
src="assets/images/ui/logo.png"
alt="Wakdo"
>
<a
class="site-header__cart"
href="cart.html"
aria-label="Voir le panier"
>
<span class="cart-icon" aria-hidden="true">&#128722;</span>
<span class="cart-badge" data-cart-count hidden aria-live="polite">0</span>
<span class="sr-only">Panier</span>
</a>
</header>
<main class="products-main" aria-label="Liste des produits">
<div class="products-header">
<!--
Heading is updated by page-products.js once the category
data is loaded — default text shown during load.
-->
<h1 id="products-heading" class="products-main__heading">Nos produits</h1>
<span class="mode-badge" data-mode-badge aria-label="Mode de consommation">Sur place</span>
</div>
<!-- Error block: hidden by default, shown if fetch fails -->
<p id="products-error" class="products-error" hidden role="alert"></p>
<ul id="products-grid" class="products-grid" aria-label="Grille de produits">
<!-- Product cards injected by page-products.js -->
</ul>
</main>
<script type="module" src="assets/js/nav.js"></script>
<script type="module" src="assets/js/page-products.js"></script>
</body>
</html>