mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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>
130 lines
5.0 KiB
TypeScript
130 lines
5.0 KiB
TypeScript
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 l’OT en cours ({enCours[0]!.reference})
|
||
</Text>
|
||
</Pressable>
|
||
) : null}
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
);
|
||
}
|