You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
456 lines
13 KiB
456 lines
13 KiB
// pages/facture_screen.dart
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import '../models/command_detail.dart';
|
|
import '../services/pdf_service.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'information.dart';
|
|
|
|
class FactureScreen extends StatefulWidget {
|
|
final PrintTemplate template;
|
|
final CommandeDetail commande;
|
|
final String paymentMethod;
|
|
final String? tablename;
|
|
|
|
const FactureScreen({
|
|
super.key,
|
|
required this.commande,
|
|
required this.paymentMethod,
|
|
this.tablename,
|
|
required this.template,
|
|
});
|
|
|
|
@override
|
|
_FactureScreenState createState() => _FactureScreenState();
|
|
}
|
|
|
|
class _FactureScreenState extends State<FactureScreen> {
|
|
bool isPrinting = false;
|
|
String get paymentMethodText {
|
|
switch (widget.paymentMethod) {
|
|
case 'mvola':
|
|
return 'MVola';
|
|
case 'carte':
|
|
return 'CB';
|
|
case 'especes':
|
|
return 'Espèces';
|
|
default:
|
|
return 'CB';
|
|
}
|
|
}
|
|
|
|
String get factureNumber {
|
|
return 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(4)}';
|
|
}
|
|
|
|
String formatTemplateContent(String content) {
|
|
return content.replaceAll('\\r\\n', '\n').replaceAll('\\n', '\n');
|
|
}
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.grey[100],
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
|
onPressed: () {
|
|
Navigator.pushNamedAndRemoveUntil(context, '/encaissement', (route) => false);
|
|
},
|
|
),
|
|
title: const Text(
|
|
'Retour',
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
actions: [
|
|
Container(
|
|
margin: const EdgeInsets.only(right: 16, top: 8, bottom: 8),
|
|
child: ElevatedButton.icon(
|
|
onPressed: _printReceipt,
|
|
icon: const Icon(Icons.print, size: 18),
|
|
label: const Text('Imprimer'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF28A745),
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: Center(
|
|
child: Container(
|
|
width: 400,
|
|
margin: const EdgeInsets.all(20),
|
|
child: Card(
|
|
elevation: 2,
|
|
color: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(40),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildHeader(),
|
|
const SizedBox(height: 30),
|
|
_buildFactureInfo(),
|
|
const SizedBox(height: 30),
|
|
_buildItemsList(),
|
|
const SizedBox(height: 20),
|
|
_buildTotal(),
|
|
const SizedBox(height: 30),
|
|
_buildFooter(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
widget.template.title,
|
|
style: const TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 1.2,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
formatTemplateContent(widget.template.content),
|
|
style: const TextStyle(fontSize: 12, color: Colors.black87),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
|
|
Widget _buildFactureInfo() {
|
|
final now = DateTime.now();
|
|
final dateStr =
|
|
'${now.day.toString().padLeft(2, '0')}/${now.month.toString().padLeft(2, '0')}/${now.year}';
|
|
final timeStr =
|
|
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
|
|
|
return Column(
|
|
children: [
|
|
Text(
|
|
'Facture n° $factureNumber',
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Date: $dateStr $timeStr',
|
|
style: const TextStyle(fontSize: 12, color: Colors.black87),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Via: ${widget.commande?.tablename ?? widget.tablename}',
|
|
style: const TextStyle(fontSize: 12, color: Colors.black87),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Paiement: $paymentMethodText',
|
|
style: const TextStyle(fontSize: 12, color: Colors.black87),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildItemsList() {
|
|
return Column(
|
|
children: [
|
|
const Padding(
|
|
padding: EdgeInsets.only(bottom: 10),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Qté Désignation',
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
|
),
|
|
Text(
|
|
'Prix',
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1, color: Colors.black26),
|
|
const SizedBox(height: 10),
|
|
...widget.commande.items
|
|
.map(
|
|
(item) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 6),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'${item.quantite} ${item.menuNom}',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
'${NumberFormat("#,##0.00", "fr_FR").format(item.prixUnitaire * item.quantite)} AR',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
.toList(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTotal() {
|
|
return Column(
|
|
children: [
|
|
const Divider(height: 1, color: Colors.black26),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Total:',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
'${NumberFormat("#,##0.00", "fr_FR").format(widget.commande.totalTtc)} AR',
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildFooter() {
|
|
return const Text(
|
|
'Merci et à bientôt !',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontStyle: FontStyle.italic,
|
|
color: Colors.black54,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _printReceipt() async {
|
|
bool isPrinting = false;
|
|
setState(() => isPrinting = true);
|
|
|
|
try {
|
|
// Vérifier si l'impression est disponible
|
|
final canPrint = await PlatformPrintService.canPrint();
|
|
|
|
if (!canPrint) {
|
|
// Si pas d'imprimante, proposer seulement la sauvegarde
|
|
_showSaveOnlyDialog();
|
|
return;
|
|
}
|
|
|
|
// Afficher les options d'impression
|
|
final action = await showDialog<String>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Icon(Icons.print, color: Theme.of(context).primaryColor),
|
|
const SizedBox(width: 8),
|
|
const Text('Options d\'impression'),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Plateforme: ${_getPlatformName()}',
|
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text('Que souhaitez-vous faire ?'),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton.icon(
|
|
onPressed: () => Navigator.of(context).pop('print'),
|
|
icon: const Icon(Icons.print),
|
|
label: const Text('Imprimer'),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: () => Navigator.of(context).pop('save'),
|
|
icon: const Icon(Icons.save),
|
|
label: const Text('Sauvegarder PDF'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop('cancel'),
|
|
child: const Text('Annuler'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
if (action == null || action == 'cancel') return;
|
|
|
|
HapticFeedback.lightImpact();
|
|
bool success = false;
|
|
|
|
if (action == 'print') {
|
|
success = await PlatformPrintService.printFacture(
|
|
commande: widget.commande,
|
|
template: widget.template,
|
|
paymentMethod: widget.paymentMethod,
|
|
);
|
|
} else if (action == 'save') {
|
|
success = await PlatformPrintService.saveFacturePdf(
|
|
commande: widget.commande,
|
|
template: widget.template,
|
|
paymentMethod: widget.paymentMethod,
|
|
);
|
|
}
|
|
|
|
if (success) {
|
|
_showSuccessMessage(
|
|
action == 'print'
|
|
? 'Facture envoyée à l\'imprimante ${_getPlatformName()}'
|
|
: 'PDF sauvegardé avec succès',
|
|
);
|
|
} else {
|
|
_showErrorMessage(
|
|
'Erreur lors de ${action == 'print' ? 'l\'impression' : 'la sauvegarde'}',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
_showErrorMessage('Erreur: $e');
|
|
} finally {
|
|
setState(() => isPrinting = false);
|
|
}
|
|
}
|
|
|
|
void _showSaveOnlyDialog() async {
|
|
final shouldSave = await showDialog<bool>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Aucune imprimante'),
|
|
content: const Text(
|
|
'Aucune imprimante détectée. Voulez-vous sauvegarder le PDF ?',
|
|
),
|
|
actions: [
|
|
TextButton.icon(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
icon: const Icon(Icons.save),
|
|
label: const Text('Sauvegarder'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
|
|
if (shouldSave == true) {
|
|
final success = await PlatformPrintService.saveFacturePdf(
|
|
commande: widget.commande,
|
|
template: widget.template,
|
|
paymentMethod: widget.paymentMethod,
|
|
);
|
|
|
|
if (success) {
|
|
_showSuccessMessage('PDF sauvegardé avec succès');
|
|
} else {
|
|
_showErrorMessage('Erreur lors de la sauvegarde');
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showSuccessMessage(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.check_circle, color: Colors.white),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: Text(message)),
|
|
],
|
|
),
|
|
backgroundColor: const Color(0xFF28A745),
|
|
duration: const Duration(seconds: 3),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
|
margin: const EdgeInsets.all(16),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
|
|
Future.delayed(const Duration(seconds: 3), () {
|
|
if (mounted) {
|
|
Navigator.pop(context);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _showErrorMessage(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.error, color: Colors.white),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: Text(message)),
|
|
],
|
|
),
|
|
backgroundColor: Colors.red,
|
|
duration: const Duration(seconds: 3),
|
|
margin: const EdgeInsets.all(16),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getPlatformName() {
|
|
if (Platform.isAndroid) return 'Android';
|
|
if (Platform.isMacOS) return 'macOS';
|
|
if (Platform.isWindows) return 'Windows';
|
|
return 'cette plateforme';
|
|
}
|
|
}
|