mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-09 13:11:54 +00:00
- 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>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
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>
|
||
</>
|
||
);
|
||
}
|