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'; class MiscSpendCard extends StatefulWidget { final List items; final MonthDao monthDao; final int monthPlanId; final VoidCallback onDataChanged; const MiscSpendCard({ super.key, required this.items, required this.monthDao, required this.monthPlanId, required this.onDataChanged, }); @override State createState() => _MiscSpendCardState(); } class _MiscSpendCardState extends State { int? _lastAddedId; Future _addItem() async { final id = await widget.monthDao.addMiscItem( widget.monthPlanId, description: 'New item', amountCents: 0, sortOrder: widget.items.length, ); setState(() => _lastAddedId = id); widget.onDataChanged(); } Future _deleteItem(int id) async { await widget.monthDao.deleteMiscItem(id); widget.onDataChanged(); } @override void didUpdateWidget(MiscSpendCard oldWidget) { super.didUpdateWidget(oldWidget); if (_lastAddedId != null && widget.items.any((i) => i.id == _lastAddedId)) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => _lastAddedId = null); }); } } @override Widget build(BuildContext context) { final totalCents = widget.items.where((m) => m.isEnabled).fold(0, (sum, m) => sum + m.amountCents); return Container( decoration: BoxDecoration( color: bgCard, border: Border.all(color: borderColor), borderRadius: BorderRadius.circular(6), ), clipBehavior: Clip.antiAlias, child: HoverBuilder(builder: (context, sectionHovering) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: cardPadding, vertical: 8), decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: dividerColor))), child: Row( children: [ const Text('MISC SPEND', style: TextStyle(fontSize: labelSize, color: textMuted, letterSpacing: 0.5)), const Spacer(), Text(formatCents(totalCents), style: const TextStyle(fontSize: labelSize, fontFamily: monoFont, color: textPrimary)), ], ), ), if (widget.items.isEmpty) const Padding( padding: EdgeInsets.all(cardPadding), child: Text('No misc items this month.', style: TextStyle(fontSize: bodySize, color: textDim)), ) else for (var i = 0; i < widget.items.length; i++) _MiscRow( item: widget.items[i], monthDao: widget.monthDao, onDataChanged: widget.onDataChanged, onDelete: () => _deleteItem(widget.items[i].id), autoEditName: widget.items[i].id == _lastAddedId, isLast: i == widget.items.length - 1, ), if (sectionHovering) AddRowButton(label: 'Add misc item', onPressed: _addItem) else const SizedBox(height: 4), ], ); }), ); } } class _MiscRow extends StatelessWidget { final MiscItem item; final MonthDao monthDao; final VoidCallback onDataChanged; final VoidCallback onDelete; final bool autoEditName; final bool isLast; const _MiscRow({ required this.item, required this.monthDao, required this.onDataChanged, required this.onDelete, this.autoEditName = false, this.isLast = false, }); @override Widget build(BuildContext context) { return HoverBuilder(builder: (context, hovering) { return Opacity( opacity: item.isEnabled ? 1.0 : 0.5, child: Container( height: rowHeight, padding: const EdgeInsets.symmetric(horizontal: cardPadding), decoration: isLast ? null : const BoxDecoration(border: Border(bottom: BorderSide(color: Color(0xFF1a1a2e)))), child: Row( children: [ // Enable/disable toggle SizedBox( width: 18, height: 18, child: Checkbox( value: item.isEnabled, onChanged: (val) async { await monthDao.updateMiscItem(item.id, MiscItemsCompanion(isEnabled: Value(val ?? true))); onDataChanged(); }, activeColor: accentGreen, checkColor: bgCard, side: BorderSide(color: item.isEnabled ? accentGreen : textDim, width: 1.5), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3)), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ), ), const SizedBox(width: 8), // Name + recurring tag + comment (tag hugs name, comment fills rest) Expanded( child: Row( children: [ IntrinsicWidth( child: InlineEditCell( displayText: item.description, editText: item.description, autoEdit: autoEditName, onSaved: (text) async { await monthDao.updateMiscItem(item.id, MiscItemsCompanion(description: Value(text))); onDataChanged(); }, style: TextStyle(fontSize: bodySize, color: item.isEnabled ? textPrimary : textDim), ), ), if (item.isRecurring) GestureDetector( onTap: () async { await monthDao.updateMiscItem(item.id, MiscItemsCompanion(isRecurring: Value(false))); onDataChanged(); }, child: MouseRegion( cursor: SystemMouseCursors.click, child: Container( margin: const EdgeInsets.only(left: 6), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( border: Border.all(color: accentGreen), borderRadius: BorderRadius.circular(3), ), child: const Text('recurring', style: TextStyle(fontSize: 9, color: accentGreen)), ), ), ) else if (hovering) GestureDetector( onTap: () async { await monthDao.updateMiscItem(item.id, MiscItemsCompanion(isRecurring: Value(true))); onDataChanged(); }, child: MouseRegion( cursor: SystemMouseCursors.click, child: Container( margin: const EdgeInsets.only(left: 6), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( border: Border.all(color: textDim), borderRadius: BorderRadius.circular(3), ), child: const Text('one-time', style: TextStyle(fontSize: 9, color: textDim)), ), ), ), // Inline comment (after tag, fills remaining space) if (item.comment != null || hovering) Expanded( child: Padding( padding: const EdgeInsets.only(left: 6), child: InlineEditCell( displayText: item.comment ?? (hovering ? 'note...' : ''), editText: item.comment ?? '', textAlign: TextAlign.right, onSaved: (text) async { await monthDao.updateMiscItem(item.id, MiscItemsCompanion(comment: Value(text.isEmpty ? null : text))); onDataChanged(); }, style: TextStyle( fontSize: labelSize, color: item.comment != null ? textDim : textDim.withValues(alpha: 0.4), fontStyle: FontStyle.italic, ), ), ), ), ], ), ), // Amount (editable) SizedBox( width: 70, child: InlineEditCell.cents( cents: item.amountCents, onSaved: (cents) async { await monthDao.updateMiscItem(item.id, MiscItemsCompanion(amountCents: Value(cents))); onDataChanged(); }, style: const TextStyle(fontSize: bodySize, fontFamily: monoFont, color: textPrimary), ), ), const SizedBox(width: 4), SizedBox( width: 20, child: hovering ? RemoveButton(onPressed: onDelete) : const SizedBox(), ), ], ), ), ); }); } }