mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-09 21:21:54 +00:00
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:
97
apps/web/src/pages/ascenseurs.tsx
Normal file
97
apps/web/src/pages/ascenseurs.tsx
Normal 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} à l’arrê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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user