mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur transition/coche/bilan → 409 « Conflit de version » contextualisé (qui, quand) ; toute écriture secondaire (coche, bilan, commentaire, conso, MO) fait avancer la version — sans version fournie, le web est inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où sur l'appareil, purge complète à la déconnexion — correctif réel : la file et le cache persisté survivaient au logout). Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès → propagation de la version fraîche aux saisies restantes du même OT (nos écritures ne se conflictent pas entre elles, un écart étranger reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour / abandonner). Transitions, coches, bilan et photos (D5, compressées ~1600 px) passent par la file avec patch optimiste du cache ; écran Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels (le bilan hors-ligne a ses vocabulaires). Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne → 3 en file → modification concurrente de Salma → conflit tranché → serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
import type { QueryClient } from '@tanstack/react-query';
|
||
import {
|
||
WORK_ORDER_STATUS_LABELS,
|
||
WORK_ORDER_TRANSITIONS,
|
||
type ChecklistState,
|
||
type ReportUpsert,
|
||
type WorkOrderDetail,
|
||
type WorkOrderStatus,
|
||
} from '@siop/shared';
|
||
import { enfiler, lireFile } from './store';
|
||
import { rejouer } from './synchro';
|
||
|
||
/** Chaque geste terrain = une saisie en file + un patch OPTIMISTE du cache
|
||
* (le technicien voit son travail tout de suite, même en mode avion — le
|
||
* cache persisté garde ce patch après redémarrage). En ligne, la file se
|
||
* vide dans la foulée : le comportement R4.2 est conservé. */
|
||
|
||
function patchDetail(
|
||
queryClient: QueryClient,
|
||
otId: string,
|
||
patch: (ot: WorkOrderDetail) => WorkOrderDetail,
|
||
): void {
|
||
queryClient.setQueryData<WorkOrderDetail>(['work-orders', otId], (courant) =>
|
||
courant ? patch(courant) : courant,
|
||
);
|
||
}
|
||
|
||
export function enfilerTransition(
|
||
queryClient: QueryClient,
|
||
ot: WorkOrderDetail,
|
||
to: WorkOrderStatus,
|
||
): void {
|
||
enfiler({
|
||
type: 'TRANSITION',
|
||
otId: ot.id,
|
||
otReference: ot.reference,
|
||
libelle:
|
||
to === 'DONE' ? 'Clôture de l’intervention' : `Passage à « ${WORK_ORDER_STATUS_LABELS[to]} »`,
|
||
baseUpdatedAt: ot.updatedAt,
|
||
payload: { to },
|
||
});
|
||
patchDetail(queryClient, ot.id, (c) => ({
|
||
...c,
|
||
status: to,
|
||
allowedTransitions: WORK_ORDER_TRANSITIONS[to],
|
||
}));
|
||
void rejouer(queryClient);
|
||
}
|
||
|
||
export function enfilerCoche(
|
||
queryClient: QueryClient,
|
||
ot: WorkOrderDetail,
|
||
item: { id: string; label: string },
|
||
state: ChecklistState,
|
||
): void {
|
||
enfiler({
|
||
type: 'COCHE',
|
||
otId: ot.id,
|
||
otReference: ot.reference,
|
||
libelle: `Coche « ${item.label} » → ${state === 'DONE' ? 'fait' : state === 'NA' ? 'N/A' : 'à faire'}`,
|
||
baseUpdatedAt: ot.updatedAt,
|
||
payload: { itemId: item.id, state },
|
||
});
|
||
patchDetail(queryClient, ot.id, (c) => ({
|
||
...c,
|
||
checklist: c.checklist.map((i) => (i.id === item.id ? { ...i, state } : i)),
|
||
}));
|
||
void rejouer(queryClient);
|
||
}
|
||
|
||
export function enfilerBilan(
|
||
queryClient: QueryClient,
|
||
ot: WorkOrderDetail,
|
||
corps: Omit<ReportUpsert, 'baseUpdatedAt'>,
|
||
labels: Partial<Record<keyof ReportUpsert, { id: string; label: string } | null>>,
|
||
): void {
|
||
enfiler({
|
||
type: 'BILAN',
|
||
otId: ot.id,
|
||
otReference: ot.reference,
|
||
libelle: 'Bilan d’intervention codé',
|
||
baseUpdatedAt: ot.updatedAt,
|
||
payload: corps,
|
||
});
|
||
patchDetail(queryClient, ot.id, (c) => ({
|
||
...c,
|
||
report: {
|
||
note: c.report?.note ?? null,
|
||
doorState: labels.doorStateId !== undefined ? (labels.doorStateId ?? null) : (c.report?.doorState ?? null),
|
||
cabinPosition:
|
||
labels.cabinPositionId !== undefined ? (labels.cabinPositionId ?? null) : (c.report?.cabinPosition ?? null),
|
||
anomaly: labels.anomalyId !== undefined ? (labels.anomalyId ?? null) : (c.report?.anomaly ?? null),
|
||
externalCause:
|
||
labels.externalCauseId !== undefined ? (labels.externalCauseId ?? null) : (c.report?.externalCause ?? null),
|
||
actionTaken:
|
||
labels.actionTakenId !== undefined ? (labels.actionTakenId ?? null) : (c.report?.actionTaken ?? null),
|
||
componentConcerned:
|
||
labels.componentConcernedId !== undefined
|
||
? (labels.componentConcernedId ?? null)
|
||
: (c.report?.componentConcerned ?? null),
|
||
},
|
||
}));
|
||
void rejouer(queryClient);
|
||
}
|
||
|
||
export function enfilerPhoto(
|
||
queryClient: QueryClient,
|
||
ot: WorkOrderDetail,
|
||
fichier: { uri: string; nom: string; mime: string },
|
||
): void {
|
||
enfiler({
|
||
type: 'PHOTO',
|
||
otId: ot.id,
|
||
otReference: ot.reference,
|
||
libelle: `Photo — ${fichier.nom}`,
|
||
baseUpdatedAt: ot.updatedAt,
|
||
payload: fichier,
|
||
});
|
||
void rejouer(queryClient);
|
||
}
|
||
|
||
/** Ce que la file porte encore pour cet OT — chips « en file » des écrans. */
|
||
export function saisiesPour(otId: string) {
|
||
return lireFile().filter((s) => s.otId === otId);
|
||
}
|