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 '../../../../core/utils/formatters.dart'; import '../../../../core/utils/responsive_utils.dart'; import '../../../../shared/models/asset_model.dart'; import '../../../../shared/providers/table_column_prefs_provider.dart'; import '../../../../shared/widgets/app_data_table.dart'; import '../../../../shared/widgets/app_empty_state.dart'; import '../../../../shared/widgets/app_form_toggle_field.dart'; import '../../../../shared/widgets/app_loading_view.dart'; import '../../../../shared/widgets/app_pagination.dart'; import '../../../../shared/widgets/app_table_action_icon.dart'; import '../../../../shared/widgets/app_table_column_selector.dart'; import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../providers/assets_provider.dart'; import '../utils/maintenance_due_display.dart'; import '../widgets/asset_maintenance_panel.dart'; class AssetMaintenanceScreen extends ConsumerStatefulWidget { const AssetMaintenanceScreen({super.key}); @override ConsumerState createState() => _AssetMaintenanceScreenState(); } class _AssetMaintenanceScreenState extends ConsumerState { bool _requestedFreshLoad = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (_requestedFreshLoad) return; _requestedFreshLoad = true; // Always refetch My Maintenance when opening this screen. ref.invalidate(myMaintenanceProvider); } @override Widget build(BuildContext context) { final maintenanceAsync = ref.watch(myMaintenanceProvider); return Padding( padding: const EdgeInsets.all(24), child: maintenanceAsync.when( loading: () => const AppLoadingView(message: 'Loading maintenance assets...'), error: (e, _) => ErrorView.fromFailure( e is Failure ? e : Failure.unknown(message: e.toString()), onRetry: () => ref.invalidate(myMaintenanceProvider), ), data: (state) => _MyMaintenanceBody(state: state), ), ); } } class _MyMaintenanceBody extends ConsumerWidget { const _MyMaintenanceBody({required this.state}); final MyMaintenanceState state; Future _openSubmit( BuildContext context, WidgetRef ref, AssetModel asset, ) async { final saved = await openSubmitMaintenancePanel(context, ref, asset: asset); if (!context.mounted) return; if (saved == true) { await ref.read(myMaintenanceProvider.notifier).refresh(); if (!context.mounted) return; showAppToastFromSnackBar( context, const SnackBar(content: Text('Maintenance log submitted')), ); } } @override Widget build(BuildContext context, WidgetRef ref) { final notifier = ref.read(myMaintenanceProvider.notifier); final isWide = context.isDesktop; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ PageHeader( title: 'My Maintenance', subtitle: 'Assets assigned to you for checklist-based maintenance', actions: [ AppTableColumnSelectorButton( tableId: _MaintenanceTable.tableId, columns: _MaintenanceTable.columnOptions, ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: () => context.go(RouteConstants.assets), icon: const Icon(Icons.inventory_2_outlined), label: const Text('Asset Master'), ), ], ), const SizedBox(height: 8), Wrap( spacing: 16, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ AppFormToggleField( label: 'Due only', subtitle: 'Show assets with maintenance due', value: state.dueOnly, onChanged: notifier.setDueOnly, ), IconButton( onPressed: state.isRefreshing ? null : () => notifier.refresh(), icon: state.isRefreshing ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), tooltip: 'Refresh', ), Text( '${state.total} asset${state.total == 1 ? '' : 's'}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), const SizedBox(height: 16), Expanded( child: state.assets.isEmpty ? AppEmptyState( title: state.dueOnly ? 'No due maintenance' : 'No maintenance assets', description: state.dueOnly ? 'Nothing is due right now. Turn off “Due only” to see all assigned assets.' : 'Assets where you are the maintenance in-charge will appear here.', icon: Icons.build_circle_outlined, ) : isWide ? _MaintenanceTable( assets: state.assets, onSubmit: (asset) => _openSubmit(context, ref, asset), onView: (asset) => context.push('${RouteConstants.assets}/${asset.id}'), ) : _MaintenanceMobileList( assets: state.assets, onSubmit: (asset) => _openSubmit(context, ref, asset), onView: (asset) => context.push('${RouteConstants.assets}/${asset.id}'), ), ), AppPagination( currentPage: state.page, totalPages: state.totalPages, totalItems: state.total, pageSize: state.limit, itemsOnPage: state.assets.length, onPageChanged: notifier.setPage, onPageSizeChanged: notifier.setPageSize, itemLabel: 'assets', ), ], ); } } class _MaintenanceTable extends ConsumerWidget { const _MaintenanceTable({ required this.assets, required this.onSubmit, required this.onView, }); static const tableId = 'my_maintenance'; static List get columnOptions => const [ AppTableColumnOption( id: 'asset_code', label: 'Asset Code', required: true, ), AppTableColumnOption(id: 'asset_name', label: 'Asset Name'), AppTableColumnOption(id: 'next_due', label: 'Next Due'), AppTableColumnOption(id: 'status', label: 'Status'), ]; final List assets; final void Function(AssetModel asset) onSubmit; final void Function(AssetModel asset) onView; List> _allColumns() { final dateFormat = DateFormat('dd MMM yyyy'); return [ AppDataColumn( id: 'asset_code', label: 'Asset Code', sortKey: 'asset_code', locked: true, flex: 1, searchText: (asset) => asset.assetCode ?? '', cellBuilder: (_, asset) => AppTableCell.link( asset.assetCode, onTap: () => onView(asset), ), ), AppDataColumn( id: 'asset_name', label: 'Asset Name', sortKey: 'asset_name', flex: 2, searchText: (asset) => asset.assetName, cellBuilder: (_, asset) => Text(asset.assetName), ), AppDataColumn( id: 'next_due', label: 'Next Due', sortKey: 'next_due', flex: 1, searchText: (asset) => DateFormatter.displayDate( asset.maintenance?.nextDueDate, ), sortValue: (asset) => asset.maintenance?.nextDueDate, cellBuilder: (_, asset) { final nextDue = asset.maintenance?.nextDueDate; return Text( nextDue != null ? dateFormat.format(nextDue) : '—', ); }, ), AppDataColumn( id: 'status', label: 'Status', sortKey: 'status', flex: 1, searchText: (asset) => asset.maintenance?.isDue == true ? 'due' : 'ok', cellBuilder: (_, asset) => _DueBadge( isDue: asset.maintenance?.isDue == true, daysUntilDue: asset.maintenance?.daysUntilDue, ), ), AppDataColumn( id: 'actions', label: 'Actions', flex: 1, alignment: Alignment.centerRight, enableSearch: false, locked: true, includeInColumnSelector: false, cellBuilder: (_, asset) => AppTableActions( children: [ AppTableActionIcon( tooltip: 'Submit checklist', icon: Icons.checklist_outlined, onPressed: () => onSubmit(asset), ), AppTableActionIcon( tooltip: 'View asset', icon: Icons.visibility_outlined, onPressed: () => onView(asset), ), ], ), ), ]; } @override Widget build(BuildContext context, WidgetRef ref) { final prefs = ref.watch(tableColumnPrefsProvider(tableId)); final columns = resolveAppDataColumns(_allColumns(), prefs); return AppDataTable( wrapInCard: false, columns: columns, rows: assets, ); } } class _MaintenanceMobileList extends StatelessWidget { const _MaintenanceMobileList({ required this.assets, required this.onSubmit, required this.onView, }); final List assets; final void Function(AssetModel asset) onSubmit; final void Function(AssetModel asset) onView; @override Widget build(BuildContext context) { final theme = Theme.of(context); final dateFormat = DateFormat('dd MMM yyyy'); return ListView.separated( itemCount: assets.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final asset = assets[index]; final nextDue = asset.maintenance?.nextDueDate; final subtitleParts = [ if (asset.assetCode?.trim().isNotEmpty == true) asset.assetCode!.trim(), if (nextDue != null) 'Next due: ${dateFormat.format(nextDue)}', ]; return Material( color: theme.colorScheme.surfaceContainerHighest.withValues( alpha: 0.35, ), borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => onSubmit(asset), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( asset.assetName, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), if (subtitleParts.isNotEmpty) ...[ const SizedBox(height: 4), Text( subtitleParts.join(' · '), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], const SizedBox(height: 8), _DueBadge( isDue: asset.maintenance?.isDue == true, daysUntilDue: asset.maintenance?.daysUntilDue, ), ], ), ), IconButton( tooltip: 'View asset', onPressed: () => onView(asset), icon: const Icon(Icons.visibility_outlined), ), ], ), ), ), ); }, ); } } class _DueBadge extends StatelessWidget { const _DueBadge({ required this.isDue, this.daysUntilDue, }); final bool isDue; final int? daysUntilDue; @override Widget build(BuildContext context) { final theme = Theme.of(context); final due = MaintenanceDueDisplay.fromDaysUntilDue(daysUntilDue); final color = due?.color ?? (isDue ? const Color(0xFFDC2626) : const Color(0xFF16A34A)); final label = due?.label ?? (isDue ? 'Due' : 'On track'); return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(999), ), child: Text( label, style: theme.textTheme.labelSmall?.copyWith( color: color, fontWeight: FontWeight.w700, letterSpacing: 0.2, ), ), ); } }