Version initiale de Gestion Prix

App Flutter de gestion de stock/prix pour tablette Android (100% local) :
scan code-barres (OpenFoodFacts), prix au kilo/litre, packs, inventaire,
prix d'achat + marge, filtres de tri, étiquettes PDF, design noir & blanc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 21:28:33 +02:00
commit 4d936d2ee3
53 changed files with 3852 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/produit.dart';
/// Résultat d'une recherche produit sur OpenFoodFacts.
class InfoProduit {
final String? nom;
final String? imageUrl;
final String? marque;
final double? quantite;
final String? unite;
const InfoProduit({
this.nom,
this.imageUrl,
this.marque,
this.quantite,
this.unite,
});
bool get estVide => (nom == null || nom!.isEmpty) && imageUrl == null;
}
/// Interroge l'API publique OpenFoodFacts pour récupérer le nom, l'image et la
/// quantité (grammage / litrage) d'un produit à partir de son code-barres.
class OpenFoodFactsService {
static const String _base = 'https://world.openfoodfacts.org/api/v2/product';
Future<InfoProduit?> chercherParCodeBarres(String codeBarres) async {
final uri = Uri.parse(
'$_base/$codeBarres.json?fields=product_name,product_name_fr,brands,'
'image_front_url,image_url,product_quantity,product_quantity_unit',
);
try {
final res = await http
.get(uri, headers: {'User-Agent': 'GestionPrixProduit/1.0'})
.timeout(const Duration(seconds: 8));
if (res.statusCode != 200) return null;
final data = jsonDecode(res.body) as Map<String, dynamic>;
// status == 1 => produit trouvé
if (data['status'] != 1) return const InfoProduit();
final p = data['product'] as Map<String, dynamic>? ?? {};
final nom = (p['product_name_fr'] as String?)?.trim();
final nomEn = (p['product_name'] as String?)?.trim();
final image = (p['image_front_url'] as String?)?.trim() ??
(p['image_url'] as String?)?.trim();
final (quantite, unite) = _quantite(p);
return InfoProduit(
nom: (nom != null && nom.isNotEmpty) ? nom : nomEn,
imageUrl: (image != null && image.isNotEmpty) ? image : null,
marque: (p['brands'] as String?)?.split(',').first.trim(),
quantite: quantite,
unite: unite,
);
} catch (_) {
// Pas de réseau ou timeout : on retourne null, l'utilisateur saisit à la main.
return null;
}
}
/// Normalise la quantité OpenFoodFacts (product_quantity + unité) vers nos unités.
(double?, String?) _quantite(Map<String, dynamic> p) {
final q = (p['product_quantity'] as num?)?.toDouble();
final u = (p['product_quantity_unit'] as String?)?.toLowerCase().trim();
if (q == null || q <= 0 || u == null) return (null, null);
return switch (u) {
'g' => (q, Unites.g),
'kg' => (q, Unites.kg),
'mg' => (q / 1000, Unites.g),
'ml' => (q, Unites.ml),
'cl' => (q, Unites.cl),
'l' => (q, Unites.l),
_ => (null, null),
};
}
}