From e1bc9c12c2c05bc73e6d77ef82d2eb82c8fc60eb Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 17 Jun 2026 23:54:55 +0530 Subject: [PATCH 01/13] feat: phase 5 - payments UI, notifications, CSV export, responsive dashboard --- lib/data/database/daos/cheques_dao.dart | 12 + lib/data/database/daos/inventory_dao.dart | 10 + lib/data/database/daos/suppliers_dao.dart | 6 + .../presentation/dashboard_screen.dart | 81 ++-- .../debtors/presentation/debtors_screen.dart | 361 +++++++++++++++++- .../reports/presentation/reports_screen.dart | 95 ++++- .../presentation/suppliers_screen.dart | 120 +++++- lib/providers/notifications_provider.dart | 69 ++++ lib/providers/suppliers_provider.dart | 5 + lib/shared/widgets/app_scaffold.dart | 8 + lib/shared/widgets/notification_bell.dart | 193 ++++++++++ lib/shared/widgets/sidebar_nav.dart | 3 + 12 files changed, 900 insertions(+), 63 deletions(-) create mode 100644 lib/providers/notifications_provider.dart create mode 100644 lib/shared/widgets/notification_bell.dart diff --git a/lib/data/database/daos/cheques_dao.dart b/lib/data/database/daos/cheques_dao.dart index 9191931..75eb447 100644 --- a/lib/data/database/daos/cheques_dao.dart +++ b/lib/data/database/daos/cheques_dao.dart @@ -37,6 +37,18 @@ class ChequesDao extends DatabaseAccessor with _$ChequesDaoMixin { .get(); } + Future> getOverdueCheques() { + final now = DateTime.now(); + return (select(cheques) + ..where( + (c) => + c.dueDate.isSmallerThanValue(now) & + c.status.isIn(['pending', 'deposited']), + ) + ..orderBy([(c) => OrderingTerm.asc(c.dueDate)])) + .get(); + } + Future updateStatus(String id, String status) => (update(cheques)..where((c) => c.id.equals(id))).write( ChequesCompanion( diff --git a/lib/data/database/daos/inventory_dao.dart b/lib/data/database/daos/inventory_dao.dart index 9f3f33b..8e01b9c 100644 --- a/lib/data/database/daos/inventory_dao.dart +++ b/lib/data/database/daos/inventory_dao.dart @@ -55,6 +55,16 @@ class InventoryDao extends DatabaseAccessor with _$InventoryDaoMixi ); } + Future> getLowStockProducts() async { + final query = select(stock).join([ + innerJoin(products, products.id.equalsExp(stock.productId)), + ]); + query.where(stock.qty.isSmallerOrEqualValue(0) | + CustomExpression('stock.qty <= products.reorder_level')); + final rows = await query.get(); + return rows.map((r) => r.readTable(products)).toList(); + } + Future upsertStock(StockCompanion entry) => into(stock).insertOnConflictUpdate(entry); diff --git a/lib/data/database/daos/suppliers_dao.dart b/lib/data/database/daos/suppliers_dao.dart index 9a3b1a2..5e0ab4d 100644 --- a/lib/data/database/daos/suppliers_dao.dart +++ b/lib/data/database/daos/suppliers_dao.dart @@ -54,6 +54,12 @@ class SuppliersDao extends DatabaseAccessor with _$SuppliersDaoMixi Future> getItemsForPurchase(String purchaseId) => (select(purchaseItems)..where((i) => i.purchaseId.equals(purchaseId))).get(); + Future> getPaymentsForSupplier(String supplierId) => + (select(supplierPayments) + ..where((p) => p.supplierId.equals(supplierId)) + ..orderBy([(p) => OrderingTerm.desc(p.createdAt)])) + .get(); + Future nextGrnNumber() async { return transaction(() async { final maxExpr = purchases.grnNumber.max(); diff --git a/lib/features/dashboard/presentation/dashboard_screen.dart b/lib/features/dashboard/presentation/dashboard_screen.dart index c7033c6..bb9c74d 100644 --- a/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/lib/features/dashboard/presentation/dashboard_screen.dart @@ -48,43 +48,50 @@ class DashboardScreen extends ConsumerWidget { padding: const EdgeInsets.all(16), children: [ // ── KPI Grid ──────────────────────────────────────────────── - GridView.extent( - maxCrossAxisExtent: 300, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 3, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - StatCard( - label: "Today's Sales", - value: CurrencyUtils.format(s.todaySales), - icon: Icons.receipt_long_outlined, - color: AppColors.success, - onTap: () => context.go(AppRoutes.pos), - ), - StatCard( - label: 'Low Stock Items', - value: '${s.lowStockCount}', - icon: Icons.warning_amber_outlined, - color: AppColors.warning, - onTap: () => context.go(AppRoutes.inventory), - ), - StatCard( - label: 'Cheques Due (7d)', - value: '${s.chequesThisWeek}', - icon: Icons.calendar_today_outlined, - color: AppColors.primary, - onTap: () => context.go(AppRoutes.cheques), - ), - StatCard( - label: 'Total Receivables', - value: CurrencyUtils.format(s.totalDebtors), - icon: Icons.people_outline, - color: AppColors.error, - onTap: () => context.go(AppRoutes.customers), - ), - ], + LayoutBuilder( + builder: (context, constraints) { + final w = constraints.maxWidth; + final cols = w < 480 ? 2 : w < 840 ? 3 : 4; + final ratio = w < 480 ? 2.1 : 3.0; + return GridView.count( + crossAxisCount: cols, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: ratio, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + StatCard( + label: "Today's Sales", + value: CurrencyUtils.format(s.todaySales), + icon: Icons.receipt_long_outlined, + color: AppColors.success, + onTap: () => context.go(AppRoutes.pos), + ), + StatCard( + label: 'Low Stock Items', + value: '${s.lowStockCount}', + icon: Icons.warning_amber_outlined, + color: AppColors.warning, + onTap: () => context.go(AppRoutes.inventory), + ), + StatCard( + label: 'Cheques Due (7d)', + value: '${s.chequesThisWeek}', + icon: Icons.calendar_today_outlined, + color: AppColors.primary, + onTap: () => context.go(AppRoutes.cheques), + ), + StatCard( + label: 'Total Receivables', + value: CurrencyUtils.format(s.totalDebtors), + icon: Icons.people_outline, + color: AppColors.error, + onTap: () => context.go(AppRoutes.customers), + ), + ], + ); + }, ), const SizedBox(height: 20), diff --git a/lib/features/debtors/presentation/debtors_screen.dart b/lib/features/debtors/presentation/debtors_screen.dart index 3b2e2c4..43dfe05 100644 --- a/lib/features/debtors/presentation/debtors_screen.dart +++ b/lib/features/debtors/presentation/debtors_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_text_styles.dart'; @@ -46,8 +47,7 @@ class DebtorsScreen extends ConsumerWidget { ); } - static int _daysSince(DateTime dt) => - DateTime.now().difference(dt).inDays; + static int _daysSince(DateTime dt) => DateTime.now().difference(dt).inDays; } class _SummaryBanner extends StatelessWidget { @@ -144,6 +144,12 @@ class _DebtorTile extends StatelessWidget { final label = _agingLabel(customer.updatedAt); return ListTile( + onTap: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (_) => _DebtorDetailSheet(customer: customer), + ), leading: CircleAvatar( backgroundColor: color.withAlpha(30), child: Text( @@ -153,15 +159,352 @@ class _DebtorTile extends StatelessWidget { ), title: Text(customer.name, style: AppTextStyles.labelLarge), subtitle: Text( - [ - if (customer.phone != null) customer.phone!, - label, - ].join(' · '), + [if (customer.phone != null) customer.phone!, label].join(' · '), style: AppTextStyles.bodySmall, ), - trailing: Text( - CurrencyUtils.format(customer.balance), - style: AppTextStyles.labelLarge.copyWith(color: color), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + CurrencyUtils.format(customer.balance), + style: AppTextStyles.labelLarge.copyWith(color: color), + ), + const SizedBox(width: 4), + Icon(Icons.chevron_right, size: 16, color: AppColors.textDisabled), + ], + ), + ); + } +} + +// ── Detail + Payment Sheet ────────────────────────────────────────────────── + +class _DebtorDetailSheet extends ConsumerWidget { + const _DebtorDetailSheet({required this.customer}); + + final Customer customer; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final historyAsync = ref.watch(customerPaymentHistoryProvider(customer.id)); + final color = customer.balance > 0 ? AppColors.error : AppColors.success; + + return DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.95, + expand: false, + builder: (context, scrollController) => ListView( + controller: scrollController, + padding: const EdgeInsets.fromLTRB(24, 16, 24, 32), + children: [ + // drag handle + Center( + child: Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: AppColors.textDisabled, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + // Header + Row( + children: [ + CircleAvatar( + radius: 28, + backgroundColor: AppColors.primary.withAlpha(20), + child: Text( + customer.name[0].toUpperCase(), + style: TextStyle( + color: AppColors.primary, + fontSize: 22, + fontWeight: FontWeight.w700), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(customer.name, style: AppTextStyles.titleLarge), + if (customer.phone != null) + Text(customer.phone!, style: AppTextStyles.bodySmall), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + + // Balance card + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withAlpha(15), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Outstanding Balance', style: AppTextStyles.bodySmall), + Text( + CurrencyUtils.format(customer.balance), + style: AppTextStyles.headlineMedium.copyWith(color: color), + ), + ], + ), + if (customer.creditLimit > 0) + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text('Credit Limit', style: AppTextStyles.bodySmall), + Text( + CurrencyUtils.format(customer.creditLimit), + style: AppTextStyles.labelLarge, + ), + ], + ), + ], + ), + ), + const SizedBox(height: 12), + + // Record Payment button + ElevatedButton.icon( + icon: const Icon(Icons.payment_rounded), + label: const Text('Record Payment'), + onPressed: () { + Navigator.of(context).pop(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (_) => _RecordPaymentSheet( + customerId: customer.id, + customerName: customer.name, + outstanding: customer.balance, + ), + ); + }, + ), + const SizedBox(height: 20), + + // Payment History + Text('Payment History', style: AppTextStyles.titleMedium), + const SizedBox(height: 8), + historyAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Text('Error: $e'), + data: (payments) => payments.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text('No payments recorded yet.', + style: AppTextStyles.bodySmall + .copyWith(color: AppColors.textSecondary)), + ) + : Column( + children: payments + .map((p) => _PaymentHistoryRow(payment: p)) + .toList(), + ), + ), + ], + ), + ); + } +} + +class _PaymentHistoryRow extends StatelessWidget { + const _PaymentHistoryRow({required this.payment}); + + final CustomerPayment payment; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.success.withAlpha(20), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.south_west_rounded, color: AppColors.success, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + payment.method.toUpperCase(), + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textSecondary, letterSpacing: 0.5), + ), + if (payment.notes != null) + Text(payment.notes!, style: AppTextStyles.bodySmall), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(CurrencyUtils.format(payment.amount), + style: AppTextStyles.labelLarge + .copyWith(color: AppColors.success, fontWeight: FontWeight.w700)), + Text( + DateFormat('dd MMM yyyy').format(payment.createdAt), + style: AppTextStyles.bodySmall + .copyWith(color: AppColors.textSecondary, fontSize: 11), + ), + ], + ), + ], + ), + ); + } +} + +// ── Record Payment Sheet ──────────────────────────────────────────────────── + +class _RecordPaymentSheet extends ConsumerStatefulWidget { + const _RecordPaymentSheet({ + required this.customerId, + required this.customerName, + required this.outstanding, + }); + + final String customerId; + final String customerName; + final double outstanding; + + @override + ConsumerState<_RecordPaymentSheet> createState() => _RecordPaymentSheetState(); +} + +class _RecordPaymentSheetState extends ConsumerState<_RecordPaymentSheet> { + final _formKey = GlobalKey(); + final _amount = TextEditingController(); + final _notes = TextEditingController(); + String _method = 'cash'; + bool _saving = false; + + @override + void dispose() { + _amount.dispose(); + _notes.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + setState(() => _saving = true); + try { + await ref.read(customerActionsProvider).recordPayment( + customerId: widget.customerId, + amount: double.parse(_amount.text), + method: _method, + notes: _notes.text.trim().isEmpty ? null : _notes.text.trim(), + ); + if (!mounted) return; + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Payment recorded.'))); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e'), backgroundColor: AppColors.error)); + } finally { + if (mounted) setState(() => _saving = false); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, MediaQuery.viewInsetsOf(context).bottom + 24), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Record Payment', style: AppTextStyles.titleLarge), + Text(widget.customerName, + style: + AppTextStyles.bodySmall.copyWith(color: AppColors.textSecondary)), + const SizedBox(height: 16), + if (widget.outstanding > 0) ...[ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppColors.error.withAlpha(12), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Outstanding', style: AppTextStyles.bodySmall), + Text(CurrencyUtils.format(widget.outstanding), + style: AppTextStyles.labelLarge + .copyWith(color: AppColors.error)), + ], + ), + ), + const SizedBox(height: 12), + ], + TextFormField( + controller: _amount, + decoration: const InputDecoration(labelText: 'Amount *', prefixText: 'Rs. '), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + autofocus: true, + validator: (v) { + if (v == null || v.trim().isEmpty) return 'Required'; + final n = double.tryParse(v); + if (n == null || n <= 0) return 'Enter a valid amount'; + return null; + }, + ), + const SizedBox(height: 12), + DropdownButtonFormField( + value: _method, + decoration: const InputDecoration(labelText: 'Payment Method'), + items: const [ + DropdownMenuItem(value: 'cash', child: Text('Cash')), + DropdownMenuItem(value: 'card', child: Text('Card')), + DropdownMenuItem(value: 'bank_transfer', child: Text('Bank Transfer')), + DropdownMenuItem(value: 'cheque', child: Text('Cheque')), + ], + onChanged: (v) => setState(() => _method = v ?? 'cash'), + ), + const SizedBox(height: 12), + TextFormField( + controller: _notes, + decoration: const InputDecoration(labelText: 'Notes (optional)'), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _saving ? null : _save, + child: _saving + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : const Text('Save Payment'), + ), + ], + ), ), ); } diff --git a/lib/features/reports/presentation/reports_screen.dart b/lib/features/reports/presentation/reports_screen.dart index 58bf545..ecdd5d9 100644 --- a/lib/features/reports/presentation/reports_screen.dart +++ b/lib/features/reports/presentation/reports_screen.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; + import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:bms/core/theme/app_colors.dart'; import 'package:bms/core/theme/app_text_styles.dart'; @@ -9,6 +13,20 @@ import 'package:bms/data/database/daos/reports_dao.dart'; import 'package:bms/providers/reports_provider.dart'; import 'package:bms/shared/widgets/bms_filter_bar.dart'; +Future _shareCsv(String filename, String csv) async { + await SharePlus.instance.share( + ShareParams( + files: [ + XFile.fromData( + utf8.encode(csv), + name: filename, + mimeType: 'text/csv', + ), + ], + ), + ); +} + class ReportsScreen extends ConsumerStatefulWidget { const ReportsScreen({super.key}); @@ -155,7 +173,35 @@ class _PLTabState extends ConsumerState<_PLTab> { ), ], ), - const SizedBox(height: 24), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton.icon( + icon: const Icon(Icons.download_outlined, size: 16), + label: const Text('Export CSV'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + textStyle: AppTextStyles.bodySmall, + visualDensity: VisualDensity.compact, + ), + onPressed: () { + final df = DateFormat('yyyy-MM-dd'); + final lines = [ + 'Date,Revenue,COGS,Gross Profit,Margin %', + ...daily.map((d) { + final gp = d.revenue - d.cogs; + final m = d.revenue > 0 ? gp / d.revenue * 100 : 0; + return '${df.format(d.date)},${d.revenue.toStringAsFixed(2)},${d.cogs.toStringAsFixed(2)},${gp.toStringAsFixed(2)},${m.toStringAsFixed(2)}'; + }), + ]; + _shareCsv( + 'pl_${df.format(_range.start)}_${df.format(_range.end)}.csv', + lines.join('\n'), + ); + }, + ), + ), + const SizedBox(height: 12), Text('Daily Revenue', style: AppTextStyles.titleMedium), const SizedBox(height: 12), _PLChart(daily: daily), @@ -314,7 +360,31 @@ class _StockTab extends ConsumerWidget { totalValue: totalValue, itemCount: rows.length, ), - const SizedBox(height: 16), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton.icon( + icon: const Icon(Icons.download_outlined, size: 16), + label: const Text('Export CSV'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + textStyle: AppTextStyles.bodySmall, + visualDensity: VisualDensity.compact, + ), + onPressed: () { + final lines = [ + 'Product,Qty,Unit Cost,Total Value', + ...rows.map((r) => + '"${r.name}",${r.qty.toStringAsFixed(2)},${r.costPrice.toStringAsFixed(2)},${r.value.toStringAsFixed(2)}'), + ]; + _shareCsv( + 'stock_valuation_${DateFormat('yyyy-MM-dd').format(DateTime.now())}.csv', + lines.join('\n'), + ); + }, + ), + ), + const SizedBox(height: 8), Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( @@ -515,6 +585,27 @@ class _AgingTab extends ConsumerWidget { ], ), ), + OutlinedButton.icon( + icon: const Icon(Icons.download_outlined, size: 16), + label: const Text('CSV'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + textStyle: AppTextStyles.bodySmall, + visualDensity: VisualDensity.compact, + ), + onPressed: () { + const buckets = ['0-30d', '31-60d', '61-90d', '90+d']; + final lines = [ + 'Customer,Balance,Aging Bucket', + ...rows.map((r) => + '"${r.name}",${r.balance.toStringAsFixed(2)},${buckets[r.agingBucket.clamp(0, 3)]}'), + ]; + _shareCsv( + 'debtor_aging_${DateFormat('yyyy-MM-dd').format(DateTime.now())}.csv', + lines.join('\n'), + ); + }, + ), ], ), ), diff --git a/lib/features/suppliers/presentation/suppliers_screen.dart b/lib/features/suppliers/presentation/suppliers_screen.dart index 37cc580..cb0d31a 100644 --- a/lib/features/suppliers/presentation/suppliers_screen.dart +++ b/lib/features/suppliers/presentation/suppliers_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_text_styles.dart'; @@ -92,18 +93,35 @@ class _SupplierTile extends StatelessWidget { } } -class _SupplierDetailSheet extends StatelessWidget { +class _SupplierDetailSheet extends ConsumerWidget { const _SupplierDetailSheet({required this.supplier}); final Supplier supplier; @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.fromLTRB(24, 24, 24, MediaQuery.viewInsetsOf(context).bottom + 24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + Widget build(BuildContext context, WidgetRef ref) { + final historyAsync = ref.watch(supplierPaymentHistoryProvider(supplier.id)); + final balanceColor = supplier.balance > 0 ? AppColors.error : AppColors.success; + + return DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.95, + expand: false, + builder: (context, scrollController) => ListView( + controller: scrollController, + padding: const EdgeInsets.fromLTRB(24, 16, 24, 32), children: [ + Center( + child: Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: AppColors.textDisabled, + borderRadius: BorderRadius.circular(2), + ), + ), + ), Row( children: [ CircleAvatar( @@ -120,8 +138,10 @@ class _SupplierDetailSheet extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(supplier.name, style: AppTextStyles.titleLarge), - if (supplier.phone != null) Text(supplier.phone!, style: AppTextStyles.bodySmall), - if (supplier.address != null) Text(supplier.address!, style: AppTextStyles.bodySmall), + if (supplier.phone != null) + Text(supplier.phone!, style: AppTextStyles.bodySmall), + if (supplier.address != null) + Text(supplier.address!, style: AppTextStyles.bodySmall), if (supplier.paymentTerms != null) Text('Terms: ${supplier.paymentTerms}', style: AppTextStyles.bodySmall), ], @@ -142,16 +162,14 @@ class _SupplierDetailSheet extends StatelessWidget { Text('Amount Payable', style: AppTextStyles.bodyMedium), Text( CurrencyUtils.format(supplier.balance), - style: AppTextStyles.titleMedium.copyWith( - color: supplier.balance > 0 ? AppColors.error : AppColors.success, - ), + style: AppTextStyles.titleMedium.copyWith(color: balanceColor), ), ], ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), OutlinedButton.icon( - icon: const Icon(Icons.payment), + icon: const Icon(Icons.payment_rounded), label: const Text('Record Payment'), onPressed: () { Navigator.of(context).pop(); @@ -159,10 +177,82 @@ class _SupplierDetailSheet extends StatelessWidget { context: context, isScrollControlled: true, useSafeArea: true, - builder: (_) => _SupplierPaymentSheet(supplierId: supplier.id, supplierName: supplier.name), + builder: (_) => _SupplierPaymentSheet( + supplierId: supplier.id, supplierName: supplier.name), ); }, ), + const SizedBox(height: 20), + Text('Payment History', style: AppTextStyles.titleMedium), + const SizedBox(height: 8), + historyAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Text('Error: $e'), + data: (payments) => payments.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text('No payments recorded yet.', + style: AppTextStyles.bodySmall + .copyWith(color: AppColors.textSecondary)), + ) + : Column( + children: payments.map((p) => _SupplierPaymentRow(payment: p)).toList(), + ), + ), + ], + ), + ); + } +} + +class _SupplierPaymentRow extends StatelessWidget { + const _SupplierPaymentRow({required this.payment}); + + final SupplierPayment payment; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.primary.withAlpha(15), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.north_east_rounded, color: AppColors.primary, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + payment.method.toUpperCase(), + style: AppTextStyles.bodySmall + .copyWith(color: AppColors.textSecondary, letterSpacing: 0.5), + ), + if (payment.notes != null) + Text(payment.notes!, style: AppTextStyles.bodySmall), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(CurrencyUtils.format(payment.amount), + style: AppTextStyles.labelLarge + .copyWith(color: AppColors.primary, fontWeight: FontWeight.w700)), + Text( + DateFormat('dd MMM yyyy').format(payment.createdAt), + style: AppTextStyles.bodySmall + .copyWith(color: AppColors.textSecondary, fontSize: 11), + ), + ], + ), ], ), ); diff --git a/lib/providers/notifications_provider.dart b/lib/providers/notifications_provider.dart new file mode 100644 index 0000000..3bae90e --- /dev/null +++ b/lib/providers/notifications_provider.dart @@ -0,0 +1,69 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../data/database/app_database.dart'; +import 'database_provider.dart'; + +enum AlertType { chequeOverdue, chequeDue, lowStock, creditExceeded } + +class AppAlert { + const AppAlert({ + required this.type, + required this.title, + required this.body, + }); + + final AlertType type; + final String title; + final String body; +} + +final notificationsProvider = FutureProvider.autoDispose>((ref) async { + final chequesDao = ref.watch(chequesDaoProvider); + final inventoryDao = ref.watch(inventoryDaoProvider); + final customersDao = ref.watch(customersDaoProvider); + + final (overdue, dueSoon, lowStock, debtors) = await ( + chequesDao.getOverdueCheques(), + chequesDao.getDueWithinDays(7), + inventoryDao.getLowStockProducts(), + customersDao.getDebtors(), + ).wait; + + final alerts = []; + + for (final Cheque c in overdue) { + alerts.add(AppAlert( + type: AlertType.chequeOverdue, + title: 'Cheque Overdue', + body: '${c.partyName} · ${c.dueDate.day}/${c.dueDate.month}/${c.dueDate.year}', + )); + } + + for (final Cheque c in dueSoon) { + alerts.add(AppAlert( + type: AlertType.chequeDue, + title: 'Cheque Due in 7 Days', + body: '${c.partyName} · ${c.dueDate.day}/${c.dueDate.month}/${c.dueDate.year}', + )); + } + + for (final Product p in lowStock) { + alerts.add(AppAlert( + type: AlertType.lowStock, + title: 'Low Stock', + body: p.name, + )); + } + + for (final Customer c in debtors) { + if (c.creditLimit > 0 && c.balance > c.creditLimit) { + alerts.add(AppAlert( + type: AlertType.creditExceeded, + title: 'Credit Limit Exceeded', + body: c.name, + )); + } + } + + return alerts; +}); diff --git a/lib/providers/suppliers_provider.dart b/lib/providers/suppliers_provider.dart index d8e854e..ed79e20 100644 --- a/lib/providers/suppliers_provider.dart +++ b/lib/providers/suppliers_provider.dart @@ -12,6 +12,11 @@ import 'database_provider.dart'; final suppliersStreamProvider = StreamProvider.autoDispose>( (ref) => ref.watch(suppliersDaoProvider).watchAll()); +final supplierPaymentHistoryProvider = + FutureProvider.autoDispose.family, String>( + (ref, supplierId) => + ref.watch(suppliersDaoProvider).getPaymentsForSupplier(supplierId)); + class SupplierActions { SupplierActions(this._ref); final Ref _ref; diff --git a/lib/shared/widgets/app_scaffold.dart b/lib/shared/widgets/app_scaffold.dart index 8568a8f..64b1a86 100644 --- a/lib/shared/widgets/app_scaffold.dart +++ b/lib/shared/widgets/app_scaffold.dart @@ -1,5 +1,6 @@ import 'package:bms/core/router/app_router.dart'; import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/shared/widgets/notification_bell.dart'; import 'package:bms/shared/widgets/sidebar_nav.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -35,6 +36,13 @@ class _AppScaffoldState extends State { ], ), bottomNavigationBar: isWide ? null : _BottomNav(currentLocation: location), + floatingActionButton: isWide + ? null + : Padding( + padding: const EdgeInsets.only(bottom: 4), + child: NotificationBell(iconColor: AppColors.primary), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.miniEndTop, ); } } diff --git a/lib/shared/widgets/notification_bell.dart b/lib/shared/widgets/notification_bell.dart new file mode 100644 index 0000000..372b251 --- /dev/null +++ b/lib/shared/widgets/notification_bell.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_text_styles.dart'; +import '../../providers/notifications_provider.dart'; + +class NotificationBell extends ConsumerWidget { + const NotificationBell({super.key, this.iconColor = Colors.white}); + + final Color iconColor; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final alertsAsync = ref.watch(notificationsProvider); + final count = alertsAsync.when( + data: (alerts) => alerts.length, + loading: () => 0, + error: (_, __) => 0, + ); + + return Stack( + clipBehavior: Clip.none, + children: [ + IconButton( + icon: Icon(Icons.notifications_outlined, color: iconColor), + tooltip: 'Alerts', + onPressed: () => _showPanel(context, ref), + ), + if (count > 0) + Positioned( + right: 6, + top: 6, + child: IgnorePointer( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + decoration: BoxDecoration( + color: AppColors.error, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + count > 99 ? '99+' : '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + height: 1.2, + ), + ), + ), + ), + ), + ], + ); + } + + void _showPanel(BuildContext context, WidgetRef ref) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (_) => const _AlertsPanel(), + ); + } +} + +class _AlertsPanel extends ConsumerWidget { + const _AlertsPanel(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final alertsAsync = ref.watch(notificationsProvider); + + return DraggableScrollableSheet( + initialChildSize: 0.5, + minChildSize: 0.3, + maxChildSize: 0.9, + expand: false, + builder: (context, scrollController) => Column( + children: [ + _PanelHandle( + onRefresh: () => ref.invalidate(notificationsProvider), + ), + Expanded( + child: alertsAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center(child: Text('Error: $e')), + data: (alerts) => alerts.isEmpty + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.check_circle_outline, + size: 48, color: AppColors.success), + const SizedBox(height: 12), + Text('All clear', style: AppTextStyles.titleMedium), + const SizedBox(height: 4), + Text('No alerts right now.', + style: AppTextStyles.bodySmall.copyWith( + color: AppColors.textSecondary)), + ], + ), + ) + : ListView.separated( + controller: scrollController, + itemCount: alerts.length, + separatorBuilder: (_, __) => + const Divider(height: 1, indent: 16, endIndent: 16), + itemBuilder: (_, i) => _AlertTile(alert: alerts[i]), + ), + ), + ), + ], + ), + ); + } +} + +class _PanelHandle extends StatelessWidget { + const _PanelHandle({required this.onRefresh}); + + final VoidCallback onRefresh; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 8, 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 36, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: AppColors.textDisabled, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Text('Alerts', style: AppTextStyles.titleLarge), + ], + ), + ), + IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Refresh', + onPressed: onRefresh, + ), + ], + ), + ); + } +} + +class _AlertTile extends StatelessWidget { + const _AlertTile({required this.alert}); + + final AppAlert alert; + + IconData get _icon => switch (alert.type) { + AlertType.chequeOverdue => Icons.warning_amber_rounded, + AlertType.chequeDue => Icons.schedule_rounded, + AlertType.lowStock => Icons.inventory_2_outlined, + AlertType.creditExceeded => Icons.credit_card_off_outlined, + }; + + Color get _color => switch (alert.type) { + AlertType.chequeOverdue => AppColors.error, + AlertType.chequeDue => AppColors.warning, + AlertType.lowStock => AppColors.warning, + AlertType.creditExceeded => AppColors.error, + }; + + @override + Widget build(BuildContext context) { + return ListTile( + leading: CircleAvatar( + radius: 18, + backgroundColor: _color.withAlpha(20), + child: Icon(_icon, color: _color, size: 18), + ), + title: Text(alert.title, + style: AppTextStyles.labelLarge.copyWith(fontWeight: FontWeight.w600)), + subtitle: Text(alert.body, style: AppTextStyles.bodySmall), + dense: true, + ); + } +} diff --git a/lib/shared/widgets/sidebar_nav.dart b/lib/shared/widgets/sidebar_nav.dart index 6d44923..be13de3 100644 --- a/lib/shared/widgets/sidebar_nav.dart +++ b/lib/shared/widgets/sidebar_nav.dart @@ -7,6 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'notification_bell.dart'; + const _kSidebarBg = Color(0xFF111827); const _kSidebarHover = Color(0xFF1F2937); const _kSidebarActive = Color(0xFF1D4ED8); @@ -156,6 +158,7 @@ class _Header extends StatelessWidget { ], ), ), + NotificationBell(iconColor: _kSidebarText), ], ), ); From 8657a45ec397c05eebb93bec5c772766393239e0 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 00:06:29 +0530 Subject: [PATCH 02/13] chore: fix all 446 lint warnings (package imports, const, types) --- lib/app.dart | 5 +- lib/core/constants/app_constants.dart | 8 +-- lib/core/router/app_router.dart | 41 ++++++----- lib/core/router/route_guard.dart | 6 +- lib/core/theme/app_text_styles.dart | 2 +- lib/core/theme/app_theme.dart | 8 +-- lib/core/utils/currency_utils.dart | 1 + lib/core/utils/date_utils.dart | 1 + lib/core/utils/logger.dart | 4 -- lib/data/database/app_database.dart | 39 ++++++----- lib/data/database/daos/audit_log_dao.dart | 5 +- lib/data/database/daos/cheques_dao.dart | 5 +- lib/data/database/daos/customers_dao.dart | 7 +- lib/data/database/daos/inventory_dao.dart | 9 ++- lib/data/database/daos/invoices_dao.dart | 5 +- lib/data/database/daos/petty_cash_dao.dart | 5 +- lib/data/database/daos/reports_dao.dart | 3 +- lib/data/database/daos/returns_dao.dart | 5 +- lib/data/database/daos/suppliers_dao.dart | 7 +- lib/data/database/daos/users_dao.dart | 5 +- lib/data/database/tables/products_table.dart | 3 +- lib/data/database/tables/returns_table.dart | 7 +- lib/data/repositories/auth_repository.dart | 13 ++-- .../repositories/inventory_repository.dart | 9 ++- lib/features/auth/domain/auth_state.dart | 3 +- .../auth/presentation/login_screen.dart | 7 +- .../cheques/presentation/cheque_screen.dart | 20 +++--- .../presentation/customers_screen.dart | 15 ++-- .../presentation/dashboard_screen.dart | 47 ++++++------- .../debtors/presentation/debtors_screen.dart | 31 ++++----- lib/features/grn/presentation/grn_screen.dart | 15 ++-- .../presentation/inventory_screen.dart | 14 ++-- .../presentation/invoice_detail_screen.dart | 41 ++++++----- .../invoices/presentation/invoice_pdf.dart | 22 +++--- .../presentation/invoices_screen.dart | 19 +++--- .../presentation/petty_cash_screen.dart | 19 +++--- lib/features/pos/presentation/pos_screen.dart | 47 +++++++------ .../pos/presentation/receipt_pdf.dart | 10 ++- .../presentation/quick_sales_screen.dart | 15 ++-- .../reports/presentation/reports_screen.dart | 38 +++++------ .../presentation/settings_screen.dart | 68 +++++++++---------- .../presentation/suppliers_screen.dart | 19 +++--- .../users/presentation/users_screen.dart | 17 +++-- lib/main.dart | 3 +- lib/providers/auth_provider.dart | 8 +-- lib/providers/cheques_provider.dart | 9 ++- lib/providers/customers_provider.dart | 9 ++- lib/providers/dashboard_provider.dart | 8 +-- lib/providers/database_provider.dart | 24 +++---- lib/providers/grn_provider.dart | 9 ++- lib/providers/inventory_provider.dart | 13 ++-- lib/providers/invoices_provider.dart | 17 +++-- lib/providers/notifications_provider.dart | 5 +- lib/providers/petty_cash_provider.dart | 11 ++- lib/providers/pos_provider.dart | 10 ++- lib/providers/quick_sale_provider.dart | 11 ++- lib/providers/settings_provider.dart | 16 ++--- lib/providers/suppliers_provider.dart | 9 ++- lib/providers/users_provider.dart | 9 ++- lib/shared/widgets/app_scaffold.dart | 4 +- lib/shared/widgets/bms_error_widget.dart | 5 +- lib/shared/widgets/bms_filter_bar.dart | 3 +- lib/shared/widgets/confirmation_dialog.dart | 3 +- lib/shared/widgets/notification_bell.dart | 18 +++-- lib/shared/widgets/sidebar_nav.dart | 13 ++-- lib/shared/widgets/stat_card.dart | 5 +- tool/preview_receipt.dart | 13 ++-- 67 files changed, 417 insertions(+), 488 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 576e0de..591c797 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,9 +1,8 @@ +import 'package:bms/core/router/app_router.dart'; +import 'package:bms/core/theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'core/router/app_router.dart'; -import 'core/theme/app_theme.dart'; - class BmsApp extends ConsumerWidget { const BmsApp({super.key}); diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart index dfcdd8d..4f05ba7 100644 --- a/lib/core/constants/app_constants.dart +++ b/lib/core/constants/app_constants.dart @@ -18,10 +18,10 @@ abstract final class AppConstants { static const List chequeReminderDaysBefore = [1, 3, 7]; // Touch targets (WCAG AA minimum) - static const double minTouchTargetSize = 48.0; + static const double minTouchTargetSize = 48; // Layout - static const double sidebarWidth = 240.0; - static const double sidebarCollapsedWidth = 64.0; - static const double sidebarBreakpoint = 900.0; + static const double sidebarWidth = 240; + static const double sidebarCollapsedWidth = 64; + static const double sidebarBreakpoint = 900; } diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 8114345..a94bd78 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,27 +1,25 @@ +import 'package:bms/core/router/route_guard.dart'; +import 'package:bms/features/auth/presentation/login_screen.dart'; +import 'package:bms/features/cheques/presentation/cheque_screen.dart'; +import 'package:bms/features/customers/presentation/customers_screen.dart'; +import 'package:bms/features/dashboard/presentation/dashboard_screen.dart'; +import 'package:bms/features/debtors/presentation/debtors_screen.dart'; +import 'package:bms/features/grn/presentation/grn_screen.dart'; +import 'package:bms/features/inventory/presentation/inventory_screen.dart'; +import 'package:bms/features/invoices/presentation/invoices_screen.dart'; +import 'package:bms/features/petty_cash/presentation/petty_cash_screen.dart'; +import 'package:bms/features/pos/presentation/pos_screen.dart'; +import 'package:bms/features/quick_sales/presentation/quick_sales_screen.dart'; +import 'package:bms/features/reports/presentation/reports_screen.dart'; +import 'package:bms/features/settings/presentation/settings_screen.dart'; +import 'package:bms/features/suppliers/presentation/suppliers_screen.dart'; +import 'package:bms/features/users/presentation/users_screen.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/shared/widgets/app_scaffold.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../features/auth/presentation/login_screen.dart'; -import '../../features/cheques/presentation/cheque_screen.dart'; -import '../../features/customers/presentation/customers_screen.dart'; -import '../../features/debtors/presentation/debtors_screen.dart'; -import '../../features/grn/presentation/grn_screen.dart'; -import '../../features/invoices/presentation/invoices_screen.dart'; -import '../../features/dashboard/presentation/dashboard_screen.dart'; -import '../../features/inventory/presentation/inventory_screen.dart'; -import '../../features/petty_cash/presentation/petty_cash_screen.dart'; -import '../../features/pos/presentation/pos_screen.dart'; -import '../../features/quick_sales/presentation/quick_sales_screen.dart'; -import '../../features/reports/presentation/reports_screen.dart'; -import '../../features/suppliers/presentation/suppliers_screen.dart'; -import '../../features/settings/presentation/settings_screen.dart'; -import '../../features/users/presentation/users_screen.dart'; -import '../../providers/auth_provider.dart'; -import '../../shared/widgets/app_scaffold.dart'; -import 'route_guard.dart'; - part 'app_router.g.dart'; class _RouterNotifier extends ChangeNotifier { @@ -32,12 +30,11 @@ class _RouterNotifier extends ChangeNotifier { GoRouter appRouter(Ref ref) { final notifier = _RouterNotifier(); - ref.listen(currentAuthStateProvider, (_, __) => notifier.notify()); + ref.listen(currentAuthStateProvider, (_, _) => notifier.notify()); ref.onDispose(notifier.dispose); return GoRouter( initialLocation: AppRoutes.login, - debugLogDiagnostics: false, refreshListenable: notifier, redirect: (context, state) => RouteGuard.redirect( state: state, diff --git a/lib/core/router/route_guard.dart b/lib/core/router/route_guard.dart index 18db083..017e9db 100644 --- a/lib/core/router/route_guard.dart +++ b/lib/core/router/route_guard.dart @@ -1,12 +1,12 @@ +import 'package:bms/core/router/app_router.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; import 'package:go_router/go_router.dart'; -import '../../features/auth/domain/auth_state.dart'; -import 'app_router.dart'; - /// Role matrix: /// developer - all routes /// admin - all except /users (user management) /// cashier - dashboard, pos, inventory (view), customers +// ignore: avoid_classes_with_only_static_members abstract final class RouteGuard { static const Set _publicRoutes = {AppRoutes.login}; diff --git a/lib/core/theme/app_text_styles.dart b/lib/core/theme/app_text_styles.dart index a272638..9789ff6 100644 --- a/lib/core/theme/app_text_styles.dart +++ b/lib/core/theme/app_text_styles.dart @@ -1,5 +1,5 @@ +import 'package:bms/core/theme/app_colors.dart'; import 'package:flutter/material.dart'; -import 'app_colors.dart'; abstract final class AppTextStyles { static const String _fontFamily = 'Inter'; diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 2cda156..ea5d6b8 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,17 +1,15 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; import 'package:flutter/material.dart'; -import 'app_colors.dart'; -import 'app_text_styles.dart'; +// ignore: avoid_classes_with_only_static_members abstract final class AppTheme { static ThemeData get light { const colorScheme = ColorScheme.light( primary: AppColors.primary, - onPrimary: AppColors.textOnPrimary, secondary: AppColors.primaryLight, onSecondary: AppColors.textOnPrimary, error: AppColors.error, - onError: AppColors.textOnPrimary, - surface: AppColors.surface, onSurface: AppColors.textPrimary, surfaceContainerHighest: AppColors.surfaceVariant, ); diff --git a/lib/core/utils/currency_utils.dart b/lib/core/utils/currency_utils.dart index 4b740a3..e1697cf 100644 --- a/lib/core/utils/currency_utils.dart +++ b/lib/core/utils/currency_utils.dart @@ -1,5 +1,6 @@ import 'package:intl/intl.dart'; +// ignore: avoid_classes_with_only_static_members abstract final class CurrencyUtils { static final NumberFormat _fmt = NumberFormat('#,##0.00'); static final NumberFormat _fmtCompact = NumberFormat('#,##0'); diff --git a/lib/core/utils/date_utils.dart b/lib/core/utils/date_utils.dart index a626059..02bafac 100644 --- a/lib/core/utils/date_utils.dart +++ b/lib/core/utils/date_utils.dart @@ -1,5 +1,6 @@ import 'package:intl/intl.dart'; +// ignore: avoid_classes_with_only_static_members abstract final class BmsDateUtils { static final DateFormat _date = DateFormat('dd MMM yyyy'); static final DateFormat _dateTime = DateFormat('dd MMM yyyy HH:mm'); diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index d66beac..a3f6f3a 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -4,10 +4,6 @@ import 'package:logger/logger.dart'; /// In production, swap PrettyPrinter for a JSON printer that emits to a log file. final appLogger = Logger( printer: PrettyPrinter( - methodCount: 2, - errorMethodCount: 8, - lineLength: 120, - colors: true, printEmojis: false, dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, ), diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index 6f9da1c..323907c 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -1,29 +1,28 @@ import 'package:bcrypt/bcrypt.dart'; +import 'package:bms/data/database/daos/audit_log_dao.dart'; +import 'package:bms/data/database/daos/cheques_dao.dart'; +import 'package:bms/data/database/daos/customers_dao.dart'; +import 'package:bms/data/database/daos/inventory_dao.dart'; +import 'package:bms/data/database/daos/invoices_dao.dart'; +import 'package:bms/data/database/daos/petty_cash_dao.dart'; +import 'package:bms/data/database/daos/returns_dao.dart'; +import 'package:bms/data/database/daos/suppliers_dao.dart'; +import 'package:bms/data/database/daos/users_dao.dart'; +import 'package:bms/data/database/tables/audit_log_table.dart'; +import 'package:bms/data/database/tables/cheques_table.dart'; +import 'package:bms/data/database/tables/customers_table.dart'; +import 'package:bms/data/database/tables/invoices_table.dart'; +import 'package:bms/data/database/tables/payments_table.dart'; +import 'package:bms/data/database/tables/petty_cash_table.dart'; +import 'package:bms/data/database/tables/products_table.dart'; +import 'package:bms/data/database/tables/returns_table.dart'; +import 'package:bms/data/database/tables/suppliers_table.dart'; +import 'package:bms/data/database/tables/users_table.dart'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:uuid/uuid.dart'; -import 'daos/audit_log_dao.dart'; -import 'daos/cheques_dao.dart'; -import 'daos/customers_dao.dart'; -import 'daos/inventory_dao.dart'; -import 'daos/invoices_dao.dart'; -import 'daos/petty_cash_dao.dart'; -import 'daos/returns_dao.dart'; -import 'daos/suppliers_dao.dart'; -import 'daos/users_dao.dart'; -import 'tables/audit_log_table.dart'; -import 'tables/cheques_table.dart'; -import 'tables/customers_table.dart'; -import 'tables/invoices_table.dart'; -import 'tables/payments_table.dart'; -import 'tables/petty_cash_table.dart'; -import 'tables/products_table.dart'; -import 'tables/returns_table.dart'; -import 'tables/suppliers_table.dart'; -import 'tables/users_table.dart'; - part 'app_database.g.dart'; @DriftDatabase( diff --git a/lib/data/database/daos/audit_log_dao.dart b/lib/data/database/daos/audit_log_dao.dart index 15a4064..3908d47 100644 --- a/lib/data/database/daos/audit_log_dao.dart +++ b/lib/data/database/daos/audit_log_dao.dart @@ -1,10 +1,9 @@ import 'dart:convert'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/audit_log_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/audit_log_table.dart'; - part 'audit_log_dao.g.dart'; @DriftAccessor(tables: [AuditLog]) diff --git a/lib/data/database/daos/cheques_dao.dart b/lib/data/database/daos/cheques_dao.dart index 75eb447..8918529 100644 --- a/lib/data/database/daos/cheques_dao.dart +++ b/lib/data/database/daos/cheques_dao.dart @@ -1,8 +1,7 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/cheques_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/cheques_table.dart'; - part 'cheques_dao.g.dart'; @DriftAccessor(tables: [Cheques]) diff --git a/lib/data/database/daos/customers_dao.dart b/lib/data/database/daos/customers_dao.dart index 19968bf..1de12d0 100644 --- a/lib/data/database/daos/customers_dao.dart +++ b/lib/data/database/daos/customers_dao.dart @@ -1,9 +1,8 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/customers_table.dart'; +import 'package:bms/data/database/tables/payments_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/customers_table.dart'; -import '../tables/payments_table.dart'; - part 'customers_dao.g.dart'; @DriftAccessor(tables: [Customers, CustomerPayments]) diff --git a/lib/data/database/daos/inventory_dao.dart b/lib/data/database/daos/inventory_dao.dart index 8e01b9c..7a97a5f 100644 --- a/lib/data/database/daos/inventory_dao.dart +++ b/lib/data/database/daos/inventory_dao.dart @@ -1,8 +1,7 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/products_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/products_table.dart'; - part 'inventory_dao.g.dart'; @DriftAccessor(tables: [Products, Categories, Stock, StockMovements, ProductUnits]) @@ -49,7 +48,7 @@ class InventoryDao extends DatabaseAccessor with _$InventoryDaoMixi ]); // Compare qty (real) against reorderLevel (int) via expression cast query.where(stock.qty.isSmallerOrEqualValue(0) | - CustomExpression('stock.qty <= products.reorder_level')); + const CustomExpression('stock.qty <= products.reorder_level')); return query.watch().map( (rows) => rows.map((r) => r.readTable(stock)).toList(), ); @@ -60,7 +59,7 @@ class InventoryDao extends DatabaseAccessor with _$InventoryDaoMixi innerJoin(products, products.id.equalsExp(stock.productId)), ]); query.where(stock.qty.isSmallerOrEqualValue(0) | - CustomExpression('stock.qty <= products.reorder_level')); + const CustomExpression('stock.qty <= products.reorder_level')); final rows = await query.get(); return rows.map((r) => r.readTable(products)).toList(); } diff --git a/lib/data/database/daos/invoices_dao.dart b/lib/data/database/daos/invoices_dao.dart index 95757f6..6f3e16b 100644 --- a/lib/data/database/daos/invoices_dao.dart +++ b/lib/data/database/daos/invoices_dao.dart @@ -1,8 +1,7 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/invoices_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/invoices_table.dart'; - part 'invoices_dao.g.dart'; @DriftAccessor(tables: [Invoices, InvoiceItems, NoInvoiceSales]) diff --git a/lib/data/database/daos/petty_cash_dao.dart b/lib/data/database/daos/petty_cash_dao.dart index a125f81..b0f1241 100644 --- a/lib/data/database/daos/petty_cash_dao.dart +++ b/lib/data/database/daos/petty_cash_dao.dart @@ -1,8 +1,7 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/petty_cash_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/petty_cash_table.dart'; - part 'petty_cash_dao.g.dart'; @DriftAccessor(tables: [PettyCash]) diff --git a/lib/data/database/daos/reports_dao.dart b/lib/data/database/daos/reports_dao.dart index a9dd10e..0e93fc9 100644 --- a/lib/data/database/daos/reports_dao.dart +++ b/lib/data/database/daos/reports_dao.dart @@ -1,6 +1,5 @@ -import 'package:drift/drift.dart'; - import 'package:bms/data/database/app_database.dart'; +import 'package:drift/drift.dart'; class DailySales { DailySales({required this.date, required this.revenue, required this.cogs}); diff --git a/lib/data/database/daos/returns_dao.dart b/lib/data/database/daos/returns_dao.dart index 467d699..1aaa4d2 100644 --- a/lib/data/database/daos/returns_dao.dart +++ b/lib/data/database/daos/returns_dao.dart @@ -1,8 +1,7 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/returns_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/returns_table.dart'; - part 'returns_dao.g.dart'; @DriftAccessor(tables: [SalesReturns, ReturnItems]) diff --git a/lib/data/database/daos/suppliers_dao.dart b/lib/data/database/daos/suppliers_dao.dart index 5e0ab4d..0c31427 100644 --- a/lib/data/database/daos/suppliers_dao.dart +++ b/lib/data/database/daos/suppliers_dao.dart @@ -1,9 +1,8 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/payments_table.dart'; +import 'package:bms/data/database/tables/suppliers_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/payments_table.dart'; -import '../tables/suppliers_table.dart'; - part 'suppliers_dao.g.dart'; @DriftAccessor(tables: [Suppliers, Purchases, PurchaseItems, SupplierPayments]) diff --git a/lib/data/database/daos/users_dao.dart b/lib/data/database/daos/users_dao.dart index 7cd4585..15cc12a 100644 --- a/lib/data/database/daos/users_dao.dart +++ b/lib/data/database/daos/users_dao.dart @@ -1,8 +1,7 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/tables/users_table.dart'; import 'package:drift/drift.dart'; -import '../app_database.dart'; -import '../tables/users_table.dart'; - part 'users_dao.g.dart'; @DriftAccessor(tables: [Users]) diff --git a/lib/data/database/tables/products_table.dart b/lib/data/database/tables/products_table.dart index 03c78f4..17d2825 100644 --- a/lib/data/database/tables/products_table.dart +++ b/lib/data/database/tables/products_table.dart @@ -1,7 +1,6 @@ +import 'package:bms/data/database/tables/users_table.dart'; import 'package:drift/drift.dart'; -import 'users_table.dart'; - class Categories extends Table { TextColumn get id => text()(); TextColumn get name => text().withLength(min: 1, max: 100).unique()(); diff --git a/lib/data/database/tables/returns_table.dart b/lib/data/database/tables/returns_table.dart index eac9e25..2ba4c9d 100644 --- a/lib/data/database/tables/returns_table.dart +++ b/lib/data/database/tables/returns_table.dart @@ -1,7 +1,6 @@ +import 'package:bms/data/database/tables/invoices_table.dart'; import 'package:drift/drift.dart'; -import 'invoices_table.dart'; - class SalesReturns extends Table { TextColumn get id => text()(); TextColumn get invoiceId => text().references(Invoices, #id)(); @@ -10,6 +9,7 @@ class SalesReturns extends Table { /// refund | credit | exchange TextColumn get type => text() .withDefault(const Constant('refund')) + // ignore: recursive_getters .check(type.isIn(['refund', 'credit', 'exchange']))(); RealColumn get totalAmount => real().withDefault(const Constant(0))(); @@ -26,8 +26,11 @@ class ReturnItems extends Table { TextColumn get returnId => text().references(SalesReturns, #id)(); TextColumn get productId => text()(); TextColumn get productName => text()(); // snapshot + // ignore: recursive_getters RealColumn get qty => real().check(qty.isBiggerThanValue(0))(); + // ignore: recursive_getters RealColumn get unitPrice => real().check(unitPrice.isBiggerOrEqualValue(0))(); + // ignore: recursive_getters RealColumn get subtotal => real().check(subtotal.isBiggerOrEqualValue(0))(); @override diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index 69885b9..d760eed 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -1,16 +1,15 @@ import 'dart:async'; import 'package:bcrypt/bcrypt.dart'; +import 'package:bms/core/constants/app_constants.dart'; +import 'package:bms/core/errors/app_exception.dart'; +import 'package:bms/core/utils/logger.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/daos/users_dao.dart'; +import 'package:bms/data/models/user_model.dart'; import 'package:drift/drift.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import '../../core/constants/app_constants.dart'; -import '../../core/errors/app_exception.dart'; -import '../../core/utils/logger.dart'; -import '../database/app_database.dart'; -import '../database/daos/users_dao.dart'; -import '../models/user_model.dart'; - class AuthRepository { AuthRepository({required UsersDao usersDao, required FlutterSecureStorage secureStorage}) : _dao = usersDao, diff --git a/lib/data/repositories/inventory_repository.dart b/lib/data/repositories/inventory_repository.dart index 0aa75aa..64de113 100644 --- a/lib/data/repositories/inventory_repository.dart +++ b/lib/data/repositories/inventory_repository.dart @@ -1,11 +1,10 @@ +import 'package:bms/core/errors/app_exception.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/daos/audit_log_dao.dart'; +import 'package:bms/data/database/daos/inventory_dao.dart'; import 'package:drift/drift.dart'; import 'package:uuid/uuid.dart'; -import '../../core/errors/app_exception.dart'; -import '../database/app_database.dart'; -import '../database/daos/audit_log_dao.dart'; -import '../database/daos/inventory_dao.dart'; - class InventoryRepository { InventoryRepository({required InventoryDao inventoryDao, required AuditLogDao auditLogDao}) : _inventory = inventoryDao, diff --git a/lib/features/auth/domain/auth_state.dart b/lib/features/auth/domain/auth_state.dart index 9010375..48bbe13 100644 --- a/lib/features/auth/domain/auth_state.dart +++ b/lib/features/auth/domain/auth_state.dart @@ -1,7 +1,6 @@ +import 'package:bms/data/models/user_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../../data/models/user_model.dart'; - part 'auth_state.freezed.dart'; @freezed diff --git a/lib/features/auth/presentation/login_screen.dart b/lib/features/auth/presentation/login_screen.dart index 9f43042..f181925 100644 --- a/lib/features/auth/presentation/login_screen.dart +++ b/lib/features/auth/presentation/login_screen.dart @@ -1,11 +1,10 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/providers/auth_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../providers/auth_provider.dart'; - class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); diff --git a/lib/features/cheques/presentation/cheque_screen.dart b/lib/features/cheques/presentation/cheque_screen.dart index 62a65ed..6863c73 100644 --- a/lib/features/cheques/presentation/cheque_screen.dart +++ b/lib/features/cheques/presentation/cheque_screen.dart @@ -1,13 +1,12 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/core/utils/date_utils.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/providers/cheques_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../core/utils/date_utils.dart'; -import '../../../data/database/app_database.dart'; -import '../../../providers/cheques_provider.dart'; - class ChequeScreen extends ConsumerStatefulWidget { const ChequeScreen({super.key}); @@ -180,7 +179,7 @@ class _CalendarGrid extends StatelessWidget { @override Widget build(BuildContext context) { - final firstDay = DateTime(monthDate.year, monthDate.month, 1); + final firstDay = DateTime(monthDate.year, monthDate.month); // weekday: Mon=1 … Sun=7. We want Mon at index 0. final startOffset = firstDay.weekday - 1; final daysInMonth = DateTime(monthDate.year, monthDate.month + 1, 0).day; @@ -323,7 +322,6 @@ class _DayChequeSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return DraggableScrollableSheet( - initialChildSize: 0.5, minChildSize: 0.3, maxChildSize: 0.9, expand: false, @@ -570,7 +568,7 @@ class _AddChequeSheetState extends ConsumerState<_AddChequeSheet> { children: [ Expanded( child: DropdownButtonFormField( - value: _type, + initialValue: _type, decoration: const InputDecoration(labelText: 'Type'), items: const [ DropdownMenuItem(value: 'received', child: Text('Received')), @@ -582,7 +580,7 @@ class _AddChequeSheetState extends ConsumerState<_AddChequeSheet> { const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( - value: _partyType, + initialValue: _partyType, decoration: const InputDecoration(labelText: 'Party Type'), items: const [ DropdownMenuItem(value: 'customer', child: Text('Customer')), diff --git a/lib/features/customers/presentation/customers_screen.dart b/lib/features/customers/presentation/customers_screen.dart index 3b506b6..8de2ab7 100644 --- a/lib/features/customers/presentation/customers_screen.dart +++ b/lib/features/customers/presentation/customers_screen.dart @@ -1,12 +1,11 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/providers/customers_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../data/database/app_database.dart'; -import '../../../providers/customers_provider.dart'; - class CustomersScreen extends ConsumerWidget { const CustomersScreen({super.key}); @@ -141,7 +140,7 @@ class _CustomerDetailSheet extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Outstanding Balance', style: AppTextStyles.bodyMedium), + const Text('Outstanding Balance', style: AppTextStyles.bodyMedium), Text( CurrencyUtils.format(customer.balance), style: AppTextStyles.titleMedium.copyWith( @@ -328,7 +327,7 @@ class _PaymentSheetState extends ConsumerState<_PaymentSheet> { ), const SizedBox(height: 12), DropdownButtonFormField( - value: _method, + initialValue: _method, decoration: const InputDecoration(labelText: 'Payment Method'), items: const [ DropdownMenuItem(value: 'cash', child: Text('Cash')), diff --git a/lib/features/dashboard/presentation/dashboard_screen.dart b/lib/features/dashboard/presentation/dashboard_screen.dart index bb9c74d..5803dd5 100644 --- a/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/lib/features/dashboard/presentation/dashboard_screen.dart @@ -1,9 +1,3 @@ -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; - import 'package:bms/core/router/app_router.dart'; import 'package:bms/core/theme/app_colors.dart'; import 'package:bms/core/theme/app_text_styles.dart'; @@ -13,6 +7,11 @@ import 'package:bms/data/database/app_database.dart'; import 'package:bms/data/database/daos/reports_dao.dart'; import 'package:bms/providers/dashboard_provider.dart'; import 'package:bms/shared/widgets/stat_card.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; class DashboardScreen extends ConsumerWidget { const DashboardScreen({super.key}); @@ -79,7 +78,6 @@ class DashboardScreen extends ConsumerWidget { label: 'Cheques Due (7d)', value: '${s.chequesThisWeek}', icon: Icons.calendar_today_outlined, - color: AppColors.primary, onTap: () => context.go(AppRoutes.cheques), ), StatCard( @@ -102,7 +100,7 @@ class DashboardScreen extends ConsumerWidget { const SizedBox(height: 28), // ── 30-Day Revenue Trend ───────────────────────────────────── - _SectionHeader( + const _SectionHeader( title: 'Revenue Trend', subtitle: 'Last 30 days - Revenue vs Gross Profit', ), @@ -112,7 +110,7 @@ class DashboardScreen extends ConsumerWidget { const SizedBox(height: 28), // ── Weekly Performance ──────────────────────────────────────── - _SectionHeader( + const _SectionHeader( title: 'Weekly Performance', subtitle: 'Last 7 days', ), @@ -122,7 +120,7 @@ class DashboardScreen extends ConsumerWidget { // ── Payment Mix ────────────────────────────────────────────── if (s.paymentMix.isNotEmpty) ...[ const SizedBox(height: 28), - _SectionHeader( + const _SectionHeader( title: 'Payment Mix', subtitle: 'Current month by method', ), @@ -165,7 +163,6 @@ class _SectionHeader extends StatelessWidget { @override Widget build(BuildContext context) => Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( @@ -358,7 +355,7 @@ class _RevenueTrendChart extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.show_chart_outlined, + const Icon(Icons.show_chart_outlined, size: 40, color: AppColors.border), const SizedBox(height: 8), Text('No sales data for the last 30 days.', @@ -386,10 +383,10 @@ class _RevenueTrendChart extends StatelessWidget { child: Column( children: [ // Legend - Row( + const Row( children: [ _ChartLegendDot(color: AppColors.primary, label: 'Revenue'), - const SizedBox(width: 16), + SizedBox(width: 16), _ChartLegendDot(color: AppColors.success, label: 'Gross Profit'), ], ), @@ -400,7 +397,6 @@ class _RevenueTrendChart extends StatelessWidget { minY: 0, maxY: maxY > 0 ? maxY * 1.25 : 100, gridData: FlGridData( - show: true, drawVerticalLine: false, horizontalInterval: maxY > 0 ? maxY / 4 : 25, getDrawingHorizontalLine: (_) => FlLine( @@ -410,7 +406,6 @@ class _RevenueTrendChart extends StatelessWidget { ), borderData: FlBorderData(show: false), lineTouchData: LineTouchData( - handleBuiltInTouches: true, touchSpotThreshold: 20, touchTooltipData: LineTouchTooltipData( fitInsideHorizontally: true, @@ -451,9 +446,9 @@ class _RevenueTrendChart extends StatelessWidget { ), titlesData: FlTitlesData( topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), + ), rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), + ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -510,7 +505,7 @@ class _RevenueTrendChart extends StatelessWidget { end: Alignment.bottomCenter, colors: [ AppColors.primary.withValues(alpha: 0.18), - AppColors.primary.withValues(alpha: 0.0), + AppColors.primary.withValues(alpha: 0), ], ), ), @@ -520,7 +515,6 @@ class _RevenueTrendChart extends StatelessWidget { isCurved: true, curveSmoothness: 0.3, color: AppColors.success, - barWidth: 2, dashArray: [4, 3], dotData: const FlDotData(show: false), belowBarData: BarAreaData( @@ -530,7 +524,7 @@ class _RevenueTrendChart extends StatelessWidget { end: Alignment.bottomCenter, colors: [ AppColors.success.withValues(alpha: 0.10), - AppColors.success.withValues(alpha: 0.0), + AppColors.success.withValues(alpha: 0), ], ), ), @@ -600,10 +594,10 @@ class _WeeklyGroupedChart extends StatelessWidget { height: 240, child: Column( children: [ - Row( + const Row( children: [ _ChartLegendDot(color: AppColors.primary, label: 'Revenue'), - const SizedBox(width: 16), + SizedBox(width: 16), _ChartLegendDot(color: AppColors.success, label: 'Gross Profit'), ], ), @@ -615,7 +609,6 @@ class _WeeklyGroupedChart extends StatelessWidget { groupsSpace: 16, barGroups: groups, gridData: FlGridData( - show: true, drawVerticalLine: false, getDrawingHorizontalLine: (_) => FlLine( color: AppColors.border.withValues(alpha: 0.6), @@ -649,9 +642,9 @@ class _WeeklyGroupedChart extends StatelessWidget { ), titlesData: FlTitlesData( topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), + ), rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), + ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -871,7 +864,7 @@ class _RecentInvoicesList extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return DecoratedBox( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), diff --git a/lib/features/debtors/presentation/debtors_screen.dart b/lib/features/debtors/presentation/debtors_screen.dart index 43dfe05..69ad2cb 100644 --- a/lib/features/debtors/presentation/debtors_screen.dart +++ b/lib/features/debtors/presentation/debtors_screen.dart @@ -1,13 +1,12 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/providers/customers_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../data/database/app_database.dart'; -import '../../../providers/customers_provider.dart'; - class DebtorsScreen extends ConsumerWidget { const DebtorsScreen({super.key}); @@ -75,7 +74,7 @@ class _SummaryBanner extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Total Outstanding', style: AppTextStyles.bodySmall), + const Text('Total Outstanding', style: AppTextStyles.bodySmall), Text(CurrencyUtils.format(total), style: AppTextStyles.headlineMedium.copyWith(color: AppColors.error)), Text('$count debtor${count == 1 ? '' : 's'}', style: AppTextStyles.bodySmall), @@ -170,7 +169,7 @@ class _DebtorTile extends StatelessWidget { style: AppTextStyles.labelLarge.copyWith(color: color), ), const SizedBox(width: 4), - Icon(Icons.chevron_right, size: 16, color: AppColors.textDisabled), + const Icon(Icons.chevron_right, size: 16, color: AppColors.textDisabled), ], ), ); @@ -219,7 +218,7 @@ class _DebtorDetailSheet extends ConsumerWidget { backgroundColor: AppColors.primary.withAlpha(20), child: Text( customer.name[0].toUpperCase(), - style: TextStyle( + style: const TextStyle( color: AppColors.primary, fontSize: 22, fontWeight: FontWeight.w700), @@ -253,7 +252,7 @@ class _DebtorDetailSheet extends ConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Outstanding Balance', style: AppTextStyles.bodySmall), + const Text('Outstanding Balance', style: AppTextStyles.bodySmall), Text( CurrencyUtils.format(customer.balance), style: AppTextStyles.headlineMedium.copyWith(color: color), @@ -264,7 +263,7 @@ class _DebtorDetailSheet extends ConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text('Credit Limit', style: AppTextStyles.bodySmall), + const Text('Credit Limit', style: AppTextStyles.bodySmall), Text( CurrencyUtils.format(customer.creditLimit), style: AppTextStyles.labelLarge, @@ -297,7 +296,7 @@ class _DebtorDetailSheet extends ConsumerWidget { const SizedBox(height: 20), // Payment History - Text('Payment History', style: AppTextStyles.titleMedium), + const Text('Payment History', style: AppTextStyles.titleMedium), const SizedBox(height: 8), historyAsync.when( loading: () => const Center(child: CircularProgressIndicator()), @@ -339,7 +338,7 @@ class _PaymentHistoryRow extends StatelessWidget { color: AppColors.success.withAlpha(20), borderRadius: BorderRadius.circular(8), ), - child: Icon(Icons.south_west_rounded, color: AppColors.success, size: 18), + child: const Icon(Icons.south_west_rounded, color: AppColors.success, size: 18), ), const SizedBox(width: 12), Expanded( @@ -439,7 +438,7 @@ class _RecordPaymentSheetState extends ConsumerState<_RecordPaymentSheet> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Record Payment', style: AppTextStyles.titleLarge), + const Text('Record Payment', style: AppTextStyles.titleLarge), Text(widget.customerName, style: AppTextStyles.bodySmall.copyWith(color: AppColors.textSecondary)), @@ -454,7 +453,7 @@ class _RecordPaymentSheetState extends ConsumerState<_RecordPaymentSheet> { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Outstanding', style: AppTextStyles.bodySmall), + const Text('Outstanding', style: AppTextStyles.bodySmall), Text(CurrencyUtils.format(widget.outstanding), style: AppTextStyles.labelLarge .copyWith(color: AppColors.error)), @@ -477,7 +476,7 @@ class _RecordPaymentSheetState extends ConsumerState<_RecordPaymentSheet> { ), const SizedBox(height: 12), DropdownButtonFormField( - value: _method, + initialValue: _method, decoration: const InputDecoration(labelText: 'Payment Method'), items: const [ DropdownMenuItem(value: 'cash', child: Text('Cash')), diff --git a/lib/features/grn/presentation/grn_screen.dart b/lib/features/grn/presentation/grn_screen.dart index 2f3a0a6..68fb336 100644 --- a/lib/features/grn/presentation/grn_screen.dart +++ b/lib/features/grn/presentation/grn_screen.dart @@ -1,7 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - import 'package:bms/core/theme/app_colors.dart'; import 'package:bms/core/theme/app_text_styles.dart'; import 'package:bms/core/utils/currency_utils.dart'; @@ -9,6 +5,9 @@ import 'package:bms/data/database/app_database.dart'; import 'package:bms/providers/grn_provider.dart'; import 'package:bms/providers/inventory_provider.dart'; import 'package:bms/providers/suppliers_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; class GrnScreen extends ConsumerStatefulWidget { const GrnScreen({super.key}); @@ -152,7 +151,7 @@ class _SupplierPickerSheetState extends ConsumerState<_SupplierPickerSheet> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Select Supplier', style: AppTextStyles.titleMedium), + const Text('Select Supplier', style: AppTextStyles.titleMedium), const SizedBox(height: 12), TextField( decoration: const InputDecoration( @@ -269,7 +268,7 @@ class _ProductPickerSheetState extends ConsumerState<_ProductPickerSheet> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Add Product', style: AppTextStyles.titleMedium), + const Text('Add Product', style: AppTextStyles.titleMedium), const SizedBox(height: 12), TextField( autofocus: true, @@ -504,9 +503,9 @@ class _GrnHistoryRow extends StatelessWidget { Widget build(BuildContext context) { final d = purchase.createdAt; return ListTile( - leading: CircleAvatar( + leading: const CircleAvatar( backgroundColor: AppColors.surfaceVariant, - child: const Icon(Icons.move_to_inbox_rounded, size: 18, color: AppColors.primary), + child: Icon(Icons.move_to_inbox_rounded, size: 18, color: AppColors.primary), ), title: Text(purchase.grnNumber ?? purchase.id, style: AppTextStyles.labelLarge), subtitle: Text( diff --git a/lib/features/inventory/presentation/inventory_screen.dart b/lib/features/inventory/presentation/inventory_screen.dart index 5054aec..2600098 100644 --- a/lib/features/inventory/presentation/inventory_screen.dart +++ b/lib/features/inventory/presentation/inventory_screen.dart @@ -1,12 +1,11 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/providers/inventory_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../data/database/app_database.dart'; -import '../../../providers/inventory_provider.dart'; - class InventoryScreen extends ConsumerStatefulWidget { const InventoryScreen({super.key}); @@ -225,7 +224,6 @@ class _ProductFormSheetState extends ConsumerState<_ProductFormSheet> { await actions.adjustStock( productId: widget.product!.id, newQty: newQty, - reason: 'manual adjustment', ); } } @@ -285,7 +283,7 @@ class _ProductFormSheetState extends ConsumerState<_ProductFormSheet> { children: [ Expanded( child: DropdownButtonFormField( - value: _unitType, + initialValue: _unitType, decoration: const InputDecoration(labelText: 'Unit Type *'), items: const [ DropdownMenuItem(value: 'pcs', child: Text('Pieces')), diff --git a/lib/features/invoices/presentation/invoice_detail_screen.dart b/lib/features/invoices/presentation/invoice_detail_screen.dart index 9be1d19..ee2959d 100644 --- a/lib/features/invoices/presentation/invoice_detail_screen.dart +++ b/lib/features/invoices/presentation/invoice_detail_screen.dart @@ -1,3 +1,13 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/features/invoices/presentation/invoice_pdf.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; +import 'package:bms/providers/inventory_provider.dart'; +import 'package:bms/providers/invoices_provider.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -5,17 +15,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:printing/printing.dart'; import 'package:uuid/uuid.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../data/database/app_database.dart'; -import '../../../features/auth/domain/auth_state.dart'; -import '../../../providers/auth_provider.dart'; -import '../../../providers/database_provider.dart'; -import '../../../providers/inventory_provider.dart'; -import '../../../providers/invoices_provider.dart'; -import 'invoice_pdf.dart'; - class InvoiceDetailScreen extends ConsumerWidget { const InvoiceDetailScreen({super.key, required this.invoiceId}); @@ -138,7 +137,7 @@ class InvoiceDetailScreen extends ConsumerWidget { useSafeArea: true, builder: (_) => _ProcessReturnSheet(detail: detail), ); - if (processed == true) { + if (processed ?? false) { ref.invalidate(invoiceReturnsProvider(detail.invoice.id)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -242,7 +241,7 @@ class _DetailBody extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Items', style: AppTextStyles.titleMedium), + const Text('Items', style: AppTextStyles.titleMedium), const SizedBox(height: 12), Table( columnWidths: const { @@ -252,8 +251,8 @@ class _DetailBody extends ConsumerWidget { 3: FixedColumnWidth(90), }, children: [ - TableRow( - decoration: const BoxDecoration( + const TableRow( + decoration: BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.border))), children: [ @@ -391,7 +390,7 @@ class _DetailBody extends ConsumerWidget { // Return history returnsAsync.when( loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), + error: (_, _) => const SizedBox.shrink(), data: (returns) { if (returns.isEmpty) return const SizedBox.shrink(); return Padding( @@ -585,7 +584,7 @@ class _ProcessReturnSheetState extends ConsumerState<_ProcessReturnSheet> { final returnId = uuid.v7(); // Use discounted unit price (subtotal / qty) so partial returns respect // any line-item discount the customer originally received. - final totalAmount = returnItems.fold(0.0, (s, e) { + final totalAmount = returnItems.fold(0, (s, e) { if (e.item.qty <= 0) return s; return s + (e.item.subtotal / e.item.qty) * e.qty; }); @@ -705,7 +704,7 @@ class _ProcessReturnSheetState extends ConsumerState<_ProcessReturnSheet> { padding: const EdgeInsets.all(20), children: [ // Items section - Text('Select Items to Return', + const Text('Select Items to Return', style: AppTextStyles.labelLarge), const SizedBox(height: 12), @@ -719,7 +718,7 @@ class _ProcessReturnSheetState extends ConsumerState<_ProcessReturnSheet> { const SizedBox(height: 20), // Return type - Text('Return Type', style: AppTextStyles.labelLarge), + const Text('Return Type', style: AppTextStyles.labelLarge), const SizedBox(height: 8), _ReturnTypeSelector( value: _type, @@ -729,7 +728,7 @@ class _ProcessReturnSheetState extends ConsumerState<_ProcessReturnSheet> { const SizedBox(height: 20), // Reason - Text('Reason', style: AppTextStyles.labelLarge), + const Text('Reason', style: AppTextStyles.labelLarge), const SizedBox(height: 8), TextFormField( controller: _reasonCtrl, @@ -752,7 +751,7 @@ class _ProcessReturnSheetState extends ConsumerState<_ProcessReturnSheet> { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Return Total', + const Text('Return Total', style: AppTextStyles.titleMedium), Text( CurrencyUtils.format(_totalReturnAmount), diff --git a/lib/features/invoices/presentation/invoice_pdf.dart b/lib/features/invoices/presentation/invoice_pdf.dart index 81c45db..dbc5c0a 100644 --- a/lib/features/invoices/presentation/invoice_pdf.dart +++ b/lib/features/invoices/presentation/invoice_pdf.dart @@ -1,11 +1,11 @@ +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/data/database/app_database.dart'; import 'package:intl/intl.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../data/database/app_database.dart'; - +// ignore: avoid_classes_with_only_static_members abstract final class InvoicePdf { static final _dateFmt = DateFormat('dd MMM yyyy'); static final _timeFmt = DateFormat('hh:mm a'); @@ -50,7 +50,7 @@ abstract final class InvoicePdf { doc.addPage( pw.Page( pageFormat: PdfPageFormat.a4, - margin: const pw.EdgeInsets.all(0), + margin: pw.EdgeInsets.zero, build: (ctx) => pw.Stack( children: [ pw.Column( @@ -179,9 +179,9 @@ abstract final class InvoicePdf { ) { return pw.Container( padding: const pw.EdgeInsets.all(16), - decoration: pw.BoxDecoration( + decoration: const pw.BoxDecoration( color: _brandLight, - borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)), + borderRadius: pw.BorderRadius.all(pw.Radius.circular(6)), ), child: pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, @@ -261,9 +261,9 @@ abstract final class InvoicePdf { 4: const pw.FixedColumnWidth(55), 5: const pw.FixedColumnWidth(70), }, - border: pw.TableBorder( - bottom: const pw.BorderSide(color: _border), - horizontalInside: const pw.BorderSide(color: _border, width: 0.5), + border: const pw.TableBorder( + bottom: pw.BorderSide(color: _border), + horizontalInside: pw.BorderSide(color: _border, width: 0.5), ), children: [ // Header row @@ -376,9 +376,9 @@ abstract final class InvoicePdf { children: [ pw.Container( padding: const pw.EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: pw.BoxDecoration( + decoration: const pw.BoxDecoration( color: _brandLight, - borderRadius: const pw.BorderRadius.all(pw.Radius.circular(4)), + borderRadius: pw.BorderRadius.all(pw.Radius.circular(4)), ), child: pw.Row( mainAxisSize: pw.MainAxisSize.min, diff --git a/lib/features/invoices/presentation/invoices_screen.dart b/lib/features/invoices/presentation/invoices_screen.dart index ece1f78..2c8ab90 100644 --- a/lib/features/invoices/presentation/invoices_screen.dart +++ b/lib/features/invoices/presentation/invoices_screen.dart @@ -1,14 +1,13 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/features/invoices/presentation/invoice_detail_screen.dart'; +import 'package:bms/providers/invoices_provider.dart'; +import 'package:bms/shared/widgets/bms_filter_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../providers/invoices_provider.dart'; -import '../../../shared/widgets/bms_filter_bar.dart'; -import 'invoice_detail_screen.dart'; - class InvoicesScreen extends ConsumerWidget { const InvoicesScreen({super.key}); @@ -16,7 +15,7 @@ class InvoicesScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar(title: const Text('Invoices')), - body: Column( + body: const Column( children: [ _FilterBar(), _SummaryBar(), @@ -56,7 +55,7 @@ class _FilterBarState extends ConsumerState<_FilterBar> { Widget build(BuildContext context) { final filter = ref.watch(invoiceFilterProvider); - return Container( + return ColoredBox( color: Colors.white, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -191,7 +190,7 @@ class _InvoiceList extends ConsumerWidget { return ListView.separated( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: rows.length, - separatorBuilder: (_, __) => + separatorBuilder: (_, _) => const Divider(height: 1, indent: 16, endIndent: 16), itemBuilder: (_, i) => _InvoiceTile(row: rows[i]), ); diff --git a/lib/features/petty_cash/presentation/petty_cash_screen.dart b/lib/features/petty_cash/presentation/petty_cash_screen.dart index 4df5a6b..b6ea5ff 100644 --- a/lib/features/petty_cash/presentation/petty_cash_screen.dart +++ b/lib/features/petty_cash/presentation/petty_cash_screen.dart @@ -1,9 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:image_picker/image_picker.dart'; - import 'package:bms/core/theme/app_colors.dart'; import 'package:bms/core/theme/app_text_styles.dart'; import 'package:bms/core/utils/currency_utils.dart'; @@ -11,6 +7,9 @@ import 'package:bms/core/utils/date_utils.dart'; import 'package:bms/data/database/app_database.dart'; import 'package:bms/providers/petty_cash_provider.dart'; import 'package:bms/shared/widgets/bms_filter_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; class PettyCashScreen extends ConsumerWidget { const PettyCashScreen({super.key}); @@ -97,7 +96,7 @@ class _FloatCard extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text('Balance', style: AppTextStyles.bodySmall), + const Text('Balance', style: AppTextStyles.bodySmall), Text( CurrencyUtils.format(net), style: AppTextStyles.titleMedium.copyWith( @@ -202,7 +201,7 @@ class _EntryRow extends ConsumerWidget { Image.file(file) else const Padding( - padding: EdgeInsets.all(24.0), + padding: EdgeInsets.all(24), child: Text('Receipt image not found'), ), TextButton( @@ -226,8 +225,8 @@ class _EntryRow extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.all(16), + const Padding( + padding: EdgeInsets.all(16), child: Text('Approve or Reject?', style: AppTextStyles.titleMedium), ), const Divider(height: 1), @@ -431,7 +430,7 @@ class _AddEntrySheetState extends ConsumerState<_AddEntrySheet> { const SizedBox(width: 12), Expanded( child: DropdownButtonFormField( - value: _type, + initialValue: _type, decoration: const InputDecoration(labelText: 'Type *'), items: const [ @@ -445,7 +444,7 @@ class _AddEntrySheetState extends ConsumerState<_AddEntrySheet> { ), const SizedBox(height: 12), DropdownButtonFormField( - value: _category, + initialValue: _category, decoration: const InputDecoration(labelText: 'Category *'), items: _categories diff --git a/lib/features/pos/presentation/pos_screen.dart b/lib/features/pos/presentation/pos_screen.dart index 122b2fd..f7f210e 100644 --- a/lib/features/pos/presentation/pos_screen.dart +++ b/lib/features/pos/presentation/pos_screen.dart @@ -1,20 +1,19 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/daos/inventory_dao.dart'; +import 'package:bms/features/pos/presentation/receipt_pdf.dart'; +import 'package:bms/providers/customers_provider.dart'; +import 'package:bms/providers/database_provider.dart'; +import 'package:bms/providers/inventory_provider.dart'; +import 'package:bms/providers/pos_provider.dart'; +import 'package:bms/providers/settings_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../data/database/app_database.dart'; -import '../../../providers/customers_provider.dart'; -import '../../../data/database/daos/inventory_dao.dart'; -import '../../../providers/database_provider.dart'; -import '../../../providers/inventory_provider.dart'; -import '../../../providers/pos_provider.dart'; -import '../../../providers/settings_provider.dart'; -import 'receipt_pdf.dart'; - class PosScreen extends ConsumerStatefulWidget { const PosScreen({super.key}); @@ -122,9 +121,9 @@ class _PosScreenState extends ConsumerState { ), const VerticalDivider(width: 1), // Right panel: cart - SizedBox( + const SizedBox( width: 380, - child: const _CartPanel(), + child: _CartPanel(), ), ], ), @@ -451,7 +450,7 @@ class _CartPanelState extends ConsumerState<_CartPanel> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Bill Discount', style: AppTextStyles.titleMedium), + const Text('Bill Discount', style: AppTextStyles.titleMedium), const SizedBox(height: 16), TextField( controller: controller, @@ -546,7 +545,7 @@ class _CartPanelState extends ConsumerState<_CartPanel> { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Cart', style: AppTextStyles.titleMedium), + const Text('Cart', style: AppTextStyles.titleMedium), Text('${state.items.length} item(s)', style: AppTextStyles.bodySmall), ], ), @@ -1073,9 +1072,9 @@ class _BarcodeScanDialogState extends State<_BarcodeScanDialog> { ..._buildCorners(), // Processing indicator if (_processing) - Container( + const ColoredBox( color: Colors.black45, - child: const Center( + child: Center( child: CircularProgressIndicator(color: Colors.white), ), ), @@ -1109,13 +1108,13 @@ class _BarcodeScanDialogState extends State<_BarcodeScanDialog> { const size = 24.0; const thickness = 3.5; const color = AppColors.primary; - Widget corner(double top, double left, bool flipH, bool flipV) => Positioned( + Widget corner(double top, double left, {required bool flipH, required bool flipV}) => Positioned( top: top, left: left, child: Transform.scale( scaleX: flipH ? -1 : 1, scaleY: flipV ? -1 : 1, - child: SizedBox( + child: const SizedBox( width: size, height: size, child: CustomPaint( @@ -1130,10 +1129,10 @@ class _BarcodeScanDialogState extends State<_BarcodeScanDialog> { const cy = (400 - 220) / 2; return [ - corner(cy - thickness, cx - thickness, false, false), - corner(cy - thickness, cx + 220 - size + thickness, true, false), - corner(cy + 220 - size + thickness, cx - thickness, false, true), - corner(cy + 220 - size + thickness, cx + 220 - size + thickness, true, true), + corner(cy - thickness, cx - thickness, flipH: false, flipV: false), + corner(cy - thickness, cx + 220 - size + thickness, flipH: true, flipV: false), + corner(cy + 220 - size + thickness, cx - thickness, flipH: false, flipV: true), + corner(cy + 220 - size + thickness, cx + 220 - size + thickness, flipH: true, flipV: true), ]; } } diff --git a/lib/features/pos/presentation/receipt_pdf.dart b/lib/features/pos/presentation/receipt_pdf.dart index f30adac..b1715e1 100644 --- a/lib/features/pos/presentation/receipt_pdf.dart +++ b/lib/features/pos/presentation/receipt_pdf.dart @@ -1,14 +1,12 @@ -import 'dart:typed_data'; - +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/providers/pos_provider.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; -import 'package:bms/providers/pos_provider.dart'; -import 'package:bms/data/database/app_database.dart'; - +// ignore: avoid_classes_with_only_static_members abstract final class ReceiptPdf { static final _dateFmt = DateFormat('dd/MM/yyyy HH:mm'); static final _priceFmt = NumberFormat('#,##0.00'); @@ -100,7 +98,7 @@ abstract final class ReceiptPdf { pw.Center( child: pw.SizedBox( height: 52, - child: pw.Image(headerImage, fit: pw.BoxFit.contain), + child: pw.Image(headerImage), ), ), pw.SizedBox(height: 3), diff --git a/lib/features/quick_sales/presentation/quick_sales_screen.dart b/lib/features/quick_sales/presentation/quick_sales_screen.dart index 4b3c204..b84b8e4 100644 --- a/lib/features/quick_sales/presentation/quick_sales_screen.dart +++ b/lib/features/quick_sales/presentation/quick_sales_screen.dart @@ -1,7 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - import 'package:bms/core/theme/app_colors.dart'; import 'package:bms/core/theme/app_text_styles.dart'; import 'package:bms/core/utils/currency_utils.dart'; @@ -9,6 +5,9 @@ import 'package:bms/data/database/app_database.dart'; import 'package:bms/providers/inventory_provider.dart'; import 'package:bms/providers/quick_sale_provider.dart'; import 'package:bms/shared/widgets/bms_filter_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; class QuickSalesScreen extends ConsumerWidget { const QuickSalesScreen({super.key}); @@ -121,9 +120,9 @@ class _SaleRow extends StatelessWidget { ? sale.qty.toStringAsFixed(0) : sale.qty.toStringAsFixed(1); return ListTile( - leading: CircleAvatar( + leading: const CircleAvatar( backgroundColor: AppColors.surfaceVariant, - child: const Icon(Icons.flash_on_rounded, size: 18, color: AppColors.primary), + child: Icon(Icons.flash_on_rounded, size: 18, color: AppColors.primary), ), title: Text(sale.productName, style: AppTextStyles.labelLarge), subtitle: Text( @@ -219,7 +218,7 @@ class _QuickSaleSheetState extends ConsumerState<_QuickSaleSheet> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('New Quick Sale', style: AppTextStyles.titleMedium), + const Text('New Quick Sale', style: AppTextStyles.titleMedium), const SizedBox(height: 16), if (_selected == null) ...[ @@ -276,7 +275,7 @@ class _QuickSaleSheetState extends ConsumerState<_QuickSaleSheet> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_selected!.name, style: AppTextStyles.labelLarge), - Text('Selected product', style: AppTextStyles.bodySmall), + const Text('Selected product', style: AppTextStyles.bodySmall), ], ), ), diff --git a/lib/features/reports/presentation/reports_screen.dart b/lib/features/reports/presentation/reports_screen.dart index ecdd5d9..056b993 100644 --- a/lib/features/reports/presentation/reports_screen.dart +++ b/lib/features/reports/presentation/reports_screen.dart @@ -1,17 +1,16 @@ import 'dart:convert'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:share_plus/share_plus.dart'; - import 'package:bms/core/theme/app_colors.dart'; import 'package:bms/core/theme/app_text_styles.dart'; import 'package:bms/core/utils/currency_utils.dart'; import 'package:bms/data/database/daos/reports_dao.dart'; import 'package:bms/providers/reports_provider.dart'; import 'package:bms/shared/widgets/bms_filter_bar.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:share_plus/share_plus.dart'; Future _shareCsv(String filename, String csv) async { await SharePlus.instance.share( @@ -96,8 +95,8 @@ class _PLTabState extends ConsumerState<_PLTab> { super.initState(); final now = DateTime.now(); _range = DateTimeRange( - start: DateTime(now.year, now.month, 1), - end: DateTime(now.year, now.month + 1, 1).subtract(const Duration(seconds: 1)), + start: DateTime(now.year, now.month), + end: DateTime(now.year, now.month + 1).subtract(const Duration(seconds: 1)), ); } @@ -123,7 +122,7 @@ class _PLTabState extends ConsumerState<_PLTab> { final revenue = daily.fold(0, (s, d) => s + d.revenue); if (revenue == 0) { - return Column( + return const Column( children: [ Expanded( child: _EmptyState( @@ -202,7 +201,7 @@ class _PLTabState extends ConsumerState<_PLTab> { ), ), const SizedBox(height: 12), - Text('Daily Revenue', style: AppTextStyles.titleMedium), + const Text('Daily Revenue', style: AppTextStyles.titleMedium), const SizedBox(height: 12), _PLChart(daily: daily), ], @@ -258,7 +257,6 @@ class _PLChart extends StatelessWidget { maxY: maxY * 1.2, barGroups: barGroups, gridData: FlGridData( - show: true, drawVerticalLine: false, getDrawingHorizontalLine: (_) => FlLine( color: AppColors.border.withValues(alpha: 0.6), @@ -281,9 +279,9 @@ class _PLChart extends StatelessWidget { ), titlesData: FlTitlesData( topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), + ), rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false)), + ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -338,7 +336,7 @@ class _StockTab extends ConsumerWidget { error: (e, _) => Center(child: Text('Error: $e')), data: (rows) { if (rows.isEmpty) { - return _EmptyState( + return const _EmptyState( icon: Icons.inventory_2_outlined, iconColor: AppColors.primary, title: 'No Stock on Hand', @@ -385,8 +383,8 @@ class _StockTab extends ConsumerWidget { ), ), const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.only(bottom: 8), + const Padding( + padding: EdgeInsets.only(bottom: 8), child: Row( children: [ Expanded( @@ -544,7 +542,7 @@ class _AgingTab extends ConsumerWidget { error: (e, _) => Center(child: Text('Error: $e')), data: (rows) { if (rows.isEmpty) { - return _EmptyState( + return const _EmptyState( icon: Icons.check_circle_outline, iconColor: AppColors.success, title: 'All Clear', @@ -554,7 +552,7 @@ class _AgingTab extends ConsumerWidget { } final total = rows.fold(0, (s, r) => s + r.balance); - final bucketAmounts = List.filled(4, 0.0); + final bucketAmounts = List.filled(4, 0); for (final r in rows) { bucketAmounts[r.agingBucket] += r.balance; } @@ -613,7 +611,7 @@ class _AgingTab extends ConsumerWidget { const SizedBox(height: 20), // Pie chart + legend - Text('Balance by Age', style: AppTextStyles.titleMedium), + const Text('Balance by Age', style: AppTextStyles.titleMedium), const SizedBox(height: 12), _AgingChart( bucketAmounts: bucketAmounts, @@ -629,7 +627,7 @@ class _AgingTab extends ConsumerWidget { const SizedBox(height: 24), // Debtor list - Text('Customers', style: AppTextStyles.titleMedium), + const Text('Customers', style: AppTextStyles.titleMedium), const SizedBox(height: 8), ...rows.map((r) => _DebtorRow(row: r, colors: _bucketColors, labels: _bucketLabels)), ], diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 52aaf1c..d330881 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -1,18 +1,16 @@ import 'dart:io'; -import 'dart:typed_data'; +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/settings_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; - -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../data/database/app_database.dart'; -import '../../../features/auth/domain/auth_state.dart'; -import '../../../providers/auth_provider.dart'; -import '../../../providers/settings_provider.dart'; +import 'package:path_provider/path_provider.dart'; class SettingsScreen extends ConsumerWidget { const SettingsScreen({super.key}); @@ -31,19 +29,19 @@ class SettingsScreen extends ConsumerWidget { children: [ // Store info (admin+) if (isAdmin) ...[ - _SectionHeader(title: 'Store Info', icon: Icons.store_outlined), + const _SectionHeader(title: 'Store Info', icon: Icons.store_outlined), const _StoreInfoTile(), const SizedBox(height: 24), ], // Language - _SectionHeader(title: 'Language', icon: Icons.language_outlined), + const _SectionHeader(title: 'Language', icon: Icons.language_outlined), const _LanguageTile(), const SizedBox(height: 24), // Products CSV import (admin+) if (isAdmin) ...[ - _SectionHeader(title: 'Products', icon: Icons.inventory_2_outlined), + const _SectionHeader(title: 'Products', icon: Icons.inventory_2_outlined), _ActionTile( icon: Icons.upload_file_outlined, title: 'Import Products from CSV', @@ -64,7 +62,7 @@ class SettingsScreen extends ConsumerWidget { // Database (developer only) if (isDev) ...[ - _SectionHeader(title: 'Database', icon: Icons.storage_outlined), + const _SectionHeader(title: 'Database', icon: Icons.storage_outlined), _ActionTile( icon: Icons.cloud_download_outlined, title: 'Export Database', @@ -85,7 +83,7 @@ class SettingsScreen extends ConsumerWidget { // Audit log (admin+) if (isAdmin) ...[ - _SectionHeader(title: 'Audit Log', icon: Icons.history_outlined), + const _SectionHeader(title: 'Audit Log', icon: Icons.history_outlined), _ActionTile( icon: Icons.list_alt_outlined, title: 'View Audit Log', @@ -99,7 +97,7 @@ class SettingsScreen extends ConsumerWidget { ], // App info (all roles) - _SectionHeader(title: 'About', icon: Icons.info_outline), + const _SectionHeader(title: 'About', icon: Icons.info_outline), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -109,9 +107,9 @@ class SettingsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('BMS - Business Manager', style: AppTextStyles.labelLarge), + const Text('BMS - Business Manager', style: AppTextStyles.labelLarge), const SizedBox(height: 4), - Text('Version 1.0.0', style: AppTextStyles.bodySmall), + const Text('Version 1.0.0', style: AppTextStyles.bodySmall), const SizedBox(height: 4), Text('Role: ${role.toUpperCase()}', style: AppTextStyles.bodySmall), ], @@ -348,25 +346,27 @@ class _LanguageTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final current = ref.watch(languageProvider); - return Container( + return DecoratedBox( decoration: BoxDecoration( color: AppColors.surfaceVariant, borderRadius: BorderRadius.circular(12), ), - child: Column( - children: supportedLanguages.map((lang) { - final (code, label) = lang; - return RadioListTile( - title: Text(label, style: AppTextStyles.bodyMedium), - value: code, - groupValue: current, - activeColor: AppColors.primary, - dense: true, - onChanged: (v) async { - if (v != null) await ref.read(languageProvider.notifier).set(v); - }, - ); - }).toList(), + child: RadioGroup( + groupValue: current, + onChanged: (v) async { + if (v != null) await ref.read(languageProvider.notifier).set(v); + }, + child: Column( + children: supportedLanguages.map((lang) { + final (code, label) = lang; + return RadioListTile( + title: Text(label, style: AppTextStyles.bodyMedium), + value: code, + activeColor: AppColors.primary, + dense: true, + ); + }).toList(), + ), ), ); } @@ -441,7 +441,7 @@ class _ActionTile extends StatelessWidget { ], ), ), - Icon(Icons.chevron_right, color: AppColors.textSecondary, size: 20), + const Icon(Icons.chevron_right, color: AppColors.textSecondary, size: 20), ], ), ), @@ -480,7 +480,7 @@ class _AuditLogScreenState extends ConsumerState<_AuditLogScreen> { tooltip: 'Filter by type', onSelected: (v) => setState(() => _filterType = v), itemBuilder: (_) => [ - const PopupMenuItem(value: null, child: Text('All')), + const PopupMenuItem(child: Text('All')), ..._types.map((t) => PopupMenuItem(value: t, child: Text(t))), ], ), diff --git a/lib/features/suppliers/presentation/suppliers_screen.dart b/lib/features/suppliers/presentation/suppliers_screen.dart index cb0d31a..5897757 100644 --- a/lib/features/suppliers/presentation/suppliers_screen.dart +++ b/lib/features/suppliers/presentation/suppliers_screen.dart @@ -1,13 +1,12 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/core/utils/currency_utils.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/providers/suppliers_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../core/utils/currency_utils.dart'; -import '../../../data/database/app_database.dart'; -import '../../../providers/suppliers_provider.dart'; - class SuppliersScreen extends ConsumerWidget { const SuppliersScreen({super.key}); @@ -159,7 +158,7 @@ class _SupplierDetailSheet extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Amount Payable', style: AppTextStyles.bodyMedium), + const Text('Amount Payable', style: AppTextStyles.bodyMedium), Text( CurrencyUtils.format(supplier.balance), style: AppTextStyles.titleMedium.copyWith(color: balanceColor), @@ -183,7 +182,7 @@ class _SupplierDetailSheet extends ConsumerWidget { }, ), const SizedBox(height: 20), - Text('Payment History', style: AppTextStyles.titleMedium), + const Text('Payment History', style: AppTextStyles.titleMedium), const SizedBox(height: 8), historyAsync.when( loading: () => const Center(child: CircularProgressIndicator()), @@ -223,7 +222,7 @@ class _SupplierPaymentRow extends StatelessWidget { color: AppColors.primary.withAlpha(15), borderRadius: BorderRadius.circular(8), ), - child: Icon(Icons.north_east_rounded, color: AppColors.primary, size: 18), + child: const Icon(Icons.north_east_rounded, color: AppColors.primary, size: 18), ), const SizedBox(width: 12), Expanded( @@ -424,7 +423,7 @@ class _SupplierPaymentSheetState extends ConsumerState<_SupplierPaymentSheet> { ), const SizedBox(height: 12), DropdownButtonFormField( - value: _method, + initialValue: _method, decoration: const InputDecoration(labelText: 'Payment Method'), items: const [ DropdownMenuItem(value: 'cash', child: Text('Cash')), diff --git a/lib/features/users/presentation/users_screen.dart b/lib/features/users/presentation/users_screen.dart index 03cc6b5..44d4ee1 100644 --- a/lib/features/users/presentation/users_screen.dart +++ b/lib/features/users/presentation/users_screen.dart @@ -1,13 +1,12 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/users_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/app_text_styles.dart'; -import '../../../data/database/app_database.dart'; -import '../../../features/auth/domain/auth_state.dart'; -import '../../../providers/auth_provider.dart'; -import '../../../providers/users_provider.dart'; - const _devUserId = '00000000-0000-0000-0000-000000000001'; class UsersScreen extends ConsumerWidget { @@ -354,7 +353,7 @@ class _AddUserSheetState extends ConsumerState<_AddUserSheet> { ), const SizedBox(height: 12), DropdownButtonFormField( - value: _role, + initialValue: _role, decoration: const InputDecoration(labelText: 'Role *'), items: [ const DropdownMenuItem(value: 'cashier', child: Text('Cashier')), @@ -461,7 +460,7 @@ class _EditUserSheetState extends ConsumerState<_EditUserSheet> { ), const SizedBox(height: 12), DropdownButtonFormField( - value: _role, + initialValue: _role, decoration: const InputDecoration(labelText: 'Role *'), items: [ const DropdownMenuItem(value: 'cashier', child: Text('Cashier')), diff --git a/lib/main.dart b/lib/main.dart index 3193c35..52568d1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,7 @@ +import 'package:bms/app.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'app.dart'; - void main() { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index b237ec2..1b8a673 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -1,11 +1,9 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:bms/data/repositories/auth_repository.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../data/repositories/auth_repository.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'database_provider.dart'; - part 'auth_provider.g.dart'; @Riverpod(keepAlive: true) diff --git a/lib/providers/cheques_provider.dart b/lib/providers/cheques_provider.dart index dddca3e..5905624 100644 --- a/lib/providers/cheques_provider.dart +++ b/lib/providers/cheques_provider.dart @@ -1,12 +1,11 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - // Manual providers - riverpod_generator cannot serialize Drift-generated types // in function signatures during the build phase, so we use the manual API. diff --git a/lib/providers/customers_provider.dart b/lib/providers/customers_provider.dart index dd9f6a3..3ddd2dc 100644 --- a/lib/providers/customers_provider.dart +++ b/lib/providers/customers_provider.dart @@ -1,12 +1,11 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - // Manual providers - avoids riverpod_generator's Drift type serialization issue. final customersStreamProvider = StreamProvider.autoDispose>( diff --git a/lib/providers/dashboard_provider.dart b/lib/providers/dashboard_provider.dart index 60152f1..e9d8872 100644 --- a/lib/providers/dashboard_provider.dart +++ b/lib/providers/dashboard_provider.dart @@ -49,7 +49,7 @@ class DashboardStats { final now = DateTime.now(); return salesTrend .where((d) => d.date.year == now.year && d.date.month == now.month) - .fold(0.0, (s, d) => s + d.grossProfit); + .fold(0, (s, d) => s + d.grossProfit); } double get mtdGrossMarginPct { @@ -70,8 +70,8 @@ Future dashboardStats(Ref ref) async { final todayStart = DateTime(now.year, now.month, now.day); final todayEnd = todayStart.add(const Duration(days: 1)); final thirtyDaysAgo = todayStart.subtract(const Duration(days: 29)); - final monthStart = DateTime(now.year, now.month, 1); - final lastMonthStart = DateTime(now.year, now.month - 1, 1); + final monthStart = DateTime(now.year, now.month); + final lastMonthStart = DateTime(now.year, now.month - 1); final (todayInvs, lowStock, debtors, cheques, recent, trend, thisMonth, lastMonth) = await ( @@ -126,5 +126,5 @@ Future todaySalesTotal(Ref ref) async { .getByDateRange(start, start.add(const Duration(days: 1))); return invoices .where((i) => i.status != 'void') - .fold(0.0, (sum, inv) => sum + inv.total); + .fold(0, (sum, inv) => sum + inv.total); } diff --git a/lib/providers/database_provider.dart b/lib/providers/database_provider.dart index 192f8d0..ac6c58f 100644 --- a/lib/providers/database_provider.dart +++ b/lib/providers/database_provider.dart @@ -1,18 +1,16 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/daos/audit_log_dao.dart'; +import 'package:bms/data/database/daos/cheques_dao.dart'; +import 'package:bms/data/database/daos/customers_dao.dart'; +import 'package:bms/data/database/daos/inventory_dao.dart'; +import 'package:bms/data/database/daos/invoices_dao.dart'; +import 'package:bms/data/database/daos/petty_cash_dao.dart'; +import 'package:bms/data/database/daos/reports_dao.dart'; +import 'package:bms/data/database/daos/returns_dao.dart'; +import 'package:bms/data/database/daos/suppliers_dao.dart'; +import 'package:bms/data/database/daos/users_dao.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../data/database/app_database.dart'; -import '../data/database/daos/audit_log_dao.dart'; -import '../data/database/daos/cheques_dao.dart'; -import '../data/database/daos/customers_dao.dart'; -import '../data/database/daos/inventory_dao.dart'; -import '../data/database/daos/invoices_dao.dart'; -import '../data/database/daos/petty_cash_dao.dart'; -import '../data/database/daos/reports_dao.dart'; -import '../data/database/daos/returns_dao.dart'; -import '../data/database/daos/suppliers_dao.dart'; -import '../data/database/daos/users_dao.dart'; - part 'database_provider.g.dart'; @Riverpod(keepAlive: true) diff --git a/lib/providers/grn_provider.dart b/lib/providers/grn_provider.dart index 75de9e9..41541b1 100644 --- a/lib/providers/grn_provider.dart +++ b/lib/providers/grn_provider.dart @@ -1,12 +1,11 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - final grnListProvider = FutureProvider.autoDispose>((ref) { return ref.watch(suppliersDaoProvider).getAllPurchases(); }); diff --git a/lib/providers/inventory_provider.dart b/lib/providers/inventory_provider.dart index d1d1b78..de61851 100644 --- a/lib/providers/inventory_provider.dart +++ b/lib/providers/inventory_provider.dart @@ -1,14 +1,13 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/data/database/daos/inventory_dao.dart'; +import 'package:bms/data/repositories/inventory_repository.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../data/database/daos/inventory_dao.dart'; -import '../data/repositories/inventory_repository.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - // Manual providers - avoids riverpod_generator's Drift type serialization issue. // inventoryRepository is keepAlive equivalent via Provider (never auto-disposed). diff --git a/lib/providers/invoices_provider.dart b/lib/providers/invoices_provider.dart index 7735fc5..c7861ac 100644 --- a/lib/providers/invoices_provider.dart +++ b/lib/providers/invoices_provider.dart @@ -1,11 +1,10 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - // Returns for a specific invoice final invoiceReturnsProvider = @@ -47,7 +46,7 @@ class InvoiceFilterNotifier extends Notifier { final now = DateTime.now(); return InvoiceFilter( dateRange: DateTimeRange( - start: DateTime(now.year, now.month, 1), + start: DateTime(now.year, now.month), end: now, ), ); @@ -140,13 +139,13 @@ final invoiceSummaryProvider = Provider.autoDispose((ref) { data: (rows) { final active = rows.where((r) => r.invoice.status != 'void').toList(); return ( - total: active.fold(0.0, (s, r) => s + r.invoice.total), - collected: active.fold(0.0, (s, r) => s + r.invoice.paidAmount), + total: active.fold(0, (s, r) => s + r.invoice.total), + collected: active.fold(0, (s, r) => s + r.invoice.paidAmount), count: active.length, ); }, loading: () => (total: 0.0, collected: 0.0, count: 0), - error: (_, __) => (total: 0.0, collected: 0.0, count: 0), + error: (_, _) => (total: 0.0, collected: 0.0, count: 0), ); }); diff --git a/lib/providers/notifications_provider.dart b/lib/providers/notifications_provider.dart index 3bae90e..905d6f8 100644 --- a/lib/providers/notifications_provider.dart +++ b/lib/providers/notifications_provider.dart @@ -1,8 +1,7 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../data/database/app_database.dart'; -import 'database_provider.dart'; - enum AlertType { chequeOverdue, chequeDue, lowStock, creditExceeded } class AppAlert { diff --git a/lib/providers/petty_cash_provider.dart b/lib/providers/petty_cash_provider.dart index 0846690..bbb7cbf 100644 --- a/lib/providers/petty_cash_provider.dart +++ b/lib/providers/petty_cash_provider.dart @@ -1,12 +1,11 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - typedef _DateRange = ({DateTime from, DateTime to}); // Manual NotifierProvider for date range - no codegen needed. @@ -20,7 +19,7 @@ class _PettyCashDateRangeNotifier extends Notifier<_DateRange> { @override _DateRange build() { final now = DateTime.now(); - return (from: DateTime(now.year, now.month, 1), to: now); + return (from: DateTime(now.year, now.month), to: now); } void set(DateTime from, DateTime to) => state = (from: from, to: to); diff --git a/lib/providers/pos_provider.dart b/lib/providers/pos_provider.dart index f71ac40..428c954 100644 --- a/lib/providers/pos_provider.dart +++ b/lib/providers/pos_provider.dart @@ -1,13 +1,11 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - part 'pos_provider.g.dart'; class CartItem { diff --git a/lib/providers/quick_sale_provider.dart b/lib/providers/quick_sale_provider.dart index 402aa80..1eb42a0 100644 --- a/lib/providers/quick_sale_provider.dart +++ b/lib/providers/quick_sale_provider.dart @@ -1,12 +1,11 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - typedef _DateRange = ({DateTime from, DateTime to}); final quickSaleDateRangeProvider = @@ -18,7 +17,7 @@ class _QuickSaleDateRangeNotifier extends Notifier<_DateRange> { @override _DateRange build() { final now = DateTime.now(); - return (from: DateTime(now.year, now.month, 1), to: now); + return (from: DateTime(now.year, now.month), to: now); } void set(DateTime from, DateTime to) => state = (from: from, to: to); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 28a1a4c..53df732 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -1,17 +1,15 @@ import 'dart:convert'; import 'dart:io'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - const _langKey = 'app_language'; const _storeNameKey = 'store_name'; @@ -271,7 +269,7 @@ class SettingsActions { final db = _db; // Products - for (final p in (payload['products'] as List? ?? [])) { + for (final Map p in ((payload['products'] as List?)?.cast>() ?? [])) { await db.into(db.products).insert( ProductsCompanion.insert( id: p['id'] as String, @@ -287,7 +285,7 @@ class SettingsActions { } // Customers - for (final c in (payload['customers'] as List? ?? [])) { + for (final Map c in ((payload['customers'] as List?)?.cast>() ?? [])) { await db.into(db.customers).insert( CustomersCompanion.insert( id: c['id'] as String, @@ -301,7 +299,7 @@ class SettingsActions { } // Stock - for (final s in (payload['stock'] as List? ?? [])) { + for (final Map s in ((payload['stock'] as List?)?.cast>() ?? [])) { await db.into(db.stock).insert( StockCompanion.insert( productId: s['product_id'] as String, diff --git a/lib/providers/suppliers_provider.dart b/lib/providers/suppliers_provider.dart index ed79e20..20df320 100644 --- a/lib/providers/suppliers_provider.dart +++ b/lib/providers/suppliers_provider.dart @@ -1,12 +1,11 @@ +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - // Manual providers - avoids riverpod_generator's Drift type serialization issue. final suppliersStreamProvider = StreamProvider.autoDispose>( diff --git a/lib/providers/users_provider.dart b/lib/providers/users_provider.dart index 450cc18..363e461 100644 --- a/lib/providers/users_provider.dart +++ b/lib/providers/users_provider.dart @@ -1,13 +1,12 @@ import 'package:bcrypt/bcrypt.dart'; +import 'package:bms/data/database/app_database.dart'; +import 'package:bms/features/auth/domain/auth_state.dart'; +import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/providers/database_provider.dart'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; -import '../data/database/app_database.dart'; -import '../features/auth/domain/auth_state.dart'; -import 'auth_provider.dart'; -import 'database_provider.dart'; - final usersStreamProvider = StreamProvider.autoDispose>( (ref) => ref.watch(usersDaoProvider).watchAll()); diff --git a/lib/shared/widgets/app_scaffold.dart b/lib/shared/widgets/app_scaffold.dart index 64b1a86..8a3b4d5 100644 --- a/lib/shared/widgets/app_scaffold.dart +++ b/lib/shared/widgets/app_scaffold.dart @@ -38,8 +38,8 @@ class _AppScaffoldState extends State { bottomNavigationBar: isWide ? null : _BottomNav(currentLocation: location), floatingActionButton: isWide ? null - : Padding( - padding: const EdgeInsets.only(bottom: 4), + : const Padding( + padding: EdgeInsets.only(bottom: 4), child: NotificationBell(iconColor: AppColors.primary), ), floatingActionButtonLocation: FloatingActionButtonLocation.miniEndTop, diff --git a/lib/shared/widgets/bms_error_widget.dart b/lib/shared/widgets/bms_error_widget.dart index 639e099..9498a65 100644 --- a/lib/shared/widgets/bms_error_widget.dart +++ b/lib/shared/widgets/bms_error_widget.dart @@ -1,8 +1,7 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; import 'package:flutter/material.dart'; -import '../../core/theme/app_colors.dart'; -import '../../core/theme/app_text_styles.dart'; - class BmsErrorWidget extends StatelessWidget { const BmsErrorWidget({ super.key, diff --git a/lib/shared/widgets/bms_filter_bar.dart b/lib/shared/widgets/bms_filter_bar.dart index b8f8b86..4c77470 100644 --- a/lib/shared/widgets/bms_filter_bar.dart +++ b/lib/shared/widgets/bms_filter_bar.dart @@ -1,8 +1,7 @@ +import 'package:bms/core/theme/app_text_styles.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:bms/core/theme/app_text_styles.dart'; - /// A date range field that sizes identically to TextField (uses readOnly TextField internally). class BmsDateRangeField extends StatelessWidget { const BmsDateRangeField({ diff --git a/lib/shared/widgets/confirmation_dialog.dart b/lib/shared/widgets/confirmation_dialog.dart index 8e8f7d6..0abc24a 100644 --- a/lib/shared/widgets/confirmation_dialog.dart +++ b/lib/shared/widgets/confirmation_dialog.dart @@ -1,7 +1,6 @@ +import 'package:bms/core/theme/app_colors.dart'; import 'package:flutter/material.dart'; -import '../../core/theme/app_colors.dart'; - class ConfirmationDialog extends StatelessWidget { const ConfirmationDialog({ super.key, diff --git a/lib/shared/widgets/notification_bell.dart b/lib/shared/widgets/notification_bell.dart index 372b251..fc8442f 100644 --- a/lib/shared/widgets/notification_bell.dart +++ b/lib/shared/widgets/notification_bell.dart @@ -1,10 +1,9 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; +import 'package:bms/providers/notifications_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../core/theme/app_colors.dart'; -import '../../core/theme/app_text_styles.dart'; -import '../../providers/notifications_provider.dart'; - class NotificationBell extends ConsumerWidget { const NotificationBell({super.key, this.iconColor = Colors.white}); @@ -16,7 +15,7 @@ class NotificationBell extends ConsumerWidget { final count = alertsAsync.when( data: (alerts) => alerts.length, loading: () => 0, - error: (_, __) => 0, + error: (_, _) => 0, ); return Stack( @@ -72,7 +71,6 @@ class _AlertsPanel extends ConsumerWidget { final alertsAsync = ref.watch(notificationsProvider); return DraggableScrollableSheet( - initialChildSize: 0.5, minChildSize: 0.3, maxChildSize: 0.9, expand: false, @@ -90,10 +88,10 @@ class _AlertsPanel extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.check_circle_outline, + const Icon(Icons.check_circle_outline, size: 48, color: AppColors.success), const SizedBox(height: 12), - Text('All clear', style: AppTextStyles.titleMedium), + const Text('All clear', style: AppTextStyles.titleMedium), const SizedBox(height: 4), Text('No alerts right now.', style: AppTextStyles.bodySmall.copyWith( @@ -104,7 +102,7 @@ class _AlertsPanel extends ConsumerWidget { : ListView.separated( controller: scrollController, itemCount: alerts.length, - separatorBuilder: (_, __) => + separatorBuilder: (_, _) => const Divider(height: 1, indent: 16, endIndent: 16), itemBuilder: (_, i) => _AlertTile(alert: alerts[i]), ), @@ -142,7 +140,7 @@ class _PanelHandle extends StatelessWidget { ), ), ), - Text('Alerts', style: AppTextStyles.titleLarge), + const Text('Alerts', style: AppTextStyles.titleLarge), ], ), ), diff --git a/lib/shared/widgets/sidebar_nav.dart b/lib/shared/widgets/sidebar_nav.dart index be13de3..5c98309 100644 --- a/lib/shared/widgets/sidebar_nav.dart +++ b/lib/shared/widgets/sidebar_nav.dart @@ -1,14 +1,13 @@ import 'package:bms/core/router/app_router.dart'; import 'package:bms/data/models/user_model.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:bms/features/auth/domain/auth_state.dart'; import 'package:bms/providers/auth_provider.dart'; +import 'package:bms/shared/widgets/notification_bell.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'notification_bell.dart'; - const _kSidebarBg = Color(0xFF111827); const _kSidebarHover = Color(0xFF1F2937); const _kSidebarActive = Color(0xFF1D4ED8); @@ -141,10 +140,10 @@ class _Header extends StatelessWidget { children: [ _logoMark, const SizedBox(width: 10), - Expanded( + const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Text('BMS', style: TextStyle( color: Colors.white, @@ -158,7 +157,7 @@ class _Header extends StatelessWidget { ], ), ), - NotificationBell(iconColor: _kSidebarText), + const NotificationBell(iconColor: _kSidebarText), ], ), ); @@ -391,7 +390,7 @@ class _SectionLabel extends StatelessWidget { color: Color(0xFF4B5563), fontSize: 10, fontWeight: FontWeight.w700, - letterSpacing: 1.0, + letterSpacing: 1, ), ), ); diff --git a/lib/shared/widgets/stat_card.dart b/lib/shared/widgets/stat_card.dart index fe88eb2..935d38c 100644 --- a/lib/shared/widgets/stat_card.dart +++ b/lib/shared/widgets/stat_card.dart @@ -1,8 +1,7 @@ +import 'package:bms/core/theme/app_colors.dart'; +import 'package:bms/core/theme/app_text_styles.dart'; import 'package:flutter/material.dart'; -import '../../core/theme/app_colors.dart'; -import '../../core/theme/app_text_styles.dart'; - class StatCard extends StatelessWidget { const StatCard({ super.key, diff --git a/tool/preview_receipt.dart b/tool/preview_receipt.dart index 7a6007b..83fcfd2 100644 --- a/tool/preview_receipt.dart +++ b/tool/preview_receipt.dart @@ -21,11 +21,11 @@ void main() async { const change = 550.00; final items = [ - _Item('Anchor Milk Powder 400g', 2, 750.00), - _Item('Milo Tin 400g', 1, 890.00), - _Item('Sunlight Soap 100g', 3, 85.00), - _Item('Dettol 250ml', 1, 460.00), - _Item('Panadol 10 Tab', 2, 55.00), + const _Item('Anchor Milk Powder 400g', 2, 750), + const _Item('Milo Tin 400g', 1, 890), + const _Item('Sunlight Soap 100g', 3, 85), + const _Item('Dettol 250ml', 1, 460), + const _Item('Panadol 10 Tab', 2, 55), ]; // ── Build PDF ──────────────────────────────────────────────────────────────── @@ -53,7 +53,7 @@ void main() async { pw.Center( child: pw.SizedBox( height: 52, - child: pw.Image(headerImage, fit: pw.BoxFit.contain), + child: pw.Image(headerImage), ), ), pw.SizedBox(height: 3), @@ -139,6 +139,7 @@ void main() async { final bytes = await doc.save(); final outPath = '${Platform.environment['HOME']}/Downloads/receipt_preview.pdf'; await File(outPath).writeAsBytes(bytes); + // ignore: avoid_print print('Saved: $outPath'); } From 253feec11cb85ac0fb1b6aa7cd5ae4cde72570dd Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 00:07:36 +0530 Subject: [PATCH 03/13] ci: add lint workflow with --fatal-infos --- .github/workflows/lint.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..40ad158 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: Lint + +on: + push: + branches: [master, feat/**] + pull_request: + branches: [master] + +permissions: + contents: read + +concurrency: + group: lint-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: flutter analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4.2.2 + with: + persist-credentials: false + + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # subosito/flutter-action@v2 + with: + flutter-version: '3.44.2' + channel: stable + cache: true + + - run: flutter pub get + + - run: dart run build_runner build --delete-conflicting-outputs + + - run: flutter analyze --fatal-infos --fatal-warnings From 613d8373e55129bd9a7cda3c45f48c7006b7ca1d Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 00:13:54 +0530 Subject: [PATCH 04/13] fix: align save button and update language tile to use Material widget --- .../presentation/settings_screen.dart | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index d330881..e0a5b20 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -321,16 +321,19 @@ class _StoreInfoTileState extends ConsumerState<_StoreInfoTile> { keyboardType: TextInputType.phone, ), const SizedBox(height: 16), - ElevatedButton.icon( - icon: _saving - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), - ) - : const Icon(Icons.save_outlined, size: 18), - label: Text(_saving ? 'Saving...' : 'Save Store Info'), - onPressed: _saving ? null : _save, + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + icon: _saving + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.save_outlined, size: 18), + label: Text(_saving ? 'Saving...' : 'Save Store Info'), + onPressed: _saving ? null : _save, + ), ), ], ), @@ -346,11 +349,9 @@ class _LanguageTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final current = ref.watch(languageProvider); - return DecoratedBox( - decoration: BoxDecoration( - color: AppColors.surfaceVariant, - borderRadius: BorderRadius.circular(12), - ), + return Material( + color: AppColors.surfaceVariant, + borderRadius: BorderRadius.circular(12), child: RadioGroup( groupValue: current, onChanged: (v) async { From 20be724bc927885681d311ddc447128eec9087a9 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 00:16:07 +0530 Subject: [PATCH 05/13] fix: update MTD Performance Card color to improve visibility --- lib/features/dashboard/presentation/dashboard_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/dashboard/presentation/dashboard_screen.dart b/lib/features/dashboard/presentation/dashboard_screen.dart index 5803dd5..deb1555 100644 --- a/lib/features/dashboard/presentation/dashboard_screen.dart +++ b/lib/features/dashboard/presentation/dashboard_screen.dart @@ -290,7 +290,7 @@ class _MtdPerformanceCard extends StatelessWidget { _MtdMetric( label: 'Gross Profit', value: CurrencyUtils.format(s.mtdGrossProfit), - color: const Color(0xFF69F0AE), + color: Colors.white, ), const SizedBox(width: 24), _MtdMetric( From 0b399ef96f8df588e27a47356748b628ffe235ea Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 00:17:15 +0530 Subject: [PATCH 06/13] docs: update changelog for phase 5 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cde5ca..14da55a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +## [Phase 5] - 2026-06-18 + +### Added +- Debtors screen: tap any debtor to open a detail sheet showing outstanding balance, credit limit, payment history, and a "Record Payment" button +- Customer payment recording: amount, payment method (cash/card/bank transfer/cheque), and optional notes; updates balance immediately +- Supplier detail sheet: payment history section listing past payments with method, notes, amount, and date +- In-app notification bell in sidebar header (desktop) and floating top-right (mobile) with red badge count +- Alerts panel: overdue cheques, cheques due within 7 days, low-stock products, and customers exceeding credit limit -- pull-to-refresh supported +- CSV export on all three Reports tabs: P&L (date, revenue, COGS, gross profit, margin), Stock Valuation (product, qty, unit cost, total value), Debtor Aging (customer, balance, bucket) -- shares via system share sheet +- Responsive dashboard KPI grid: 2 columns on phones (<480px), 3 on tablets (<840px), 4 on desktop (>=840px) +- Lint CI workflow running `flutter analyze --fatal-infos --fatal-warnings` on every push and PR + +### Changed +- Gross Profit value on MTD Performance card changed from mint green to white for consistency with the blue card background +- Save Store Info button in Settings moved to bottom-right of the Store Info section +- Language selector in Settings converted from `DecoratedBox` to `Material` to prevent invisible ink-splash assertion +- All relative imports in `lib/` converted to `package:bms/` URIs + +### Fixed +- 446 lint warnings resolved: package imports, `prefer_const_constructors`, `avoid_redundant_argument_values`, `prefer_int_literals`, `directives_ordering`, `unnecessary_underscores`, `avoid_dynamic_calls`, deprecated `Radio.groupValue`/`onChanged` (migrated to `RadioGroup`), positional boolean parameters, and type errors introduced by over-aggressive int-literal substitution + ### Added - Login screen BMS SVG logo from `assets/images/bms_logo.svg` with dynamic copyright footer (`DateTime.now().year`) - POS fractional quantity support for weight/volume unit types (`kg`, `g`, `l`, `ml`) - product card tap opens qty dialog with decimal input, stepper uses unit-appropriate increments (0.25 for kg/l, 50 for g/ml), cart displays formatted decimal quantities From 89417df88c05035edfa14eaa52db18b5b1cbca32 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 00:34:23 +0530 Subject: [PATCH 07/13] docs: remove Phase 5 release date from changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14da55a..777e74c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] -## [Phase 5] - 2026-06-18 - ### Added - Debtors screen: tap any debtor to open a detail sheet showing outstanding balance, credit limit, payment history, and a "Record Payment" button - Customer payment recording: amount, payment method (cash/card/bank transfer/cheque), and optional notes; updates balance immediately From 36d026ac91de5b851b5f79a1c65b91e5efc3fa81 Mon Sep 17 00:00:00 2001 From: Virul Nirmala Wickramasinghe <89099391+iamvirul@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:35:40 +0530 Subject: [PATCH 08/13] Update lib/features/debtors/presentation/debtors_screen.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/features/debtors/presentation/debtors_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/debtors/presentation/debtors_screen.dart b/lib/features/debtors/presentation/debtors_screen.dart index 69ad2cb..a46f40f 100644 --- a/lib/features/debtors/presentation/debtors_screen.dart +++ b/lib/features/debtors/presentation/debtors_screen.dart @@ -217,7 +217,7 @@ class _DebtorDetailSheet extends ConsumerWidget { radius: 28, backgroundColor: AppColors.primary.withAlpha(20), child: Text( - customer.name[0].toUpperCase(), + customer.name.isNotEmpty ? customer.name[0].toUpperCase() : '?', style: const TextStyle( color: AppColors.primary, fontSize: 22, From 88f957ea25be574831d77b16ba293f1cc0670a77 Mon Sep 17 00:00:00 2001 From: Virul Nirmala Wickramasinghe <89099391+iamvirul@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:35:55 +0530 Subject: [PATCH 09/13] Update lib/features/reports/presentation/reports_screen.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/features/reports/presentation/reports_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/reports/presentation/reports_screen.dart b/lib/features/reports/presentation/reports_screen.dart index 056b993..9417036 100644 --- a/lib/features/reports/presentation/reports_screen.dart +++ b/lib/features/reports/presentation/reports_screen.dart @@ -96,7 +96,7 @@ class _PLTabState extends ConsumerState<_PLTab> { final now = DateTime.now(); _range = DateTimeRange( start: DateTime(now.year, now.month), - end: DateTime(now.year, now.month + 1).subtract(const Duration(seconds: 1)), + end: DateTime(now.year, now.month + 1).subtract(const Duration(microseconds: 1)), ); } From 2c9c9954bfb601eebb2e3fa13dce72370ed4edd0 Mon Sep 17 00:00:00 2001 From: Virul Nirmala Wickramasinghe <89099391+iamvirul@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:37:05 +0530 Subject: [PATCH 10/13] Update lib/features/pos/presentation/receipt_pdf.dart Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/features/pos/presentation/receipt_pdf.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/features/pos/presentation/receipt_pdf.dart b/lib/features/pos/presentation/receipt_pdf.dart index b1715e1..fb598c4 100644 --- a/lib/features/pos/presentation/receipt_pdf.dart +++ b/lib/features/pos/presentation/receipt_pdf.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:bms/data/database/app_database.dart'; import 'package:bms/providers/pos_provider.dart'; import 'package:flutter/services.dart'; From d4eb5380ca12a8efb5f3934240633b305d5513bd Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 00:42:11 +0530 Subject: [PATCH 11/13] refactor: remove unused import from receipt_pdf.dart --- lib/features/pos/presentation/receipt_pdf.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/features/pos/presentation/receipt_pdf.dart b/lib/features/pos/presentation/receipt_pdf.dart index fb598c4..b1715e1 100644 --- a/lib/features/pos/presentation/receipt_pdf.dart +++ b/lib/features/pos/presentation/receipt_pdf.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:bms/data/database/app_database.dart'; import 'package:bms/providers/pos_provider.dart'; import 'package:flutter/services.dart'; From f5c534a981648b57f512c589deb583f9006e7e75 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 04:17:43 +0530 Subject: [PATCH 12/13] fix: update bundle identifier and copyright information across project files --- macos/Runner.xcodeproj/project.pbxproj | 6 +++--- macos/Runner/Configs/AppInfo.xcconfig | 4 ++-- windows/runner/Runner.rc | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 6970368..c3a8aa0 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -395,7 +395,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.bms.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = lk.getbms.bms.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bms.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bms"; @@ -409,7 +409,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.bms.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = lk.getbms.bms.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bms.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bms"; @@ -423,7 +423,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.bms.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = lk.getbms.bms.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bms.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/bms"; diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 380d008..9bc733f 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = bms // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.bms +PRODUCT_BUNDLE_IDENTIFIER = lk.getbms.bms // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2026 lk.getbms. All rights reserved. diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index b42f4ef..31ca5c3 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -89,11 +89,11 @@ BEGIN BEGIN BLOCK "040904e4" BEGIN - VALUE "CompanyName", "com.example" "\0" + VALUE "CompanyName", "lk.getbms" "\0" VALUE "FileDescription", "bms" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "bms" "\0" - VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 lk.getbms. All rights reserved." "\0" VALUE "OriginalFilename", "bms.exe" "\0" VALUE "ProductName", "bms" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" From 395b93315e785134f9437d599d3363f029546851 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Thu, 18 Jun 2026 14:55:29 +0530 Subject: [PATCH 13/13] refactor: update input decoration styles and enhance CSV export functionality --- lib/core/theme/app_theme.dart | 12 +-- .../reports/presentation/reports_screen.dart | 97 ++++++++++++------- 2 files changed, 67 insertions(+), 42 deletions(-) diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index ea5d6b8..9f0a71d 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -52,8 +52,8 @@ abstract final class AppTheme { inputDecorationTheme: InputDecorationTheme( isDense: true, filled: true, - fillColor: AppColors.surface, - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + fillColor: AppColors.surfaceVariant, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppColors.border), @@ -70,23 +70,21 @@ abstract final class AppTheme { borderRadius: BorderRadius.circular(8), borderSide: const BorderSide(color: AppColors.error), ), - // Resting label inside the field (13sp keeps it proportional to dense inputs) labelStyle: const TextStyle( fontFamily: 'Inter', - fontSize: 13, + fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.textSecondary, ), - // Floating label above the field when focused/filled floatingLabelStyle: const TextStyle( fontFamily: 'Inter', - fontSize: 11, + fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.borderFocus, ), hintStyle: const TextStyle( fontFamily: 'Inter', - fontSize: 13, + fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.textDisabled, ), diff --git a/lib/features/reports/presentation/reports_screen.dart b/lib/features/reports/presentation/reports_screen.dart index 9417036..487c34f 100644 --- a/lib/features/reports/presentation/reports_screen.dart +++ b/lib/features/reports/presentation/reports_screen.dart @@ -26,6 +26,12 @@ Future _shareCsv(String filename, String csv) async { ); } +String _csvField(String value) { + final escaped = value.replaceAll('"', '""'); + final safe = RegExp(r'^[=+\-@]').hasMatch(escaped) ? '\t$escaped' : escaped; + return '"$safe"'; +} + class ReportsScreen extends ConsumerStatefulWidget { const ReportsScreen({super.key}); @@ -183,20 +189,27 @@ class _PLTabState extends ConsumerState<_PLTab> { textStyle: AppTextStyles.bodySmall, visualDensity: VisualDensity.compact, ), - onPressed: () { - final df = DateFormat('yyyy-MM-dd'); - final lines = [ - 'Date,Revenue,COGS,Gross Profit,Margin %', - ...daily.map((d) { - final gp = d.revenue - d.cogs; - final m = d.revenue > 0 ? gp / d.revenue * 100 : 0; - return '${df.format(d.date)},${d.revenue.toStringAsFixed(2)},${d.cogs.toStringAsFixed(2)},${gp.toStringAsFixed(2)},${m.toStringAsFixed(2)}'; - }), - ]; - _shareCsv( - 'pl_${df.format(_range.start)}_${df.format(_range.end)}.csv', - lines.join('\n'), - ); + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + final df = DateFormat('yyyy-MM-dd'); + final lines = [ + 'Date,Revenue,COGS,Gross Profit,Margin %', + ...daily.map((d) { + final gp = d.revenue - d.cogs; + final m = d.revenue > 0 ? gp / d.revenue * 100 : 0; + return '${df.format(d.date)},${d.revenue.toStringAsFixed(2)},${d.cogs.toStringAsFixed(2)},${gp.toStringAsFixed(2)},${m.toStringAsFixed(2)}'; + }), + ]; + await _shareCsv( + 'pl_${df.format(_range.start)}_${df.format(_range.end)}.csv', + lines.join('\n'), + ); + } catch (_) { + messenger.showSnackBar( + const SnackBar(content: Text('Export failed. Please try again.')), + ); + } }, ), ), @@ -369,16 +382,23 @@ class _StockTab extends ConsumerWidget { textStyle: AppTextStyles.bodySmall, visualDensity: VisualDensity.compact, ), - onPressed: () { - final lines = [ - 'Product,Qty,Unit Cost,Total Value', - ...rows.map((r) => - '"${r.name}",${r.qty.toStringAsFixed(2)},${r.costPrice.toStringAsFixed(2)},${r.value.toStringAsFixed(2)}'), - ]; - _shareCsv( - 'stock_valuation_${DateFormat('yyyy-MM-dd').format(DateTime.now())}.csv', - lines.join('\n'), - ); + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + final lines = [ + 'Product,Qty,Unit Cost,Total Value', + ...rows.map((r) => + '${_csvField(r.name)},${r.qty.toStringAsFixed(2)},${r.costPrice.toStringAsFixed(2)},${r.value.toStringAsFixed(2)}'), + ]; + await _shareCsv( + 'stock_valuation_${DateFormat('yyyy-MM-dd').format(DateTime.now())}.csv', + lines.join('\n'), + ); + } catch (_) { + messenger.showSnackBar( + const SnackBar(content: Text('Export failed. Please try again.')), + ); + } }, ), ), @@ -591,17 +611,24 @@ class _AgingTab extends ConsumerWidget { textStyle: AppTextStyles.bodySmall, visualDensity: VisualDensity.compact, ), - onPressed: () { - const buckets = ['0-30d', '31-60d', '61-90d', '90+d']; - final lines = [ - 'Customer,Balance,Aging Bucket', - ...rows.map((r) => - '"${r.name}",${r.balance.toStringAsFixed(2)},${buckets[r.agingBucket.clamp(0, 3)]}'), - ]; - _shareCsv( - 'debtor_aging_${DateFormat('yyyy-MM-dd').format(DateTime.now())}.csv', - lines.join('\n'), - ); + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + const buckets = ['0-30d', '31-60d', '61-90d', '90+d']; + final lines = [ + 'Customer,Balance,Aging Bucket', + ...rows.map((r) => + '${_csvField(r.name)},${r.balance.toStringAsFixed(2)},${buckets[r.agingBucket.clamp(0, 3)]}'), + ]; + await _shareCsv( + 'debtor_aging_${DateFormat('yyyy-MM-dd').format(DateTime.now())}.csv', + lines.join('\n'), + ); + } catch (_) { + messenger.showSnackBar( + const SnackBar(content: Text('Export failed. Please try again.')), + ); + } }, ), ],