kz_educa/lib/screens/savings_simulator_screen.dart

539 lines
20 KiB
Dart
Executable File

// lib/screens/savings_simulator_screen.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:provider/provider.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:kzeduca_app/services/i18n_service.dart';
import 'package:kzeduca_app/services/firebase_service.dart';
import 'package:kzeduca_app/models/app_models.dart';
// KzEduca Palette
const _kGradientStart = Color(0xFF512DA8); // Roxo vibrante
const _kGradientEnd = Color(0xFF000000); // Preto profundo
const _kAccent = Color(0xFF00BFA5); // Verde água
const _kAction = Color(0xFFFFD600); // Amarelo/dourado
const _kCardColor = Color(0x33FFFFFF); // Vidro fosco (opacidade de 20%)
class SavingsSimulatorScreen extends StatefulWidget {
const SavingsSimulatorScreen({super.key});
@override
State<SavingsSimulatorScreen> createState() => _SavingsSimulatorScreenState();
}
class _SavingsSimulatorScreenState extends State<SavingsSimulatorScreen> {
final TextEditingController _initialValueController = TextEditingController();
final TextEditingController _monthlyDepositController = TextEditingController();
final TextEditingController _interestRateController = TextEditingController();
final TextEditingController _periodMonthsController = TextEditingController();
double? _result;
bool _isLoading = false;
String _error = '';
List<FlSpot> _chartData = [];
List<SavingsSimulation> _savedSimulations = [];
@override
void initState() {
super.initState();
_loadSavedSimulations();
}
@override
void dispose() {
_initialValueController.dispose();
_monthlyDepositController.dispose();
_interestRateController.dispose();
_periodMonthsController.dispose();
super.dispose();
}
void _showAlertDialog(String message, {bool isError = false}) {
final i18n = Provider.of<I18nService>(context, listen: false);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: Colors.grey[900],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Text(
isError ? i18n.t('error_prefix') : 'Info',
style: GoogleFonts.montserrat(color: Colors.white, fontWeight: FontWeight.bold),
),
content: Text(
message,
style: GoogleFonts.montserrat(color: Colors.white70),
),
actions: <Widget>[
TextButton(
child: Text('Ok', style: GoogleFonts.montserrat(color: _kAccent)),
onPressed: () {
Navigator.of(ctx).pop();
},
)
],
),
);
}
Future<void> _loadSavedSimulations() async {
final firebaseService = Provider.of<FirebaseService>(context, listen: false);
final userId = FirebaseAuth.instance.currentUser?.uid;
if (userId == null) return;
firebaseService.streamSavedSimulations(userId).listen((snapshot) {
if (mounted) {
setState(() {
_savedSimulations = snapshot.docs
.map((doc) => SavingsSimulation.fromFirestore(doc))
.toList();
});
}
}, onError: (error) {
if (mounted) {
_showAlertDialog("Erro ao carregar simulações salvas.", isError: true);
}
});
}
void _simulateSavings() {
final i18n = Provider.of<I18nService>(context, listen: false);
setState(() {
_error = '';
_isLoading = true;
_result = null;
_chartData = [];
});
final initialValue = double.tryParse(_initialValueController.text);
final monthlyDeposit = double.tryParse(_monthlyDepositController.text);
final interestRate = double.tryParse(_interestRateController.text);
final periodMonths = int.tryParse(_periodMonthsController.text);
if (initialValue == null || monthlyDeposit == null || interestRate == null || periodMonths == null) {
_showAlertDialog(i18n.t('fill_all_fields'), isError: true);
setState(() { _isLoading = false; });
return;
}
if (initialValue < 0 || monthlyDeposit < 0 || interestRate < 0 || periodMonths <= 0) {
_showAlertDialog(i18n.t('invalid_numeric_input'), isError: true);
setState(() { _isLoading = false; });
return;
}
final monthlyInterestRate = interestRate / 100 / 12;
double currentAmount = initialValue;
List<FlSpot> spots = [FlSpot(0, initialValue)];
for (int i = 0; i < periodMonths; i++) {
currentAmount = (currentAmount + monthlyDeposit) * (1 + monthlyInterestRate);
spots.add(FlSpot(i + 1.0, double.parse(currentAmount.toStringAsFixed(2))));
}
setState(() {
_result = double.parse(currentAmount.toStringAsFixed(2));
_chartData = spots;
_isLoading = false;
});
}
Future<void> _saveSimulation() async {
final i18n = Provider.of<I18nService>(context, listen: false);
final firebaseService = Provider.of<FirebaseService>(context, listen: false);
final userId = FirebaseAuth.instance.currentUser?.uid;
if (_result == null || userId == null) {
_showAlertDialog(i18n.t('simulation_error'), isError: true);
return;
}
setState(() { _isLoading = true; });
try {
await firebaseService.saveSimulation(userId, {
'valorInicial': double.tryParse(_initialValueController.text) ?? 0.0,
'depositoMensal': double.tryParse(_monthlyDepositController.text) ?? 0.0,
'taxaJuros': double.tryParse(_interestRateController.text) ?? 0.0,
'periodoMeses': int.tryParse(_periodMonthsController.text) ?? 0,
'resultadoFinal': _result,
'simulationData': _chartData.map((spot) => {'x': spot.x.toInt(), 'y': spot.y}).toList(),
'timestamp': DateTime.now().toIso8601String(),
});
_showAlertDialog(i18n.t('simulation_saved'));
} catch (e) {
_showAlertDialog(i18n.t('save_simulation_error'), isError: true);
} finally {
setState(() { _isLoading = false; });
}
}
@override
Widget build(BuildContext context) {
final i18n = Provider.of<I18nService>(context);
return Scaffold(
backgroundColor: Colors.black,
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [_kGradientStart, _kGradientEnd],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_rounded, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 8),
Flexible(
child: Text(
i18n.t('savings_simulator'),
style: GoogleFonts.montserrat(
color: const Color.fromARGB(255, 185, 185, 185),
fontSize: 28,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 24),
// Cartão Principal de Simulação
_GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
i18n.t('calculate_growth'),
style: GoogleFonts.montserrat(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
_buildInputField(i18n.t('initial_value'), _initialValueController, i18n: i18n),
_buildInputField(i18n.t('monthly_deposit'), _monthlyDepositController, i18n: i18n),
_buildInputField(i18n.t('annual_interest_rate'), _interestRateController, i18n: i18n),
_buildInputField(i18n.t('period_months'), _periodMonthsController, i18n: i18n),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _simulateSavings,
style: ElevatedButton.styleFrom(
backgroundColor: _kAccent,
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 8,
shadowColor: _kAccent.withOpacity(0.5),
),
child: _isLoading
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text(
i18n.t('simulate_savings'),
style: GoogleFonts.montserrat(fontSize: 18, fontWeight: FontWeight.w600),
),
),
],
),
),
if (_result != null) ...[
const SizedBox(height: 24),
// Cartão de Resultado e Gráfico
_GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
i18n.t('estimated_final_value'),
style: GoogleFonts.montserrat(
fontSize: 18,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'${_result!.toStringAsFixed(2)} ${i18n.t('kwanza_symbol')}',
style: GoogleFonts.montserrat(
fontSize: 32,
color: _kAction,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
AspectRatio(
aspectRatio: 1.5, // Define uma proporção fixa, que é mais responsiva que uma altura fixa
child: LineChart(
_buildLineChartData(_chartData, i18n),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _isLoading ? null : _saveSimulation,
icon: _isLoading ? const SizedBox.shrink() : const Icon(Icons.save_rounded, size: 20),
label: _isLoading
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text(i18n.t('save_simulation'), style: GoogleFonts.montserrat(fontSize: 16, fontWeight: FontWeight.w600)),
style: ElevatedButton.styleFrom(
backgroundColor: _kAction,
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 4,
),
),
],
),
),
],
const SizedBox(height: 32),
_buildSectionHeader(i18n.t('saved_simulations')),
const SizedBox(height: 16),
// Lista de Simulações Salvas
_savedSimulations.isEmpty
? Text(
i18n.t('no_simulations'),
style: GoogleFonts.montserrat(color: Colors.white54, fontStyle: FontStyle.italic),
textAlign: TextAlign.center,
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _savedSimulations.length,
itemBuilder: (context, index) {
final sim = _savedSimulations[index];
return _SimulationResultCard(
sim: sim,
i18n: i18n,
);
},
),
const SizedBox(height: 40),
],
),
),
),
),
);
}
LineChartData _buildLineChartData(List<FlSpot> spots, I18nService i18n) {
return LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: true,
getDrawingHorizontalLine: (value) => FlLine(color: Colors.white12, strokeWidth: 1),
getDrawingVerticalLine: (value) => FlLine(color: Colors.white12, strokeWidth: 1),
),
titlesData: FlTitlesData(
show: true,
// CORREÇÃO: Usando a nova estrutura de `getTitlesWidget` (sem SideTitleWidget)
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
// Retorna o Widget Text diretamente com um Padding
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Mês ${value.toInt()}',
style: GoogleFonts.montserrat(fontSize: 10, color: Colors.white70),
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
// Retorna o Widget Text diretamente
return Text(
'${value.toInt()}',
style: GoogleFonts.montserrat(fontSize: 10, color: Colors.white70),
textAlign: TextAlign.right,
);
},
),
),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.white24, width: 1),
),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: _kAccent,
barWidth: 4,
isStrokeCapRound: true,
dotData: FlDotData(show: true, getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter(radius: 4, color: _kAccent, strokeWidth: 2, strokeColor: Colors.black)),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [_kAccent.withOpacity(0.5), _kAccent.withOpacity(0)],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
),
),
],
);
}
Widget _buildInputField(String label, TextEditingController controller, {required I18nService i18n}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: GoogleFonts.montserrat(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white70,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: _kCardColor, // Fundo transparente
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white24),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: TextFormField(
controller: controller,
keyboardType: TextInputType.number,
style: GoogleFonts.montserrat(color: const Color.fromARGB(255, 0, 0, 0)),
decoration: InputDecoration(
hintText: 'Ex: 100',
hintStyle: GoogleFonts.montserrat(color: const Color.fromARGB(137, 26, 25, 25)),
border: InputBorder.none, // Remove a borda padrão
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
),
],
),
);
}
Widget _buildSectionHeader(String title) {
return Column(
children: [
const Divider(height: 20, thickness: 1.5, color: Colors.white12),
const SizedBox(height: 16),
Text(
title,
style: GoogleFonts.montserrat(fontSize: 22, fontWeight: FontWeight.bold, color: const Color.fromARGB(255, 255, 255, 255)),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
],
);
}
}
class _GlassCard extends StatelessWidget {
final Widget child;
const _GlassCard({required this.child});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: _kCardColor,
borderRadius: BorderRadius.circular(24.0),
border: Border.all(color: const Color.fromARGB(255, 255, 255, 255).withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: child,
);
}
}
class _SimulationResultCard extends StatelessWidget {
final SavingsSimulation sim;
final I18nService i18n;
const _SimulationResultCard({
required this.sim,
required this.i18n,
});
@override
Widget build(BuildContext context) {
return _GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${i18n.t('estimated_final_value')}: ${sim.resultadoFinal.toStringAsFixed(2)} ${i18n.t('kwanza_symbol')}',
style: GoogleFonts.montserrat(fontSize: 18, fontWeight: FontWeight.w600, color: _kAccent),
),
const SizedBox(height: 12),
Text(
'${i18n.t('initial_value')}: ${sim.valorInicial.toStringAsFixed(2)} ${i18n.t('kwanza_symbol')}',
style: GoogleFonts.montserrat(fontSize: 14, color: const Color.fromARGB(205, 255, 255, 255)),
),
Text(
'${i18n.t('monthly_deposit')}: ${sim.depositoMensal.toStringAsFixed(2)} ${i18n.t('kwanza_symbol')}',
style: GoogleFonts.montserrat(fontSize: 14, color: Colors.white70),
),
Text(
'${i18n.t('annual_interest_rate')}: ${sim.taxaJuros.toStringAsFixed(1)}%',
style: GoogleFonts.montserrat(fontSize: 14, color: Colors.white70),
),
Text(
'${i18n.t('period_months')}: ${sim.periodoMeses}',
style: GoogleFonts.montserrat(fontSize: 14, color: Colors.white70),
),
Text(
'Salvo em: ${DateTime.parse(sim.timestamp).toLocal().toString().split(' ')[0]}',
style: GoogleFonts.montserrat(fontSize: 12, color: Colors.white54),
),
],
),
);
}
}