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
+59
View File
@@ -0,0 +1,59 @@
import { useCallback } from 'react';
import { createMMKV, useMMKVObject } from 'react-native-mmkv';
import { Batch } from '../types';
const storage = createMMKV({ id: 'vaultdrop-batches' });
export function useBatchStore() {
const [batchIds, setBatchIds] = useMMKVObject<string[]>('batch-ids', storage);
const saveBatch = useCallback((batch: Batch) => {
storage.set(`batch_${batch.id}`, JSON.stringify(batch));
const current = batchIds ?? [];
if (!current.includes(batch.id)) {
setBatchIds([batch.id, ...current]);
}
}, [batchIds, setBatchIds]);
const getBatch = useCallback((id: string): Batch | undefined => {
const raw = storage.getString(`batch_${id}`);
if (!raw) return undefined;
return JSON.parse(raw) as Batch;
}, []);
const getAllBatches = useCallback((): Batch[] => {
return (batchIds ?? [])
.map((id) => getBatch(id))
.filter((b): b is Batch => b !== undefined);
}, [batchIds, getBatch]);
const addTagToBatch = useCallback((batchId: string, tag: string) => {
const batch = getBatch(batchId);
if (!batch) return;
if (batch.tags.includes(tag)) return;
batch.tags = [...batch.tags, tag];
storage.set(`batch_${batchId}`, JSON.stringify(batch));
}, [getBatch]);
const removeTagFromBatch = useCallback((batchId: string, tag: string) => {
const batch = getBatch(batchId);
if (!batch) return;
batch.tags = batch.tags.filter((t) => t !== tag);
storage.set(`batch_${batchId}`, JSON.stringify(batch));
}, [getBatch]);
const deleteBatch = useCallback((batchId: string) => {
storage.set(`batch_${batchId}`, undefined as any);
storage.remove(`batch_${batchId}` as any);
setBatchIds((batchIds ?? []).filter((id) => id !== batchId));
}, [batchIds, setBatchIds]);
return {
saveBatch,
getBatch,
getAllBatches,
addTagToBatch,
removeTagFromBatch,
deleteBatch,
};
}
+39 -7
View File
@@ -1,8 +1,9 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useCameraDevice, useCameraPermission, usePhotoOutput, type TorchMode, type CameraRef } from 'react-native-vision-camera';
import { useUpload } from './useUpload';
import { CapturedPhoto } from '../types';
type CaptureStatus = 'idle' | 'capturing' | 'uploading' | 'processing' | 'success' | 'error';
type CaptureStatus = 'idle' | 'capturing' | 'uploading' | 'success' | 'error';
export function useCameraCapture() {
const { hasPermission, requestPermission } = useCameraPermission();
@@ -13,6 +14,7 @@ export function useCameraCapture() {
const [captureError, setCaptureError] = useState<string>();
const [torchMode, setTorchMode] = useState<TorchMode>('off');
const [cameraReady, setCameraReady] = useState(false);
const [capturedPhotos, setCapturedPhotos] = useState<CapturedPhoto[]>([]);
const upload = useUpload();
@@ -53,35 +55,61 @@ export function useCameraCapture() {
setCameraReady(false);
}, []);
const addCapturedPhoto = useCallback((photo: CapturedPhoto) => {
setCapturedPhotos((prev) => [...prev, photo]);
}, []);
const removeCapturedPhoto = useCallback((id: string) => {
setCapturedPhotos((prev) => prev.filter((p) => p.id !== id));
}, []);
const clearCapturedPhotos = useCallback(() => {
setCapturedPhotos([]);
}, []);
const capturePhoto = useCallback(async () => {
if (!photoOutput) return;
const photoId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
try {
setCaptureStatus('capturing');
setCaptureError(undefined);
const { filePath } = await photoOutput.capturePhotoToFile(
{},
{}
);
const { filePath } = await photoOutput.capturePhotoToFile({}, {});
setCaptureStatus('uploading');
const capturedPhoto: CapturedPhoto = {
id: photoId,
filePath,
uri: 'file://' + filePath,
};
addCapturedPhoto(capturedPhoto);
const result = await upload.mutateAsync([
{ uri: 'file://' + filePath, type: 'image/jpeg', name: 'scan.jpg' },
{ uri: capturedPhoto.uri, type: 'image/jpeg', name: `scan_${photoId}.jpg` },
]);
if (result.errors.length > 0) {
setCaptureStatus('error');
setCaptureError(result.errors[0].message);
} else {
capturedPhoto.uploadedId = result.uploaded[0]?.id;
capturedPhoto.uploadedAt = new Date().toISOString();
setCaptureStatus('success');
}
} catch (err) {
setCaptureStatus('error');
setCaptureError(err instanceof Error ? err.message : 'Erreur lors de la capture');
}
}, [photoOutput, upload]);
setTimeout(() => {
setCaptureStatus('idle');
setCaptureError(undefined);
}, 1500);
}, [photoOutput, upload, addCapturedPhoto]);
return {
cameraRef,
@@ -97,5 +125,9 @@ export function useCameraCapture() {
toggleTorch,
onStarted,
onStopped,
capturedPhotos,
removeCapturedPhoto,
clearCapturedPhotos,
capturedCount: capturedPhotos.length,
};
}
+53
View File
@@ -0,0 +1,53 @@
import { useState, useCallback } from 'react';
import * as Print from 'expo-print';
import * as FileSystem from 'expo-file-system';
import { CapturedPhoto } from '../types';
export function usePdfGeneration() {
const [generating, setGenerating] = useState(false);
const generatePdf = useCallback(async (photos: CapturedPhoto[]): Promise<string | null> => {
if (photos.length === 0) return null;
setGenerating(true);
try {
const imagesHtml = photos.map((p) => {
const ext = p.filePath.split('.').pop()?.toLowerCase() || 'jpeg';
const mime = ext === 'png' ? 'image/png' : 'image/jpeg';
return `<div style="page-break-after: always; display: flex; justify-content: center; align-items: center; height: 100vh;">
<img src="${p.uri}" style="max-width: 100%; max-height: 100vh; object-fit: contain;" />
</div>`;
}).join('');
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #fff; }
@media print {
@page { margin: 0; }
}
</style>
</head>
<body>${imagesHtml}</body>
</html>`;
const { uri } = await Print.printToFileAsync({ html });
return uri;
} catch (err) {
console.error('PDF generation failed:', err);
return null;
} finally {
setGenerating(false);
}
}, []);
return {
generatePdf,
generating,
};
}