Files
siop2/apps/mobile/app/ascenseur/[id].tsx
pr-daaif a22ea60f83 feat(r4.2): terrain mobile — scan QR, fiches ascenseur/OT, clôture, grille
Scanner expo-camera (QR seulement) : analyseScan testée — URL portail
…/q/REF des étiquettes A6 ou référence tapée, QR étrangers refusés ;
résolution D4 dans le parc en cache d'abord (sous-sol inclus), message
honnête sinon. Fiche ascenseur en consultation (D3, historique scopé
« voir autre »). Fiche OT : un bouton principal selon la machine à
états, coûts figés, garde alimentée par closureBlockers (source unique
API). Clôture terrain : bilan codé 6 champs au pouce (sélecteur plein
écran), garde visible, clôture en ligne. Préventif : mes grilles →
checklist cochable, appui long = N/A, aria-checked (leçon RN web :
accessibilityState.checked ne produit pas aria-checked).

Écritures en ligne assumées en R4.2 (bandeaux hors-ligne explicites) —
la file persistée et le verrou optimiste sont R4.3 (D1/D2).

Vérifié 12/12 en Expo web piloté : scan → fiche → OT-0341 (505 MAD,
garde), coche/décoche restituée, OT de test Démarrer → bilan → Terminé,
zéro erreur console, données purgées. 12 tests jest-expo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:47:04 +01:00

130 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { router, useLocalSearchParams } from 'expo-router';
import { Pressable, ScrollView, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { ASSET_STATUS_LABELS } from '@siop/shared';
import { useAsset, useWorkOrders } from '@/api/exploitation';
import { Carte, ChipStatut, EnteteFiche, LigneInfo } from '@/composants/ui';
import { triJournee } from '@/lib/journee';
import { useTokens } from '@/theme/tokens';
/** Écran 5 de la maquette R4 : tout ce qu'il faut DEVANT la machine.
* Consultation seule (D3) — l'historique montre ce que le rôle a le droit
* de voir (invariant « voir autre », même règle que partout). */
export default function PageAscenseur() {
const t = useTokens();
const { id } = useLocalSearchParams<{ id: string }>();
const { data: appareil } = useAsset(id);
const { data: workOrders } = useWorkOrders();
if (!appareil) return null;
const interventions = (workOrders ?? [])
.filter((w) => w.assetId === appareil.id)
.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
const enCours = triJournee(interventions);
const statut = ASSET_STATUS_LABELS[appareil.status];
return (
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
<ScrollView contentContainerStyle={{ padding: 14, gap: 10 }}>
<EnteteFiche
titre={`Asc. ${appareil.reference}`}
apres={
<Text
style={{
fontFamily: 'Manrope_700Bold',
fontSize: 10.5,
color: appareil.status === 'IN_SERVICE' ? t.stTermine : t.stAttente,
backgroundColor: appareil.status === 'IN_SERVICE' ? t.stTermineFond : t.stAttenteFond,
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 999,
overflow: 'hidden',
}}
>
{statut}
</Text>
}
/>
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 16, color: t.encre }}>
{appareil.brand}
{appareil.model ? ` ${appareil.model}` : ''} {appareil.siteName}
</Text>
<Carte titre="Identité">
<LigneInfo nom="Emplacement" valeur={appareil.locationName} />
<LigneInfo nom="Catégorie" valeur={appareil.categoryName} />
<LigneInfo
nom="Mise en service"
valeur={
appareil.commissionedAt
? new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format(
new Date(appareil.commissionedAt),
)
: '—'
}
/>
<LigneInfo
nom="Charge / niveaux"
valeur={`${appareil.loadKg != null ? `${appareil.loadKg} kg` : '—'} · ${appareil.floors != null ? `${appareil.floors} niveaux` : '—'}`}
/>
{appareil.serialNumber ? (
<LigneInfo nom="N° de série" valeur={appareil.serialNumber} />
) : null}
</Carte>
<Carte titre="Organes suivis">
{appareil.components.length ? (
appareil.components.map((c) => (
<LigneInfo key={c.id} nom={c.typeName} valeur={c.designation ?? '—'} />
))
) : (
<Text style={{ color: t.encre3, fontFamily: 'Manrope_400Regular', fontSize: 12.5 }}>
Aucun organe déclaré.
</Text>
)}
</Carte>
<Carte titre={`Interventions visibles (${interventions.length})`}>
{interventions.slice(0, 6).map((w) => (
<Pressable
key={w.id}
accessibilityRole="button"
onPress={() => router.push(`/ot/${w.id}`)}
style={{ flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 3 }}
>
<Text style={{ fontFamily: 'Manrope_700Bold', fontSize: 12.5, color: t.primaire }}>
{w.reference}
</Text>
<Text
numberOfLines={1}
style={{ flex: 1, fontFamily: 'Manrope_400Regular', fontSize: 12.5, color: t.encre }}
>
{w.title}
</Text>
<ChipStatut statut={w.status} />
</Pressable>
))}
{!interventions.length ? (
<Text style={{ color: t.encre3, fontFamily: 'Manrope_400Regular', fontSize: 12.5 }}>
Aucune intervention visible pour votre rôle.
</Text>
) : null}
</Carte>
{enCours.length ? (
<Pressable
accessibilityRole="button"
onPress={() => router.push(`/ot/${enCours[0]!.id}`)}
style={{ backgroundColor: t.primaire, borderRadius: 10, padding: 12, alignItems: 'center' }}
>
<Text style={{ color: '#fff', fontFamily: 'Manrope_700Bold', fontSize: 14 }}>
Ouvrir lOT en cours ({enCours[0]!.reference})
</Text>
</Pressable>
) : null}
</ScrollView>
</SafeAreaView>
);
}