feat(admin): dashboard branche aux donnees reelles (compteurs catalogue + stock critique)
Some checks failed
CI / secret-scan (push) Successful in 23s
CI / php-lint (push) Successful in 21s
CI / static-tests (push) Successful in 50s
CI / js-tests (push) Successful in 28s
CI / secret-scan (pull_request) Successful in 28s
CI / php-lint (pull_request) Successful in 46s
CI / static-tests (pull_request) Successful in 58s
CI / js-tests (pull_request) Successful in 28s
CI / auto-merge (push) Has been skipped
CI / auto-merge (pull_request) Failing after 7s

KPI du tableau de bord cables sur StatsRepository (seam testable comme StatsController) :
produits actifs, categories, menus, ingredients en stock critique (RG-T21). Vue A+C
(tuiles + alerte rouge conditionnelle). Le feed d'activite reste a faire (choix produit
sur le contenu/gating des evenements audit). Tests verts (PHPUnit 301, PHPStan L6).
This commit is contained in:
Imugiii 2026-06-18 10:16:03 +00:00
parent 8d1a69f5cf
commit 4d87c341aa
3 changed files with 99 additions and 12 deletions

View file

@ -4,12 +4,14 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Catalogue\StatsRepository;
use App\Core\Response; use App\Core\Response;
/** /**
* Tableau de bord back-office. GET /admin/dashboard (landing par defaut du role * Tableau de bord back-office. GET /admin/dashboard (landing par defaut du role
* admin, cf. seed role.default_route). Accessible a tout utilisateur authentifie ; * admin, cf. seed role.default_route). Accessible a tout utilisateur authentifie.
* les KPI reels (stats.read) seront ajoutes au chunk statistiques. * Affiche des indicateurs synthetiques (catalogue + sante stock) ; le detail vit
* sous /admin/stats (permission stats.read).
* *
* Non `final` : les tests sous-classent pour injecter des doubles via les hooks. * Non `final` : les tests sous-classent pour injecter des doubles via les hooks.
*/ */
@ -25,10 +27,22 @@ class DashboardController extends AdminController
return $guard; return $guard;
} }
$stats = $this->statsRepository();
return $this->adminView( return $this->adminView(
'admin/dashboard', 'admin/dashboard',
['title' => 'Tableau de bord - Wakdo Admin', 'activeNav' => 'dashboard'], [
'title' => 'Tableau de bord - Wakdo Admin',
'activeNav' => 'dashboard',
'counts' => $stats->counts(),
'stock' => $stats->stockHealth(),
],
$guard, $guard,
); );
} }
protected function statsRepository(): StatsRepository
{
return new StatsRepository($this->db());
}
} }

View file

