539 lines
20 KiB
Dart
Executable File
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |