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>
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
import { useLocalSearchParams } from 'expo-router';
|
||
import { Alert, Pressable, ScrollView, Text, View } from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import { useCocheChecklist, useWorkOrder } from '@/api/exploitation';
|
||
import { useHorsLigne } from '@/auth/session';
|
||
import { EnteteFiche } from '@/composants/ui';
|
||
import { prochainEtat, progression } from '@/lib/checklist';
|
||
import { useTokens } from '@/theme/tokens';
|
||
|
||
/** Écran 6 de la maquette R4 : la grille au pouce. Appui simple = coche,
|
||
* appui long = non-applicable. En R4.2 chaque coche part immédiatement à
|
||
* l'API (hors-ligne : lecture seule — la file individuelle arrive en R4.3). */
|
||
export default function PageGrille() {
|
||
const t = useTokens();
|
||
const horsLigne = useHorsLigne();
|
||
const { id } = useLocalSearchParams<{ id: string }>();
|
||
const { data: ot } = useWorkOrder(id);
|
||
const coche = useCocheChecklist(id);
|
||
|
||
if (!ot) return null;
|
||
const { faits, total, pct } = progression(ot.checklist);
|
||
|
||
const basculer = (itemId: string, etat: Parameters<typeof prochainEtat>[0], versNA = false) => {
|
||
if (horsLigne) return;
|
||
coche.mutate(
|
||
{ itemId, state: versNA ? (etat === 'NA' ? 'PENDING' : 'NA') : prochainEtat(etat) },
|
||
{ onError: (e) => Alert.alert('Coche refusée', e.message) },
|
||
);
|
||
};
|
||
|
||
return (
|
||
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||
<ScrollView contentContainerStyle={{ padding: 14, gap: 10 }}>
|
||
<EnteteFiche
|
||
titre={`Grille — ${ot.assetReference}`}
|
||
apres={
|
||
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 13, color: t.encre2 }}>
|
||
{faits}/{total}
|
||
</Text>
|
||
}
|
||
/>
|
||
<View style={{ height: 8, borderRadius: 999, backgroundColor: t.surface2, overflow: 'hidden' }}>
|
||
<View
|
||
style={{ width: `${Math.round(pct * 100)}%`, height: '100%', backgroundColor: t.succes }}
|
||
/>
|
||
</View>
|
||
{horsLigne ? (
|
||
<View
|
||
style={{
|
||
backgroundColor: t.stAttenteFond,
|
||
borderColor: t.stAttente,
|
||
borderWidth: 1,
|
||
borderRadius: 10,
|
||
padding: 9,
|
||
}}
|
||
>
|
||
<Text style={{ color: t.stAttente, fontFamily: 'Manrope_700Bold', fontSize: 12 }}>
|
||
⚠ Hors-ligne — les coches en file arrivent en R4.3 ; pour l’instant la grille est en
|
||
lecture.
|
||
</Text>
|
||
</View>
|
||
) : null}
|
||
{ot.checklist.map((item) => (
|
||
<Pressable
|
||
key={item.id}
|
||
accessibilityRole="checkbox"
|
||
aria-checked={item.state === 'DONE'}
|
||
accessibilityLabel={item.label}
|
||
disabled={horsLigne || coche.isPending}
|
||
onPress={() => basculer(item.id, item.state)}
|
||
onLongPress={() => basculer(item.id, item.state, true)}
|
||
style={{
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
backgroundColor: t.surface,
|
||
borderColor: t.bordure,
|
||
borderWidth: 1,
|
||
borderRadius: 10,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 11,
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
width: 22,
|
||
height: 22,
|
||
borderRadius: 6,
|
||
borderWidth: 2,
|
||
borderColor: item.state === 'DONE' ? t.succes : t.bordureForte,
|
||
backgroundColor: item.state === 'DONE' ? t.succes : 'transparent',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
{item.state === 'DONE' ? (
|
||
<Text style={{ color: '#fff', fontSize: 13, fontFamily: 'Manrope_800ExtraBold' }}>✓</Text>
|
||
) : item.state === 'NA' ? (
|
||
<Text style={{ color: t.encre3, fontSize: 10, fontFamily: 'Manrope_800ExtraBold' }}>NA</Text>
|
||
) : null}
|
||
</View>
|
||
<Text
|
||
style={{
|
||
flex: 1,
|
||
fontFamily: 'Manrope_600SemiBold',
|
||
fontSize: 13,
|
||
color: item.state === 'PENDING' ? t.encre : t.encre2,
|
||
}}
|
||
>
|
||
{item.label}
|
||
</Text>
|
||
{item.doneBy ? (
|
||
<Text style={{ fontFamily: 'Manrope_400Regular', fontSize: 10.5, color: t.encre3 }}>
|
||
{item.doneBy.displayName.split(' ')[0]}
|
||
</Text>
|
||
) : null}
|
||
</Pressable>
|
||
))}
|
||
<Text style={{ color: t.encre3, fontFamily: 'Manrope_400Regular', fontSize: 11.5 }}>
|
||
Appui simple : cocher / décocher · appui long : non-applicable.
|
||
</Text>
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
);
|
||
}
|