Agrega PRIVACY.md, actualiza AGENTS.md
This commit is contained in:
@@ -4,6 +4,7 @@ import 'providers/servicio_provider.dart';
|
||||
import 'providers/factura_provider.dart';
|
||||
import 'providers/configuracion_provider.dart';
|
||||
import 'screens/splash_screen.dart';
|
||||
import 'utils/transitions.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -25,14 +26,88 @@ class HogarControlApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'HogarControl',
|
||||
title: 'Factura del Hogar',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorSchemeSeed: Colors.teal,
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
theme: _buildLightTheme(),
|
||||
darkTheme: _buildDarkTheme(),
|
||||
themeMode: ThemeMode.system,
|
||||
home: const SplashScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData _buildLightTheme() {
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: Colors.teal,
|
||||
brightness: Brightness.light,
|
||||
);
|
||||
return ThemeData(
|
||||
colorScheme: colorScheme,
|
||||
useMaterial3: true,
|
||||
cardTheme: CardTheme(
|
||||
elevation: 1,
|
||||
shadowColor: colorScheme.shadow,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: SmoothPageTransitionBuilder(),
|
||||
TargetPlatform.iOS: SmoothPageTransitionBuilder(),
|
||||
},
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
titleLarge: TextStyle(fontWeight: FontWeight.w600),
|
||||
titleMedium: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData _buildDarkTheme() {
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: Colors.teal,
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
return ThemeData(
|
||||
colorScheme: colorScheme,
|
||||
useMaterial3: true,
|
||||
cardTheme: CardTheme(
|
||||
elevation: 1,
|
||||
shadowColor: colorScheme.shadow,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: SmoothPageTransitionBuilder(),
|
||||
TargetPlatform.iOS: SmoothPageTransitionBuilder(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
import '../providers/factura_provider.dart';
|
||||
import '../providers/servicio_provider.dart';
|
||||
import '../providers/configuracion_provider.dart';
|
||||
import '../utils/currency_utils.dart';
|
||||
|
||||
class CalendarioScreen extends StatefulWidget {
|
||||
const CalendarioScreen({super.key});
|
||||
@@ -54,7 +54,7 @@ class _CalendarioScreenState extends State<CalendarioScreen> {
|
||||
context.watch<FacturaProvider>();
|
||||
context.watch<ServicioProvider>();
|
||||
final configProv = context.watch<ConfiguracionProvider>();
|
||||
final symbol = configProv.config?.moneda == 'USD' ? '\$' : configProv.config?.moneda == 'EUR' ? '€' : 'MX\$';
|
||||
final moneda = configProv.config?.moneda ?? 'USD';
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Calendario de pagos')),
|
||||
@@ -103,17 +103,41 @@ class _CalendarioScreenState extends State<CalendarioScreen> {
|
||||
Expanded(
|
||||
child: _selectedDay == null || (_eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)]?.isEmpty ?? true)
|
||||
? Center(child: Text('Sin facturas en esta fecha', style: Theme.of(context).textTheme.bodyLarge))
|
||||
: ListView(
|
||||
children: (_eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)] ?? []).map((e) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(backgroundColor: e['color'] as Color, child: Text((e['servicio'] as String).substring(0, 2).toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 12))),
|
||||
title: Text(e['servicio'] as String),
|
||||
subtitle: Text('$symbol${NumberFormat('#,##0.00').format(e['monto'])}'),
|
||||
trailing: Chip(label: Text(e['estado'] as String, style: TextStyle(fontSize: 12, color: e['estado'] == 'Pagada' ? Colors.green : Colors.orange))),
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: (_eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)] ?? []).length,
|
||||
itemBuilder: (_, i) {
|
||||
final e = _eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)]![i];
|
||||
final esPagada = e['estado'] == 'Pagada';
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: Duration(milliseconds: 250 + i * 80),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 16 * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(backgroundColor: e['color'] as Color, child: Text((e['servicio'] as String).substring(0, 2).toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 12))),
|
||||
title: Text(e['servicio'] as String, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(CurrencyUtils.formatPrice((e['monto'] as num).toDouble(), moneda)),
|
||||
trailing: Chip(
|
||||
label: Text(e['estado'] as String, style: TextStyle(fontSize: 12, color: esPagada ? Colors.green : Colors.orange)),
|
||||
backgroundColor: esPagada ? Colors.green.withValues(alpha: 0.12) : Colors.orange.withValues(alpha: 0.12),
|
||||
side: BorderSide.none,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/configuracion.dart';
|
||||
import '../providers/configuracion_provider.dart';
|
||||
import '../utils/currency_utils.dart';
|
||||
|
||||
class ConfiguracionScreen extends StatefulWidget {
|
||||
const ConfiguracionScreen({super.key});
|
||||
@@ -21,7 +22,7 @@ class _ConfiguracionScreenState extends State<ConfiguracionScreen> {
|
||||
{'label': 'Día de vencimiento', 'value': 0},
|
||||
];
|
||||
|
||||
final List<String> _monedas = ['USD', 'EUR', 'MXN', 'COP', 'ARS', 'PEN', 'CLP', 'BRL'];
|
||||
final List<CurrencyInfo> _monedas = CurrencyUtils.allCurrencies;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,7 +60,7 @@ class _ConfiguracionScreenState extends State<ConfiguracionScreen> {
|
||||
DropdownButtonFormField<String>(
|
||||
value: _moneda,
|
||||
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||
items: _monedas.map((m) => DropdownMenuItem(value: m, child: Text(m))).toList(),
|
||||
items: _monedas.map((m) => DropdownMenuItem(value: m.code, child: Text(CurrencyUtils.getDisplayName(m.code)))).toList(),
|
||||
onChanged: (v) => setState(() => _moneda = v!),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:intl/intl.dart';
|
||||
import '../models/factura.dart';
|
||||
import '../providers/factura_provider.dart';
|
||||
import '../providers/servicio_provider.dart';
|
||||
import '../providers/configuracion_provider.dart';
|
||||
import '../utils/currency_utils.dart';
|
||||
|
||||
class CrearFacturaScreen extends StatefulWidget {
|
||||
const CrearFacturaScreen({super.key});
|
||||
@@ -40,6 +42,9 @@ class _CrearFacturaScreenState extends State<CrearFacturaScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final servicioProv = context.watch<ServicioProvider>();
|
||||
final facturaProv = context.read<FacturaProvider>();
|
||||
final configProv = context.watch<ConfiguracionProvider>();
|
||||
final moneda = configProv.config?.moneda ?? 'USD';
|
||||
final symbol = CurrencyUtils.getSymbol(moneda);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Nueva factura')),
|
||||
@@ -58,7 +63,7 @@ class _CrearFacturaScreenState extends State<CrearFacturaScreen> {
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _montoCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Monto', border: OutlineInputBorder(), prefixText: '\$ '),
|
||||
decoration: InputDecoration(labelText: 'Monto', border: const OutlineInputBorder(), prefixText: '$symbol '),
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return 'Ingresa el monto';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../providers/servicio_provider.dart';
|
||||
import '../providers/factura_provider.dart';
|
||||
import '../providers/configuracion_provider.dart';
|
||||
import '../widgets/stat_card.dart';
|
||||
import '../utils/currency_utils.dart';
|
||||
import 'servicios_screen.dart';
|
||||
import 'facturas_screen.dart';
|
||||
import 'calendario_screen.dart';
|
||||
@@ -37,13 +37,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
final servicioProv = context.watch<ServicioProvider>();
|
||||
final configProv = context.watch<ConfiguracionProvider>();
|
||||
final moneda = configProv.config?.moneda ?? 'USD';
|
||||
final symbol = moneda == 'USD' ? '\$' : moneda == 'EUR' ? '€' : moneda == 'MXN' ? 'MX\$' : '\$';
|
||||
|
||||
final pendientes = facturaProv.facturas.where((f) => f.estado == 'Pendiente').length;
|
||||
final vencidas = facturaProv.facturasVencidas.length;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('HogarControl'), centerTitle: true, actions: [
|
||||
appBar: AppBar(title: const Text('Factura del Hogar'), centerTitle: true, actions: [
|
||||
IconButton(icon: const Icon(Icons.settings), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ConfiguracionScreen()))),
|
||||
]),
|
||||
body: RefreshIndicator(
|
||||
@@ -58,6 +57,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
value: '$pendientes',
|
||||
icon: Icons.pending_actions,
|
||||
color: Colors.orange,
|
||||
delayMs: 50,
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: StatCard(
|
||||
@@ -65,6 +65,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
value: '$vencidas',
|
||||
icon: Icons.warning_rounded,
|
||||
color: Colors.red,
|
||||
delayMs: 100,
|
||||
)),
|
||||
],
|
||||
),
|
||||
@@ -76,36 +77,74 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
value: facturaProv.facturas.where((f) => f.estado == 'Pagada').length.toString(),
|
||||
icon: Icons.check_circle,
|
||||
color: Colors.green,
|
||||
delayMs: 150,
|
||||
)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: StatCard(
|
||||
title: 'Total gastado',
|
||||
value: '$symbol${NumberFormat('#,##0.00').format(facturaProv.facturas.where((f) => f.estado == 'Pagada').fold(0.0, (sum, f) => sum + f.monto))}',
|
||||
value: CurrencyUtils.formatPrice(facturaProv.facturas.where((f) => f.estado == 'Pagada').fold(0.0, (sum, f) => sum + f.monto), moneda),
|
||||
icon: Icons.attach_money,
|
||||
color: Colors.blue,
|
||||
delayMs: 200,
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text('Próximos vencimientos', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.event_note, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text('Próximos vencimientos', style: Theme.of(context).textTheme.titleMedium),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...facturaProv.facturas
|
||||
.where((f) => f.estado == 'Pendiente')
|
||||
.take(5)
|
||||
.map((f) {
|
||||
.toList()
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final i = entry.key;
|
||||
final f = entry.value;
|
||||
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Color(servicio?.color ?? 0xFF6200EE),
|
||||
child: Text(servicio?.nombre.substring(0, 2).toUpperCase() ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||
),
|
||||
title: Text(servicio?.nombre ?? 'Desconocido'),
|
||||
subtitle: Text('Ven: ${f.fechaVencimiento} $symbol${NumberFormat('#,##0.00').format(f.monto)}'),
|
||||
trailing: TextButton(
|
||||
onPressed: () => facturaProv.marcarPagada(f.id!),
|
||||
child: const Text('Pagar'),
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: Duration(milliseconds: 350 + i * 80),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 16 * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Color(servicio?.color ?? 0xFF6200EE),
|
||||
child: Text(servicio?.nombre.substring(0, 2).toUpperCase() ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||
),
|
||||
title: Text(servicio?.nombre ?? 'Desconocido'),
|
||||
subtitle: Text('Vence: ${f.fechaVencimiento}'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(CurrencyUtils.formatPrice(f.monto, moneda),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary)),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
icon: Icon(Icons.check_circle_outline, color: Theme.of(context).colorScheme.primary),
|
||||
onPressed: () => facturaProv.marcarPagada(f.id!),
|
||||
tooltip: 'Pagar',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.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});
|
||||
@@ -26,7 +26,6 @@ class _EstadisticasScreenState extends State<EstadisticasScreen> {
|
||||
final facturaProv = context.watch<FacturaProvider>();
|
||||
final configProv = context.watch<ConfiguracionProvider>();
|
||||
final moneda = configProv.config?.moneda ?? 'USD';
|
||||
final symbol = moneda == 'USD' ? '\$' : moneda == 'EUR' ? '€' : moneda == 'MXN' ? 'MX\$' : '\$';
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Estadísticas')),
|
||||
@@ -48,19 +47,33 @@ class _EstadisticasScreenState extends State<EstadisticasScreen> {
|
||||
children: [
|
||||
Text('Resumen $_year', style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Gasto mensual actual', style: Theme.of(context).textTheme.titleSmall),
|
||||
Text('$symbol${NumberFormat('#,##0.00').format(totalMes)}',
|
||||
style: Theme.of(context).textTheme.headlineMedium),
|
||||
],
|
||||
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),
|
||||
@@ -86,7 +99,7 @@ class _EstadisticasScreenState extends State<EstadisticasScreen> {
|
||||
);
|
||||
}),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 40, getTitlesWidget: (v, _) => Text('$symbol${v.toInt()}', style: const TextStyle(fontSize: 10)))),
|
||||
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));
|
||||
@@ -109,27 +122,43 @@ class _EstadisticasScreenState extends State<EstadisticasScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: gastoPorServicio.entries.map((e) {
|
||||
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 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,
|
||||
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('$symbol${NumberFormat('#,##0.00').format(e.value)}', style: const TextStyle(fontSize: 12), textAlign: TextAlign.right)),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 70, child: Text(CurrencyUtils.formatPrice(e.value, moneda), style: const TextStyle(fontSize: 12), textAlign: TextAlign.right)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../providers/factura_provider.dart';
|
||||
import '../providers/servicio_provider.dart';
|
||||
import '../providers/configuracion_provider.dart';
|
||||
import '../utils/currency_utils.dart';
|
||||
|
||||
class FacturasScreen extends StatefulWidget {
|
||||
const FacturasScreen({super.key});
|
||||
@@ -28,7 +28,6 @@ class _FacturasScreenState extends State<FacturasScreen> {
|
||||
final servicioProv = context.watch<ServicioProvider>();
|
||||
final configProv = context.watch<ConfiguracionProvider>();
|
||||
final moneda = configProv.config?.moneda ?? 'USD';
|
||||
final symbol = moneda == 'USD' ? '\$' : moneda == 'EUR' ? '€' : moneda == 'MXN' ? 'MX\$' : '\$';
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Facturas')),
|
||||
@@ -75,28 +74,44 @@ class _FacturasScreenState extends State<FacturasScreen> {
|
||||
final f = facturaProv.facturas[i];
|
||||
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
|
||||
final vencida = f.estado == 'Pendiente' && DateTime.tryParse(f.fechaVencimiento)?.isBefore(DateTime.now()) == true;
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Color(servicio?.color ?? 0xFF6200EE),
|
||||
child: Text(servicio?.nombre.substring(0, 2).toUpperCase() ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||
),
|
||||
title: Text('${servicio?.nombre ?? 'Desconocido'} - $symbol${NumberFormat('#,##0.00').format(f.monto)}'),
|
||||
subtitle: Text('Vence: ${f.fechaVencimiento}${f.estado == 'Pagada' ? ' Pagada: ${f.fechaPago ?? ''}' : ''}'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (f.estado == 'Pendiente')
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: Duration(milliseconds: 300 + i * 50),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 16 * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Color(servicio?.color ?? 0xFF6200EE),
|
||||
child: Text(servicio?.nombre.substring(0, 2).toUpperCase() ?? '??',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12)),
|
||||
),
|
||||
title: Text('${servicio?.nombre ?? 'Desconocido'} - ${CurrencyUtils.formatPrice(f.monto, moneda)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text('Vence: ${f.fechaVencimiento}${f.estado == 'Pagada' ? ' Pagada: ${f.fechaPago ?? ''}' : ''}'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (f.estado == 'Pendiente')
|
||||
IconButton(
|
||||
icon: Icon(Icons.check_circle, color: vencida ? Colors.red : Colors.green),
|
||||
onPressed: () => facturaProv.marcarPagada(f.id!),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.check_circle, color: vencida ? Colors.red : Colors.green),
|
||||
onPressed: () => facturaProv.marcarPagada(f.id!),
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () => facturaProv.eliminarFactura(f.id!),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () => facturaProv.eliminarFactura(f.id!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -32,35 +32,50 @@ class ServiciosScreen extends StatelessWidget {
|
||||
itemCount: provider.servicios.length,
|
||||
itemBuilder: (_, i) {
|
||||
final s = provider.servicios[i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Color(s.color),
|
||||
child: Icon(_getIcon(s.icono), color: Colors.white),
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: Duration(milliseconds: 300 + i * 60),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - value)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Color(s.color),
|
||||
child: Icon(_getIcon(s.icono), color: Colors.white),
|
||||
),
|
||||
title: Text(s.nombre, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text('Vence día ${s.diaVencimiento}'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Eliminar servicio'),
|
||||
content: Text('¿Eliminar ${s.nombre}? Se eliminarán sus facturas.'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancelar')),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Eliminar')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) provider.eliminarServicio(s.id!);
|
||||
},
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => CrearServicioScreen(servicio: s)),
|
||||
).then((_) => provider.cargarServicios()),
|
||||
),
|
||||
title: Text(s.nombre),
|
||||
subtitle: Text('Vence día ${s.diaVencimiento}'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Eliminar servicio'),
|
||||
content: Text('¿Eliminar ${s.nombre}? Se eliminarán sus facturas.'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancelar')),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Eliminar')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok == true) provider.eliminarServicio(s.id!);
|
||||
},
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => CrearServicioScreen(servicio: s)),
|
||||
).then((_) => provider.cargarServicios()),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,10 +11,39 @@ class SplashScreen extends StatefulWidget {
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen> {
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnim;
|
||||
late Animation<double> _fadeAnim;
|
||||
late Animation<Offset> _slideAnim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
_scaleAnim = Tween<double>(begin: 0.5, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
|
||||
);
|
||||
_fadeAnim = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.3, 0.7, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
_slideAnim = Tween<Offset>(
|
||||
begin: const Offset(0, 0.3),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.4, 0.8, curve: Curves.easeOutCubic),
|
||||
),
|
||||
);
|
||||
_controller.forward();
|
||||
_init();
|
||||
}
|
||||
|
||||
@@ -22,6 +51,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
await NotificationService.instance.init();
|
||||
await context.read<ConfiguracionProvider>().cargarConfiguracion();
|
||||
await NotificationService.instance.scheduleRecordatorios();
|
||||
await Future.delayed(const Duration(milliseconds: 1200));
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const DashboardScreen()),
|
||||
@@ -29,21 +59,75 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.home_rounded, size: 80, color: Theme.of(context).colorScheme.onPrimary),
|
||||
const SizedBox(height: 16),
|
||||
Text('HogarControl',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onPrimary)),
|
||||
const SizedBox(height: 24),
|
||||
CircularProgressIndicator(color: Theme.of(context).colorScheme.onPrimary),
|
||||
],
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.primary,
|
||||
colorScheme.primaryContainer,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: _scaleAnim,
|
||||
child: Icon(
|
||||
Icons.home_rounded,
|
||||
size: 80,
|
||||
color: colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
FadeTransition(
|
||||
opacity: _fadeAnim,
|
||||
child: SlideTransition(
|
||||
position: _slideAnim,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Factura del Hogar',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Controla tus gastos del hogar',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.onPrimary.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
FadeTransition(
|
||||
opacity: _fadeAnim,
|
||||
child: CircularProgressIndicator(
|
||||
color: colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
76
lib/utils/currency_utils.dart
Normal file
76
lib/utils/currency_utils.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class CurrencyInfo {
|
||||
final String code;
|
||||
final String name;
|
||||
final String symbol;
|
||||
|
||||
const CurrencyInfo({
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
});
|
||||
}
|
||||
|
||||
class CurrencyUtils {
|
||||
static const List<CurrencyInfo> allCurrencies = [
|
||||
CurrencyInfo(code: 'USD', name: 'US Dollar', symbol: r'$'),
|
||||
CurrencyInfo(code: 'EUR', name: 'Euro', symbol: '€'),
|
||||
CurrencyInfo(code: 'GBP', name: 'British Pound', symbol: '£'),
|
||||
CurrencyInfo(code: 'JPY', name: 'Japanese Yen', symbol: '¥'),
|
||||
CurrencyInfo(code: 'MXN', name: 'Mexican Peso', symbol: r'MX$'),
|
||||
CurrencyInfo(code: 'COP', name: 'Colombian Peso', symbol: r'$'),
|
||||
CurrencyInfo(code: 'ARS', name: 'Argentine Peso', symbol: r'$'),
|
||||
CurrencyInfo(code: 'PEN', name: 'Peruvian Sol', symbol: 'S/'),
|
||||
CurrencyInfo(code: 'CLP', name: 'Chilean Peso', symbol: r'$'),
|
||||
CurrencyInfo(code: 'BRL', name: 'Brazilian Real', symbol: r'R$'),
|
||||
CurrencyInfo(code: 'CAD', name: 'Canadian Dollar', symbol: r'CA$'),
|
||||
CurrencyInfo(code: 'AUD', name: 'Australian Dollar', symbol: r'A$'),
|
||||
CurrencyInfo(code: 'CHF', name: 'Swiss Franc', symbol: 'CHF'),
|
||||
CurrencyInfo(code: 'CNY', name: 'Chinese Yuan', symbol: '¥'),
|
||||
CurrencyInfo(code: 'INR', name: 'Indian Rupee', symbol: '₹'),
|
||||
CurrencyInfo(code: 'KRW', name: 'South Korean Won', symbol: '₩'),
|
||||
CurrencyInfo(code: 'SEK', name: 'Swedish Krona', symbol: 'kr'),
|
||||
CurrencyInfo(code: 'NOK', name: 'Norwegian Krone', symbol: 'kr'),
|
||||
CurrencyInfo(code: 'NZD', name: 'New Zealand Dollar', symbol: r'NZ$'),
|
||||
CurrencyInfo(code: 'UYU', name: 'Uruguayan Peso', symbol: r'$U'),
|
||||
CurrencyInfo(code: 'PYG', name: 'Paraguayan Guarani', symbol: '₲'),
|
||||
CurrencyInfo(code: 'BOB', name: 'Bolivian Boliviano', symbol: 'Bs'),
|
||||
CurrencyInfo(code: 'CRC', name: 'Costa Rican Colón', symbol: '₡'),
|
||||
CurrencyInfo(code: 'DOP', name: 'Dominican Peso', symbol: r'RD$'),
|
||||
CurrencyInfo(code: 'GTQ', name: 'Guatemalan Quetzal', symbol: 'Q'),
|
||||
CurrencyInfo(code: 'HNL', name: 'Honduran Lempira', symbol: 'L'),
|
||||
CurrencyInfo(code: 'NIO', name: 'Nicaraguan Córdoba', symbol: r'C$'),
|
||||
CurrencyInfo(code: 'PAB', name: 'Panamanian Balboa', symbol: 'B/.'),
|
||||
CurrencyInfo(code: 'VES', name: 'Venezuelan Bolívar', symbol: 'Bs.S'),
|
||||
CurrencyInfo(code: 'SGD', name: 'Singapore Dollar', symbol: r'S$'),
|
||||
CurrencyInfo(code: 'HKD', name: 'Hong Kong Dollar', symbol: r'HK$'),
|
||||
CurrencyInfo(code: 'TRY', name: 'Turkish Lira', symbol: '₺'),
|
||||
CurrencyInfo(code: 'TWD', name: 'Taiwan Dollar', symbol: r'NT$'),
|
||||
CurrencyInfo(code: 'THB', name: 'Thai Baht', symbol: '฿'),
|
||||
CurrencyInfo(code: 'ZAR', name: 'South African Rand', symbol: 'R'),
|
||||
CurrencyInfo(code: 'ILS', name: 'Israeli Shekel', symbol: '₪'),
|
||||
CurrencyInfo(code: 'DKK', name: 'Danish Krone', symbol: 'kr'),
|
||||
CurrencyInfo(code: 'PLN', name: 'Polish Zloty', symbol: 'zł'),
|
||||
];
|
||||
|
||||
static const _zeroDecimal = {'JPY', 'KRW', 'CLP', 'PYG', 'COP'};
|
||||
|
||||
static String getSymbol(String code) {
|
||||
final idx = allCurrencies.indexWhere((c) => c.code == code);
|
||||
return idx >= 0 ? allCurrencies[idx].symbol : r'$';
|
||||
}
|
||||
|
||||
static String getDisplayName(String code) {
|
||||
final idx = allCurrencies.indexWhere((c) => c.code == code);
|
||||
return idx >= 0 ? '${allCurrencies[idx].code} - ${allCurrencies[idx].name}' : code;
|
||||
}
|
||||
|
||||
static String formatPrice(double amount, String code) {
|
||||
final symbol = getSymbol(code);
|
||||
final fmt = _zeroDecimal.contains(code)
|
||||
? NumberFormat('#,##0')
|
||||
: NumberFormat('#,##0.00');
|
||||
return '$symbol${fmt.format(amount)}';
|
||||
}
|
||||
}
|
||||
29
lib/utils/transitions.dart
Normal file
29
lib/utils/transitions.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SmoothPageTransitionBuilder extends PageTransitionsBuilder {
|
||||
const SmoothPageTransitionBuilder();
|
||||
|
||||
@override
|
||||
Widget buildTransitions<T>(
|
||||
PageRoute<T> route,
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
const begin = Offset(0.0, 0.04);
|
||||
const end = Offset.zero;
|
||||
const curve = Curves.easeInOutCubic;
|
||||
final tween = Tween(begin: begin, end: end).chain(
|
||||
CurveTween(curve: curve),
|
||||
);
|
||||
final fadeTween = Tween<double>(begin: 0.0, end: 1.0);
|
||||
return SlideTransition(
|
||||
position: animation.drive(tween),
|
||||
child: FadeTransition(
|
||||
opacity: animation.drive(fadeTween),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatCard extends StatelessWidget {
|
||||
class StatCard extends StatefulWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final int delayMs;
|
||||
|
||||
const StatCard({
|
||||
super.key,
|
||||
@@ -12,26 +13,79 @@ class StatCard extends StatelessWidget {
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
this.delayMs = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatCard> createState() => _StatCardState();
|
||||
}
|
||||
|
||||
class _StatCardState extends State<StatCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _fadeAnim;
|
||||
late Animation<Offset> _slideAnim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
vsync: this,
|
||||
);
|
||||
_fadeAnim = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
||||
);
|
||||
_slideAnim = Tween<Offset>(
|
||||
begin: const Offset(0, 0.15),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
||||
);
|
||||
if (widget.delayMs > 0) {
|
||||
Future.delayed(Duration(milliseconds: widget.delayMs), () {
|
||||
if (mounted) _controller.forward();
|
||||
});
|
||||
} else {
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
final theme = Theme.of(context);
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnim,
|
||||
child: SlideTransition(
|
||||
position: _slideAnim,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(title, style: Theme.of(context).textTheme.bodySmall),
|
||||
Row(
|
||||
children: [
|
||||
Icon(widget.icon, color: widget.color, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(widget.title, style: theme.textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.value,
|
||||
style: theme.textTheme.headlineSmall
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(value, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user