import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app/providers.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/utils/formatters.dart'; import '../../../domain/entities/store_account.dart'; import '../../auth/providers/auth_controller.dart'; import '../providers/printer_settings.dart'; import '../../sync/providers/sync_controller.dart'; import '../widgets/module_widgets.dart'; /// Terminal and store configuration. class SettingsView extends ConsumerStatefulWidget { const SettingsView({super.key}); @override ConsumerState createState() => _SettingsViewState(); } class _SettingsViewState extends ConsumerState { bool _scannerSound = true; bool _roundOff = true; bool _autoLoyalty = true; @override Widget build(BuildContext context) { final store = ref.watch(currentStoreProvider); final user = ref.watch(currentUserProvider); return ModulePage( children: [ LayoutBuilder( builder: (context, constraints) { final wide = constraints.maxWidth >= 1040; final left = Column( children: [ _storeCard(store), const SizedBox(height: AppSpacing.lg), _taxCard(), const SizedBox(height: AppSpacing.lg), _loyaltyCard(), ], ); final right = Column( children: [ _hardwareCard(), const SizedBox(height: AppSpacing.lg), _connectivityCard(), const SizedBox(height: AppSpacing.lg), _staffCard(store, user), const SizedBox(height: AppSpacing.lg), _aboutCard(), ], ); if (!wide) { return Column( children: [left, const SizedBox(height: AppSpacing.lg), right], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: left), const SizedBox(width: AppSpacing.lg), Expanded(child: right), ], ); }, ), ], ); } Widget _storeCard(StoreAccount? store) => PanelCard( title: 'Store details', subtitle: 'Printed on every invoice', action: TextButton(onPressed: () {}, child: const Text('Edit')), child: Column( mainAxisSize: MainAxisSize.min, children: [ _row('Store name', store?.name ?? AppConstants.storeName), _row('Address', store?.address ?? AppConstants.storeAddress), _row('GSTIN', store?.gstin ?? AppConstants.storeGstin, mono: true), _row('Phone', store?.phone ?? AppConstants.storePhone, mono: true), _row('Plan', store?.plan ?? 'Business'), ], ), ); Widget _taxCard() => PanelCard( title: 'Tax & pricing', child: Column( mainAxisSize: MainAxisSize.min, children: [ _row( 'Default GST slab', Formatters.percent(AppConstants.defaultGstRate), ), _row('Prices include tax', 'Yes'), _toggle( 'Round bills to nearest rupee', 'Shows the adjustment as a Round Off line', _roundOff, (v) => setState(() => _roundOff = v), ), ], ), ); Widget _loyaltyCard() => PanelCard( title: 'Loyalty programme', child: Column( mainAxisSize: MainAxisSize.min, children: [ _row( 'Earn rate', '1 point per ' '${Formatters.money(AppConstants.loyaltyRupeesPerPoint)}', ), _row( 'Point value', Formatters.money(AppConstants.loyaltyPointValue), ), _toggle( 'Apply tier discount automatically', 'Silver 2%, Gold 5%, Platinum 8%', _autoLoyalty, (v) => setState(() => _autoLoyalty = v), ), ], ), ); Widget _hardwareCard() { final settings = ref.watch(printerSettingsProvider); final controller = ref.read(printerSettingsProvider.notifier); final printers = ref.watch(availablePrintersProvider); return PanelCard( title: 'Printer & peripherals', subtitle: 'Install the printer in the operating system first — it then ' 'appears in this list.', action: TextButton.icon( onPressed: () => ref.invalidate(availablePrintersProvider), icon: const Icon(Icons.refresh_rounded, size: 16), label: const Text('Rescan'), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ printers.when( loading: () => const Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.lg), child: Center(child: CircularProgressIndicator()), ), error: (e, _) => Text( 'Could not list printers: $e', style: const TextStyle(color: AppColors.danger, fontSize: 12.5), ), data: (list) { if (list.isEmpty) { return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: const BoxDecoration( color: AppColors.warningSurface, borderRadius: AppRadius.brSm, ), child: const Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.print_disabled_rounded, size: 18, color: AppColors.warning,), SizedBox(width: AppSpacing.sm), Expanded( child: Text( 'No printers found. Add the thermal printer in your ' 'operating system settings, then tap Rescan. Bills ' 'still print to screen in the meantime.', style: TextStyle( fontSize: 12.5, color: AppColors.warning, height: 1.45, ), ), ), ], ), ); } return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DropdownButtonFormField( initialValue: list.any((p) => p.url == settings.printerUrl) ? settings.printerUrl : null, isExpanded: true, decoration: const InputDecoration( labelText: 'Receipt printer', prefixIcon: Icon(Icons.print_outlined), isDense: true, ), hint: const Text('Print to screen only'), items: [ for (final p in list) DropdownMenuItem( value: p.url, child: Text( p.isDefault ? '${p.name} (system default)' : p.name, overflow: TextOverflow.ellipsis, ), ), ], onChanged: (url) => controller.selectPrinter( url == null ? null : list.firstWhere((p) => p.url == url), ), ), const SizedBox(height: AppSpacing.md), Wrap( spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: [ OutlinedButton.icon( onPressed: () async { final ok = await ref .read(receiptServiceProvider) .printTestPage( printerUrl: settings.printerUrl,); if (!context.mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( backgroundColor: ok ? AppColors.success : AppColors.danger, content: Text(ok ? 'Test slip sent to the printer.' : 'Could not reach the printer.',), ),); }, icon: const Icon(Icons.receipt_long_rounded, size: 17), label: const Text('Print test slip'), ), if (settings.hasPrinter) TextButton( onPressed: () => controller.selectPrinter(null), style: TextButton.styleFrom( foregroundColor: AppColors.textSecondary, ), child: const Text('Use screen only'), ), ], ), ], ); }, ), const Divider(height: AppSpacing.xxl), _toggle( 'Scanner beep', 'Audible confirmation on every scan', _scannerSound, (v) => setState(() => _scannerSound = v), ), _toggle( 'Print receipt automatically', settings.hasPrinter ? 'Sends to the selected printer the moment a sale completes' : 'Select a printer above to enable this', settings.autoPrint, settings.hasPrinter ? controller.setAutoPrint : null, ), _toggle( 'Open cash drawer on cash sales', 'Needs a raw ESC/POS link — see the notes in ReceiptService', settings.openDrawer, controller.setOpenDrawer, ), ], ), ); } Widget _staffCard(StoreAccount? store, StaffUser? current) => PanelCard( title: 'Users & roles', action: TextButton(onPressed: () {}, child: const Text('Manage')), child: ResponsiveTable( stackBelow: 360, columns: const [ TableCol('Name', flex: 3), TableCol('Role', flex: 3), TableCol('', flex: 2, numeric: true), ], rows: (store?.staff ?? const []) .map((s) => [ Cell(s.name, bold: true), Cell(s.role.label, color: AppColors.textSecondary), s.id == current?.id ? const TagChip('Signed in', color: AppColors.success,) : const SizedBox.shrink(), ],) .toList(), ), ); Widget _connectivityCard() { final ready = ref.watch(catalogueReadyProvider); final lastImport = ref.watch(lastImportAtProvider); final outstanding = ref.watch(unsyncedCountProvider).value ?? 0; return PanelCard( title: 'Connectivity & sync', subtitle: 'This terminal only needs a connection to import the ' 'catalogue and to push the shift report.', child: Column( mainAxisSize: MainAxisSize.min, children: [ _row('Catalogue', ready ? 'Loaded' : 'Not imported'), _row( 'Last import', lastImport == null ? 'Never' : Formatters.dateTime(lastImport), ), _row('Unsynced bills', '$outstanding'), _toggle( 'Simulate offline', 'Forces import and sync to fail, so you can confirm nothing is ' 'lost when the network drops', // Read straight from the provider — no local copy to drift. ref.watch(simulateOfflineProvider), (v) { ref.read(simulateOfflineProvider.notifier).state = v; // Clear any stale failure banner left by the previous setting. ref.read(catalogueImportProvider.notifier).reset(); ref.read(orderSyncProvider.notifier).reset(); }, ), ], ), ); } Widget _aboutCard() => PanelCard( title: 'About', child: Column( mainAxisSize: MainAxisSize.min, children: [ _row('Application', '${AppConstants.appName} 1.0.0'), _row('Terminal', 'TERM-01'), _row('Data store', 'SQLite (on device)'), const SizedBox(height: AppSpacing.md), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () {}, icon: const Icon(Icons.sync_rounded, size: 17), label: const Text('Check for updates'), ), ), ], ), ); Widget _row(String label, String value, {bool mono = false}) => Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( flex: 3, child: Text( label, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, color: AppColors.textSecondary, ), ), ), const SizedBox(width: AppSpacing.md), Flexible( flex: 4, child: Text( value, textAlign: TextAlign.right, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textPrimary, fontFamily: mono ? 'monospace' : null, ), ), ), ], ), ); Widget _toggle( String title, String subtitle, bool value, ValueChanged? onChanged, ) => Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 13.5, fontWeight: FontWeight.w500, ), ), Text( subtitle, style: const TextStyle( fontSize: 11.5, color: AppColors.textTertiary, height: 1.4, ), ), ], ), ), const SizedBox(width: AppSpacing.md), Switch(value: value, onChanged: onChanged), ], ), ); }