mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 20:51:53 +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>
159 lines
5.5 KiB
TypeScript
159 lines
5.5 KiB
TypeScript
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||
import { router } from 'expo-router';
|
||
import { useRef, useState } from 'react';
|
||
import { Platform, Text, TextInput, View } from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import { useAssets } from '@/api/exploitation';
|
||
import { useHorsLigne } from '@/auth/session';
|
||
import { BoutonTel } from '@/composants/ui';
|
||
import { analyseScan } from '@/lib/scan';
|
||
import { useTokens } from '@/theme/tokens';
|
||
|
||
/** Écran 4 de la maquette R4 (D4) : le QR de l'étiquette A6 contient l'URL
|
||
* portail `…/q/REF` — la résolution se fait D'ABORD dans le parc déjà en
|
||
* cache (donc en sous-sol aussi). Repli : saisie de la référence. */
|
||
export default function PageScanner() {
|
||
const t = useTokens();
|
||
const horsLigne = useHorsLigne();
|
||
const [permission, demanderPermission] = useCameraPermissions();
|
||
const { data: assets, refetch } = useAssets();
|
||
const [manuel, setManuel] = useState('');
|
||
const [erreur, setErreur] = useState<string | null>(null);
|
||
const dernierScan = useRef(0);
|
||
|
||
const resoudre = async (brut: string) => {
|
||
const reference = analyseScan(brut);
|
||
if (!reference) {
|
||
setErreur('Ce code n’est pas une étiquette SIOP.');
|
||
return;
|
||
}
|
||
// 1 · le parc en cache (fonctionne hors-ligne)
|
||
let appareil = (assets ?? []).find((a) => a.reference.toUpperCase() === reference);
|
||
// 2 · sinon, un rafraîchissement si le réseau est là
|
||
if (!appareil && !horsLigne) {
|
||
const frais = await refetch();
|
||
appareil = (frais.data ?? []).find((a) => a.reference.toUpperCase() === reference);
|
||
}
|
||
if (!appareil) {
|
||
setErreur(
|
||
horsLigne
|
||
? `« ${reference} » n'est pas dans le parc synchronisé — réessayez au retour du réseau.`
|
||
: `Aucun appareil « ${reference} » dans le parc.`,
|
||
);
|
||
return;
|
||
}
|
||
setErreur(null);
|
||
setManuel('');
|
||
router.push(`/ascenseur/${appareil.id}`);
|
||
};
|
||
|
||
const surScan = ({ data }: { data: string }) => {
|
||
const maintenant = Date.now();
|
||
if (maintenant - dernierScan.current < 1500) return; // anti-rafale
|
||
dernierScan.current = maintenant;
|
||
void resoudre(data);
|
||
};
|
||
|
||
const cameraUtilisable = Platform.OS !== 'web' && permission?.granted;
|
||
|
||
return (
|
||
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
||
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||
Scanner
|
||
</Text>
|
||
{cameraUtilisable ? (
|
||
<View style={{ flex: 1, borderRadius: 12, overflow: 'hidden' }}>
|
||
<CameraView
|
||
style={{ flex: 1 }}
|
||
barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
|
||
onBarcodeScanned={surScan}
|
||
/>
|
||
<Text
|
||
style={{
|
||
position: 'absolute',
|
||
bottom: 12,
|
||
alignSelf: 'center',
|
||
color: '#dfe7f2',
|
||
fontFamily: 'Manrope_600SemiBold',
|
||
fontSize: 12.5,
|
||
}}
|
||
>
|
||
Visez le QR de l’étiquette de cabine
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<View
|
||
style={{
|
||
flex: 1,
|
||
borderRadius: 12,
|
||
backgroundColor: '#131c2c',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 12,
|
||
padding: 20,
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
width: 150,
|
||
height: 150,
|
||
borderRadius: 14,
|
||
borderWidth: 2.5,
|
||
borderColor: t.safran,
|
||
}}
|
||
/>
|
||
<Text
|
||
style={{
|
||
color: '#dfe7f2',
|
||
fontFamily: 'Manrope_600SemiBold',
|
||
fontSize: 12.5,
|
||
textAlign: 'center',
|
||
}}
|
||
>
|
||
{Platform.OS === 'web'
|
||
? 'Caméra indisponible sur web — saisissez la référence ci-dessous.'
|
||
: 'L’appareil photo sert uniquement à lire les étiquettes du parc.'}
|
||
</Text>
|
||
{Platform.OS !== 'web' && !permission?.granted ? (
|
||
<BoutonTel libelle="Autoriser la caméra" surAppui={() => void demanderPermission()} />
|
||
) : null}
|
||
</View>
|
||
)}
|
||
|
||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||
<TextInput
|
||
accessibilityLabel="Référence de l'appareil"
|
||
placeholder="Référence (A1, B2…)"
|
||
placeholderTextColor={t.encre3}
|
||
autoCapitalize="characters"
|
||
value={manuel}
|
||
onChangeText={(v) => {
|
||
setManuel(v);
|
||
setErreur(null);
|
||
}}
|
||
onSubmitEditing={() => void resoudre(manuel)}
|
||
style={{
|
||
flex: 1,
|
||
backgroundColor: t.surface,
|
||
borderColor: t.bordureForte,
|
||
borderWidth: 1.5,
|
||
borderRadius: 10,
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 10,
|
||
color: t.encre,
|
||
fontFamily: 'Manrope_600SemiBold',
|
||
}}
|
||
/>
|
||
<BoutonTel libelle="Ouvrir" desactive={!manuel.trim()} surAppui={() => void resoudre(manuel)} />
|
||
</View>
|
||
{erreur ? (
|
||
<Text style={{ color: t.danger, fontFamily: 'Manrope_600SemiBold', fontSize: 12.5 }}>
|
||
{erreur}
|
||
</Text>
|
||
) : null}
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|