import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../../../core/utils/formatters.dart'; import '../../../../shared/models/asset_model.dart'; import '../../../../shared/widgets/api_feedback.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_date_popup.dart'; import '../../../../shared/widgets/app_dropdown.dart'; import '../../../../shared/widgets/app_empty_state.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_text_field.dart'; import '../../data/repositories/asset_repository_impl.dart'; import '../providers/assets_provider.dart'; List checklistForAsset(AssetModel asset) { final fromSummary = asset.maintenance?.checklist; if (fromSummary != null && fromSummary.isNotEmpty) { return fromSummary; } return asset.maintenanceChecklistJson ?? const []; } Future openSubmitMaintenancePanel( BuildContext context, WidgetRef ref, { required AssetModel asset, }) { return showSidePanel( context, SubmitMaintenanceLogPanel(asset: asset), width: 600, ); } class SubmitMaintenanceLogPanel extends ConsumerStatefulWidget { const SubmitMaintenanceLogPanel({super.key, required this.asset}); final AssetModel asset; @override ConsumerState createState() => _SubmitMaintenanceLogPanelState(); } class _ChecklistRowState { _ChecklistRowState({ required this.label, required this.required, }) : status = required ? null : 'OK'; final String label; final bool required; /// Null until the user picks a status (required items must choose explicitly). String? status; final TextEditingController remarksController = TextEditingController(); void dispose() => remarksController.dispose(); } class _SubmitMaintenanceLogPanelState extends ConsumerState { final _formKey = GlobalKey(); final _remarksController = TextEditingController(); DateTime _performedDate = DateTime.now(); late final List<_ChecklistRowState> _rows; bool _isSubmitting = false; @override void initState() { super.initState(); final checklist = checklistForAsset(widget.asset); _rows = checklist .where((item) => item.label.trim().isNotEmpty) .map( (item) => _ChecklistRowState( label: item.label.trim(), required: item.required, ), ) .toList(); } @override void dispose() { _remarksController.dispose(); for (final row in _rows) { row.dispose(); } super.dispose(); } Future _pickPerformedDate() async { final picked = await showAppDatePopup( context: context, initialDate: _performedDate, firstDate: DateTime(2000), lastDate: DateTime(2100), helpText: 'Performed date', ); if (picked != null) { setState(() => _performedDate = picked); } } Future _save() async { if (!_formKey.currentState!.validate()) return; if (_rows.isEmpty) { showSidePanelSnackBar(context, 'No checklist items configured for this asset'); return; } final missingRequired = _rows.where((row) { if (!row.required) return false; final statusMissing = row.status == null || row.status!.trim().isEmpty; final remarksMissing = row.remarksController.text.trim().isEmpty; return statusMissing || remarksMissing; }).toList(); if (missingRequired.isNotEmpty) { showSidePanelSnackBar( context, 'Complete required checklist items: ${missingRequired.map((r) => r.label).join(', ')}', ); return; } setState(() => _isSubmitting = true); try { final payload = { 'performed_date': DateFormatter.toApiDate(_performedDate), 'checklist_json': _rows .map( (row) { final remarks = row.remarksController.text.trim(); return { 'label': row.label, 'status': row.status, 'remarks': remarks.isEmpty ? null : remarks, 'required': row.required, }; }, ) .toList(), if (_remarksController.text.trim().isNotEmpty) 'remarks': _remarksController.text.trim(), }; final result = await ref .read(assetRepositoryProvider) .createMaintenanceLog(widget.asset.id, payload); if (result.failure != null) throw result.failure!; ref.invalidate(myMaintenanceProvider); ref.invalidate(assetDetailProvider(widget.asset.id)); if (mounted) Navigator.of(context, rootNavigator: true).pop(true); } catch (e) { if (mounted) { showSidePanelApiError(context, e); } } finally { if (mounted) setState(() => _isSubmitting = false); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final assetLabel = widget.asset.assetCode?.trim().isNotEmpty == true ? '${widget.asset.assetName} (${widget.asset.assetCode})' : widget.asset.assetName; return SidePanelScaffold( title: 'Submit Maintenance', footer: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ OutlinedButton( onPressed: _isSubmitting ? null : () => Navigator.of(context, rootNavigator: true).pop(), child: const Text('Cancel'), ), const SizedBox(width: 12), AppButton( label: 'Submit Log', expand: false, icon: Icons.check, isLoading: _isSubmitting, onPressed: _isSubmitting ? null : _save, ), ], ), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( assetLabel, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( 'Record checklist results for this maintenance visit.', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), SidePanelSection( title: 'Visit', children: [ _DateField( label: 'Performed Date *', value: _performedDate, onPick: _pickPerformedDate, ), const SizedBox(height: 12), AppTextField( controller: _remarksController, label: 'Remarks', maxLines: 3, ), ], ), const SizedBox(height: 16), SidePanelSection( title: 'Checklist', children: [ if (_rows.isEmpty) const AppEmptyState( title: 'No checklist items', description: 'Configure a maintenance checklist on the asset first.', icon: Icons.checklist_outlined, ) else ..._rows.asMap().entries.map((entry) { final index = entry.key; final row = entry.value; return Padding( padding: EdgeInsets.only( bottom: index == _rows.length - 1 ? 0 : 12, ), child: _ChecklistItemCard( row: row, onStatusChanged: (status) { if (status == null) return; setState(() => row.status = status); }, ), ); }), ], ), const SizedBox(height: 16), AssetRecentMaintenanceLogsSection(assetId: widget.asset.id), ], ), ), ); } } /// Expandable recent maintenance logs (asset detail + submit panel). class AssetRecentMaintenanceLogsSection extends ConsumerStatefulWidget { const AssetRecentMaintenanceLogsSection({ super.key, required this.assetId, this.limit = 5, this.leadingActions, this.trailingActions, }); final String assetId; final int limit; /// Optional actions shown before Recent Logs (e.g. Log Maintenance). final List? leadingActions; /// Optional actions shown after Recent Logs (e.g. Transfer History). final List? trailingActions; @override ConsumerState createState() => _AssetRecentMaintenanceLogsSectionState(); } class _AssetRecentMaintenanceLogsSectionState extends ConsumerState { bool _showLogs = false; List? _logs; bool _logsLoading = false; String? _logsError; Future _loadLogs() async { setState(() { _showLogs = true; _logsLoading = true; _logsError = null; }); final result = await ref .read(assetRepositoryProvider) .getMaintenanceLogs(widget.assetId); if (!mounted) return; setState(() { _logsLoading = false; if (result.failure != null) { _logsError = result.failure!.message; _logs = null; } else { _logs = result.data ?? []; } }); } void _toggle() { if (_showLogs && _logs != null) { setState(() => _showLogs = !_showLogs); return; } _loadLogs(); } @override Widget build(BuildContext context) { // Refresh open logs after a new maintenance submit updates the asset. ref.listen(assetDetailProvider(widget.assetId), (previous, next) { if (_showLogs && previous != next) { _loadLogs(); } }); final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Align( alignment: Alignment.centerLeft, child: Wrap( spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ ...?widget.leadingActions, OutlinedButton.icon( onPressed: _logsLoading ? null : _toggle, icon: Icon( _showLogs ? Icons.expand_less : Icons.history, size: 18, ), label: const Text('Recent Logs'), ), ...?widget.trailingActions, ], ), ), if (_showLogs) ...[ const SizedBox(height: 12), if (_logsLoading) const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Center(child: CircularProgressIndicator()), ) else if (_logsError != null) Text( _logsError!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error, ), ) else if (_logs == null || _logs!.isEmpty) const AppEmptyState( title: 'No logs yet', description: 'Submitted maintenance visits will appear here.', icon: Icons.history_toggle_off_outlined, ) else ..._logs!.take(widget.limit).map( (log) => Padding( padding: const EdgeInsets.only(bottom: 8), child: _MaintenanceLogTile(log: log), ), ), ], ], ); } } class _DateField extends StatelessWidget { const _DateField({ required this.label, required this.value, required this.onPick, }); final String label; final DateTime? value; final VoidCallback onPick; @override Widget build(BuildContext context) { return TextFormField( readOnly: true, onTap: onPick, decoration: InputDecoration( labelText: label, hintText: 'Select date', suffixIcon: const Icon(Icons.calendar_today_outlined, size: 20), ), controller: TextEditingController( text: value != null ? DateFormatter.displayDate(value) : '', ), validator: (_) => value == null ? 'Required' : null, ); } } class _ChecklistItemCard extends StatelessWidget { const _ChecklistItemCard({ required this.row, required this.onStatusChanged, }); final _ChecklistRowState row; final ValueChanged onStatusChanged; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border.all(color: theme.dividerColor), borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( row.required ? '${row.label} *' : row.label, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), AppDropdown( label: row.required ? 'Status *' : 'Status', isDense: true, value: row.status, hint: 'Select status', options: const [ AppDropdownOption(value: 'OK', label: 'OK'), AppDropdownOption(value: 'NOT_OK', label: 'Not OK'), AppDropdownOption(value: 'NA', label: 'N/A'), ], onChanged: onStatusChanged, validator: row.required ? (v) => (v == null || v.trim().isEmpty) ? 'Status is required' : null : null, ), const SizedBox(height: 10), AppTextField( controller: row.remarksController, label: row.required ? 'Item remarks *' : 'Item remarks', validator: row.required ? (v) => (v == null || v.trim().isEmpty) ? 'Remarks are required for this checklist item' : null : null, ), ], ), ); } } class _MaintenanceLogTile extends StatelessWidget { const _MaintenanceLogTile({required this.log}); final AssetMaintenanceLogModel log; @override Widget build(BuildContext context) { final theme = Theme.of(context); final dateFormat = DateFormat('dd MMM yyyy'); final performed = log.performedDate != null ? dateFormat.format(log.performedDate!) : 'โ€”'; final nextDue = log.nextDueDate != null ? dateFormat.format(log.nextDueDate!) : null; final subtitleParts = [ 'Performed: $performed', if (nextDue != null) 'Next due: $nextDue', if (log.createdByName?.trim().isNotEmpty == true) log.createdByName!.trim(), ]; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( subtitleParts.join(' ยท '), style: theme.textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w600, ), ), if (log.remarks?.trim().isNotEmpty == true) ...[ const SizedBox(height: 4), Text( log.remarks!, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], if (log.checklistJson.isNotEmpty) ...[ const SizedBox(height: 6), Wrap( spacing: 6, runSpacing: 4, children: log.checklistJson.map((item) { final label = item['label']?.toString().trim(); final rawStatus = item['status']?.toString().trim() ?? 'โ€”'; final status = rawStatus.replaceAll('_', ' '); final remarks = item['remarks']?.toString().trim(); final title = (label != null && label.isNotEmpty) ? label : 'Item'; final chipText = (remarks != null && remarks.isNotEmpty) ? '$title: $status โ€” $remarks' : '$title: $status'; return Chip( visualDensity: VisualDensity.compact, label: Text( chipText, style: theme.textTheme.labelSmall, ), padding: EdgeInsets.zero, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); }).toList(), ), ], ], ), ); } }