@ -3,24 +3,65 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* Fragment du tableau de bord, injecte dans admin/layout.php. Volontairement * Tableau de bord, injecte dans admin/layout.php (direction UI A+C).
* minimal en chunk shell : les KPI reels (ventes, commandes) viendront avec le * Indicateurs synthetiques catalogue + sante stock (StatsRepository).
* chunk statistiques (permission stats.read).
* *
* @var string $currentUserName * @var string $currentUserName
* @var array<string, array<string,int>> $counts
* @var array{bands:array<string,int>} $stock
*/ */
$name = htmlspecialchars($currentUserName ?? 'Utilisateur', ENT_QUOTES, 'UTF-8'); $name = htmlspecialchars($currentUserName ?? 'Utilisateur', ENT_QUOTES, 'UTF-8');
$kpi = isset($counts) && is_array($counts) ? $counts : [];
$stk = isset($stock) && is_array($stock) ? $stock : [];
$nProducts = (int) ($kpi['products']['available'] ?? 0);
$nCategories = (int) ($kpi['categories']['total'] ?? 0);
$nMenus = (int) ($kpi['menus']['total'] ?? 0);
$nCritical = (int) ($stk['bands']['critical'] ?? 0);
?> ?>
<div class="page-header"> <div class="page-header">
<div> <div>
<h1 class="page-title">Tableau de bord</h1> <h1 class="page-title">Tableau de bord</h1>
<p class="page-subtitle">Bienvenue, <?= $name ?>.</p> <p class="page-subtitle">Bienvenue, <?= $name ?> &mdash; voici l'essentiel de votre restaurant aujourd'hui.</p>
</div> </div>
</div> </div>
<section> <section class="dash-tiles" aria-label="Indicateurs cles">
<p>Le back-office est en ligne. Utilisez la navigation pour gerer le catalogue, <article class="tile">
les commandes et les utilisateurs selon vos permissions.</p> <div class="tile-top">
<p><small>Les indicateurs (ventes, commandes du jour) seront ajoutes prochainement.</small></p> <span class="tile-ico"><svg width="27" height="27" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8h14l-1 11a2 2 0 01-2 2H8a2 2 0 01-2-2L5 8z"/><path d="M9 8a3 3 0 016 0"/></svg></span>
<span class="tile-tag">En vente</span>
</div>
<div class="tile-value"><?= $nProducts ?></div>
<div class="tile-label">Produits actifs</div>
</article>
<article class="tile">
<div class="tile-top">
<span class="tile-ico"><svg width="27" height="27" 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" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg></span>
<span class="tile-tag">Classees</span>
</div>
<div class="tile-value"><?= $nCategories ?></div>
<div class="tile-label">Categories</div>
</article>
<article class="tile">
<div class="tile-top">
<span class="tile-ico"><svg width="27" height="27" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3h14a1 1 0 011 1v17l-8-4-8 4V4a1 1 0 011-1z"/></svg></span>
<span class="tile-tag">Proposes</span>
</div>
<div class="tile-value"><?= $nMenus ?></div>
<div class="tile-label">Menus</div>
</article>
<article class="tile<?= $nCritical > 0 ? ' alert' : '' ?>">
<div class="tile-top">
<span class="tile-ico"><svg width="27" height="27" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l9 16H3l9-16z"/><path d="M12 9v5"/><path d="M12 17.5h.01"/></svg></span>
<span class="tile-tag"><?= $nCritical > 0 ? 'A recommander' : 'OK' ?></span>
</div>
<div class="tile-value"><?= $nCritical ?></div>
<div class="tile-label">Stock critique</div>
</article>
</section> </section>

View file

@ -10,6 +10,7 @@ use App\Auth\GuardResult;
use App\Auth\SessionGuard; use App\Auth\SessionGuard;
use App\Auth\SessionManager; use App\Auth\SessionManager;
use App\Auth\UserDirectory; use App\Auth\UserDirectory;
use App\Catalogue\StatsRepository;
use App\Controllers\DashboardController; use App\Controllers\DashboardController;
use App\Core\Config; use App\Core\Config;
use App\Core\Database; use App\Core\Database;
@ -17,6 +18,32 @@ use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Tests\Support\FakeDatabase; use App\Tests\Support\FakeDatabase;
/**
* Stub de StatsRepository : KPIs canned, sans base (les agregats reels sont
* couverts par StatsRepositoryDbTest).
*/
final class DashStubStatsRepository extends StatsRepository
{
public function counts(): array
{
return [
'products' => ['total' => 53, 'available' => 50],
'categories' => ['total' => 9, 'active' => 9],
'menus' => ['total' => 13, 'available' => 12],
'ingredients' => ['total' => 7, 'active' => 6],
];
}
public function stockHealth(): array
{
return [
'active_total' => 6,
'bands' => ['normal' => 4, 'low' => 1, 'critical' => 1],
'alerts' => [],
];
}
}
/** /**
* Sous-classe de test : injecte session test + FakeDatabase dans la garde, * Sous-classe de test : injecte session test + FakeDatabase dans la garde,
* l'autorisation et l'annuaire, sans base reelle. * l'autorisation et l'annuaire, sans base reelle.
@ -53,6 +80,11 @@ final class TestDashboardController extends DashboardController
return new UserDirectory($this->fakeDb); return new UserDirectory($this->fakeDb);
} }
protected function statsRepository(): StatsRepository
{
return new DashStubStatsRepository($this->fakeDb);
}
/** /**
* Expose le chemin garde par permission d'AdminController::guard() (RG-T03), * Expose le chemin garde par permission d'AdminController::guard() (RG-T03),
* que le dashboard (auth seule) n'exerce pas. * que le dashboard (auth seule) n'exerce pas.