diff --git a/lib/screens/branches_screen.dart b/lib/screens/branches_screen.dart index 2f9a4b8..5f327d0 100644 --- a/lib/screens/branches_screen.dart +++ b/lib/screens/branches_screen.dart @@ -83,11 +83,17 @@ class _BranchesScreenState extends State { } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error loading branches. Check connection.'), - ), - ); + final errStr = e.toString().toLowerCase(); + final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') || + errStr.contains('session expired') || + errStr.contains('session_expired'); + if (!isSessionExpired) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error loading branches. Check connection.'), + ), + ); + } } } } diff --git a/lib/screens/loyalty_dashboard.dart b/lib/screens/loyalty_dashboard.dart index f69cf46..f10e23f 100644 --- a/lib/screens/loyalty_dashboard.dart +++ b/lib/screens/loyalty_dashboard.dart @@ -52,9 +52,15 @@ class _LoyaltyDashboardState extends State { } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error loading data: $e'))); + final errStr = e.toString().toLowerCase(); + final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') || + errStr.contains('session expired') || + errStr.contains('session_expired'); + if (!isSessionExpired) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to load dashboard. Please try again.')), + ); + } } } } diff --git a/lib/screens/notifications_screen.dart b/lib/screens/notifications_screen.dart index f7b057e..132d725 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -55,9 +55,15 @@ class _NotificationsScreenState extends State { } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error loading notifications: $e')), - ); + final errStr = e.toString().toLowerCase(); + final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') || + errStr.contains('session expired') || + errStr.contains('session_expired'); + if (!isSessionExpired) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to load notifications. Please try again.')), + ); + } } } } diff --git a/lib/screens/orders_screen.dart b/lib/screens/orders_screen.dart index 88fa834..eb28538 100644 --- a/lib/screens/orders_screen.dart +++ b/lib/screens/orders_screen.dart @@ -33,9 +33,15 @@ class _OrdersScreenState extends State { } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error loading history: $e'))); + final errStr = e.toString().toLowerCase(); + final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') || + errStr.contains('session expired') || + errStr.contains('session_expired'); + if (!isSessionExpired) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to load order history. Please try again.')), + ); + } } } } diff --git a/lib/services/odoo_service.dart b/lib/services/odoo_service.dart index 8bf35d7..63ccad8 100644 --- a/lib/services/odoo_service.dart +++ b/lib/services/odoo_service.dart @@ -154,6 +154,7 @@ class OdooService { 'brand_logo': (res['brand_logo'] as String?) ?? '', 'primary_color': (res['primary_color'] as String?) ?? '#C62828', 'secondary_color': (res['secondary_color'] as String?) ?? '#FF8F00', + 'tertiary_color': (res['tertiary_color'] as String?) ?? '#4A7C59', 'background_color': (res['background_color'] as String?) ?? '#FAF6EE', 'background_gradient_color': (res['background_gradient_color'] as String?) ?? '#F3EAD3', }; @@ -161,6 +162,7 @@ class OdooService { await ThemeManager.instance.updateConfig( primaryHex: configMap['primary_color']!, secondaryHex: configMap['secondary_color']!, + tertiaryHex: configMap['tertiary_color']!, backgroundHex: configMap['background_color']!, backgroundGradientHex: configMap['background_gradient_color']!, brandLogoB64: configMap['brand_logo']!, diff --git a/lib/services/theme_manager.dart b/lib/services/theme_manager.dart index e248a9e..89a7128 100644 --- a/lib/services/theme_manager.dart +++ b/lib/services/theme_manager.dart @@ -11,12 +11,14 @@ class ThemeManager extends ChangeNotifier { Color _primaryColor = AppTheme.primary; Color _secondaryColor = AppTheme.secondary; + Color _tertiaryColor = AppTheme.tertiary; Color _backgroundColor = AppTheme.surface; Color _backgroundGradientColor = const Color(0xFFF3EAD3); String _brandLogo = ''; Color get primaryColor => _primaryColor; Color get secondaryColor => _secondaryColor; + Color get tertiaryColor => _tertiaryColor; Color get backgroundColor => _backgroundColor; Color get backgroundGradientColor => _backgroundGradientColor; String get brandLogo => _brandLogo; @@ -24,6 +26,7 @@ class ThemeManager extends ChangeNotifier { ThemeData get themeData => AppTheme.getTheme( primaryColor: _primaryColor, secondaryColor: _secondaryColor, + tertiaryColor: _tertiaryColor, backgroundColor: _backgroundColor, ); @@ -32,6 +35,7 @@ class ThemeManager extends ChangeNotifier { final prefs = await SharedPreferences.getInstance(); final primHex = prefs.getString('theme_primary_color'); final secHex = prefs.getString('theme_secondary_color'); + final terHex = prefs.getString('theme_tertiary_color'); final bgHex = prefs.getString('theme_background_color'); final bgGradHex = prefs.getString('theme_background_gradient_color'); _brandLogo = prefs.getString('theme_brand_logo') ?? ''; @@ -42,6 +46,9 @@ class ThemeManager extends ChangeNotifier { if (secHex != null) { _secondaryColor = _parseHexColor(secHex) ?? AppTheme.secondary; } + if (terHex != null) { + _tertiaryColor = _parseHexColor(terHex) ?? AppTheme.tertiary; + } if (bgHex != null) { _backgroundColor = _parseHexColor(bgHex) ?? AppTheme.surface; } @@ -54,6 +61,7 @@ class ThemeManager extends ChangeNotifier { Future updateConfig({ required String primaryHex, required String secondaryHex, + required String tertiaryHex, required String backgroundHex, required String backgroundGradientHex, required String brandLogoB64, @@ -61,12 +69,14 @@ class ThemeManager extends ChangeNotifier { final prefs = await SharedPreferences.getInstance(); await prefs.setString('theme_primary_color', primaryHex); await prefs.setString('theme_secondary_color', secondaryHex); + await prefs.setString('theme_tertiary_color', tertiaryHex); await prefs.setString('theme_background_color', backgroundHex); await prefs.setString('theme_background_gradient_color', backgroundGradientHex); await prefs.setString('theme_brand_logo', brandLogoB64); _primaryColor = _parseHexColor(primaryHex) ?? AppTheme.primary; _secondaryColor = _parseHexColor(secondaryHex) ?? AppTheme.secondary; + _tertiaryColor = _parseHexColor(tertiaryHex) ?? AppTheme.tertiary; _backgroundColor = _parseHexColor(backgroundHex) ?? AppTheme.surface; _backgroundGradientColor = _parseHexColor(backgroundGradientHex) ?? const Color(0xFFF3EAD3); _brandLogo = brandLogoB64; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 09ebcf5..34e8110 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -7,8 +7,12 @@ class AppTheme { static const Color primaryContainer = Color(0xFF8A1C14); static const Color secondary = Color(0xFFB58428); // Warm Gold/Honey Amber static const Color secondaryContainer = Color(0xFFF3DCA2); + static const Color tertiary = Color(0xFF4A7C59); // Warm Sage/Forest Green + static const Color tertiaryContainer = Color(0xFFCCE8D6); // Soft sage tint static const Color onPrimaryContainer = Colors.white; static const Color onSecondaryContainer = Color(0xFF5A3E00); + static const Color onTertiary = Colors.white; + static const Color onTertiaryContainer = Color(0xFF0D3320); // Deep forest text // Warm Ivory & Earthy Surface Hierarchy static const Color surface = Color(0xFFFAF6EE); // Warm paper ivory background @@ -25,15 +29,28 @@ class AppTheme { static ThemeData get lightTheme => getTheme(); - static ThemeData getTheme({Color? primaryColor, Color? secondaryColor, Color? backgroundColor}) { + static ThemeData getTheme({Color? primaryColor, Color? secondaryColor, Color? tertiaryColor, Color? backgroundColor}) { final baseTheme = ThemeData.light(); final pColor = primaryColor ?? primary; final sColor = secondaryColor ?? secondary; + final tColor = tertiaryColor ?? tertiary; final bg = backgroundColor ?? surface; // Dynamically compute readable contrast text colors - final onPrimaryColor = pColor.computeLuminance() > 0.5 ? Color(0xFF2E251B) : Colors.white; - final onSecondaryColor = sColor.computeLuminance() > 0.5 ? Color(0xFF2E251B) : Colors.white; + final onPrimaryColor = pColor.computeLuminance() > 0.5 ? const Color(0xFF2E251B) : Colors.white; + final onSecondaryColor = sColor.computeLuminance() > 0.5 ? const Color(0xFF2E251B) : Colors.white; + final onTertiaryColor = tColor.computeLuminance() > 0.5 ? const Color(0xFF2E251B) : Colors.white; + // Tertiary container: a desaturated/lightened tint of the tertiary color + final tContainerColor = Color.lerp(tColor, Colors.white, 0.75) ?? tertiaryContainer; + final onTertiaryContainerColor = tColor.computeLuminance() > 0.5 + ? const Color(0xFF2E251B) + : Color.lerp(tColor, Colors.black, 0.7) ?? onTertiaryContainer; + + final isWhiteBg = bg.r > 0.98 && bg.g > 0.98 && bg.b > 0.98; + final lowestColor = isWhiteBg ? const Color(0xFFFCFAF7) : const Color(0xFFFCFAF6); + final lowColor = isWhiteBg ? const Color(0xFFF5F3ED) : const Color(0xFFF7F1E3); + final containerColor = isWhiteBg ? const Color(0xFFEDEAE1) : const Color(0xFFF2EAD8); + final highColor = isWhiteBg ? const Color(0xFFE2DDD2) : const Color(0xFFE5D5BA); return ThemeData( useMaterial3: true, @@ -41,13 +58,22 @@ class AppTheme { colorScheme: ColorScheme.light( primary: pColor, primaryContainer: pColor, + onPrimary: onPrimaryColor, secondary: sColor, secondaryContainer: sColor.withValues(alpha: 0.15), + onSecondary: onSecondaryColor, onSecondaryContainer: onSecondaryColor, + tertiary: tColor, + tertiaryContainer: tContainerColor, + onTertiary: onTertiaryColor, + onTertiaryContainer: onTertiaryContainerColor, surface: bg, + surfaceContainerLowest: lowestColor, + surfaceContainerLow: lowColor, + surfaceContainer: containerColor, + surfaceContainerHigh: highColor, onSurface: onSurface, onSurfaceVariant: onSurfaceVariant, - onPrimary: onPrimaryColor, error: const Color(0xFFB02500), ), textTheme: baseTheme.textTheme.copyWith( diff --git a/lib/widgets/subscription_list_widget.dart b/lib/widgets/subscription_list_widget.dart index e10915e..594935a 100644 --- a/lib/widgets/subscription_list_widget.dart +++ b/lib/widgets/subscription_list_widget.dart @@ -100,13 +100,18 @@ class _SubscriptionCard extends StatelessWidget { return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: PhysicalShape( - clipper: TicketClipper(), - color: colorScheme.surfaceContainerLowest, - elevation: 3, - shadowColor: Colors.black.withValues(alpha: 0.15), - child: Container( - padding: const EdgeInsets.all(20), + child: CustomPaint( + foregroundPainter: TicketBorderPainter( + color: colorScheme.outline.withValues(alpha: 0.15), + strokeWidth: 1.0, + ), + child: PhysicalShape( + clipper: TicketClipper(), + color: colorScheme.surfaceContainerLow, + elevation: 2, + shadowColor: Colors.black.withValues(alpha: 0.08), + child: Container( + padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -282,8 +287,9 @@ class _SubscriptionCard extends StatelessWidget { ), ), ), - ); - } + ), + ); +} } /// Custom Clipper for a ticket-like appearance with side notches. @@ -345,3 +351,47 @@ class DashedLinePainter extends CustomPainter { @override bool shouldRepaint(CustomPainter oldDelegate) => false; } + +/// Painter to draw the outer outline of the ticket shape. +class TicketBorderPainter extends CustomPainter { + final Color color; + final double strokeWidth; + + TicketBorderPainter({required this.color, this.strokeWidth = 1.0}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; + + final path = Path(); + path.moveTo(0, 0); + path.lineTo(size.width, 0); + + const double notchY = 49.0; + const double notchRadius = 8.0; + path.lineTo(size.width, notchY - notchRadius); + path.arcToPoint( + Offset(size.width, notchY + notchRadius), + radius: const Radius.circular(notchRadius), + clockwise: false, + ); + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + + path.lineTo(0, notchY + notchRadius); + path.arcToPoint( + const Offset(0, notchY - notchRadius), + radius: const Radius.circular(notchRadius), + clockwise: false, + ); + path.close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +}