feat(r1.2): écrans web du référentiel — 7 écrans fidèles à maquette-r1

- Sites : liste + carte Leaflet/OSM réelle (pins maquette, positions
  PostGIS), création en modale avec position posée au clic sur la carte
- Fiche site : arbre site→zones, appareils rattachés, ajout de zone
- Ascenseurs : table dense, filtres site/statut, strie rouge à l'arrêt ;
  fiche appareil (organes ± ajout/retrait, statut, QR réel) ; création
  identité → rattachement → organes ; étiquette A6 imprimable (objet
  papier, window.print n'imprime qu'elle)
- Personnes & équipes : invitation → lien d'activation affiché à copier
  (pas d'email en R1, décision explicite), renvoi de lien, équipes ;
  page /activation (choix du mot de passe, connexion directe)
- Catégories : ajout, renommage inline, désactivation
- navigation et actions pilotées par la matrice (usePermissions) —
  l'API re-vérifie chaque requête
- e2e Playwright : recette R1 officielle rejouée intégralement (site →
  zone → appareil+organe → étiquette → invitation → activation) +
  parcours « la matrice pilote l'UI » ; purge idempotente en globalSetup
- alias Vite @siop/shared → source TS (exports nommés CJS ↯ workspace lié)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 13:06:45 +01:00
parent 266ffaaf1b
commit 9778629827
29 changed files with 2350 additions and 35 deletions

View File

@@ -0,0 +1,97 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ASSET_STATUS_LABELS, ASSET_STATUSES, type AssetStatus } from '@siop/shared';
import { useAssets } from '@/api/referentiel';
import { usePermissions } from '@/auth/use-permissions';
import { StatutEquipement } from '@/components/statut-equipement';
import { Button } from '@/components/ui/button';
/** Écran 3 des maquettes R1 : inventaire dense du parc. */
export default function PageAscenseurs() {
const navigate = useNavigate();
const { data: assets } = useAssets();
const { can } = usePermissions();
const [site, setSite] = useState('');
const [statut, setStatut] = useState<'' | AssetStatus>('');
const tous = assets ?? [];
const sites = [...new Set(tous.map((a) => a.siteName))].sort();
const visibles = tous.filter(
(a) => (!site || a.siteName === site) && (!statut || a.status === statut),
);
const arret = tous.filter((a) => a.status === 'OUT_OF_SERVICE').length;
return (
<>
<div className="entete-page">
<h1>Ascenseurs</h1>
<span className="filajout">
{tous.length} appareils{arret ? ` · ${arret} à larrêt` : ''}
</span>
<div className="actions">
{can('ASSETS', 'create') ? (
<Button variant="prim" onClick={() => navigate('/ascenseurs/nouveau')}>
+ Nouvel ascenseur
</Button>
) : null}
</div>
</div>
<div className="filtres">
<select aria-label="Filtrer par site" value={site} onChange={(e) => setSite(e.target.value)}>
<option value="">Site : tous</option>
{sites.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
<select
aria-label="Filtrer par statut"
value={statut}
onChange={(e) => setStatut(e.target.value as '' | AssetStatus)}
>
<option value="">Statut : tous</option>
{ASSET_STATUSES.map((s) => (
<option key={s} value={s}>{ASSET_STATUS_LABELS[s]}</option>
))}
</select>
</div>
<div className="carte" style={{ padding: 0 }}>
<div className="table">
<table>
<thead>
<tr>
<th>Appareil</th>
<th>Marque · modèle</th>
<th>Site / emplacement</th>
<th>Catégorie</th>
<th>Statut</th>
<th>Organes</th>
</tr>
</thead>
<tbody>
{visibles.map((a) => (
<tr
key={a.id}
className={a.status === 'OUT_OF_SERVICE' ? 'ligne-bloque' : undefined}
onClick={() => navigate(`/ascenseurs/${a.id}`)}
style={{ cursor: 'pointer' }}
>
<td className="ref num">{a.reference}</td>
<td>{a.brand} {a.model ?? ''}</td>
<td>
{a.siteName}
{a.locationName !== a.siteName ? (
<span className="sous">{a.locationName}</span>
) : null}
</td>
<td>{a.categoryName}</td>
<td><StatutEquipement statut={a.status} /></td>
<td className="num">{a.componentCount}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
);
}