Actualizacion del programa
This commit is contained in:
244
lib/screens/admin/export_pdf_screen.dart
Normal file
244
lib/screens/admin/export_pdf_screen.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../database/db_helper.dart';
|
||||
|
||||
class ExportPdfScreen extends StatefulWidget {
|
||||
const ExportPdfScreen({super.key});
|
||||
@override State<ExportPdfScreen> createState() => _ExportPdfScreenState();
|
||||
}
|
||||
|
||||
class _ExportPdfScreenState extends State<ExportPdfScreen> {
|
||||
bool _generating = false;
|
||||
String? _lastPath;
|
||||
|
||||
pw.TableRow _pdfRow(String label, String value, {bool isHeader = false}) =>
|
||||
pw.TableRow(
|
||||
decoration: isHeader ? pw.BoxDecoration(color: PdfColors.grey100) : null,
|
||||
children: [
|
||||
pw.Padding(padding: const pw.EdgeInsets.all(6),
|
||||
child: pw.Text(label, style: pw.TextStyle(
|
||||
fontSize: 10, fontWeight: isHeader ? pw.FontWeight.bold : null))),
|
||||
pw.Padding(padding: const pw.EdgeInsets.all(6),
|
||||
child: pw.Text(value, style: pw.TextStyle(
|
||||
fontSize: 10, fontWeight: pw.FontWeight.bold))),
|
||||
]);
|
||||
|
||||
Future<void> _generatePdf() async {
|
||||
setState(() => _generating = true);
|
||||
try {
|
||||
final stats = await DbHelper.getAdminStats();
|
||||
final colonias = await DbHelper.getReportesByColonia();
|
||||
final incidentes = await DbHelper.getIncidentesByRoute();
|
||||
final reviews = await DbHelper.getReviewSummaryByColonia();
|
||||
final now = DateTime.now();
|
||||
const meses = ['','Enero','Febrero','Marzo','Abril','Mayo','Junio',
|
||||
'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
|
||||
|
||||
final pdf = pw.Document();
|
||||
pdf.addPage(pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.all(32),
|
||||
header: (ctx) => pw.Column(children: [
|
||||
pw.Row(mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [
|
||||
pw.Column(crossAxisAlignment: pw.CrossAxisAlignment.start, children: [
|
||||
pw.Text('H. AYUNTAMIENTO DE CELAYA', style: pw.TextStyle(
|
||||
fontSize: 14, fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColor.fromHex('6D1E3A'))),
|
||||
pw.Text('Direccion de Servicios Publicos',
|
||||
style: const pw.TextStyle(fontSize: 11, color: PdfColors.grey700)),
|
||||
pw.Text('Sistema de Recoleccion de Residuos',
|
||||
style: const pw.TextStyle(fontSize: 10, color: PdfColors.grey600)),
|
||||
]),
|
||||
pw.Column(crossAxisAlignment: pw.CrossAxisAlignment.end, children: [
|
||||
pw.Text('REPORTE MENSUAL', style: pw.TextStyle(
|
||||
fontSize: 12, fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColor.fromHex('6D1E3A'))),
|
||||
pw.Text('${meses[now.month]} ${now.year}',
|
||||
style: const pw.TextStyle(fontSize: 11)),
|
||||
pw.Text('Generado: ${now.day}/${now.month}/${now.year}',
|
||||
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey600)),
|
||||
]),
|
||||
]),
|
||||
pw.Divider(color: PdfColor.fromHex('C9A84C'), thickness: 2),
|
||||
pw.SizedBox(height: 8),
|
||||
]),
|
||||
build: (ctx) => [
|
||||
pw.Text('RESUMEN EJECUTIVO', style: pw.TextStyle(
|
||||
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColor.fromHex('6D1E3A'))),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||
columnWidths: {0: const pw.FlexColumnWidth(2), 1: const pw.FlexColumnWidth(1)},
|
||||
children: [
|
||||
_pdfRow('Total de reportes ciudadanos', '${stats["total_reportes"]}', isHeader: true),
|
||||
_pdfRow('Total de resenas recibidas', '${stats["total_reviews"]}'),
|
||||
_pdfRow('Calificacion promedio',
|
||||
'${(stats["avg_rating"] as double? ?? 0).toStringAsFixed(2)} / 5.0'),
|
||||
_pdfRow('Alertas activas', '${stats["alertas_activas"]}'),
|
||||
_pdfRow('Conductores', '${stats["total_conductores"]}'),
|
||||
]),
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
if (colonias.isNotEmpty) ...[
|
||||
pw.Text('REPORTES POR COLONIA', style: pw.TextStyle(
|
||||
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColor.fromHex('6D1E3A'))),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||
columnWidths: {
|
||||
0: const pw.FlexColumnWidth(3), 1: const pw.FlexColumnWidth(1),
|
||||
2: const pw.FlexColumnWidth(1), 3: const pw.FlexColumnWidth(1),
|
||||
},
|
||||
children: [
|
||||
pw.TableRow(
|
||||
decoration: pw.BoxDecoration(color: PdfColor.fromHex('6D1E3A')),
|
||||
children: ['Colonia','Total','Resueltos','Pendientes'].map((h) =>
|
||||
pw.Padding(padding: const pw.EdgeInsets.all(6),
|
||||
child: pw.Text(h, style: pw.TextStyle(color: PdfColors.white,
|
||||
fontWeight: pw.FontWeight.bold, fontSize: 10)))).toList()),
|
||||
...colonias.map((c) {
|
||||
final total = c['total'] as int? ?? 0;
|
||||
final res = c['resueltos'] as int? ?? 0;
|
||||
return pw.TableRow(children: [
|
||||
c['colonia'] as String? ?? '', '$total', '$res', '${total - res}',
|
||||
].map((v) => pw.Padding(padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Text(v, style: const pw.TextStyle(fontSize: 9)))).toList());
|
||||
}),
|
||||
]),
|
||||
pw.SizedBox(height: 20),
|
||||
],
|
||||
|
||||
if (incidentes.isNotEmpty) ...[
|
||||
pw.Text('INCIDENTES POR RUTA', style: pw.TextStyle(
|
||||
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColor.fromHex('6D1E3A'))),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||
columnWidths: {0: const pw.FlexColumnWidth(2), 1: const pw.FlexColumnWidth(1)},
|
||||
children: [
|
||||
pw.TableRow(decoration: pw.BoxDecoration(color: PdfColor.fromHex('6D1E3A')),
|
||||
children: ['Ruta','Incidentes'].map((h) => pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(6),
|
||||
child: pw.Text(h, style: pw.TextStyle(color: PdfColors.white,
|
||||
fontWeight: pw.FontWeight.bold, fontSize: 10)))).toList()),
|
||||
...incidentes.map((r) => pw.TableRow(children: [
|
||||
r['route_id'] as String? ?? '', '${r["total"]}',
|
||||
].map((v) => pw.Padding(padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Text(v, style: const pw.TextStyle(fontSize: 9)))).toList())),
|
||||
]),
|
||||
pw.SizedBox(height: 20),
|
||||
],
|
||||
|
||||
if (reviews.isNotEmpty) ...[
|
||||
pw.Text('CALIFICACIONES POR COLONIA', style: pw.TextStyle(
|
||||
fontSize: 13, fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColor.fromHex('6D1E3A'))),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||
columnWidths: {0: const pw.FlexColumnWidth(3),
|
||||
1: const pw.FlexColumnWidth(1), 2: const pw.FlexColumnWidth(1)},
|
||||
children: [
|
||||
pw.TableRow(decoration: pw.BoxDecoration(color: PdfColor.fromHex('6D1E3A')),
|
||||
children: ['Colonia','Promedio','Total'].map((h) => pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(6),
|
||||
child: pw.Text(h, style: pw.TextStyle(color: PdfColors.white,
|
||||
fontWeight: pw.FontWeight.bold, fontSize: 10)))).toList()),
|
||||
...reviews.map((r) => pw.TableRow(children: [
|
||||
r['colonia'] as String? ?? '',
|
||||
'${(r["promedio"] as num? ?? 0).toStringAsFixed(1)}/5',
|
||||
'${r["total"]}',
|
||||
].map((v) => pw.Padding(padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Text(v, style: const pw.TextStyle(fontSize: 9)))).toList())),
|
||||
]),
|
||||
pw.SizedBox(height: 20),
|
||||
],
|
||||
|
||||
pw.Divider(color: PdfColor.fromHex('C9A84C')),
|
||||
pw.Text('Celaya Limpia - H. Ayuntamiento de Celaya, Gto. - ${now.year}',
|
||||
style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey500),
|
||||
textAlign: pw.TextAlign.center),
|
||||
],
|
||||
));
|
||||
|
||||
// Guardar en directorio temporal y compartir con share_plus
|
||||
final bytes = await pdf.save();
|
||||
final dir = await getTemporaryDirectory();
|
||||
final file = File('${dir.path}/reporte_celaya_${now.month}_${now.year}.pdf');
|
||||
await file.writeAsBytes(bytes);
|
||||
|
||||
setState(() => _lastPath = file.path);
|
||||
|
||||
await Share.shareXFiles(
|
||||
[XFile(file.path, mimeType: 'application/pdf')],
|
||||
subject: 'Reporte Mensual Celaya Limpia - ${meses[now.month]} ${now.year}',
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error al generar PDF: $e'),
|
||||
backgroundColor: AppColors.rojoError));
|
||||
}
|
||||
if (mounted) setState(() => _generating = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
title: const Text('Exportar Reporte PDF'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado))),
|
||||
body: Center(child: Padding(padding: const EdgeInsets.all(32), child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Container(width: 100, height: 100,
|
||||
decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1),
|
||||
shape: BoxShape.circle),
|
||||
child: const Icon(Icons.picture_as_pdf, size: 52, color: AppColors.verdeAdmin)),
|
||||
const SizedBox(height: 24),
|
||||
const Text('Reporte Mensual', style: TextStyle(fontSize: 22,
|
||||
fontWeight: FontWeight.bold, color: AppColors.verdeAdmin)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Genera un PDF con el resumen completo:\nreportes, incidentes y calificaciones.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.grisTexto)),
|
||||
const SizedBox(height: 32),
|
||||
SizedBox(width: double.infinity, height: 52,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _generating ? null : _generatePdf,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
||||
icon: _generating
|
||||
? const SizedBox(width: 20, height: 20,
|
||||
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: const Icon(Icons.download),
|
||||
label: Text(_generating ? 'Generando...' : 'Generar y Compartir PDF',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)))),
|
||||
if (_lastPath != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(color: AppColors.verdeExito.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.verdeExito.withOpacity(0.3))),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.check_circle, color: AppColors.verdeExito, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(child: Text('PDF generado correctamente',
|
||||
style: TextStyle(color: AppColors.verdeExito, fontWeight: FontWeight.w600,
|
||||
fontSize: 13))),
|
||||
TextButton(onPressed: _generatePdf,
|
||||
child: const Text('Compartir de nuevo',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.verdeAdmin))),
|
||||
])),
|
||||
],
|
||||
]))));
|
||||
}
|
||||
Reference in New Issue
Block a user