Skip to content

Commit bc6a367

Browse files
authored
Merge pull request #56 from BEKO2210/claude/fix-camera-scan-pjnJq
feat: Produktbilder automatisch per Barcode beim Import nachladen
2 parents bd95638 + aa6f233 commit bc6a367

8 files changed

Lines changed: 114 additions & 16 deletions

File tree

src/components/Settings.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { version as appVersion } from '../../package.json';
44
import { useLiveQuery } from 'dexie-react-hooks';
5-
import { db, addStorageLocation, deleteStorageLocation, exportData, exportCSV, importData, ImportResult } from '../lib/db';
5+
import { db, addStorageLocation, deleteStorageLocation, exportData, exportCSV, importData, loadImportedImages, ImportResult } from '../lib/db';
66
import { requestNotificationPermission, getNotificationPermissionStatus } from '../lib/notifications';
77
import { useDarkMode } from '../hooks/useDarkMode';
88
import { usePWAInstall } from '../hooks/usePWAInstall';
@@ -28,6 +28,7 @@ import {
2828
ChevronUp,
2929
Info,
3030
Globe,
31+
Loader2,
3132
} from 'lucide-react';
3233

3334
const LANGUAGES = [
@@ -47,6 +48,7 @@ export function Settings() {
4748
const allProducts = useLiveQuery(() => db.products.toArray()) ?? [];
4849
const [newLocation, setNewLocation] = useState('');
4950
const [importStatus, setImportStatus] = useState<{ message: string; type: 'success' | 'warning' | 'error' } | null>(null);
51+
const [imageLoadProgress, setImageLoadProgress] = useState<{ loaded: number; total: number } | null>(null);
5052
const [showImpressum, setShowImpressum] = useState(false);
5153
const [showDatenschutz, setShowDatenschutz] = useState(false);
5254
const [showAGB, setShowAGB] = useState(false);
@@ -80,17 +82,30 @@ export function Settings() {
8082
downloadFile(data, `preptrack-export-${new Date().toISOString().split('T')[0]}.csv`, 'text/csv;charset=utf-8');
8183
}
8284

85+
async function startImageLoading(productIds: number[]) {
86+
if (productIds.length === 0) return;
87+
setImageLoadProgress({ loaded: 0, total: productIds.length });
88+
await loadImportedImages(productIds, (loaded, total) => {
89+
setImageLoadProgress({ loaded, total });
90+
});
91+
setImageLoadProgress(null);
92+
}
93+
8394
async function handleImport(e: React.ChangeEvent<HTMLInputElement>) {
8495
const file = e.target.files?.[0];
8596
if (!file) return;
8697

8798
try {
8899
const text = await file.text();
89-
const count = await importData(text);
90-
setImportStatus({ message: t('import.success', { count }), type: 'success' });
100+
const result = await importData(text);
101+
setImportStatus({ message: t('import.success', { count: result.imported }), type: 'success' });
102+
// Bilder im Hintergrund nachladen
103+
startImageLoading(result.productsNeedingImages);
91104
} catch (err) {
92105
if (err instanceof ImportResult) {
93106
setImportStatus({ message: err.message, type: 'warning' });
107+
// Auch bei teilweisem Import Bilder nachladen
108+
startImageLoading(err.productsNeedingImages);
94109
} else {
95110
setImportStatus({ message: t('import.error', { message: err instanceof Error ? err.message : t('import.importFailed') }), type: 'error' });
96111
}
@@ -374,6 +389,27 @@ export function Settings() {
374389
{importStatus.message}
375390
</p>
376391
)}
392+
393+
{imageLoadProgress && (
394+
<div className="space-y-2 rounded-lg bg-blue-500/10 px-3 py-2">
395+
<div className="flex items-center gap-2 text-sm text-blue-400">
396+
<Loader2 size={16} className="animate-spin" />
397+
<span>
398+
{t('import.loadingImages', {
399+
loaded: imageLoadProgress.loaded,
400+
total: imageLoadProgress.total,
401+
defaultValue: 'Lade Produktbilder… {{loaded}} / {{total}}',
402+
})}
403+
</span>
404+
</div>
405+
<div className="h-1.5 w-full overflow-hidden rounded-full bg-primary-700">
406+
<div
407+
className="h-full rounded-full bg-blue-500 transition-all duration-300"
408+
style={{ width: `${(imageLoadProgress.loaded / imageLoadProgress.total) * 100}%` }}
409+
/>
410+
</div>
411+
</div>
412+
)}
377413
</div>
378414
</section>
379415

src/i18n/locales/ar/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@
284284
"import": {
285285
"success": "تم استيراد {{count}} منتجات بنجاح.",
286286
"error": "خطأ: {{message}}",
287-
"importFailed": "فشل الاستيراد"
287+
"importFailed": "فشل الاستيراد",
288+
"loadingImages": "جاري تحميل صور المنتجات… {{loaded}} / {{total}}"
288289
},
289290
"notifications": {
290291
"expiredTitle": "{{name}} انتهت صلاحيته!",

src/i18n/locales/de/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@
284284
"import": {
285285
"success": "{{count}} Produkte erfolgreich importiert.",
286286
"error": "Fehler: {{message}}",
287-
"importFailed": "Import fehlgeschlagen"
287+
"importFailed": "Import fehlgeschlagen",
288+
"loadingImages": "Lade Produktbilder… {{loaded}} / {{total}}"
288289
},
289290
"notifications": {
290291
"expiredTitle": "{{name}} ist abgelaufen!",

src/i18n/locales/en/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@
284284
"import": {
285285
"success": "{{count}} products successfully imported.",
286286
"error": "Error: {{message}}",
287-
"importFailed": "Import failed"
287+
"importFailed": "Import failed",
288+
"loadingImages": "Loading product images… {{loaded}} / {{total}}"
288289
},
289290
"notifications": {
290291
"expiredTitle": "{{name}} has expired!",

src/i18n/locales/fr/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@
284284
"import": {
285285
"success": "{{count}} produits import\u00e9s avec succ\u00e8s.",
286286
"error": "Erreur : {{message}}",
287-
"importFailed": "L'importation a \u00e9chou\u00e9"
287+
"importFailed": "L'importation a \u00e9chou\u00e9",
288+
"loadingImages": "Chargement des images… {{loaded}} / {{total}}"
288289
},
289290
"notifications": {
290291
"expiredTitle": "{{name}} a expir\u00e9 !",

src/i18n/locales/it/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@
284284
"import": {
285285
"success": "{{count}} prodotti importati con successo.",
286286
"error": "Errore: {{message}}",
287-
"importFailed": "Importazione fallita"
287+
"importFailed": "Importazione fallita",
288+
"loadingImages": "Caricamento immagini prodotti… {{loaded}} / {{total}}"
288289
},
289290
"notifications": {
290291
"expiredTitle": "{{name}} \u00e8 scaduto!",

src/i18n/locales/pt/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@
284284
"import": {
285285
"success": "{{count}} produtos importados com sucesso.",
286286
"error": "Erro: {{message}}",
287-
"importFailed": "Importação falhou"
287+
"importFailed": "Importação falhou",
288+
"loadingImages": "Carregando imagens dos produtos… {{loaded}} / {{total}}"
288289
},
289290
"notifications": {
290291
"expiredTitle": "{{name}} venceu!",

src/lib/db.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Dexie, { type Table } from 'dexie';
22
import { version as appVersion } from '../../package.json';
33
import i18n from '../i18n/i18n';
4-
import { getLocale } from './utils';
4+
import { getLocale, lookupBarcode, fetchAndCompressImage } from './utils';
55
import type {
66
Product,
77
StorageLocation,
@@ -200,7 +200,14 @@ export async function exportCSV(): Promise<string> {
200200
return BOM + [headers.join(';'), ...rows.map((r) => r.join(';'))].join('\r\n');
201201
}
202202

203-
export async function importData(jsonString: string): Promise<number> {
203+
export interface ImportDataResult {
204+
imported: number;
205+
skipped: number;
206+
/** IDs von importierten Produkten die einen Barcode aber kein Foto haben */
207+
productsNeedingImages: number[];
208+
}
209+
210+
export async function importData(jsonString: string): Promise<ImportDataResult> {
204211
const t = i18n.t.bind(i18n);
205212
let data: Record<string, unknown>;
206213
try {
@@ -219,6 +226,7 @@ export async function importData(jsonString: string): Promise<number> {
219226

220227
let imported = 0;
221228
let skipped = 0;
229+
const productsNeedingImages: number[] = [];
222230

223231
await db.transaction(
224232
'rw',
@@ -273,12 +281,13 @@ export async function importData(jsonString: string): Promise<number> {
273281
// Clean up photo field - don't import placeholder markers
274282
const rawPhoto = product.photo;
275283
const photo = rawPhoto && rawPhoto !== '[FOTO]' && typeof rawPhoto === 'string' ? rawPhoto : undefined;
284+
const barcode = typeof product.barcode === 'string' ? product.barcode : undefined;
276285
const now = new Date().toISOString();
277286

278287
// Only import known fields to prevent injection of unexpected data
279-
await db.products.add({
288+
const newId = await db.products.add({
280289
name: String(product.name),
281-
barcode: typeof product.barcode === 'string' ? product.barcode : undefined,
290+
barcode,
282291
category: typeof product.category === 'string' ? product.category as Product['category'] : 'sonstiges',
283292
storageLocation: typeof product.storageLocation === 'string' ? product.storageLocation : 'Keller',
284293
quantity: typeof product.quantity === 'number' ? product.quantity : 1,
@@ -293,6 +302,11 @@ export async function importData(jsonString: string): Promise<number> {
293302
updatedAt: typeof product.updatedAt === 'string' ? product.updatedAt : now,
294303
});
295304
imported++;
305+
306+
// Produkt hat Barcode aber kein Foto → Bild nachladen
307+
if (barcode && !photo) {
308+
productsNeedingImages.push(newId);
309+
}
296310
}
297311

298312
// Import consumption logs
@@ -305,23 +319,65 @@ export async function importData(jsonString: string): Promise<number> {
305319
);
306320

307321
if (skipped > 0) {
308-
throw new ImportResult(imported, skipped);
322+
throw new ImportResult(imported, skipped, productsNeedingImages);
323+
}
324+
325+
return { imported, skipped, productsNeedingImages };
326+
}
327+
328+
/**
329+
* Lädt Produktbilder im Hintergrund per Barcode von Open Food Facts.
330+
* Ruft für jedes Produkt lookupBarcode auf, holt das Bild und speichert es.
331+
* @param productIds - IDs der Produkte die ein Bild brauchen
332+
* @param onProgress - Callback für Fortschritt (geladen, gesamt)
333+
*/
334+
export async function loadImportedImages(
335+
productIds: number[],
336+
onProgress?: (loaded: number, total: number) => void
337+
): Promise<number> {
338+
let loaded = 0;
339+
const total = productIds.length;
340+
341+
for (const id of productIds) {
342+
try {
343+
const product = await db.products.get(id);
344+
if (!product?.barcode || product.photo) {
345+
onProgress?.(++loaded, total);
346+
continue;
347+
}
348+
349+
const result = await lookupBarcode(product.barcode);
350+
if (result?.imageUrl) {
351+
const photo = await fetchAndCompressImage(result.imageUrl);
352+
if (photo) {
353+
await db.products.update(id, {
354+
photo,
355+
updatedAt: new Date().toISOString(),
356+
});
357+
}
358+
}
359+
} catch {
360+
// Einzelnes Bild fehlgeschlagen — weiter mit dem nächsten
361+
}
362+
onProgress?.(++loaded, total);
309363
}
310364

311-
return imported;
365+
return loaded;
312366
}
313367

314368
// Custom class to pass both imported and skipped counts
315369
export class ImportResult extends Error {
316370
imported: number;
317371
skipped: number;
372+
productsNeedingImages: number[];
318373

319-
constructor(imported: number, skipped: number) {
374+
constructor(imported: number, skipped: number, productsNeedingImages: number[] = []) {
320375
const t = i18n.t.bind(i18n);
321376
const msg = t('dbErrors.importResult', { imported, skipped });
322377
super(msg);
323378
this.name = 'ImportResult';
324379
this.imported = imported;
325380
this.skipped = skipped;
381+
this.productsNeedingImages = productsNeedingImages;
326382
}
327383
}

0 commit comments

Comments
 (0)