import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import '../database/daos/month_dao.dart'; import '../database/database.dart'; import '../theme.dart'; import 'inline_edit_cell.dart'; /// Balance type labels and display order. enum BalanceTypeLabel { credit('Credit', 0), loan('Loan', 1), mortgage('Mortgage', 2), personal('Personal', 3); final String label; final int value; const BalanceTypeLabel(this.label, this.value); } class TopBar extends StatelessWidget { final int remainingCents; final int bufferCents; final int eatingOutBudgetCents; final MonthPlan currentPlan; final List allMonths; final List bills; final MonthDao monthDao; final ValueChanged onMonthSelected; final VoidCallback onCloneMonth; final ValueChanged onDeleteMonth; final VoidCallback onSettingsTap; final VoidCallback onDataChanged; const TopBar({ super.key, required this.remainingCents, required this.bufferCents, required this.eatingOutBudgetCents, required this.currentPlan, required this.allMonths, required this.bills, required this.monthDao, required this.onMonthSelected, required this.onCloneMonth, required this.onDeleteMonth, required this.onSettingsTap, required this.onDataChanged, }); static const _monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; static const _fullMonthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; @override Widget build(BuildContext context) { final bufferOk = remainingCents >= bufferCents; // Group outstanding by balance type final typeTotals = {}; // balanceType → total cents final outstandingBills = []; for (final bill in bills) { if (bill.outstandingBalanceCents != null && bill.outstandingBalanceCents! > 0) { outstandingBills.add(bill); final bt = bill.balanceType ?? 0; // default to Credit typeTotals[bt] = (typeTotals[bt] ?? 0) + bill.outstandingBalanceCents!; } } final hasOutstanding = typeTotals.isNotEmpty; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: const BoxDecoration( color: bgCard, border: Border(bottom: BorderSide(color: borderColor)), ), child: Row( children: [ // Net Balance + Buffer Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('NET BALANCE', style: TextStyle(fontSize: 11, color: textMuted, letterSpacing: 0.5)), Text( formatCents(remainingCents), style: TextStyle( fontSize: 28, fontFamily: monoFont, fontWeight: FontWeight.w600, color: remainingCents >= 0 ? accentGreen : accentRed, ), ), if (bufferCents > 0) ...[ const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: bufferOk ? accentGreen.withValues(alpha: 0.15) : accentAmber.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(3), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ IntrinsicWidth( child: InlineEditCell.cents( cents: bufferCents, onSaved: (cents) async { final clamped = cents < 0 ? 0 : cents; await monthDao.updateMonthPlan( currentPlan.id, MonthPlansCompanion(bufferAmountCents: Value(clamped)), ); onDataChanged(); }, style: TextStyle( fontSize: 11, fontFamily: monoFont, color: bufferOk ? accentGreen : accentAmber, ), textAlign: TextAlign.left, ), ), Text( ' BUFFER: ${bufferOk ? "OK" : "LOW"} (${bufferOk ? "+" : ""}${formatCents(remainingCents - bufferCents)})', style: TextStyle( fontSize: 11, fontFamily: monoFont, color: bufferOk ? accentGreen : accentAmber, ), ), ], ), ), ], ], ), // Outstanding balances (clickable) if (hasOutstanding) ...[ const SizedBox(width: 24), GestureDetector( onTap: () => _showOutstandingOverlay(context, outstandingBills), child: MouseRegion( cursor: SystemMouseCursors.click, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('OUTSTANDING', style: TextStyle(fontSize: 11, color: textMuted, letterSpacing: 0.5)), const SizedBox(height: 4), Row( children: [ for (final bt in BalanceTypeLabel.values) if (typeTotals.containsKey(bt.value)) ...[ if (bt.value > 0 && typeTotals.entries.where((e) => e.key < bt.value).isNotEmpty) const SizedBox(width: 18), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(bt.label, style: const TextStyle(fontSize: 11, color: textDim)), Text( formatCents(typeTotals[bt.value]!), style: const TextStyle(fontSize: 15, fontFamily: monoFont, color: accentAmber), ), ], ), ], ], ), ], ), ), ), ], const Spacer(), Text( _fullMonthNames[currentPlan.month - 1], style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: textPrimary), ), const SizedBox(width: 6), Text('${currentPlan.year}', style: const TextStyle(fontSize: 14, color: textMuted)), const SizedBox(width: 20), _buildMonthGrid(context), const SizedBox(width: 16), IconButton( icon: const Icon(Icons.settings, color: textMuted, size: 20), onPressed: onSettingsTap, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), ), ], ), ); } void _showOutstandingOverlay(BuildContext context, List outstandingBills) { final renderBox = context.findRenderObject() as RenderBox; final offset = renderBox.localToGlobal(Offset.zero); showDialog( context: context, barrierColor: Colors.transparent, builder: (ctx) => Stack( children: [ // Dismiss on tap outside Positioned.fill( child: GestureDetector( onTap: () => Navigator.pop(ctx), behavior: HitTestBehavior.opaque, ), ), Positioned( left: offset.dx + 180, top: offset.dy + 60, child: _OutstandingOverlay( bills: outstandingBills, monthDao: monthDao, onDataChanged: () { onDataChanged(); Navigator.pop(ctx); }, ), ), ], ), ); } bool _isFutureMonth(MonthPlan plan) { final now = DateTime.now(); return plan.year > now.year || (plan.year == now.year && plan.month > now.month); } Future _confirmClone(BuildContext context) async { final currentYearMonths = allMonths.where((m) => m.year == currentPlan.year).toList(); final lastMonth = currentYearMonths.isNotEmpty ? currentYearMonths.last.month : currentPlan.month; final nextMonth = lastMonth == 12 ? 1 : lastMonth + 1; final nextMonthName = _fullMonthNames[nextMonth - 1]; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: bgCard, title: Text('Ready to start planning $nextMonthName?', style: const TextStyle(fontSize: 16, color: textPrimary)), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('No', style: TextStyle(color: textMuted))), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Yes', style: TextStyle(color: accentGreen))), ], ), ); if (confirmed == true) onCloneMonth(); } Future _confirmDelete(BuildContext context, MonthPlan plan) async { final monthName = _fullMonthNames[plan.month - 1]; final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: bgCard, title: Text('Delete $monthName ${plan.year}?', style: const TextStyle(fontSize: 16, color: textPrimary)), content: const Text('This cannot be undone.', style: TextStyle(fontSize: 13, color: textMuted)), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('No', style: TextStyle(color: textMuted))), TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Yes, delete', style: TextStyle(color: accentRed))), ], ), ); if (confirmed == true) onDeleteMonth(plan.id); } Widget _buildMonthGrid(BuildContext context) { final currentYearMonths = allMonths.where((m) => m.year == currentPlan.year).toList(); final monthSet = currentYearMonths.map((m) => m.month).toSet(); final rows = >[]; var currentRow = []; for (final m in currentYearMonths) { final isCurrent = m.id == currentPlan.id; currentRow.add(_monthSquare( _monthNames[m.month - 1], active: true, current: isCurrent, onTap: () { if (isCurrent && _isFutureMonth(m)) { _confirmDelete(context, m); } else { onMonthSelected(m.id); } }, )); if (currentRow.length == 4) { rows.add(currentRow); currentRow = []; } } final lastMonth = currentYearMonths.isNotEmpty ? currentYearMonths.last.month : 0; if (lastMonth < 12 && !monthSet.contains(lastMonth + 1)) { currentRow.add(_monthSquare('+', isAdd: true, onTap: () => _confirmClone(context))); } if (currentRow.isNotEmpty) { rows.add(currentRow); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: rows.map((row) { return Padding( padding: const EdgeInsets.only(bottom: 2), child: Row(children: row), ); }).toList(), ); } Widget _monthSquare(String label, {bool active = false, bool current = false, bool isAdd = false, VoidCallback? onTap}) { return GestureDetector( onTap: onTap, child: MouseRegion( cursor: SystemMouseCursors.click, child: Container( width: 38, height: 26, margin: const EdgeInsets.only(right: 3), decoration: BoxDecoration( color: current ? accentGreen.withValues(alpha: 0.3) : active ? accentGreen.withValues(alpha: 0.1) : Colors.transparent, borderRadius: BorderRadius.circular(4), border: Border.all( color: current ? accentGreen : active ? accentGreen.withValues(alpha: 0.4) : isAdd ? textDim : borderColor, ), ), alignment: Alignment.center, child: isAdd ? const Icon(Icons.add, size: 16, color: textDim) : Text( label, style: TextStyle( fontSize: 11, fontWeight: current ? FontWeight.w600 : FontWeight.normal, color: current ? textPrimary : (active ? textMuted : textDim), ), ), ), ), ); } } /// Overlay listing bills with outstanding balances, allowing type assignment. class _OutstandingOverlay extends StatelessWidget { final List bills; final MonthDao monthDao; final VoidCallback onDataChanged; const _OutstandingOverlay({ required this.bills, required this.monthDao, required this.onDataChanged, }); @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: Container( width: 340, decoration: BoxDecoration( color: bgCard, border: Border.all(color: borderColor), borderRadius: BorderRadius.circular(6), boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.4), blurRadius: 12, offset: const Offset(0, 4))], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Padding( padding: EdgeInsets.fromLTRB(12, 10, 12, 6), child: Text('OUTSTANDING BALANCES', style: TextStyle(fontSize: 11, color: textMuted, letterSpacing: 0.5)), ), const Divider(color: dividerColor, height: 1), for (final bill in bills) _OutstandingBillRow(bill: bill, monthDao: monthDao, onDataChanged: onDataChanged), ], ), ), ); } } class _OutstandingBillRow extends StatelessWidget { final BillDefinition bill; final MonthDao monthDao; final VoidCallback onDataChanged; const _OutstandingBillRow({ required this.bill, required this.monthDao, required this.onDataChanged, }); @override Widget build(BuildContext context) { final currentType = bill.balanceType ?? 0; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: const BoxDecoration( border: Border(bottom: BorderSide(color: dividerColor, width: 0.5)), ), child: Row( children: [ // Bill name + note Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text(bill.name, style: const TextStyle(fontSize: bodySize, color: textPrimary)), if (bill.subtitle != null) ...[ const SizedBox(width: 6), Flexible( child: Text( bill.subtitle!, style: const TextStyle(fontSize: 11, color: textDim, fontStyle: FontStyle.italic), overflow: TextOverflow.ellipsis, ), ), ], ], ), Text( formatCents(bill.outstandingBalanceCents ?? 0), style: const TextStyle(fontSize: 11, fontFamily: monoFont, color: accentAmber), ), ], ), ), // Type selector chips Row( mainAxisSize: MainAxisSize.min, children: [ for (final bt in BalanceTypeLabel.values) ...[ if (bt.value > 0) const SizedBox(width: 3), _typeChip(bt, currentType == bt.value), ], ], ), ], ), ); } Widget _typeChip(BalanceTypeLabel bt, bool active) { return GestureDetector( onTap: () async { await monthDao.updateBill( bill.id, BillDefinitionsCompanion(balanceType: Value(bt.value)), ); onDataChanged(); }, child: MouseRegion( cursor: SystemMouseCursors.click, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: active ? accentAmber.withValues(alpha: 0.15) : Colors.transparent, border: Border.all(color: active ? accentAmber.withValues(alpha: 0.5) : borderColor), borderRadius: BorderRadius.circular(3), ), child: Text( bt.label, style: TextStyle( fontSize: 9, color: active ? accentAmber : textDim, fontWeight: active ? FontWeight.w600 : FontWeight.normal, ), ), ), ), ); } }