From afe9a0736d7cf7dbaf3b2fba7d39cf73b1b5e152 Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Tue, 14 Jul 2026 18:15:26 +0530 Subject: [PATCH] toast --- lib/app.dart | 19 +- lib/core/theme/app_theme.dart | 9 +- .../screens/asset_detail_screen.dart | 15 +- .../screens/asset_list_screen.dart | 5 +- .../screens/asset_qr_generate_screen.dart | 3 +- .../widgets/asset_form_panel.dart | 3 +- .../screens/audit_logs_screen.dart | 7 +- .../screens/change_password_screen.dart | 3 +- .../presentation/screens/login_screen.dart | 3 +- .../screens/reset_password_screen.dart | 3 +- .../screens/grn_detail_screen.dart | 13 +- .../presentation/screens/grn_form_screen.dart | 15 +- .../presentation/screens/grn_list_screen.dart | 3 +- .../widgets/grn_attachments_card.dart | 15 +- .../screens/master_list_screen.dart | 9 +- .../widgets/master_form_panel.dart | 3 +- .../widgets/master_inline_create_form.dart | 3 +- .../widgets/master_quick_add.dart | 5 +- .../screens/purchase_order_detail_screen.dart | 22 +- .../screens/purchase_order_form_screen.dart | 13 +- .../screens/purchase_order_list_screen.dart | 9 +- .../screens/users_role_management_screen.dart | 19 +- .../presentation/widgets/add_user_panel.dart | 5 +- .../widgets/create_role_panel.dart | 3 +- .../screens/depreciation_report_screen.dart | 9 +- .../screens/permission_matrix_screen.dart | 3 +- .../screens/appearance_settings_screen.dart | 3 +- .../screens/asset_settings_screen.dart | 3 +- .../company_profile_settings_screen.dart | 15 +- .../screens/email_configuration_screen.dart | 9 +- .../screens/general_settings_screen.dart | 3 +- .../screens/notification_settings_screen.dart | 5 +- .../screens/security_settings_screen.dart | 3 +- .../screens/user_detail_screen.dart | 3 +- .../screens/user_form_screen.dart | 7 +- .../screens/user_list_screen.dart | 5 +- .../screens/user_profile_screen.dart | 15 +- .../screens/vendor_detail_screen.dart | 19 +- .../screens/vendor_list_screen.dart | 5 +- .../widgets/vendor_form_panel.dart | 7 +- .../widgets/vendor_sub_resource_panels.dart | 9 +- lib/shared/utils/file_download_helper.dart | 7 + lib/shared/widgets/api_feedback.dart | 37 +- lib/shared/widgets/app_side_panel.dart | 13 +- lib/shared/widgets/app_sidebar.dart | 1 - lib/shared/widgets/app_toast.dart | 485 ++++++++++++++++++ .../widgets/entity_attachments_card.dart | 15 +- 47 files changed, 701 insertions(+), 187 deletions(-) create mode 100644 lib/shared/widgets/app_toast.dart diff --git a/lib/app.dart b/lib/app.dart index 452f3bd..bb3bd85 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,7 @@ import 'package:responsive_framework/responsive_framework.dart'; import 'core/constants/app_constants.dart'; import 'core/theme/theme_provider.dart'; import 'shared/routes/app_router.dart'; +import 'shared/widgets/app_toast.dart'; class BharatErpApp extends ConsumerWidget { const BharatErpApp({super.key}); @@ -23,14 +24,16 @@ class BharatErpApp extends ConsumerWidget { themeMode: resolveThemeMode(themeMode), themeAnimationDuration: Duration.zero, routerConfig: router, - builder: (context, child) => ResponsiveBreakpoints.builder( - child: child!, - breakpoints: const [ - Breakpoint(start: 0, end: 599, name: MOBILE), - Breakpoint(start: 600, end: 1023, name: TABLET), - Breakpoint(start: 1024, end: 1439, name: DESKTOP), - Breakpoint(start: 1440, end: double.infinity, name: '4K'), - ], + builder: (context, child) => AppToastHost( + child: ResponsiveBreakpoints.builder( + child: child ?? const SizedBox.shrink(), + breakpoints: const [ + Breakpoint(start: 0, end: 599, name: MOBILE), + Breakpoint(start: 600, end: 1023, name: TABLET), + Breakpoint(start: 1024, end: 1439, name: DESKTOP), + Breakpoint(start: 1440, end: double.infinity, name: '4K'), + ], + ), ), ); } diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index ae04dd6..05445b7 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -196,8 +196,13 @@ class AppTheme { ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - contentTextStyle: textTheme.bodyMedium?.copyWith(color: colorScheme.onInverseSurface), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + backgroundColor: colorScheme.inverseSurface, + contentTextStyle: textTheme.bodyMedium?.copyWith( + color: colorScheme.onInverseSurface, + ), + insetPadding: const EdgeInsets.fromLTRB(16, 16, 16, 16), ), dataTableTheme: DataTableThemeData( headingRowColor: WidgetStateProperty.all( diff --git a/lib/modules/assets/presentation/screens/asset_detail_screen.dart b/lib/modules/assets/presentation/screens/asset_detail_screen.dart index 63e090e..d5308b9 100644 --- a/lib/modules/assets/presentation/screens/asset_detail_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_detail_screen.dart @@ -24,6 +24,7 @@ import '../providers/assets_provider.dart'; import '../providers/asset_form_lookups_provider.dart'; import '../widgets/asset_form_panel.dart'; import '../widgets/asset_side_panels.dart'; +import '../../../../shared/widgets/app_toast.dart'; class AssetDetailScreen extends ConsumerStatefulWidget { const AssetDetailScreen({super.key, required this.assetId}); @@ -161,7 +162,7 @@ class _AssetDetailScreenState extends ConsumerState width: 520, ); if (transferred == true && mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Asset transferred successfully')), ); } @@ -476,7 +477,7 @@ class _AmcTab extends ConsumerWidget { width: 520, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('AMC contract created')), ); } @@ -493,7 +494,7 @@ class _AmcTab extends ConsumerWidget { width: 520, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('AMC contract updated')), ); } @@ -571,7 +572,7 @@ class _ServiceVisitsTab extends ConsumerWidget { width: 560, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Service visit logged')), ); } @@ -592,7 +593,7 @@ class _ServiceVisitsTab extends ConsumerWidget { width: 560, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Service visit updated')), ); } @@ -662,7 +663,7 @@ class _InsuranceTab extends ConsumerWidget { width: 520, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Insurance policy created')), ); } @@ -679,7 +680,7 @@ class _InsuranceTab extends ConsumerWidget { width: 520, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Insurance policy updated')), ); } diff --git a/lib/modules/assets/presentation/screens/asset_list_screen.dart b/lib/modules/assets/presentation/screens/asset_list_screen.dart index da80b36..ad48efc 100644 --- a/lib/modules/assets/presentation/screens/asset_list_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_list_screen.dart @@ -26,6 +26,7 @@ import '../providers/asset_categories_provider.dart'; import '../providers/asset_form_lookups_provider.dart'; import '../providers/assets_provider.dart'; import '../widgets/asset_form_panel.dart'; +import '../../../../shared/widgets/app_toast.dart'; class AssetListScreen extends ConsumerStatefulWidget { const AssetListScreen({super.key}); @@ -49,10 +50,10 @@ class _AssetListScreenState extends ConsumerState { final error = next.valueOrNull?.actionError; final success = next.valueOrNull?.actionSuccess; if (error != null && error != prev?.valueOrNull?.actionError) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); + showAppToastFromSnackBar(context, SnackBar(content: Text(error))); } if (success != null && success != prev?.valueOrNull?.actionSuccess) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(success))); + showAppToastFromSnackBar(context, SnackBar(content: Text(success))); } }); diff --git a/lib/modules/assets/presentation/screens/asset_qr_generate_screen.dart b/lib/modules/assets/presentation/screens/asset_qr_generate_screen.dart index 3537c38..ffd971c 100644 --- a/lib/modules/assets/presentation/screens/asset_qr_generate_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_qr_generate_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:qr_flutter/qr_flutter.dart'; import '../../../../shared/widgets/page_header.dart'; +import '../../../../shared/widgets/app_toast.dart'; class AssetQrGenerateScreen extends StatelessWidget { const AssetQrGenerateScreen({super.key}); @@ -32,7 +33,7 @@ class AssetQrGenerateScreen extends StatelessWidget { const SizedBox(height: 16), FilledButton.icon( onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar( content: Text('Print command hooked for platform print service.'), ), diff --git a/lib/modules/assets/presentation/widgets/asset_form_panel.dart b/lib/modules/assets/presentation/widgets/asset_form_panel.dart index 722f197..330e1ed 100644 --- a/lib/modules/assets/presentation/widgets/asset_form_panel.dart +++ b/lib/modules/assets/presentation/widgets/asset_form_panel.dart @@ -23,6 +23,7 @@ import '../../data/repositories/asset_repository_impl.dart'; import '../providers/asset_categories_provider.dart'; import '../providers/asset_form_lookups_provider.dart'; import '../providers/assets_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; Future openAssetFormPanel( BuildContext context, @@ -37,7 +38,7 @@ Future openAssetFormPanel( width: panelWidth, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( assetId == null diff --git a/lib/modules/audit/presentation/screens/audit_logs_screen.dart b/lib/modules/audit/presentation/screens/audit_logs_screen.dart index d6bef42..358ebb0 100644 --- a/lib/modules/audit/presentation/screens/audit_logs_screen.dart +++ b/lib/modules/audit/presentation/screens/audit_logs_screen.dart @@ -25,6 +25,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../providers/audit_provider.dart'; import '../widgets/audit_log_detail_panel.dart'; +import '../../../../shared/widgets/app_toast.dart'; class AuditLogsScreen extends ConsumerStatefulWidget { const AuditLogsScreen({super.key}); @@ -49,7 +50,7 @@ class _AuditLogsScreenState extends ConsumerState { if (file == null) { final error = ref.read(auditLogsListProvider).valueOrNull?.actionError; if (error != null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); + showAppToastFromSnackBar(context, SnackBar(content: Text(error))); } return; } @@ -60,7 +61,7 @@ class _AuditLogsScreenState extends ConsumerState { ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(saved ? 'Downloaded ${file.fileName}' : 'Export cancelled'), ), @@ -111,7 +112,7 @@ class _AuditLogsScreenState extends ConsumerState { ref.listen(auditLogsListProvider, (prev, next) { final error = next.valueOrNull?.actionError; if (error != null && error != prev?.valueOrNull?.actionError) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); + showAppToastFromSnackBar(context, SnackBar(content: Text(error))); } }); diff --git a/lib/modules/auth/presentation/screens/change_password_screen.dart b/lib/modules/auth/presentation/screens/change_password_screen.dart index 70ecc9d..5d5d926 100644 --- a/lib/modules/auth/presentation/screens/change_password_screen.dart +++ b/lib/modules/auth/presentation/screens/change_password_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import '../../../../core/utils/validators.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/app_toast.dart'; class ChangePasswordScreen extends StatefulWidget { const ChangePasswordScreen({super.key}); @@ -66,7 +67,7 @@ class _ChangePasswordScreenState extends State { label: 'Update Password', onPressed: () { if (_formKey.currentState!.validate()) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Password updated (API pending)')), ); Navigator.of(context).pop(); diff --git a/lib/modules/auth/presentation/screens/login_screen.dart b/lib/modules/auth/presentation/screens/login_screen.dart index 919c2bc..1e2d59f 100644 --- a/lib/modules/auth/presentation/screens/login_screen.dart +++ b/lib/modules/auth/presentation/screens/login_screen.dart @@ -14,6 +14,7 @@ import '../../../../shared/widgets/app_button.dart'; import '../widgets/bcpl_logo.dart'; import '../widgets/login_colors.dart'; import '../widgets/login_hero_panel.dart'; +import '../../../../shared/widgets/app_toast.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @@ -87,7 +88,7 @@ class _LoginScreenState extends ConsumerState { context.go(RouteConstants.dashboard); } else { final error = ref.read(authStateProvider).error; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(error ?? 'Invalid credentials')), ); } diff --git a/lib/modules/auth/presentation/screens/reset_password_screen.dart b/lib/modules/auth/presentation/screens/reset_password_screen.dart index c5f952c..f450ce7 100644 --- a/lib/modules/auth/presentation/screens/reset_password_screen.dart +++ b/lib/modules/auth/presentation/screens/reset_password_screen.dart @@ -5,6 +5,7 @@ import '../../../../core/constants/route_constants.dart'; import '../../../../core/utils/validators.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/app_toast.dart'; class ResetPasswordScreen extends StatefulWidget { const ResetPasswordScreen({super.key}); @@ -59,7 +60,7 @@ class _ResetPasswordScreenState extends State { label: 'Reset Password', onPressed: () { if (_formKey.currentState!.validate()) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Password reset (API token flow pending)')), ); context.go(RouteConstants.login); diff --git a/lib/modules/grn/presentation/screens/grn_detail_screen.dart b/lib/modules/grn/presentation/screens/grn_detail_screen.dart index 88087ff..b5dc22f 100644 --- a/lib/modules/grn/presentation/screens/grn_detail_screen.dart +++ b/lib/modules/grn/presentation/screens/grn_detail_screen.dart @@ -18,6 +18,7 @@ import '../providers/grn_provider.dart'; import '../widgets/grn_attachments_card.dart'; import '../widgets/grn_line_items_editor.dart'; import '../widgets/grn_status_chip.dart'; +import '../../../../shared/widgets/app_toast.dart'; class GrnDetailScreen extends ConsumerStatefulWidget { const GrnDetailScreen({super.key, required this.grnId}); @@ -116,13 +117,11 @@ class _GrnDetailScreenState extends ConsumerState { try { await action(); if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(success))); + showAppToastFromSnackBar(context, SnackBar(content: Text(success))); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isWorking = false); @@ -186,13 +185,11 @@ class _GrnDetailScreenState extends ConsumerState { fileName: '${grn.grnNumber ?? 'GRN-${grn.id}'}.pdf', ); if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('PDF downloaded'))); + showAppToastFromSnackBar(context, const SnackBar(content: Text('PDF downloaded'))); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isDownloadingPdf = false); diff --git a/lib/modules/grn/presentation/screens/grn_form_screen.dart b/lib/modules/grn/presentation/screens/grn_form_screen.dart index b455e8d..96f67a4 100644 --- a/lib/modules/grn/presentation/screens/grn_form_screen.dart +++ b/lib/modules/grn/presentation/screens/grn_form_screen.dart @@ -25,6 +25,7 @@ import '../providers/grn_lookups_provider.dart'; import '../providers/grn_provider.dart'; import '../widgets/grn_line_items_editor.dart'; import '../widgets/grn_status_chip.dart'; +import '../../../../shared/widgets/app_toast.dart'; class GrnFormScreen extends ConsumerStatefulWidget { const GrnFormScreen({super.key, this.grnId}); @@ -226,7 +227,7 @@ class _GrnFormScreenState extends ConsumerState { if (formState == null) return; if (!formState.validate()) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar( content: Text('Please fix the highlighted errors before saving'), ), @@ -236,15 +237,14 @@ class _GrnFormScreenState extends ConsumerState { if (!widget.isEditing) { if (_selectedPoId == null || _warehouseId == null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Please select PO and warehouse')), ); return; } final lineError = _lineItemsError(); if (lineError != null) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(lineError))); + showAppToastFromSnackBar(context, SnackBar(content: Text(lineError))); return; } } @@ -263,7 +263,7 @@ class _GrnFormScreenState extends ConsumerState { } if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(widget.isEditing ? 'GRN updated' : 'GRN created'), ), @@ -279,8 +279,7 @@ class _GrnFormScreenState extends ConsumerState { if (!mounted) return; final message = e is Failure ? validationErrorMessage(e) : e.toString(); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(message))); + showAppToastFromSnackBar(context, SnackBar(content: Text(message))); } finally { if (mounted) setState(() => _isSubmitting = false); } @@ -443,7 +442,7 @@ class _GrnFormScreenState extends ConsumerState { } } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())), ); } diff --git a/lib/modules/grn/presentation/screens/grn_list_screen.dart b/lib/modules/grn/presentation/screens/grn_list_screen.dart index bb01be2..e098c0a 100644 --- a/lib/modules/grn/presentation/screens/grn_list_screen.dart +++ b/lib/modules/grn/presentation/screens/grn_list_screen.dart @@ -23,6 +23,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../providers/grn_provider.dart'; import '../widgets/grn_status_chip.dart'; +import '../../../../shared/widgets/app_toast.dart'; class GrnListScreen extends ConsumerStatefulWidget { const GrnListScreen({super.key}); @@ -137,7 +138,7 @@ class _GrnListScreenState extends ConsumerState { void _editGrn(GrnModel grn) { if (!grn.canEdit) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Only posted GRNs can be edited')), ); return; diff --git a/lib/modules/grn/presentation/widgets/grn_attachments_card.dart b/lib/modules/grn/presentation/widgets/grn_attachments_card.dart index a7724ca..435e2fe 100644 --- a/lib/modules/grn/presentation/widgets/grn_attachments_card.dart +++ b/lib/modules/grn/presentation/widgets/grn_attachments_card.dart @@ -9,6 +9,7 @@ import '../../../../shared/models/grn_model.dart'; import '../../../../shared/utils/file_download_helper.dart'; import '../../../../shared/widgets/app_confirmation_dialog.dart'; import '../providers/grn_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; const _allowedExtensions = ['pdf', 'jpg', 'jpeg', 'png', 'webp']; @@ -65,7 +66,7 @@ class _GrnAttachmentsCardState extends ConsumerState { final bytes = file.bytes; if (bytes == null || bytes.isEmpty) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Could not read the selected file')), ); return; @@ -74,7 +75,7 @@ class _GrnAttachmentsCardState extends ConsumerState { final ext = (file.extension ?? '').toLowerCase(); if (!_allowedExtensions.contains(ext)) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar( content: Text('Only PDF, JPEG, PNG, and WebP files are allowed'), ), @@ -89,14 +90,14 @@ class _GrnAttachmentsCardState extends ConsumerState { filename: file.name, ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text('${file.name} uploaded')), ); } catch (e) { if (!mounted) return; final message = e is Failure ? validationErrorMessage(e) : e.toString(); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(message)), ); } finally { @@ -117,7 +118,7 @@ class _GrnAttachmentsCardState extends ConsumerState { ); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())), ); } finally { @@ -144,14 +145,14 @@ class _GrnAttachmentsCardState extends ConsumerState { .read(grnDetailProvider(widget.grn.id).notifier) .deleteAttachment(attachment.id); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Attachment deleted')), ); } catch (e) { if (!mounted) return; final message = e is Failure ? validationErrorMessage(e) : e.toString(); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(message)), ); } finally { diff --git a/lib/modules/master_data/presentation/screens/master_list_screen.dart b/lib/modules/master_data/presentation/screens/master_list_screen.dart index 8736cb2..7831ee8 100644 --- a/lib/modules/master_data/presentation/screens/master_list_screen.dart +++ b/lib/modules/master_data/presentation/screens/master_list_screen.dart @@ -22,6 +22,7 @@ import '../../../../shared/widgets/page_header.dart'; import '../../domain/entities/master_definition.dart'; import '../providers/master_provider.dart'; import '../widgets/master_form_panel.dart'; +import '../../../../shared/widgets/app_toast.dart'; class MasterListScreen extends ConsumerStatefulWidget { const MasterListScreen({super.key, required this.masterId}); @@ -63,7 +64,7 @@ class _MasterListScreenState extends ConsumerState { if (file == null) { final error = ref.read(masterListProvider(widget.masterId)).valueOrNull?.actionError; if (error != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(error)), ); } @@ -76,7 +77,7 @@ class _MasterListScreenState extends ConsumerState { ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( saved ? 'Downloaded ${file.fileName}' : 'Export cancelled', @@ -105,7 +106,7 @@ class _MasterListScreenState extends ConsumerState { ); if (saved != null && mounted) { ref.invalidate(masterListProvider(widget.masterId)); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( recordId == null @@ -134,7 +135,7 @@ class _MasterListScreenState extends ConsumerState { await ref.read(masterListProvider(widget.masterId).notifier).deleteRecord(id); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( success diff --git a/lib/modules/master_data/presentation/widgets/master_form_panel.dart b/lib/modules/master_data/presentation/widgets/master_form_panel.dart index 07d1b51..0c467de 100644 --- a/lib/modules/master_data/presentation/widgets/master_form_panel.dart +++ b/lib/modules/master_data/presentation/widgets/master_form_panel.dart @@ -12,6 +12,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../domain/entities/master_definition.dart'; import '../providers/master_provider.dart'; import 'master_quick_add.dart'; +import '../../../../shared/widgets/app_toast.dart'; class MasterFormPanel extends ConsumerStatefulWidget { const MasterFormPanel({ @@ -69,7 +70,7 @@ class _MasterFormPanelState extends ConsumerState { } final error = ref.read(masterFormProvider(_args)).valueOrNull?.errorMessage; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( error ?? diff --git a/lib/modules/master_data/presentation/widgets/master_inline_create_form.dart b/lib/modules/master_data/presentation/widgets/master_inline_create_form.dart index f816550..5002a7d 100644 --- a/lib/modules/master_data/presentation/widgets/master_inline_create_form.dart +++ b/lib/modules/master_data/presentation/widgets/master_inline_create_form.dart @@ -10,6 +10,7 @@ import '../../../../shared/widgets/app_searchable_dropdown.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../domain/entities/master_definition.dart'; import '../providers/master_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; /// Compact create form for inline Quick Add (no side panel / dialog). class MasterInlineCreateForm extends ConsumerStatefulWidget { @@ -63,7 +64,7 @@ class _MasterInlineCreateFormState } final error = ref.read(masterFormProvider(_args)).valueOrNull?.errorMessage; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( error ?? 'Failed to create ${_definition.title.toLowerCase()}', diff --git a/lib/modules/master_data/presentation/widgets/master_quick_add.dart b/lib/modules/master_data/presentation/widgets/master_quick_add.dart index f901eaf..ed6d5e0 100644 --- a/lib/modules/master_data/presentation/widgets/master_quick_add.dart +++ b/lib/modules/master_data/presentation/widgets/master_quick_add.dart @@ -8,6 +8,7 @@ import '../../../../shared/widgets/app_searchable_dropdown.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../providers/master_provider.dart'; import 'master_inline_create_form.dart'; +import '../../../../shared/widgets/app_toast.dart'; /// Dropdown with inline Quick Add that expands below the field (no dialog). /// @@ -72,7 +73,7 @@ class _MasterQuickAddDropdownState void _openInlineForm() { if (!_canQuickAdd) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar( content: Text('You do not have permission to add master data'), ), @@ -126,7 +127,7 @@ class _MasterQuickAddDropdownState _collapse(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text('${_titleCase(masterQuickAddNoun(widget.masterId))} added'), diff --git a/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart b/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart index 39e953a..f2604ed 100644 --- a/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart +++ b/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart @@ -21,6 +21,7 @@ import '../providers/purchase_orders_provider.dart'; import '../../data/repositories/purchase_order_repository_impl.dart'; import '../widgets/po_status_chip.dart'; import '../widgets/purchase_order_line_items_editor.dart'; +import '../../../../shared/widgets/app_toast.dart'; class PurchaseOrderDetailScreen extends ConsumerStatefulWidget { const PurchaseOrderDetailScreen({super.key, required this.purchaseOrderId}); @@ -145,13 +146,11 @@ class _PurchaseOrderDetailScreenState try { await action(); if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(success))); + showAppToastFromSnackBar(context, SnackBar(content: Text(success))); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isWorking = false); @@ -203,7 +202,7 @@ class _PurchaseOrderDetailScreenState final remarks = remarksController.text.trim(); remarksController.dispose(); if (remarks.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Rejection remarks are required')), ); return; @@ -231,15 +230,14 @@ class _PurchaseOrderDetailScreenState .read(purchaseOrderDetailProvider(widget.purchaseOrderId).notifier) .amend(); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Amendment draft created')), ); context.go('${RouteConstants.purchaseOrders}/${amended.id}'); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isWorking = false); @@ -281,15 +279,14 @@ class _PurchaseOrderDetailScreenState if (result.failure != null) throw result.failure!; ref.invalidate(purchaseOrdersListProvider); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Purchase order deleted')), ); context.go(RouteConstants.purchaseOrders); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isWorking = false); @@ -309,8 +306,7 @@ class _PurchaseOrderDetailScreenState ); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isDownloadingPdf = false); diff --git a/lib/modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart b/lib/modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart index 4e834b1..2158d35 100644 --- a/lib/modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart +++ b/lib/modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart @@ -25,6 +25,7 @@ import '../providers/purchase_order_lookups_provider.dart'; import '../providers/purchase_orders_provider.dart'; import '../widgets/po_status_chip.dart'; import '../widgets/purchase_order_line_items_editor.dart'; +import '../../../../shared/widgets/app_toast.dart'; class PurchaseOrderFormScreen extends ConsumerStatefulWidget { const PurchaseOrderFormScreen({super.key, this.purchaseOrderId}); @@ -235,7 +236,7 @@ class _PurchaseOrderFormScreenState extends ConsumerState _deleteOrder(PurchaseOrderModel order) async { if (!order.canDelete) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar( content: Text('Only draft or rejected orders can be deleted'), ), diff --git a/lib/modules/rbac/presentation/screens/users_role_management_screen.dart b/lib/modules/rbac/presentation/screens/users_role_management_screen.dart index d63ac24..1fed7a0 100644 --- a/lib/modules/rbac/presentation/screens/users_role_management_screen.dart +++ b/lib/modules/rbac/presentation/screens/users_role_management_screen.dart @@ -30,6 +30,7 @@ import '../../../roles/presentation/providers/roles_provider.dart'; import '../widgets/add_user_panel.dart'; import '../widgets/create_role_panel.dart'; import '../widgets/rbac_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; RbacTab rbacTabFromLocation(String location) { if (location.startsWith(RouteConstants.roles)) return RbacTab.roles; @@ -69,7 +70,7 @@ class _UsersRoleManagementScreenState ); if (saved == true && mounted) { ref.invalidate(usersListProvider); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( userId == null ? 'User added successfully' : 'User updated successfully', @@ -90,7 +91,7 @@ class _UsersRoleManagementScreenState if (saved == true && mounted) { ref.invalidate(rolesListProvider); ref.invalidate(usersListProvider); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( roleId == null ? 'Role created successfully' : 'Role updated successfully', @@ -106,7 +107,7 @@ class _UsersRoleManagementScreenState Future _deleteRole(RoleCardModel role) async { if (isProtectedRole(role)) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Super Admin role cannot be deleted')), ); return; @@ -128,7 +129,7 @@ class _UsersRoleManagementScreenState ref.invalidate(usersListProvider); } - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(success ? 'Role deleted successfully' : 'Failed to delete role'), ), @@ -594,7 +595,7 @@ class _UsersTabState extends ConsumerState<_UsersTab> { ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( result.failure == null @@ -612,7 +613,7 @@ class _UsersTabState extends ConsumerState<_UsersTab> { if (file == null) { final error = ref.read(usersListProvider).valueOrNull?.actionError; if (error != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(error)), ); } @@ -625,7 +626,7 @@ class _UsersTabState extends ConsumerState<_UsersTab> { ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( saved ? 'Downloaded ${file.fileName}' : 'Export cancelled', @@ -647,7 +648,7 @@ class _UsersTabState extends ConsumerState<_UsersTab> { final success = await ref.read(usersListProvider.notifier).deactivateUser(user.id); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(success ? 'User deactivated' : 'Failed to deactivate user'), ), @@ -1232,7 +1233,7 @@ class _PermissionMatrixTabState extends ConsumerState<_PermissionMatrixTab> { if (!mounted) return; setState(() => _isSaving = false); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Row( children: [ diff --git a/lib/modules/rbac/presentation/widgets/add_user_panel.dart b/lib/modules/rbac/presentation/widgets/add_user_panel.dart index e295e14..3adebf1 100644 --- a/lib/modules/rbac/presentation/widgets/add_user_panel.dart +++ b/lib/modules/rbac/presentation/widgets/add_user_panel.dart @@ -14,6 +14,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../../../master_data/presentation/widgets/master_quick_add.dart'; import '../providers/add_user_form_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; class AddUserPanel extends ConsumerStatefulWidget { const AddUserPanel({super.key, this.userId}); @@ -109,7 +110,7 @@ class _AddUserPanelState extends ConsumerState { .whereType() .toList(); if (roleIds.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Please select at least one role')), ); return; @@ -169,7 +170,7 @@ class _AddUserPanelState extends ConsumerState { } final error = ref.read(addUserFormProvider(widget.userId)).valueOrNull?.errorMessage; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( error ?? (widget.isEditing ? 'Failed to update user' : 'Failed to create user'), diff --git a/lib/modules/rbac/presentation/widgets/create_role_panel.dart b/lib/modules/rbac/presentation/widgets/create_role_panel.dart index b11d7ad..03f66f0 100644 --- a/lib/modules/rbac/presentation/widgets/create_role_panel.dart +++ b/lib/modules/rbac/presentation/widgets/create_role_panel.dart @@ -12,6 +12,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../providers/role_form_provider.dart'; import 'rbac_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class RoleFormPanel extends ConsumerStatefulWidget { const RoleFormPanel({super.key, this.roleId}); @@ -82,7 +83,7 @@ class _RoleFormPanelState extends ConsumerState { } final error = ref.read(roleFormProvider(widget.roleId)).valueOrNull?.errorMessage; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( error ?? diff --git a/lib/modules/reports/presentation/screens/depreciation_report_screen.dart b/lib/modules/reports/presentation/screens/depreciation_report_screen.dart index 00df0a8..45436c8 100644 --- a/lib/modules/reports/presentation/screens/depreciation_report_screen.dart +++ b/lib/modules/reports/presentation/screens/depreciation_report_screen.dart @@ -24,6 +24,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../domain/entities/depreciation_report.dart'; import '../providers/depreciation_report_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; class DepreciationReportScreen extends ConsumerStatefulWidget { const DepreciationReportScreen({super.key}); @@ -52,8 +53,7 @@ class _DepreciationReportScreenState final error = ref.read(depreciationReportProvider).valueOrNull?.actionError; if (error != null) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(error))); + showAppToastFromSnackBar(context, SnackBar(content: Text(error))); } return; } @@ -64,7 +64,7 @@ class _DepreciationReportScreenState ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( saved ? 'Downloaded ${file.fileName}' : 'Export cancelled', @@ -119,8 +119,7 @@ class _DepreciationReportScreenState ref.listen(depreciationReportProvider, (prev, next) { final error = next.valueOrNull?.actionError; if (error != null && error != prev?.valueOrNull?.actionError) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(error))); + showAppToastFromSnackBar(context, SnackBar(content: Text(error))); } }); diff --git a/lib/modules/roles/presentation/screens/permission_matrix_screen.dart b/lib/modules/roles/presentation/screens/permission_matrix_screen.dart index 7c0d3f3..add0a60 100644 --- a/lib/modules/roles/presentation/screens/permission_matrix_screen.dart +++ b/lib/modules/roles/presentation/screens/permission_matrix_screen.dart @@ -11,6 +11,7 @@ import '../../../../shared/widgets/app_loading_view.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../providers/roles_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; class PermissionMatrixScreen extends ConsumerStatefulWidget { const PermissionMatrixScreen({super.key, required this.roleId}); @@ -30,7 +31,7 @@ class _PermissionMatrixScreenState extends ConsumerState await ref.read(permissionMatrixProvider(widget.roleId).notifier).save(); if (!mounted) return; setState(() => _isSaving = false); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(success ? 'Permissions saved' : 'Failed to save permissions'), ), diff --git a/lib/modules/settings/presentation/screens/appearance_settings_screen.dart b/lib/modules/settings/presentation/screens/appearance_settings_screen.dart index 802d3f9..f424e1a 100644 --- a/lib/modules/settings/presentation/screens/appearance_settings_screen.dart +++ b/lib/modules/settings/presentation/screens/appearance_settings_screen.dart @@ -7,6 +7,7 @@ import '../../../../core/theme/theme_provider.dart'; import '../../../../shared/widgets/app_button.dart'; import '../providers/settings_provider.dart'; import '../widgets/settings_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class AppearanceSettingsScreen extends ConsumerWidget { const AppearanceSettingsScreen({super.key}); @@ -182,7 +183,7 @@ class AppearanceSettingsScreen extends ConsumerWidget { AppButton( label: 'Settings Auto-Saved', onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Appearance settings are saved automatically')), ); }, diff --git a/lib/modules/settings/presentation/screens/asset_settings_screen.dart b/lib/modules/settings/presentation/screens/asset_settings_screen.dart index d0f7eaa..593e82f 100644 --- a/lib/modules/settings/presentation/screens/asset_settings_screen.dart +++ b/lib/modules/settings/presentation/screens/asset_settings_screen.dart @@ -7,6 +7,7 @@ import '../../../../shared/widgets/app_text_field.dart'; import '../../domain/entities/app_settings.dart'; import '../providers/settings_provider.dart'; import '../widgets/settings_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class AssetSettingsScreen extends ConsumerStatefulWidget { const AssetSettingsScreen({super.key}); @@ -73,7 +74,7 @@ class _AssetSettingsScreenState extends ConsumerState { ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Asset settings saved')), ); } diff --git a/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart b/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart index dd9060a..6a1949c 100644 --- a/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart +++ b/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart @@ -16,6 +16,7 @@ import '../../../../shared/widgets/sidebar_logo.dart'; import '../../domain/entities/app_settings.dart'; import '../providers/settings_provider.dart'; import '../widgets/settings_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class CompanyProfileSettingsScreen extends ConsumerStatefulWidget { const CompanyProfileSettingsScreen({super.key}); @@ -82,7 +83,7 @@ class _CompanyProfileSettingsScreenState _applyProfile(ref.read(appSettingsProvider).companyProfile); setState(() => _loading = false); if (failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(failure.message), backgroundColor: Theme.of(context).colorScheme.error, @@ -158,7 +159,7 @@ class _CompanyProfileSettingsScreenState setState(() => _saving = false); if (failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(validationErrorMessage(failure)), backgroundColor: Theme.of(context).colorScheme.error, @@ -175,7 +176,7 @@ class _CompanyProfileSettingsScreenState ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Company profile saved')), ); } @@ -207,14 +208,14 @@ class _CompanyProfileSettingsScreenState uploadedUrl != null && uploadedUrl.isNotEmpty) { setState(() => _logoUrlController.text = uploadedUrl); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Logo uploaded')), ); return; } if (uploadResult.failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(uploadResult.failure!.message), backgroundColor: Theme.of(context).colorScheme.error, @@ -268,14 +269,14 @@ class _CompanyProfileSettingsScreenState updateFavicon(previewDataUri); setState(() => _faviconUrlController.text = uploadedUrl); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Favicon uploaded')), ); return; } if (uploadResult.failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(uploadResult.failure!.message), backgroundColor: Theme.of(context).colorScheme.error, diff --git a/lib/modules/settings/presentation/screens/email_configuration_screen.dart b/lib/modules/settings/presentation/screens/email_configuration_screen.dart index 1d2a074..bacc890 100644 --- a/lib/modules/settings/presentation/screens/email_configuration_screen.dart +++ b/lib/modules/settings/presentation/screens/email_configuration_screen.dart @@ -8,6 +8,7 @@ import '../../../../shared/widgets/app_text_field.dart'; import '../../domain/entities/app_settings.dart'; import '../providers/settings_provider.dart'; import '../widgets/settings_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class EmailConfigurationScreen extends ConsumerStatefulWidget { const EmailConfigurationScreen({super.key}); @@ -51,7 +52,7 @@ class _EmailConfigurationScreenState _applyEmail(ref.read(appSettingsProvider).email); setState(() => _loading = false); if (failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(failure.message), backgroundColor: Theme.of(context).colorScheme.error, @@ -87,7 +88,7 @@ class _EmailConfigurationScreenState final port = int.tryParse(_portController.text.trim()); if (port == null || port <= 0) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Enter a valid SMTP port')), ); return; @@ -109,7 +110,7 @@ class _EmailConfigurationScreenState setState(() => _saving = false); if (failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(validationErrorMessage(failure)), backgroundColor: Theme.of(context).colorScheme.error, @@ -118,7 +119,7 @@ class _EmailConfigurationScreenState return; } - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Email configuration saved')), ); } diff --git a/lib/modules/settings/presentation/screens/general_settings_screen.dart b/lib/modules/settings/presentation/screens/general_settings_screen.dart index dd7bf10..92c29c1 100644 --- a/lib/modules/settings/presentation/screens/general_settings_screen.dart +++ b/lib/modules/settings/presentation/screens/general_settings_screen.dart @@ -7,6 +7,7 @@ import '../../../../shared/widgets/app_text_field.dart'; import '../../domain/entities/app_settings.dart'; import '../providers/settings_provider.dart'; import '../widgets/settings_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class GeneralSettingsScreen extends ConsumerStatefulWidget { const GeneralSettingsScreen({super.key}); @@ -64,7 +65,7 @@ class _GeneralSettingsScreenState extends ConsumerState { ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('General settings saved')), ); } diff --git a/lib/modules/settings/presentation/screens/notification_settings_screen.dart b/lib/modules/settings/presentation/screens/notification_settings_screen.dart index 45e48a1..2151458 100644 --- a/lib/modules/settings/presentation/screens/notification_settings_screen.dart +++ b/lib/modules/settings/presentation/screens/notification_settings_screen.dart @@ -5,6 +5,7 @@ import '../../../../shared/widgets/app_button.dart'; import '../../domain/entities/app_settings.dart'; import '../providers/settings_provider.dart'; import '../widgets/settings_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class NotificationSettingsScreen extends ConsumerWidget { const NotificationSettingsScreen({super.key}); @@ -16,7 +17,7 @@ class NotificationSettingsScreen extends ConsumerWidget { Future update(NotificationSettingsConfig updated) async { await ref.read(appSettingsProvider.notifier).updateNotifications(updated); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Notification settings saved')), ); } @@ -86,7 +87,7 @@ class NotificationSettingsScreen extends ConsumerWidget { AppButton( label: 'Settings Auto-Saved', onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar( content: Text('Notification settings are saved automatically'), ), diff --git a/lib/modules/settings/presentation/screens/security_settings_screen.dart b/lib/modules/settings/presentation/screens/security_settings_screen.dart index d5bbd9c..906eff3 100644 --- a/lib/modules/settings/presentation/screens/security_settings_screen.dart +++ b/lib/modules/settings/presentation/screens/security_settings_screen.dart @@ -8,6 +8,7 @@ import '../../../../shared/widgets/app_text_field.dart'; import '../../domain/entities/app_settings.dart'; import '../providers/settings_provider.dart'; import '../widgets/settings_widgets.dart'; +import '../../../../shared/widgets/app_toast.dart'; class SecuritySettingsScreen extends ConsumerStatefulWidget { const SecuritySettingsScreen({super.key}); @@ -86,7 +87,7 @@ class _SecuritySettingsScreenState extends ConsumerState ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Security settings saved')), ); } diff --git a/lib/modules/users/presentation/screens/user_detail_screen.dart b/lib/modules/users/presentation/screens/user_detail_screen.dart index f11af20..f7776be 100644 --- a/lib/modules/users/presentation/screens/user_detail_screen.dart +++ b/lib/modules/users/presentation/screens/user_detail_screen.dart @@ -13,6 +13,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/models/user_management_models.dart'; import '../providers/users_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; class UserDetailScreen extends ConsumerWidget { const UserDetailScreen({super.key, required this.userId}); @@ -99,7 +100,7 @@ class UserDetailScreen extends ConsumerWidget { final success = await ref.read(userDetailProvider(userId).notifier).deactivate(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(success ? 'User deactivated' : 'Failed to deactivate')), ); if (success) context.pop(); diff --git a/lib/modules/users/presentation/screens/user_form_screen.dart b/lib/modules/users/presentation/screens/user_form_screen.dart index fea4b92..1989fe6 100644 --- a/lib/modules/users/presentation/screens/user_form_screen.dart +++ b/lib/modules/users/presentation/screens/user_form_screen.dart @@ -13,6 +13,7 @@ import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../../master_data/presentation/widgets/master_quick_add.dart'; import '../providers/users_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; class UserFormScreen extends ConsumerStatefulWidget { const UserFormScreen({super.key, this.userId}); @@ -83,7 +84,7 @@ class _UserFormScreenState extends ConsumerState { Future _submit() async { if (!_formKey.currentState!.validate()) return; if (_selectedRoleId == null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Please select a role')), ); return; @@ -127,13 +128,13 @@ class _UserFormScreenState extends ConsumerState { } if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(isEditing ? 'User updated' : 'User created')), ); context.pop(); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())), ); } finally { diff --git a/lib/modules/users/presentation/screens/user_list_screen.dart b/lib/modules/users/presentation/screens/user_list_screen.dart index 156a166..5870678 100644 --- a/lib/modules/users/presentation/screens/user_list_screen.dart +++ b/lib/modules/users/presentation/screens/user_list_screen.dart @@ -22,6 +22,7 @@ import '../../../../shared/widgets/app_table_action_icon.dart'; import '../../../../shared/widgets/page_header.dart'; import '../widgets/user_rich_data_table.dart'; import '../providers/users_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; class UserListScreen extends ConsumerStatefulWidget { const UserListScreen({super.key}); @@ -151,7 +152,7 @@ class _UserListScreenState extends ConsumerState { Future _toggleStatus(ManagedUserModel user) async { final success = await ref.read(usersListProvider.notifier).toggleUserStatus(user); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text(success ? 'User status updated' : 'Failed to update status'), ), @@ -170,7 +171,7 @@ class _UserListScreenState extends ConsumerState { final success = await ref.read(usersListProvider.notifier).deactivateUser(user.id); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(success ? 'User deactivated' : 'Failed to deactivate user')), ); } diff --git a/lib/modules/users/presentation/screens/user_profile_screen.dart b/lib/modules/users/presentation/screens/user_profile_screen.dart index 22abc14..cdc8506 100644 --- a/lib/modules/users/presentation/screens/user_profile_screen.dart +++ b/lib/modules/users/presentation/screens/user_profile_screen.dart @@ -12,6 +12,7 @@ import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_text_field.dart'; import '../../../auth/data/repositories/auth_repository_impl.dart'; import '../providers/users_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; class UserProfileScreen extends ConsumerStatefulWidget { const UserProfileScreen({super.key}); @@ -62,7 +63,7 @@ class _UserProfileScreenState extends ConsumerState { ); if (result == null || result.files.isEmpty) return; setState(() => _avatarUrl = result.files.first.path); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Image selected. Upload will use avatar_url on save.')), ); } @@ -84,21 +85,21 @@ class _UserProfileScreenState extends ConsumerState { setState(() => _isUpdatingProfile = false); if (result.failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(result.failure.toString())), ); return; } await ref.read(authStateProvider.notifier).checkAuth(); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Profile updated')), ); } Future _changePassword() async { if (_newPasswordController.text != _confirmPasswordController.text) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Passwords do not match')), ); return; @@ -106,7 +107,7 @@ class _UserProfileScreenState extends ConsumerState { final passwordError = Validators.password(_newPasswordController.text); if (passwordError != null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(passwordError))); + showAppToastFromSnackBar(context, SnackBar(content: Text(passwordError))); return; } @@ -124,7 +125,7 @@ class _UserProfileScreenState extends ConsumerState { setState(() => _isChangingPassword = false); if (result.failure != null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(result.failure.toString())), ); return; @@ -133,7 +134,7 @@ class _UserProfileScreenState extends ConsumerState { _currentPasswordController.clear(); _newPasswordController.clear(); _confirmPasswordController.clear(); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Password changed successfully')), ); } diff --git a/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart b/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart index b4ee67a..8f155ba 100644 --- a/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart +++ b/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart @@ -17,6 +17,7 @@ import '../../../../shared/widgets/page_header.dart'; import '../providers/vendors_provider.dart'; import '../widgets/vendor_form_panel.dart'; import '../widgets/vendor_sub_resource_panels.dart'; +import '../../../../shared/widgets/app_toast.dart'; class VendorDetailScreen extends ConsumerStatefulWidget { const VendorDetailScreen({super.key, required this.vendorId}); @@ -171,13 +172,13 @@ class _VendorDetailScreenState extends ConsumerState .read(vendorDetailProvider(widget.vendorId).notifier) .updateStatus(selected!); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Vendor status updated')), ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } } @@ -197,7 +198,7 @@ class _VendorDetailScreenState extends ConsumerState if (mounted) context.go(RouteConstants.vendors); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } } @@ -382,7 +383,7 @@ class _AddressesTab extends ConsumerWidget { vendorId: vendorId, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Address saved')), ); } @@ -450,7 +451,7 @@ class _AddressesTab extends ConsumerWidget { await ref.read(vendorDetailProvider(vendorId).notifier).deleteAddress(addressId); } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } } @@ -482,7 +483,7 @@ class _ContactsTab extends ConsumerWidget { vendorId: vendorId, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Contact saved')), ); } @@ -550,7 +551,7 @@ class _ContactsTab extends ConsumerWidget { await ref.read(vendorDetailProvider(vendorId).notifier).deleteContact(contactId); } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } } @@ -582,7 +583,7 @@ class _BankDetailsTab extends ConsumerWidget { vendorId: vendorId, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Bank detail saved')), ); } @@ -652,7 +653,7 @@ class _BankDetailsTab extends ConsumerWidget { .deleteBankDetail(bankDetailId); } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } } diff --git a/lib/modules/vendors/presentation/screens/vendor_list_screen.dart b/lib/modules/vendors/presentation/screens/vendor_list_screen.dart index dd6d9b0..345db28 100644 --- a/lib/modules/vendors/presentation/screens/vendor_list_screen.dart +++ b/lib/modules/vendors/presentation/screens/vendor_list_screen.dart @@ -25,6 +25,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../providers/vendors_provider.dart'; import '../widgets/vendor_form_panel.dart'; +import '../../../../shared/widgets/app_toast.dart'; class VendorListScreen extends ConsumerStatefulWidget { const VendorListScreen({super.key}); @@ -52,10 +53,10 @@ class _VendorListScreenState extends ConsumerState { final error = next.valueOrNull?.actionError; final success = next.valueOrNull?.actionSuccess; if (error != null && error != prev?.valueOrNull?.actionError) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); + showAppToastFromSnackBar(context, SnackBar(content: Text(error))); } if (success != null && success != prev?.valueOrNull?.actionSuccess) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(success))); + showAppToastFromSnackBar(context, SnackBar(content: Text(success))); } }); diff --git a/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart b/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart index 2cc0bd6..7b13722 100644 --- a/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart +++ b/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart @@ -16,6 +16,7 @@ import '../../../masters/data/datasources/master_remote_data_source.dart'; import '../../../master_data/presentation/widgets/master_quick_add.dart'; import '../../data/repositories/vendor_repository_impl.dart'; import '../providers/vendors_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; Future openVendorFormPanel( BuildContext context, @@ -29,7 +30,7 @@ Future openVendorFormPanel( width: 560, ); if (saved == true && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar( content: Text( vendorId == null @@ -136,7 +137,7 @@ class _VendorFormPanelState extends ConsumerState { Future _submit() async { if (!_formKey.currentState!.validate()) return; if (_vendorType == null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Please select vendor type')), ); return; @@ -154,7 +155,7 @@ class _VendorFormPanelState extends ConsumerState { if (mounted) Navigator.of(context, rootNavigator: true).pop(true); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())), ); } diff --git a/lib/modules/vendors/presentation/widgets/vendor_sub_resource_panels.dart b/lib/modules/vendors/presentation/widgets/vendor_sub_resource_panels.dart index 26763c0..89007ad 100644 --- a/lib/modules/vendors/presentation/widgets/vendor_sub_resource_panels.dart +++ b/lib/modules/vendors/presentation/widgets/vendor_sub_resource_panels.dart @@ -8,6 +8,7 @@ import '../../../../shared/widgets/app_dropdown.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_text_field.dart'; import '../providers/vendors_provider.dart'; +import '../../../../shared/widgets/app_toast.dart'; Future openVendorAddressPanel( BuildContext context, { @@ -102,7 +103,7 @@ class _VendorAddressPanelState extends ConsumerState { Future _save() async { if (!_formKey.currentState!.validate()) return; if (_addressType == null) { - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Please select address type')), ); return; @@ -137,7 +138,7 @@ class _VendorAddressPanelState extends ConsumerState { if (mounted) Navigator.of(context, rootNavigator: true).pop(true); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isSubmitting = false); @@ -279,7 +280,7 @@ class _VendorContactPanelState extends ConsumerState { if (mounted) Navigator.of(context, rootNavigator: true).pop(true); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isSubmitting = false); @@ -427,7 +428,7 @@ class _VendorBankDetailPanelState extends ConsumerState { if (mounted) Navigator.of(context, rootNavigator: true).pop(true); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); } } finally { if (mounted) setState(() => _isSubmitting = false); diff --git a/lib/shared/utils/file_download_helper.dart b/lib/shared/utils/file_download_helper.dart index 9acd8ed..7d35aef 100644 --- a/lib/shared/utils/file_download_helper.dart +++ b/lib/shared/utils/file_download_helper.dart @@ -1,8 +1,14 @@ import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; /// Saves [bytes] as a file download (web: same-tab download, desktop: save dialog). +/// +/// Returns `true` when the download was started / the file was saved. +/// On web, [FilePicker.saveFile] always returns `null` after triggering the +/// browser download (no filesystem path), so success is assumed when the call +/// completes without error. Future downloadFile({ required List bytes, required String fileName, @@ -11,5 +17,6 @@ Future downloadFile({ fileName: fileName, bytes: Uint8List.fromList(bytes), ); + if (kIsWeb) return true; return path != null; } diff --git a/lib/shared/widgets/api_feedback.dart b/lib/shared/widgets/api_feedback.dart index c699319..8f54575 100644 --- a/lib/shared/widgets/api_feedback.dart +++ b/lib/shared/widgets/api_feedback.dart @@ -3,22 +3,13 @@ import 'package:flutter/material.dart'; import '../../core/errors/failure.dart'; import '../../core/network/api_handler.dart'; import 'app_side_panel.dart'; +import 'app_toast.dart'; void showAccessDeniedSnackBar(BuildContext context, {String? message}) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.block, color: Colors.white, size: 18), - const SizedBox(width: 8), - Expanded( - child: Text(message ?? 'Access denied'), - ), - ], - ), - backgroundColor: const Color(0xFFDC2626), - behavior: SnackBarBehavior.floating, - ), + showAppToast( + context, + message ?? 'Access denied', + type: AppToastType.error, ); } @@ -38,19 +29,15 @@ void showApiFailureSnackBar(BuildContext context, Failure failure) { } if (isConflictFailure(failure)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(failure.message)), - ); + showAppToast(context, failure.message, type: AppToastType.warning); return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - failure is ValidationFailure - ? validationErrorMessage(failure) - : failure.message, - ), - ), + showAppToast( + context, + failure is ValidationFailure + ? validationErrorMessage(failure) + : failure.message, + type: AppToastType.error, ); } diff --git a/lib/shared/widgets/app_side_panel.dart b/lib/shared/widgets/app_side_panel.dart index db19ee5..39d2c9c 100644 --- a/lib/shared/widgets/app_side_panel.dart +++ b/lib/shared/widgets/app_side_panel.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/utils/responsive_utils.dart'; +import 'app_toast.dart'; /// Holds a full-width Quick Add form below a form row (so it stays clickable). class QuickAddInlineController extends ChangeNotifier { @@ -151,13 +152,13 @@ void showSidePanelSnackBar( BuildContext context, String message, { Duration duration = const Duration(seconds: 4), + AppToastType? type, }) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - behavior: SnackBarBehavior.floating, - duration: duration, - ), + showAppToast( + context, + message, + type: type ?? inferToastType(message), + duration: duration, ); } diff --git a/lib/shared/widgets/app_sidebar.dart b/lib/shared/widgets/app_sidebar.dart index d04daf6..b0ff48a 100644 --- a/lib/shared/widgets/app_sidebar.dart +++ b/lib/shared/widgets/app_sidebar.dart @@ -9,7 +9,6 @@ import '../../core/constants/enums.dart'; import '../../core/constants/route_constants.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/theme_provider.dart'; -import '../../core/theme/theme_provider.dart'; import '../../core/utils/favicon_store.dart'; import '../../modules/settings/presentation/providers/settings_provider.dart'; import '../models/user_model.dart'; diff --git a/lib/shared/widgets/app_toast.dart b/lib/shared/widgets/app_toast.dart new file mode 100644 index 0000000..753c238 --- /dev/null +++ b/lib/shared/widgets/app_toast.dart @@ -0,0 +1,485 @@ +import 'package:flutter/material.dart'; + +/// Semantic toast variants with matching icons. +enum AppToastType { + success, + error, + warning, + info, +} + +class _ToastEntry { + _ToastEntry({ + required this.id, + required this.message, + required this.type, + required this.duration, + }); + + final String id; + final String message; + final AppToastType type; + final Duration duration; +} + +/// Global controller for stacked top-center toasts. +class AppToastController extends ChangeNotifier { + AppToastController._(); + + static final AppToastController instance = AppToastController._(); + + final List<_ToastEntry> _entries = []; + int _seq = 0; + + String show({ + required String message, + AppToastType type = AppToastType.info, + Duration duration = const Duration(seconds: 4), + }) { + final trimmed = message.trim(); + if (trimmed.isEmpty) return ''; + + final id = 'toast_${++_seq}_${DateTime.now().microsecondsSinceEpoch}'; + _entries.add( + _ToastEntry( + id: id, + message: trimmed, + type: type, + duration: duration, + ), + ); + notifyListeners(); + return id; + } + + void dismiss(String id) { + final index = _entries.indexWhere((e) => e.id == id); + if (index < 0) return; + _entries.removeAt(index); + notifyListeners(); + } + + void clear() { + _entries.clear(); + notifyListeners(); + } +} + +/// Shows a premium toast at the top-center. Safe to call from any [context] +/// under [AppToastHost] (wired in [MaterialApp.builder]). +void showAppToast( + BuildContext context, + String message, { + AppToastType type = AppToastType.info, + Duration duration = const Duration(seconds: 4), +}) { + // Prefer overlay host; fall back to themed SnackBar if host is missing. + if (AppToastHost.of(context) != null || _toastHostMounted) { + AppToastController.instance.show( + message: message, + type: type, + duration: duration, + ); + return; + } + + final messenger = ScaffoldMessenger.maybeOf(context); + if (messenger == null) return; + messenger.showSnackBar( + _fallbackSnackBar(context, message, type: type, duration: duration), + ); +} + +void showSuccessToast(BuildContext context, String message) => + showAppToast(context, message, type: AppToastType.success); + +void showErrorToast(BuildContext context, String message) => + showAppToast(context, message, type: AppToastType.error); + +void showWarningToast(BuildContext context, String message) => + showAppToast(context, message, type: AppToastType.warning); + +void showInfoToast(BuildContext context, String message) => + showAppToast(context, message, type: AppToastType.info); + +bool _toastHostMounted = false; + +/// Infers toast type from message / snackbar styling for migration helpers. +AppToastType inferToastType( + String message, { + Color? backgroundColor, +}) { + final lower = message.toLowerCase(); + if (backgroundColor != null) { + final r = backgroundColor.r; + final g = backgroundColor.g; + final b = backgroundColor.b; + if (r > g && r > b && r > 0.5) return AppToastType.error; + if (g > r && g > b && g > 0.4) return AppToastType.success; + if (r > 0.6 && g > 0.4 && b < 0.4) return AppToastType.warning; + } + if (lower.contains('access denied') || + lower.contains('forbidden') || + lower.contains('failed') || + lower.contains('error') || + lower.contains('invalid') || + lower.contains('required') || + lower.contains('could not') || + lower.contains('unable') || + lower.contains('denied')) { + return AppToastType.error; + } + if (lower.contains('warning') || lower.contains('only posted')) { + return AppToastType.warning; + } + if (lower.contains('success') || + lower.contains('saved') || + lower.contains('updated') || + lower.contains('created') || + lower.contains('deleted') || + lower.contains('uploaded') || + lower.contains('downloaded') || + lower.contains('submitted') || + lower.contains('approved') || + lower.contains('cancelled') || + lower.contains('canceled') || + lower.contains('sent')) { + return AppToastType.success; + } + return AppToastType.info; +} + +/// Drop-in for legacy `ScaffoldMessenger.showSnackBar(SnackBar(...))` call sites. +void showAppToastFromSnackBar(BuildContext context, SnackBar snackBar) { + final message = _extractSnackBarMessage(snackBar.content) ?? 'Notification'; + final type = inferToastType( + message, + backgroundColor: snackBar.backgroundColor, + ); + showAppToast( + context, + message, + type: type, + duration: snackBar.duration, + ); +} + +String? _extractSnackBarMessage(Widget content) { + if (content is Text) return content.data; + if (content is RichText) { + return content.text.toPlainText(); + } + if (content is Row) { + final texts = []; + for (final child in content.children) { + final msg = _extractSnackBarMessage(child); + if (msg != null && msg.trim().isNotEmpty) texts.add(msg.trim()); + } + if (texts.isNotEmpty) return texts.join(' '); + } + if (content is Expanded) { + return _extractSnackBarMessage(content.child); + } + if (content is Flexible) { + return _extractSnackBarMessage(content.child); + } + if (content is Padding) { + final child = content.child; + if (child != null) return _extractSnackBarMessage(child); + } + if (content is Center) { + final child = content.child; + if (child != null) return _extractSnackBarMessage(child); + } + return null; +} + +SnackBar _fallbackSnackBar( + BuildContext context, + String message, { + required AppToastType type, + required Duration duration, +}) { + final scheme = Theme.of(context).colorScheme; + return SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + duration: duration, + backgroundColor: switch (type) { + AppToastType.success => scheme.primary, + AppToastType.error => scheme.error, + AppToastType.warning => scheme.tertiary, + AppToastType.info => scheme.secondary, + }, + ); +} + +/// Overlays stacked toasts at the top-center of the app. +class AppToastHost extends StatefulWidget { + const AppToastHost({super.key, required this.child}); + + final Widget child; + + static AppToastHost? of(BuildContext context) { + return context.findAncestorWidgetOfExactType(); + } + + @override + State createState() => _AppToastHostState(); +} + +class _AppToastHostState extends State { + @override + void initState() { + super.initState(); + _toastHostMounted = true; + AppToastController.instance.addListener(_onChange); + } + + @override + void dispose() { + AppToastController.instance.removeListener(_onChange); + _toastHostMounted = false; + super.dispose(); + } + + void _onChange() { + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + final media = MediaQuery.of(context); + final topInset = media.padding.top + 12; + final maxWidth = media.size.width < 600 + ? media.size.width - 24 + : media.size.width < 1024 + ? 420.0 + : 480.0; + + final entries = List<_ToastEntry>.from(AppToastController.instance._entries); + + return Stack( + children: [ + widget.child, + Positioned( + top: topInset, + left: 0, + right: 0, + child: IgnorePointer( + ignoring: entries.isEmpty, + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < entries.length; i++) ...[ + if (i > 0) const SizedBox(height: 8), + _ToastCard( + key: ValueKey(entries[i].id), + entry: entries[i], + onClose: () => + AppToastController.instance.dismiss(entries[i].id), + ), + ], + ], + ), + ), + ), + ), + ), + ), + ], + ); + } +} + +class _ToastCard extends StatefulWidget { + const _ToastCard({ + super.key, + required this.entry, + required this.onClose, + }); + + final _ToastEntry entry; + final VoidCallback onClose; + + @override + State<_ToastCard> createState() => _ToastCardState(); +} + +class _ToastCardState extends State<_ToastCard> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _fade; + late final Animation _slide; + bool _closing = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 280), + reverseDuration: const Duration(milliseconds: 220), + ); + _fade = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic); + _slide = Tween( + begin: const Offset(0, -0.35), + end: Offset.zero, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); + _controller.forward(); + Future.delayed(widget.entry.duration, () { + if (mounted && !_closing) _close(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _close() async { + if (_closing) return; + _closing = true; + await _controller.reverse(); + if (mounted) widget.onClose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scheme = theme.colorScheme; + final tokens = _ToastTokens.from(scheme, widget.entry.type); + + return SlideTransition( + position: _slide, + child: FadeTransition( + opacity: _fade, + child: Material( + color: scheme.surface.withValues(alpha: 0), + child: DecoratedBox( + decoration: BoxDecoration( + color: tokens.background, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: tokens.border), + boxShadow: [ + BoxShadow( + color: scheme.shadow.withValues(alpha: theme.brightness == Brightness.dark ? 0.45 : 0.14), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 12, 6, 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: tokens.iconBackground, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Icon(tokens.icon, size: 18, color: tokens.iconColor), + ), + const SizedBox(width: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 5), + child: Text( + widget.entry.message, + style: theme.textTheme.bodyMedium?.copyWith( + color: tokens.foreground, + height: 1.35, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 4), + Semantics( + button: true, + label: 'Dismiss', + child: InkWell( + onTap: _close, + customBorder: const CircleBorder(), + child: SizedBox( + width: 36, + height: 36, + child: Icon( + Icons.close_rounded, + size: 18, + color: tokens.foreground.withValues(alpha: 0.65), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _ToastTokens { + const _ToastTokens({ + required this.background, + required this.border, + required this.foreground, + required this.icon, + required this.iconColor, + required this.iconBackground, + }); + + final Color background; + final Color border; + final Color foreground; + final IconData icon; + final Color iconColor; + final Color iconBackground; + + factory _ToastTokens.from(ColorScheme scheme, AppToastType type) { + final accent = switch (type) { + AppToastType.success => scheme.primary, + AppToastType.error => scheme.error, + AppToastType.warning => scheme.tertiary, + AppToastType.info => scheme.secondary, + }; + final icon = switch (type) { + AppToastType.success => Icons.check_circle_rounded, + AppToastType.error => Icons.cancel_rounded, + AppToastType.warning => Icons.warning_amber_rounded, + AppToastType.info => Icons.info_rounded, + }; + + // Surface-based card so light/dark themes stay cohesive. + final background = Color.alphaBlend( + accent.withValues(alpha: 0.08), + scheme.surface, + ); + final border = accent.withValues(alpha: 0.28); + final foreground = scheme.onSurface; + final iconBackground = accent.withValues(alpha: 0.14); + + return _ToastTokens( + background: background, + border: border, + foreground: foreground, + icon: icon, + iconColor: accent, + iconBackground: iconBackground, + ); + } +} diff --git a/lib/shared/widgets/entity_attachments_card.dart b/lib/shared/widgets/entity_attachments_card.dart index 5e4f2e2..3e00c4c 100644 --- a/lib/shared/widgets/entity_attachments_card.dart +++ b/lib/shared/widgets/entity_attachments_card.dart @@ -7,6 +7,7 @@ import '../../core/utils/formatters.dart'; import '../models/entity_attachment_model.dart'; import '../utils/file_download_helper.dart'; import 'app_confirmation_dialog.dart'; +import 'app_toast.dart'; const kEntityAttachmentExtensions = ['pdf', 'jpg', 'jpeg', 'png', 'webp']; @@ -80,7 +81,7 @@ class _EntityAttachmentsCardState extends State { final bytes = file.bytes; if (bytes == null || bytes.isEmpty) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Could not read the selected file')), ); return; @@ -89,7 +90,7 @@ class _EntityAttachmentsCardState extends State { final ext = (file.extension ?? '').toLowerCase(); if (!kEntityAttachmentExtensions.contains(ext)) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar( content: Text('Only PDF, JPEG, PNG, and WebP files are allowed'), ), @@ -101,14 +102,14 @@ class _EntityAttachmentsCardState extends State { try { await widget.onUpload(bytes: bytes, filename: file.name); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text('${file.name} uploaded')), ); } catch (e) { if (!mounted) return; final message = e is Failure ? validationErrorMessage(e) : e.toString(); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(message)), ); } finally { @@ -127,7 +128,7 @@ class _EntityAttachmentsCardState extends State { ); } catch (e) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())), ); } finally { @@ -152,14 +153,14 @@ class _EntityAttachmentsCardState extends State { try { await widget.onDelete(attachment.id); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, const SnackBar(content: Text('Attachment deleted')), ); } catch (e) { if (!mounted) return; final message = e is Failure ? validationErrorMessage(e) : e.toString(); - ScaffoldMessenger.of(context).showSnackBar( + showAppToastFromSnackBar(context, SnackBar(content: Text(message)), ); } finally {