175 lines
8.2 KiB
Dart
175 lines
8.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import '../providers/factura_provider.dart';
|
|
import '../providers/configuracion_provider.dart';
|
|
import '../utils/currency_utils.dart';
|
|
|
|
class EstadisticasScreen extends StatefulWidget {
|
|
const EstadisticasScreen({super.key});
|
|
|
|
@override
|
|
State<EstadisticasScreen> createState() => _EstadisticasScreenState();
|
|
}
|
|
|
|
class _EstadisticasScreenState extends State<EstadisticasScreen> {
|
|
late int _year;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_year = DateTime.now().year;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final facturaProv = context.watch<FacturaProvider>();
|
|
final configProv = context.watch<ConfiguracionProvider>();
|
|
final moneda = configProv.config?.moneda ?? 'USD';
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Estadísticas')),
|
|
body: FutureBuilder(
|
|
future: Future.wait([
|
|
facturaProv.gastoMensual(_year),
|
|
facturaProv.gastosPorServicio(_year, DateTime.now().month),
|
|
facturaProv.totalGastadoMes(_year, DateTime.now().month),
|
|
]),
|
|
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
|
|
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
|
|
|
|
final gastoMensual = snapshot.data![0] as List<Map<String, dynamic>>;
|
|
final gastoPorServicio = snapshot.data![1] as Map<String, double>;
|
|
final totalMes = snapshot.data![2] as double;
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
Text('Resumen $_year', style: Theme.of(context).textTheme.titleLarge),
|
|
const SizedBox(height: 8),
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.primaryContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(Icons.attach_money, color: Theme.of(context).colorScheme.primary, size: 28),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Gasto mensual actual', style: Theme.of(context).textTheme.titleSmall),
|
|
const SizedBox(height: 4),
|
|
Text(CurrencyUtils.formatPrice(totalMes, moneda),
|
|
style: Theme.of(context).textTheme.headlineMedium),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text('Gasto mensual ($_year)', style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
SizedBox(
|
|
height: 200,
|
|
child: BarChart(
|
|
BarChartData(
|
|
alignment: BarChartAlignment.spaceAround,
|
|
maxY: (gastoMensual.isEmpty ? 1000 : gastoMensual.map((e) => (e['total'] as num).toDouble()).reduce((a, b) => a > b ? a : b)) * 1.2,
|
|
barGroups: List.generate(12, (i) {
|
|
final mes = (i + 1).toString().padLeft(2, '0');
|
|
final data = gastoMensual.where((e) => e['mes'] == mes).firstOrNull;
|
|
return BarChartGroupData(
|
|
x: i,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY: data != null ? (data['total'] as num).toDouble() : 0,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
width: 16,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
titlesData: FlTitlesData(
|
|
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 40, getTitlesWidget: (v, _) => Text('${CurrencyUtils.getSymbol(moneda)}${v.toInt()}', style: const TextStyle(fontSize: 10)))),
|
|
bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, getTitlesWidget: (v, _) {
|
|
const meses = ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];
|
|
return Text(meses[v.toInt()], style: const TextStyle(fontSize: 10));
|
|
})),
|
|
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
),
|
|
gridData: FlGridData(show: true, drawVerticalLine: false, horizontalInterval: (gastoMensual.isEmpty ? 1000 : gastoMensual.map((e) => (e['total'] as num).toDouble()).reduce((a, b) => a > b ? a : b)) / 4),
|
|
borderData: FlBorderData(show: false),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text('Distribución por servicio', style: Theme.of(context).textTheme.titleMedium),
|
|
const SizedBox(height: 8),
|
|
if (gastoPorServicio.isEmpty)
|
|
Card(child: Padding(padding: const EdgeInsets.all(16), child: Text('Sin datos este mes')))
|
|
else
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: gastoPorServicio.entries.toList().asMap().entries.map((entry) {
|
|
final i = entry.key;
|
|
final e = entry.value;
|
|
final total = gastoPorServicio.values.fold(0.0, (a, b) => a + b);
|
|
final pct = total > 0 ? e.value / total * 100 : 0.0;
|
|
return TweenAnimationBuilder<double>(
|
|
tween: Tween(begin: 0, end: 1),
|
|
duration: Duration(milliseconds: 300 + i * 80),
|
|
curve: Curves.easeOutCubic,
|
|
builder: (context, value, child) {
|
|
return Opacity(
|
|
opacity: value,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 12 * (1 - value)),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
children: [
|
|
SizedBox(width: 100, child: Text(e.key, style: const TextStyle(fontSize: 13))),
|
|
Expanded(
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: LinearProgressIndicator(
|
|
value: pct / 100,
|
|
backgroundColor: Colors.grey[200],
|
|
minHeight: 12,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
SizedBox(width: 70, child: Text(CurrencyUtils.formatPrice(e.value, moneda), style: const TextStyle(fontSize: 12), textAlign: TextAlign.right)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|