mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 20:51:53 +00:00
Bug remonté par le référent : une fois authentifié, impossible de revenir en arrière ou de se déconnecter. Seule « Ma journée » portait ce contrôle (onLongPress non découvrable) ; les 3 autres onglets n'avaient rien. EnteteTabs() factorise l'entête (composants/ui.tsx) : tap simple + confirmation Alert, rôle affiché = celui du compte connecté (plus de libellé "Technicien" figé — le mobile n'est plus réservé aux techniciens). Réutilisée dans les 4 onglets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
160 lines
5.5 KiB
TypeScript
160 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, EnteteTabs } 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 }}>
|
||
<EnteteTabs />
|
||
<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>
|
||
);
|
||
}
|