Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### 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
Expand Down
5 changes: 2 additions & 3 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -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});

Expand Down
8 changes: 4 additions & 4 deletions lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ abstract final class AppConstants {
static const List<int> 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;
}
41 changes: 19 additions & 22 deletions lib/core/router/app_router.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions lib/core/router/route_guard.dart
Original file line number Diff line number Diff line change
@@ -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<String> _publicRoutes = {AppRoutes.login};

Expand Down
2 changes: 1 addition & 1 deletion lib/core/theme/app_text_styles.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
20 changes: 8 additions & 12 deletions lib/core/theme/app_theme.dart
Original file line number Diff line number Diff line change
@@ -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,
);
Expand Down Expand Up @@ -54,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),
Expand All @@ -72,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,
),
Expand Down
1 change: 1 addition & 0 deletions lib/core/utils/currency_utils.dart
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
1 change: 1 addition & 0 deletions lib/core/utils/date_utils.dart
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
4 changes: 0 additions & 4 deletions lib/core/utils/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
39 changes: 19 additions & 20 deletions lib/data/database/app_database.dart
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
5 changes: 2 additions & 3 deletions lib/data/database/daos/audit_log_dao.dart
Original file line number Diff line number Diff line change
@@ -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])
Expand Down
17 changes: 14 additions & 3 deletions lib/data/database/daos/cheques_dao.dart
Original file line number Diff line number Diff line change
@@ -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])
Expand Down Expand Up @@ -37,6 +36,18 @@ class ChequesDao extends DatabaseAccessor<AppDatabase> with _$ChequesDaoMixin {
.get();
}

Future<List<Cheque>> 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<void> updateStatus(String id, String status) =>
(update(cheques)..where((c) => c.id.equals(id))).write(
ChequesCompanion(
Expand Down
7 changes: 3 additions & 4 deletions lib/data/database/daos/customers_dao.dart
Original file line number Diff line number Diff line change
@@ -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])
Expand Down
17 changes: 13 additions & 4 deletions lib/data/database/daos/inventory_dao.dart
Original file line number Diff line number Diff line change
@@ -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])
Expand Down Expand Up @@ -49,12 +48,22 @@ class InventoryDao extends DatabaseAccessor<AppDatabase> with _$InventoryDaoMixi
]);
// Compare qty (real) against reorderLevel (int) via expression cast
query.where(stock.qty.isSmallerOrEqualValue(0) |
CustomExpression<bool>('stock.qty <= products.reorder_level'));
const CustomExpression<bool>('stock.qty <= products.reorder_level'));
return query.watch().map(
(rows) => rows.map((r) => r.readTable(stock)).toList(),
);
}

Future<List<Product>> getLowStockProducts() async {
final query = select(stock).join([
innerJoin(products, products.id.equalsExp(stock.productId)),
]);
query.where(stock.qty.isSmallerOrEqualValue(0) |
const CustomExpression<bool>('stock.qty <= products.reorder_level'));
final rows = await query.get();
return rows.map((r) => r.readTable(products)).toList();
}

Future<void> upsertStock(StockCompanion entry) =>
into(stock).insertOnConflictUpdate(entry);

Expand Down
Loading
Loading