feat(r3.3): écrans web de la gestion — stock, achats, coûts OT, documents, statistiques

- Stock : alertes sous seuil (strie ambre), « Préparer le BC » sur la ligne ;
  fiche pièce où les mouvements SONT le stock (entrée / ajustement motivé)
- Bons de commande : liste + panneau (envoi, annulation, réception → entrées
  de stock à PU figé) ; création préremplie depuis l'alerte de seuil
- Fiche OT : carte « Pièces & main-d'œuvre » réelle (consommation à prix figé
  annoncé avant le clic, temps au taux figé) + carte Documents (upload/download) ;
  même carte Documents sur la fiche ascenseur
- Statistiques : 4 KPI, pannes par organe (bilans codés), coûts par mois,
  top équipements ; le tableau de bord allume « Pannes par organe »
- Tiers, bibliothèque (upload rattaché, filtre par type), taux horaire
  éditable dans Personnes ; nav Ressources/Pilotage activée par la matrice
- Recette R3 Playwright (2 parcours autonomes) ; cleanup-e2e étendu aux
  entités R3 ; le formulaire BC attend la fin du refetch (stock périmé en cache)

13/13 e2e verts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 22:16:56 +01:00
parent a4e98c2000
commit c5382a5538
20 changed files with 2056 additions and 28 deletions

View File

@@ -6,6 +6,7 @@ import {
useResendInvitation,
useRoles,
useTeams,
useUpdateUser,
useUsers,
} from '@/api/referentiel';
import { usePermissions } from '@/auth/use-permissions';
@@ -44,6 +45,7 @@ export default function PagePersonnes() {
<th>Personne</th>
<th>Rôle</th>
<th>Équipe</th>
<th>Taux horaire</th>
<th>Statut</th>
<th></th>
</tr>
@@ -66,6 +68,13 @@ export default function PagePersonnes() {
</td>
<td>{u.role.name}</td>
<td>{u.teams.map((t) => t.name).join(', ') || '—'}</td>
<td>
<CelluleTaux
userId={u.id}
taux={u.hourlyRate}
editable={can('PEOPLE_TEAMS', 'edit')}
/>
</td>
<td>
{u.status === 'active' ? (
<span className="st termine">Actif</span>
@@ -139,6 +148,64 @@ export default function PagePersonnes() {
);
}
/** Taux horaire courant (R3) — appliqué aux futures saisies de temps ; les
* temps déjà valorisés gardent leur taux figé. */
function CelluleTaux({
userId,
taux,
editable,
}: {
userId: string;
taux: number | null;
editable: boolean;
}) {
const maj = useUpdateUser();
const [edition, setEdition] = useState(false);
if (!edition) {
return (
<span className="num">
{taux != null ? `${new Intl.NumberFormat('fr-FR').format(taux)} MAD/h` : '—'}
{editable ? (
<Button
variant="ghost"
className="ml-1 px-2 py-0.5 text-[12px]"
aria-label="Modifier le taux horaire"
onClick={() => setEdition(true)}
>
</Button>
) : null}
</span>
);
}
return (
<input
type="number"
min={0}
step="0.01"
autoFocus
defaultValue={taux ?? ''}
aria-label="Nouveau taux horaire (MAD/h)"
className="num w-24 rounded-lg border border-bordure-forte bg-surface px-2 py-1 text-encre"
disabled={maj.isPending}
onBlur={(e) => {
const valeur = e.target.value.trim();
const nombre = Number(valeur);
if (valeur && nombre > 0 && nombre !== taux) {
maj.mutate({ id: userId, hourlyRate: nombre }, { onSettled: () => setEdition(false) });
} else {
setEdition(false);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
if (e.key === 'Escape') setEdition(false);
}}
/>
);
}
function ModaleInvitation({
ouverte,
surFermeture,