feat(admin): dashboard donnees reelles (lot 2) (#50)
This commit is contained in:
parent
8d1a69f5cf
commit
1b29cd420f
3 changed files with 99 additions and 12 deletions
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ?> — 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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue