import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/constants/enums.dart'; import '../../../../core/constants/route_constants.dart'; import '../../../../core/errors/failure.dart'; import '../../../../core/utils/formatters.dart'; import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/utils/file_download_helper.dart'; import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/app_loading_view.dart'; import '../../../../shared/widgets/detail_overview_widgets.dart'; import '../../../../shared/widgets/error_view.dart'; import '../providers/grn_lookups_provider.dart'; import '../providers/grn_provider.dart'; import '../widgets/grn_attachments_card.dart'; import '../widgets/grn_line_items_editor.dart'; import '../widgets/grn_status_chip.dart'; import '../../../../shared/widgets/api_feedback.dart'; import '../../../../shared/widgets/app_toast.dart'; class GrnDetailScreen extends ConsumerStatefulWidget { const GrnDetailScreen({super.key, required this.grnId}); final String grnId; @override ConsumerState createState() => _GrnDetailScreenState(); } class _GrnDetailScreenState extends ConsumerState { bool _isWorking = false; bool _isDownloadingPdf = false; bool _requestedFreshLoad = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (_requestedFreshLoad) return; _requestedFreshLoad = true; // Always hit GET /grn/{id} when opening view. ref.invalidate(grnDetailProvider(widget.grnId)); } @override void didUpdateWidget(covariant GrnDetailScreen oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.grnId != widget.grnId) { ref.invalidate(grnDetailProvider(widget.grnId)); } } @override Widget build(BuildContext context) { final detailAsync = ref.watch(grnDetailProvider(widget.grnId)); final lookupsAsync = ref.watch(grnLookupsProvider); final canEdit = ref.can('grn', PermissionAction.update); final canDelete = ref.can('grn', PermissionAction.delete); final canExport = ref.can('grn', PermissionAction.export); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: detailAsync.when( loading: () => const AppLoadingView(message: 'Loading Purchase Receipt...'), error: (e, _) => ErrorView.fromFailure( e is Failure ? e : Failure.unknown(message: e.toString()), onRetry: () => ref.invalidate(grnDetailProvider(widget.grnId)), ), data: (grn) { final lookups = lookupsAsync.asData?.value; return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _DetailHeader( grn: grn, isWorking: _isWorking, isDownloadingPdf: _isDownloadingPdf, canEdit: canEdit, canExport: canExport, onBack: () => context.go(RouteConstants.grn), onPdf: () => _downloadPdf(grn), onEdit: () => context.push( '${RouteConstants.grn}/${grn.id}/edit', ), onCancel: () => _cancel(grn), ), if (grn.cancellationReason != null && grn.cancellationReason!.isNotEmpty) ...[ const SizedBox(height: 12), _CancellationBanner(reason: grn.cancellationReason!), ], const SizedBox(height: 16), _ReceiptDetailsCard(grn: grn, lookups: lookups), const SizedBox(height: 16), _LineItemsCard(grn: grn), const SizedBox(height: 16), GrnAttachmentsCard( grn: grn, canUpload: canEdit, canDelete: canDelete, ), if (grn.remarks?.trim().isNotEmpty == true) ...[ const SizedBox(height: 16), _SectionCard( title: 'REMARKS', child: Text( grn.remarks!.trim(), style: Theme.of(context).textTheme.bodyLarge, ), ), ], const SizedBox(height: 20), _DetailFooter(grn: grn), ], ), ); }, ), ); } Future _runWorkflow( Future Function() action, String success, ) async { setState(() => _isWorking = true); try { await action(); if (mounted) { showAppToastFromSnackBar(context, SnackBar(content: Text(success))); } } catch (e) { if (mounted) { showAppToastFromSnackBar( context, SnackBar(content: Text(errorDisplayMessage(e))), ); } } finally { if (mounted) setState(() => _isWorking = false); } } Future _cancel(GrnModel grn) async { final reasonController = TextEditingController(); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Cancel Purchase Receipt'), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Cancel ${grn.grnNumber ?? grn.id}? This will reverse PO receipts.', ), const SizedBox(height: 16), AppTextField( label: 'Cancellation Reason *', controller: reasonController, maxLines: 3, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Close'), ), FilledButton( onPressed: () { if (reasonController.text.trim().isEmpty) return; Navigator.pop(context, true); }, child: const Text('Cancel Purchase Receipt'), ), ], ), ); if (confirmed != true || !mounted) return; await _runWorkflow( () => ref.read(grnDetailProvider(widget.grnId).notifier).cancel( cancellationReason: reasonController.text.trim(), ), 'Purchase Receipt cancelled', ); reasonController.dispose(); } Future _downloadPdf(GrnModel grn) async { setState(() => _isDownloadingPdf = true); try { final bytes = await ref .read(grnDetailProvider(widget.grnId).notifier) .downloadPdf(); await downloadFile( bytes: bytes, fileName: '${grn.grnNumber ?? 'PurchaseReceipt-${grn.id}'}.pdf', ); if (mounted) { showAppToastFromSnackBar(context, const SnackBar(content: Text('PDF downloaded'))); } } catch (e) { if (mounted) { showAppToastFromSnackBar( context, SnackBar(content: Text(errorDisplayMessage(e))), ); } } finally { if (mounted) setState(() => _isDownloadingPdf = false); } } } String _userLabel(List? users, int? userId) { if (userId == null || users == null) return '—'; for (final user in users) { if (int.tryParse(user.id) == userId) return user.name; } return 'User #$userId'; } String _displayOrDash(String? value) { final trimmed = value?.trim(); if (trimmed == null || trimmed.isEmpty) return '—'; return trimmed; } class _DetailHeader extends StatelessWidget { const _DetailHeader({ required this.grn, required this.isWorking, required this.isDownloadingPdf, required this.canEdit, required this.canExport, required this.onBack, required this.onPdf, required this.onEdit, required this.onCancel, }); final GrnModel grn; final bool isWorking; final bool isDownloadingPdf; final bool canEdit; final bool canExport; final VoidCallback onBack; final VoidCallback onPdf; final VoidCallback onEdit; final VoidCallback onCancel; @override Widget build(BuildContext context) { final theme = Theme.of(context); final subtitleParts = [ if (grn.poNumber?.trim().isNotEmpty == true) 'PO ${grn.poNumber!.trim()}', if (grn.vendorName?.trim().isNotEmpty == true) grn.vendorName!.trim(), if (grn.locationName?.trim().isNotEmpty == true) grn.locationName!.trim(), ]; final actions = Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: [ if (canExport) _HeaderActionButton( label: 'PDF', icon: Icons.description_outlined, isLoading: isDownloadingPdf, onPressed: (isWorking || isDownloadingPdf) ? null : onPdf, ), if (canEdit && grn.canEdit) _HeaderActionButton( label: 'Edit', icon: Icons.edit_outlined, onPressed: isWorking ? null : onEdit, ), if (canEdit && grn.canCancel) _HeaderActionButton( label: 'Cancel', icon: Icons.block_outlined, destructive: true, onPressed: isWorking ? null : onCancel, ), ], ); final titleBlock = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( grn.grnNumber ?? 'Purchase Receipt #${grn.id}', style: theme.textTheme.headlineSmall, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 10), GrnStatusChip(status: grn.status, compact: true), ], ), if (subtitleParts.isNotEmpty) ...[ const SizedBox(height: 4), Text( subtitleParts.join(' · '), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ], ], ); return LayoutBuilder( builder: (context, constraints) { final stack = constraints.maxWidth < 800; if (stack) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ IconButton( tooltip: 'Back', onPressed: onBack, icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 4), Expanded(child: titleBlock), ], ), const SizedBox(height: 12), actions, ], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ IconButton( tooltip: 'Back', onPressed: onBack, icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 4), Expanded(child: titleBlock), const SizedBox(width: 12), actions, ], ); }, ); } } class _HeaderActionButton extends StatelessWidget { const _HeaderActionButton({ required this.label, required this.icon, required this.onPressed, this.isLoading = false, this.destructive = false, }); final String label; final IconData icon; final VoidCallback? onPressed; final bool isLoading; final bool destructive; static const double _height = 40; static const double _radius = 8; static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 14, vertical: 0); @override Widget build(BuildContext context) { final theme = Theme.of(context); final error = theme.colorScheme.error; final style = ButtonStyle( minimumSize: const WidgetStatePropertyAll(Size(0, _height)), fixedSize: const WidgetStatePropertyAll(Size.fromHeight(_height)), padding: const WidgetStatePropertyAll(_padding), shape: WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: BorderRadius.circular(_radius)), ), visualDensity: VisualDensity.standard, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ); final child = Row( mainAxisSize: MainAxisSize.min, children: [ if (isLoading) SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: destructive ? error : null, ), ) else Icon(icon, size: 18), const SizedBox(width: 8), Text(label), ], ); if (destructive) { return OutlinedButton( onPressed: onPressed, style: style.copyWith( foregroundColor: WidgetStatePropertyAll(error), side: WidgetStatePropertyAll(BorderSide(color: error)), ), child: child, ); } return OutlinedButton( onPressed: onPressed, style: style, child: child, ); } } class _CancellationBanner extends StatelessWidget { const _CancellationBanner({required this.reason}); final String reason; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: theme.colorScheme.errorContainer.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(8), border: Border.all( color: theme.colorScheme.error.withValues(alpha: 0.35), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline, color: theme.colorScheme.error, size: 20), const SizedBox(width: 10), Expanded( child: Text( 'Cancellation reason: $reason', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, ), ), ), ], ), ); } } class _SectionCard extends StatelessWidget { const _SectionCard({ required this.title, required this.child, }); final String title; final Widget child; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( color: theme.colorScheme.outline.withValues(alpha: 0.2), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( title, style: theme.textTheme.labelMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, letterSpacing: 0.8, ), ), const SizedBox(height: 16), child, ], ), ); } } class _ReceiptDetailsCard extends StatelessWidget { const _ReceiptDetailsCard({ required this.grn, required this.lookups, }); final GrnModel grn; final GrnLookups? lookups; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final locationDisplay = _displayOrDash( [ if (grn.locationName?.trim().isNotEmpty == true) grn.locationName!.trim(), if (grn.locationType?.trim().isNotEmpty == true) '(${grn.locationType!.trim()})', ].join(' ').trim(), ); return _SectionCard( title: 'RECEIPT DETAILS', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DetailOverviewSection( title: 'Summary', child: DetailSummaryStrip( metrics: [ DetailSummaryMetric( icon: Icons.flag_outlined, label: 'Status', accent: scheme.secondary, child: GrnStatusChip(status: grn.status, compact: true), ), DetailSummaryMetric( icon: Icons.calendar_today_outlined, label: 'Purchase Receipt Date', child: Text( DateFormatter.displayDate(grn.grnDate), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), ), DetailSummaryMetric( icon: Icons.receipt_long_outlined, label: 'PO Number', child: Text( _displayOrDash(grn.poNumber), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), ), DetailSummaryMetric( icon: Icons.payments_outlined, label: 'Invoice Amount', accent: scheme.primary, child: Text( grn.vendorInvoiceAmount != null ? CurrencyFormatter.format(grn.vendorInvoiceAmount!) : '—', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, ), ), ), ], ), ), DetailOverviewSection( title: 'Receipt', child: DetailInfoGrid( items: [ DetailInfoItem('Vendor', _displayOrDash(grn.vendorName)), DetailInfoItem('Location', locationDisplay), DetailInfoItem( 'Vendor Invoice No', _displayOrDash(grn.vendorInvoiceNo), ), DetailInfoItem( 'Vendor Invoice Date', DateFormatter.displayDate(grn.vendorInvoiceDate), ), DetailInfoItem( 'Received By', _userLabel(lookups?.users, grn.receivedBy), ), DetailInfoItem( 'Quality Checked By', _userLabel(lookups?.users, grn.qualityCheckedBy), ), ], ), ), DetailOverviewSection( title: 'Transport', showDivider: false, child: DetailInfoGrid( items: [ DetailInfoItem('Vehicle No', _displayOrDash(grn.vehicleNo)), DetailInfoItem('LR No', _displayOrDash(grn.lrNo)), DetailInfoItem( 'LR Date', DateFormatter.displayDate(grn.lrDate), ), ], ), ), ], ), ); } } class _LineItemsCard extends StatelessWidget { const _LineItemsCard({required this.grn}); final GrnModel grn; @override Widget build(BuildContext context) { return _SectionCard( title: 'LINE ITEMS · ${grn.items.length}', child: GrnItemsTable(items: grn.items), ); } } class _DetailFooter extends StatelessWidget { const _DetailFooter({required this.grn}); final GrnModel grn; @override Widget build(BuildContext context) { final theme = Theme.of(context); final parts = [ if (grn.createdAt != null) 'Created ${DateFormatter.displayDateTime(grn.createdAt)}', if (grn.updatedAt != null) 'Updated ${DateFormatter.displayDateTime(grn.updatedAt)}', ]; if (parts.isEmpty) return const SizedBox.shrink(); return Text( parts.join(' · '), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ); } }