regroup etc ...

This commit is contained in:
m
2026-07-12 15:41:17 +02:00
parent 64e7ae1679
commit db3d18cf33
10 changed files with 599 additions and 9 deletions
+262
View File
@@ -0,0 +1,262 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, Alert, ActivityIndicator, Share } from 'react-native';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { useBatchStore } from '../hooks/useBatchStore';
import { usePdfGeneration } from '../hooks/usePdfGeneration';
type BatchReviewRouteParams = {
BatchReview: { batchId: string };
};
export function BatchReviewScreen() {
const route = useRoute<RouteProp<BatchReviewRouteParams, 'BatchReview'>>();
const navigation = useNavigation();
const { getBatch, addTagToBatch, removeTagFromBatch } = useBatchStore();
const { generatePdf, generating } = usePdfGeneration();
const batch = getBatch(route.params.batchId);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [editingTags, setEditingTags] = useState(false);
const [tagInput, setTagInput] = useState('');
if (!batch) {
return (
<View style={styles.center}>
<Text style={styles.errorText}>Lot introuvable</Text>
</View>
);
}
const toggleSelection = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const selectedPhotos = batch.photos.filter((p) => selectedIds.has(p.id));
const selectedCount = selectedIds.size;
const handleGeneratePdf = async () => {
const photos = selectedCount > 0 ? selectedPhotos : batch.photos;
if (photos.length === 0) return;
const uri = await generatePdf(photos);
if (uri) {
Alert.alert('PDF généré', 'Voulez-vous partager le fichier ?', [
{ text: 'Annuler', style: 'cancel' },
{
text: 'Partager',
onPress: () => {
Share.share({ url: uri, title: batch.name });
},
},
]);
} else {
Alert.alert('Erreur', 'Impossible de générer le PDF');
}
};
const handleAddTag = () => {
const tag = tagInput.trim().toLowerCase();
if (!tag || batch.tags.includes(tag)) return;
addTagToBatch(batch.id, tag);
setTagInput('');
};
const formatDate = (iso: string) => {
const d = new Date(iso);
return d.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>{batch.name}</Text>
<Text style={styles.subtitle}>
{batch.photos.length} photo{batch.photos.length > 1 ? 's' : ''} {formatDate(batch.createdAt)}
</Text>
</View>
<FlatList
data={batch.photos}
numColumns={3}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.grid}
renderItem={({ item }) => {
const isSelected = selectedIds.has(item.id);
return (
<TouchableOpacity
style={[styles.gridItem, isSelected && styles.gridItemSelected]}
onPress={() => toggleSelection(item.id)}
>
<Image source={{ uri: item.uri }} style={styles.thumb} />
{isSelected && (
<View style={styles.selectedOverlay}>
<Text style={styles.selectedCheck}></Text>
</View>
)}
</TouchableOpacity>
);
}}
/>
<View style={styles.footer}>
<View style={styles.tagSection}>
<View style={styles.tagRow}>
{batch.tags.map((tag) => (
<TouchableOpacity
key={tag}
style={styles.tagChip}
onPress={() => removeTagFromBatch(batch.id, tag)}
>
<Text style={styles.tagText}>{tag} </Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.disabledNote}>Tags disponibles avec le backend (bientôt)</Text>
</View>
<View style={styles.actions}>
<Text style={styles.selectionInfo}>
{selectedCount > 0 ? `${selectedCount} sélectionnée${selectedCount > 1 ? 's' : ''}` : 'Toutes les photos'}
</Text>
<TouchableOpacity
style={[styles.actionButton, generating && styles.actionButtonDisabled]}
onPress={handleGeneratePdf}
disabled={generating}
>
{generating ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.actionText}>📄 Générer PDF</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
errorText: {
fontSize: 16,
color: '#666',
},
header: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 20,
fontWeight: '700',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#666',
},
grid: {
padding: 4,
},
gridItem: {
flex: 1 / 3,
aspectRatio: 1,
margin: 4,
borderRadius: 6,
overflow: 'hidden',
backgroundColor: '#f0f0f0',
},
gridItemSelected: {
borderWidth: 3,
borderColor: '#1976D2',
},
thumb: {
width: '100%',
height: '100%',
},
selectedOverlay: {
...StyleSheet.absoluteFill,
backgroundColor: 'rgba(25,118,210,0.3)',
justifyContent: 'center',
alignItems: 'center',
},
selectedCheck: {
color: '#fff',
fontSize: 32,
fontWeight: '700',
},
footer: {
padding: 16,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
gap: 12,
},
tagSection: {
gap: 8,
},
tagRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
},
tagChip: {
backgroundColor: '#E3F2FD',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
},
tagText: {
fontSize: 13,
color: '#1976D2',
},
disabledNote: {
fontSize: 12,
color: '#999',
fontStyle: 'italic',
},
actions: {
gap: 8,
alignItems: 'center',
},
selectionInfo: {
fontSize: 14,
color: '#666',
},
actionButton: {
backgroundColor: '#1976D2',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
width: '100%',
alignItems: 'center',
},
actionButtonDisabled: {
opacity: 0.6,
},
actionText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
+80 -2
View File
@@ -1,10 +1,25 @@
import React, { useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Camera } from 'react-native-vision-camera';
import { useCameraCapture } from '../hooks/useCameraCapture';
import { UploadProgress } from '../components/UploadProgress';
import { PhotoThumbnailStrip } from '../components/PhotoThumbnailStrip';
import { useBatchStore } from '../hooks/useBatchStore';
type RootStackParamList = {
Home: undefined;
Upload: undefined;
Scan: undefined;
Search: undefined;
BatchReview: { batchId: string };
};
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
export function ScanScreen() {
const navigation = useNavigation<NavigationProp>();
const {
cameraRef,
hasPermission,
@@ -19,8 +34,14 @@ export function ScanScreen() {
toggleTorch,
onStarted,
onStopped,
capturedPhotos,
removeCapturedPhoto,
clearCapturedPhotos,
capturedCount,
} = useCameraCapture();
const { saveBatch } = useBatchStore();
useEffect(() => {
if (!hasPermission) {
requestPermission();
@@ -31,11 +52,29 @@ export function ScanScreen() {
idle: 'idle',
capturing: 'uploading',
uploading: 'uploading',
processing: 'processing',
success: 'success',
error: 'error',
};
const finishBatch = () => {
if (capturedPhotos.length === 0) return;
const batchId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
const now = new Date();
const name = `Lot du ${now.toLocaleDateString('fr-FR')} ${now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
saveBatch({
id: batchId,
name,
createdAt: now.toISOString(),
photos: capturedPhotos,
tags: [],
});
clearCapturedPhotos();
navigation.navigate('BatchReview', { batchId });
};
if (!hasPermission) {
return (
<View style={styles.center}>
@@ -79,8 +118,16 @@ export function ScanScreen() {
totalCount={1}
uploadedCount={captureStatus === 'success' ? 1 : 0}
/>
{capturedCount > 0 && (
<View style={styles.batchInfo}>
<Text style={styles.batchInfoText}>{capturedCount} photo{capturedCount > 1 ? 's' : ''} prise{capturedCount > 1 ? 's' : ''}</Text>
</View>
)}
</View>
<PhotoThumbnailStrip photos={capturedPhotos} onRemove={removeCapturedPhoto} />
<View style={styles.bottomBar}>
<TouchableOpacity style={styles.torchButton} onPress={toggleTorch}>
<Text style={styles.torchIcon}>{torchMode === 'on' ? '🔦' : '💡'}</Text>
@@ -94,7 +141,13 @@ export function ScanScreen() {
<View style={styles.captureInner} />
</TouchableOpacity>
<View style={styles.torchButton} />
{capturedCount > 0 ? (
<TouchableOpacity style={styles.finishButton} onPress={finishBatch}>
<Text style={styles.finishText}>✓</Text>
</TouchableOpacity>
) : (
<View style={styles.torchButton} />
)}
</View>
</View>
);
@@ -153,6 +206,18 @@ const styles = StyleSheet.create({
left: 16,
right: 16,
},
batchInfo: {
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 8,
padding: 8,
marginTop: 8,
alignItems: 'center',
},
batchInfoText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
bottomBar: {
position: 'absolute',
bottom: 50,
@@ -192,4 +257,17 @@ const styles = StyleSheet.create({
borderRadius: 30,
backgroundColor: '#fff',
},
finishButton: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#4CAF50',
justifyContent: 'center',
alignItems: 'center',
},
finishText: {
color: '#fff',
fontSize: 22,
fontWeight: '700',
},
});