Files
gestion_prix_produit/lib/screens/etiquettes_screen.dart
T
Mathew 4d936d2ee3 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>
2026-07-17 21:28:33 +02:00

277 lines
8.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:printing/printing.dart';
import '../models/produit.dart';
import '../services/etiquette_pdf.dart';
import '../services/produit_repository.dart';
import '../theme.dart';
import '../utils/format.dart';
import '../utils/tri.dart';
import '../widgets/produit_image.dart';
/// Onglet 3 : sélection de produits puis génération/impression des étiquettes.
class EtiquettesScreen extends StatefulWidget {
const EtiquettesScreen({super.key});
@override
State<EtiquettesScreen> createState() => _EtiquettesScreenState();
}
class _EtiquettesScreenState extends State<EtiquettesScreen> {
final _repo = ProduitRepository.instance;
final Set<String> _selection = {};
Tri _tri = Tri.recent;
bool _tousSelectionnes(List<Produit> produits) =>
produits.isNotEmpty && _selection.length == produits.length;
void _basculerTout(List<Produit> produits) {
setState(() {
if (_tousSelectionnes(produits)) {
_selection.clear();
} else {
_selection
..clear()
..addAll(produits.map((p) => p.id));
}
});
}
Future<void> _imprimer(List<Produit> produits) async {
final choisis =
produits.where((p) => _selection.contains(p.id)).toList();
if (choisis.isEmpty) return;
await Printing.layoutPdf(
name: 'etiquettes_prix',
onLayout: (format) => EtiquettePdf.construire(choisis),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: ListenableBuilder(
listenable: _repo,
builder: (context, _) {
final produits = trierProduits(_repo.produits, _tri);
// Nettoie la sélection des produits supprimés.
_selection.retainWhere((id) => produits.any((p) => p.id == id));
return Column(
children: [
_entete(produits),
Expanded(
child: produits.isEmpty
? _vide()
: ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 120),
itemCount: produits.length,
separatorBuilder: (_, _) =>
const SizedBox(height: 8),
itemBuilder: (_, i) {
final p = produits[i];
final sel = _selection.contains(p.id);
return _LigneSelection(
produit: p,
selectionne: sel,
onTap: () => setState(() {
sel
? _selection.remove(p.id)
: _selection.add(p.id);
}),
);
},
),
),
],
);
},
),
),
floatingActionButton: ListenableBuilder(
listenable: _repo,
builder: (context, _) {
final n = _selection.length;
return FloatingActionButton.extended(
onPressed: n == 0
? null
: () => _imprimer(trierProduits(_repo.produits, _tri)),
backgroundColor: n == 0 ? Colors.grey : AppTheme.accent,
foregroundColor: Colors.white,
icon: const Icon(Icons.print),
label: Text(n == 0 ? 'Imprimer' : 'Imprimer ($n)'),
);
},
),
);
}
Widget _entete(List<Produit> produits) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Étiquettes',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.w800,
letterSpacing: -1)),
const SizedBox(height: 4),
const Text('Sélectionne les produits à imprimer',
style: TextStyle(color: AppTheme.grisTexte, fontSize: 14)),
const SizedBox(height: 12),
Row(
children: [
Text('${_selection.length} sélectionné(s)',
style: const TextStyle(fontWeight: FontWeight.w600)),
const Spacer(),
TextButton.icon(
onPressed:
produits.isEmpty ? null : () => _basculerTout(produits),
icon: Icon(_tousSelectionnes(produits)
? Icons.remove_done
: Icons.done_all),
label: Text(_tousSelectionnes(produits)
? 'Tout désélectionner'
: 'Tout sélectionner'),
style: TextButton.styleFrom(foregroundColor: AppTheme.accent),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: 36,
child: ListView(
scrollDirection: Axis.horizontal,
children: [for (final t in Tri.values) _puceTri(t)],
),
),
],
),
);
}
Widget _puceTri(Tri t) {
final sel = _tri == t;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(t.libelle),
selected: sel,
onSelected: (_) => setState(() => _tri = t),
showCheckmark: false,
labelStyle: TextStyle(
fontWeight: FontWeight.w600,
color: sel ? Colors.white : AppTheme.grisTexte,
),
selectedColor: AppTheme.accent,
backgroundColor: Theme.of(context).colorScheme.surface,
side: BorderSide(
color: sel
? AppTheme.accent
: Theme.of(context).colorScheme.outlineVariant,
),
),
);
}
Widget _vide() {
return const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text(
'Ajoute dabord des produits pour pouvoir imprimer leurs étiquettes.',
textAlign: TextAlign.center,
style: TextStyle(color: AppTheme.grisTexte, fontSize: 15),
),
),
);
}
}
class _LigneSelection extends StatelessWidget {
final Produit produit;
final bool selectionne;
final VoidCallback onTap;
const _LigneSelection({
required this.produit,
required this.selectionne,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Material(
color: selectionne
? AppTheme.accent.withValues(alpha: 0.08)
: scheme.surface,
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: selectionne
? AppTheme.accent
: scheme.outlineVariant.withValues(alpha: 0.5),
width: selectionne ? 1.6 : 1,
),
),
child: Row(
children: [
_Case(coche: selectionne),
const SizedBox(width: 12),
ProduitImage(url: produit.imageUrl, taille: 44, radius: 10),
const SizedBox(width: 12),
Expanded(
child: Text(produit.titre,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
),
const SizedBox(width: 8),
Text(euro(produit.prix),
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w800)),
],
),
),
),
);
}
}
class _Case extends StatelessWidget {
final bool coche;
const _Case({required this.coche});
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 120),
width: 26,
height: 26,
decoration: BoxDecoration(
color: coche ? AppTheme.accent : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: coche ? AppTheme.accent : AppTheme.grisTexte,
width: 2,
),
),
child: coche
? const Icon(Icons.check, size: 18, color: Colors.white)
: null,
);
}
}