bharat_erp/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart

417 lines
14 KiB
Dart

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<AssetMaintenanceScreen> createState() =>
_AssetMaintenanceScreenState();
}
class _AssetMaintenanceScreenState
extends ConsumerState<AssetMaintenanceScreen> {
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<void> _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<AppTableColumnOption> 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<AssetModel> assets;
final void Function(AssetModel asset) onSubmit;
final void Function(AssetModel asset) onView;
List<AppDataColumn<AssetModel>> _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<AssetModel>(
wrapInCard: false,
columns: columns,
rows: assets,
);
}
}
class _MaintenanceMobileList extends StatelessWidget {
const _MaintenanceMobileList({
required this.assets,
required this.onSubmit,
required this.onView,
});
final List<AssetModel> 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,
),
),
);
}
}