/// Unités de vente supportées (poids et volume). class Unites { static const g = 'g'; static const kg = 'kg'; static const ml = 'ml'; static const cl = 'cl'; static const l = 'L'; /// Ancienne valeur (produits d'avant) : vendu à l'unité. Plus proposée. static const piece = 'piece'; static const tout = [g, kg, ml, cl, l]; /// Vrai si le code est une unité de mesure valide (donc pas 'piece'/null). static bool estValide(String? code) => tout.contains(code); static String libelle(String? code) => switch (code) { g => 'g', kg => 'kg', ml => 'ml', cl => 'cl', l => 'L', _ => '', }; } /// Un produit du stock. class Produit { final String id; final String? codeBarres; final String nom; final String? imageUrl; /// Prix de vente (au client). final double prix; /// Prix d'achat (coût). 0 = non renseigné. final double prixAchat; /// Contenance d'UN contenant (ex : 330 pour 330 ml, 400 pour 400 g). final double? quantite; /// Unité de la quantité : voir [Unites]. final String? unite; /// Nombre de contenants pour un pack (ex : 6 pour un pack de 6 bières). /// Null ou 1 = produit à l'unité (pas un pack). final int? nbContenants; /// Horodatage de création (ms) — sert au tri par ordre d'ajout. final int creeLe; /// Quantité en stock (inventaire). final int stock; const Produit({ required this.id, this.codeBarres, required this.nom, this.imageUrl, required this.prix, this.prixAchat = 0, this.quantite, this.unite, this.nbContenants, this.creeLe = 0, this.stock = 0, }); bool get estPack => (nbContenants ?? 1) > 1; /// Gain en euros (vente − achat). Pertinent seulement si un prix d'achat est saisi. double get gain => prix - prixAchat; /// Coefficient multiplicateur (prix de vente ÷ prix d'achat), ex : 1,5. /// Null si le prix d'achat n'est pas renseigné. Sert au tri. double? get coefficient => prixAchat > 0 ? prix / prixAchat : null; /// Marge affichée : le coefficient (1,5) suivi d'un « % », ex : « 1,5 % », « 2 % ». /// Vide si le prix d'achat n'est pas renseigné. String get margeLibelle { final c = coefficient; if (c == null) return ''; var s = c.toStringAsFixed(2).replaceAll('.', ','); if (s.contains(',')) { s = s.replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r',$'), ''); } return '$s %'; } Produit copyWith({ String? id, String? codeBarres, String? nom, String? imageUrl, double? prix, double? prixAchat, double? quantite, String? unite, int? nbContenants, int? creeLe, int? stock, }) { return Produit( id: id ?? this.id, codeBarres: codeBarres ?? this.codeBarres, nom: nom ?? this.nom, imageUrl: imageUrl ?? this.imageUrl, prix: prix ?? this.prix, prixAchat: prixAchat ?? this.prixAchat, quantite: quantite ?? this.quantite, unite: unite ?? this.unite, nbContenants: nbContenants ?? this.nbContenants, creeLe: creeLe ?? this.creeLe, stock: stock ?? this.stock, ); } /// Prix à l'unité de mesure (€/kg ou €/L) — obligation légale pour les /// produits vendus au poids/volume. Null pour les produits à la pièce. ({double valeur, String suffixe})? get prixMesure { final q = quantite; if (q == null || q <= 0) return null; // Pour un pack, on calcule sur le volume/poids TOTAL (contenance × nombre). final total = q * (nbContenants ?? 1); return switch (unite) { Unites.g => (valeur: prix / (total / 1000), suffixe: '/kg'), Unites.kg => (valeur: prix / total, suffixe: '/kg'), Unites.ml => (valeur: prix / (total / 1000), suffixe: '/L'), Unites.cl => (valeur: prix / (total / 100), suffixe: '/L'), Unites.l => (valeur: prix / total, suffixe: '/L'), _ => null, }; } /// Libellé compact de la quantité, ex : « 330ml », « 1kg », « 75cl », /// ou « 6x33cl » pour un pack. Vide si absent. String get quantiteLibelle { final q = quantite; if (q == null || q <= 0 || !Unites.estValide(unite)) return ''; final n = q == q.roundToDouble() ? q.toInt().toString() : q.toString().replaceAll('.', ','); final base = '$n${Unites.libelle(unite)}'; return estPack ? '${nbContenants}X$base' : base; } /// Titre affiché (liste + étiquette) : la quantité est intégrée devant le nom, /// ex : « 1kg Farine », « 75cl Coca-Cola ». Sinon juste le nom. String get titre { final ql = quantiteLibelle; return ql.isEmpty ? nom : '$ql $nom'; } factory Produit.fromMap(Map map) { return Produit( id: map['id'].toString(), codeBarres: map['code_barres'] as String?, nom: (map['nom'] as String?) ?? '', imageUrl: map['image_url'] as String?, prix: (map['prix'] as num?)?.toDouble() ?? 0, prixAchat: (map['prix_achat'] as num?)?.toDouble() ?? 0, quantite: (map['quantite'] as num?)?.toDouble(), unite: map['unite'] as String?, nbContenants: (map['nb_contenants'] as num?)?.toInt(), creeLe: (map['cree_le'] as num?)?.toInt() ?? 0, stock: (map['stock'] as num?)?.toInt() ?? 0, ); } /// Map pour insertion/mise à jour dans la base (sans l'id, généré par SQLite). Map toInsertMap() { return { 'code_barres': codeBarres, 'nom': nom, 'image_url': imageUrl, 'prix': prix, 'prix_achat': prixAchat, 'quantite': quantite, 'unite': unite, 'nb_contenants': nbContenants, 'stock': stock, }; } }