import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../../core/constants/route_constants.dart'; import '../../../../core/errors/failure.dart'; import '../../../../shared/models/asset_model.dart'; import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_dropdown.dart'; import '../../../../shared/widgets/app_empty_state.dart'; import '../../../../shared/widgets/app_loading_view.dart'; import '../../../../shared/widgets/app_pagination.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/app_segmented_tab_bar.dart'; import '../providers/assets_provider.dart'; class AssetAlertsScreen extends ConsumerStatefulWidget { const AssetAlertsScreen({super.key}); @override ConsumerState createState() => _AssetAlertsScreenState(); } class _AssetAlertsScreenState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final alertsAsync = ref.watch(assetAlertsProvider); return Padding( padding: const EdgeInsets.all(24), child: alertsAsync.when( loading: () => const AppLoadingView(message: 'Loading alerts...'), error: (e, _) => ErrorView.fromFailure( e is Failure ? e : Failure.unknown(message: e.toString()), onRetry: () => ref.invalidate(assetAlertsProvider), ), data: (state) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ PageHeader( title: 'Asset Alerts', subtitle: 'Expiry and service due notifications', actions: [ OutlinedButton.icon( onPressed: () => context.go(RouteConstants.assets), icon: const Icon(Icons.inventory_2_outlined), label: const Text('Asset Master'), ), ], ), AppSegmentedTabBar( controller: _tabController, tabs: const [ AppSegmentedTab( label: 'Expiry Alerts', icon: Icons.event_busy_outlined, ), AppSegmentedTab( label: 'Service Alerts', icon: Icons.build_circle_outlined, ), ], ), const SizedBox(height: 12), Expanded( child: TabBarView( controller: _tabController, clipBehavior: Clip.none, children: [ _ExpiryAlertsTab(state: state), _ServiceAlertsTab(state: state), ], ), ), ], ), ), ); } } class _ExpiryAlertsTab extends ConsumerWidget { const _ExpiryAlertsTab({required this.state}); final AssetAlertsState state; @override Widget build(BuildContext context, WidgetRef ref) { final notifier = ref.read(assetAlertsProvider.notifier); final dateFormat = DateFormat('dd MMM yyyy'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ SizedBox( width: 160, child: AppDropdown( label: 'Days Ahead', isDense: true, value: state.expiryDays, options: const [7, 15, 30, 60, 90] .map((d) => AppDropdownOption(value: d, label: '$d Days')) .toList(), onChanged: (v) { if (v != null) notifier.setExpiryDays(v); }, ), ), SizedBox( width: 180, child: AppDropdown( label: 'Type', isDense: true, value: state.expiryType, options: const [ AppDropdownOption(value: null, label: 'All'), AppDropdownOption(value: 'AMC', label: 'AMC'), AppDropdownOption(value: 'INSURANCE', label: 'Insurance'), AppDropdownOption(value: 'WARRANTY', label: 'Warranty'), ], onChanged: notifier.setExpiryType, ), ), Padding( padding: const EdgeInsets.only(top: 8), child: IconButton( onPressed: () => notifier.refresh(), icon: const Icon(Icons.refresh), tooltip: 'Refresh', ), ), ], ), const SizedBox(height: 16), _AlertsOverview( total: state.expiryAlerts.length, label: 'Expiry Alerts In Selected Window', ), const SizedBox(height: 12), Expanded( child: state.expiryAlerts.isEmpty ? const AppEmptyState( title: 'No expiry alerts', description: 'No AMC, insurance or warranty expiries in this window.', icon: Icons.event_available_outlined, ) : ListView.separated( itemCount: state.expiryAlerts.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final alert = state.expiryAlerts[index]; return _AlertCard( alert: alert, dateLabel: alert.expiryDate != null ? dateFormat.format(alert.expiryDate!) : null, onTap: alert.assetId != null ? () => context.push('${RouteConstants.assets}/${alert.assetId}') : null, ); }, ), ), AppPagination( currentPage: state.expiryPage, totalPages: state.expiryTotalPages, totalItems: state.expiryAlerts.length, pageSize: 20, onPageChanged: notifier.setExpiryPage, onPageSizeChanged: (_) {}, ), ], ); } } class _ServiceAlertsTab extends ConsumerWidget { const _ServiceAlertsTab({required this.state}); final AssetAlertsState state; @override Widget build(BuildContext context, WidgetRef ref) { final notifier = ref.read(assetAlertsProvider.notifier); final dateFormat = DateFormat('dd MMM yyyy'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ SizedBox( width: 200, child: AppDropdown( label: 'Status', isDense: true, value: state.serviceStatus, options: const [ AppDropdownOption(value: null, label: 'All'), AppDropdownOption(value: 'OVERDUE', label: 'Overdue'), AppDropdownOption(value: 'DUE_THIS_WEEK', label: 'Due This Week'), AppDropdownOption(value: 'DUE_THIS_MONTH', label: 'Due This Month'), AppDropdownOption(value: 'UPCOMING', label: 'Upcoming'), ], onChanged: notifier.setServiceStatus, ), ), Padding( padding: const EdgeInsets.only(top: 8), child: IconButton( onPressed: () => notifier.refresh(), icon: const Icon(Icons.refresh), tooltip: 'Refresh', ), ), ], ), const SizedBox(height: 16), _AlertsOverview( total: state.serviceAlerts.length, label: 'Service Reminders', ), const SizedBox(height: 12), Expanded( child: state.serviceAlerts.isEmpty ? const AppEmptyState( title: 'No service alerts', description: 'All services are up to date.', icon: Icons.build_circle_outlined, ) : ListView.separated( itemCount: state.serviceAlerts.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final alert = state.serviceAlerts[index]; return _AlertCard( alert: alert, dateLabel: alert.dueDate != null ? dateFormat.format(alert.dueDate!) : null, onTap: alert.assetId != null ? () => context.push('${RouteConstants.assets}/${alert.assetId}') : null, ); }, ), ), ], ); } } class _AlertCard extends StatelessWidget { const _AlertCard({ required this.alert, this.dateLabel, this.onTap, }); final AssetAlertModel alert; final String? dateLabel; final VoidCallback? onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final visual = _alertVisualStyle(alert); final subtitleParts = [ if (alert.assetCode?.trim().isNotEmpty == true) alert.assetCode!.trim(), if (alert.locationName?.trim().isNotEmpty == true) alert.locationName!.trim(), if (dateLabel != null) 'Due: $dateLabel', ]; return AppCard( child: InkWell( borderRadius: BorderRadius.circular(12), onTap: onTap, child: LayoutBuilder( builder: (context, constraints) { final isCompact = constraints.maxWidth < 680; return Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: visual.color.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(10), ), child: Icon( _alertTypeIcon(alert.type), color: visual.color, size: 20, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (isCompact) ...[ Text( alert.assetName ?? alert.title ?? 'Asset ${alert.assetId ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), Align( alignment: Alignment.centerRight, child: Text( visual.description, textAlign: TextAlign.right, style: theme.textTheme.bodySmall?.copyWith( color: visual.color, fontWeight: FontWeight.w600, ), ), ), ] else Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( alert.assetName ?? alert.title ?? 'Asset ${alert.assetId ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: 10), Flexible( child: Text( visual.description, textAlign: TextAlign.right, style: theme.textTheme.bodySmall?.copyWith( color: visual.color, fontWeight: FontWeight.w600, ), ), ), ], ), if (subtitleParts.isNotEmpty) ...[ const SizedBox(height: 3), Text( subtitleParts.join(' ยท '), maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 6, children: [ if (alert.type?.trim().isNotEmpty == true) _AlertTag( label: alert.type!.replaceAll('_', ' ').toUpperCase(), color: theme.colorScheme.primary, ), if (alert.status?.trim().isNotEmpty == true) _AlertTag( label: alert.status!.replaceAll('_', ' '), color: theme.colorScheme.secondary, ), ], ), ], ), ), ], ), ); }, ), ), ); } } class _AlertTag extends StatelessWidget { const _AlertTag({ required this.label, required this.color, this.isEmphasized = false, }); final String label; final Color color; final bool isEmphasized; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withValues(alpha: isEmphasized ? 0.16 : 0.12), borderRadius: BorderRadius.circular(999), ), child: Text( label, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: color, fontWeight: FontWeight.w700, letterSpacing: 0.2, ), ), ); } } class _AlertVisualStyle { const _AlertVisualStyle({ required this.levelLabel, required this.description, required this.color, }); final String levelLabel; final String description; final Color color; } _AlertVisualStyle _alertVisualStyle(AssetAlertModel alert) { final days = alert.daysRemaining; if (days == null) { return const _AlertVisualStyle( levelLabel: 'warning', description: 'Check this asset notification.', color: Color(0xFF2563EB), ); } if (days <= -2) { final expiredDays = days.abs(); return _AlertVisualStyle( levelLabel: 'warning', description: 'Expired $expiredDays day${expiredDays == 1 ? '' : 's'} ago', color: const Color(0xFFDC2626), ); } if (days == -1) { return const _AlertVisualStyle( levelLabel: 'warning', description: 'Expired yesterday', color: Color(0xFFEA580C), ); } if (days == 0) { return const _AlertVisualStyle( levelLabel: 'warning', description: 'Expires today. Immediate action required.', color: Color(0xFFCA8A04), ); } return _AlertVisualStyle( levelLabel: 'warning', description: 'Expires in $days day${days == 1 ? '' : 's'}', color: const Color(0xFF16A34A), ); } IconData _alertTypeIcon(String? type) { switch (type?.toUpperCase()) { case 'AMC': return Icons.handshake_outlined; case 'INSURANCE': return Icons.shield_outlined; case 'WARRANTY': return Icons.verified_user_outlined; case 'SERVICE': return Icons.build_outlined; default: return Icons.notifications_outlined; } } class _AlertsOverview extends StatelessWidget { const _AlertsOverview({ required this.total, required this.label, }); final int total; final String label; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(10), ), child: Wrap( spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( '$total alerts', style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), ), Text( label, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const _LegendDot(color: Color(0xFFDC2626), label: 'Expired'), const _LegendDot(color: Color(0xFFEA580C), label: 'Yesterday'), const _LegendDot(color: Color(0xFFCA8A04), label: 'Today'), const _LegendDot(color: Color(0xFF16A34A), label: 'Upcoming'), ], ), ); } } class _LegendDot extends StatelessWidget { const _LegendDot({ required this.color, required this.label, }); final Color color; final String label; @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text(label, style: Theme.of(context).textTheme.labelSmall), ], ); } }