import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../models/work_order.dart'; import '../providers/work_order_provider.dart'; import '../theme.dart'; import '../l10n/app_localizations.dart'; import '../widgets/skeleton.dart'; class WorkOrderDetailPage extends ConsumerStatefulWidget { final String orderId; const WorkOrderDetailPage({super.key, required this.orderId}); @override ConsumerState createState() => _WorkOrderDetailPageState(); } class _WorkOrderDetailPageState extends ConsumerState { bool _actionInProgress = false; @override void initState() { super.initState(); Future.microtask(() { ref.read(workOrderProvider.notifier).getDetail(widget.orderId); }); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final state = ref.watch(workOrderProvider); final hasMatch = state.orders.any((o) => o.id == widget.orderId); if (state.isLoading && !hasMatch) { return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(title: Text(l10n.workOrderDetail)), body: const DetailPageSkeleton(), ); } final order = state.orders.firstWhere( (o) => o.id == widget.orderId, orElse: () => state.orders.isNotEmpty ? state.orders.first : _mockOrder(context, l10n), ); return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(title: Text(l10n.workOrderDetail)), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(context, order, l10n), const SizedBox(height: 20), _buildDescriptionCard(context, order, l10n), const SizedBox(height: 20), _buildInfoCard(context, order, l10n), if (order.status != WorkOrderStatus.pending) ...[ const SizedBox(height: 20), _buildTimeline(context, order, l10n), ], ], ), ), bottomNavigationBar: _buildBottomBar(context, order, l10n), ); } Widget? _buildBottomBar(BuildContext context, WorkOrder order, AppLocalizations l10n) { if (order.status == WorkOrderStatus.pending) { return _ActionBar( children: [ Expanded( child: _SecondaryButton( label: l10n.transferOrder, icon: Icons.swap_horiz, onPressed: _actionInProgress ? null : () => _onTransfer(order, l10n), ), ), const SizedBox(width: 12), Expanded( flex: 2, child: _PrimaryButton( label: l10n.acceptOrder, icon: Icons.check_circle_outline, loading: _actionInProgress, onPressed: _actionInProgress ? null : () => _onAccept(order, l10n), ), ), ], ); } if (order.status == WorkOrderStatus.processing) { return _ActionBar( children: [ Expanded( child: _PrimaryButton( label: l10n.completeOrder, icon: Icons.task_alt, loading: _actionInProgress, onPressed: _actionInProgress ? null : () => _onComplete(order, l10n), ), ), ], ); } return null; } Future _onAccept(WorkOrder order, AppLocalizations l10n) async { setState(() => _actionInProgress = true); final ok = await ref.read(workOrderProvider.notifier).acceptOrder(order.id); if (!mounted) return; setState(() => _actionInProgress = false); _showToast(ok ? l10n.acceptSuccess : l10n.acceptFailed, success: ok); } Future _onComplete(WorkOrder order, AppLocalizations l10n) async { setState(() => _actionInProgress = true); final ok = await ref.read(workOrderProvider.notifier).completeOrder(order.id); if (!mounted) return; setState(() => _actionInProgress = false); _showToast(ok ? l10n.completeSuccess : l10n.completeFailed, success: ok); } Future _onTransfer(WorkOrder order, AppLocalizations l10n) async { final department = await showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (_) => _DepartmentPicker(currentAssignee: order.assigneeName), ); if (department == null || !mounted) return; setState(() => _actionInProgress = true); final ok = await ref.read(workOrderProvider.notifier).transferOrder(order.id, department); if (!mounted) return; setState(() => _actionInProgress = false); _showToast(ok ? l10n.transferSuccess : l10n.transferFailed, success: ok); } void _showToast(String message, {required bool success}) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Row( children: [ Icon( success ? Icons.check_circle : Icons.error_outline, color: Colors.white, size: 20, ), const SizedBox(width: 10), Expanded(child: Text(message)), ], ), backgroundColor: success ? const Color(0xFF10B981) : const Color(0xFFEF4444), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), duration: const Duration(seconds: 2), ), ); } Widget _buildHeader(BuildContext context, WorkOrder order, AppLocalizations l10n) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: AppGradients.primary, borderRadius: BorderRadius.circular(20), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(20), ), child: Text( order.statusText, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 16), Text( order.title, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 8), Row( children: [ Icon(Icons.location_on_outlined, size: 16, color: Colors.white.withOpacity(0.8)), const SizedBox(width: 6), Text( order.location ?? l10n.unknownLocation, style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.85), ), ), ], ), ], ), ); } Widget _buildInfoCard(BuildContext context, WorkOrder order, AppLocalizations l10n) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), ), child: Column( children: [ _buildInfoRow(l10n.workOrderId, order.id, Icons.tag), const Divider(height: 24), _buildInfoRow(l10n.creator, order.creatorName, Icons.person_outline), const Divider(height: 24), _buildInfoRow( order.status == WorkOrderStatus.pending ? l10n.transferDept : l10n.assignee, order.assigneeName ?? l10n.notAssigned, Icons.assignment_ind_outlined, ), const Divider(height: 24), _buildInfoRow(l10n.priority, _getPriorityText(context, order.priority), Icons.flag_outlined), const Divider(height: 24), _buildInfoRow( l10n.createTime, '${order.createdAt.month}/${order.createdAt.day} ${order.createdAt.hour.toString().padLeft(2, '0')}:${order.createdAt.minute.toString().padLeft(2, '0')}', Icons.access_time, ), ], ), ); } Widget _buildInfoRow(String label, String value, IconData icon) { return Row( children: [ Icon(icon, size: 18, color: AppColors.primary), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle(fontSize: 12, color: AppColors.textTertiary), ), const SizedBox(height: 2), Text( value, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: AppColors.textPrimary, ), ), ], ), ), ], ); } Widget _buildDescriptionCard(BuildContext context, WorkOrder order, AppLocalizations l10n) { final imageUrl = order.images.isNotEmpty ? order.images.first : 'https://picsum.photos/seed/wo${order.id}/800/450'; final heroTag = 'workOrderImage_${order.id}'; return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.description_outlined, size: 18, color: AppColors.primary), const SizedBox(width: 8), Text( l10n.problemDesc, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ], ), const SizedBox(height: 12), Text( order.description, style: TextStyle( fontSize: 15, color: AppColors.textSecondary, height: 1.6, ), ), const SizedBox(height: 16), GestureDetector( onTap: () => _openImageViewer(context, imageUrl, heroTag), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Hero( tag: heroTag, child: AspectRatio( aspectRatio: 16 / 9, child: CachedNetworkImage( imageUrl: imageUrl, fit: BoxFit.cover, placeholder: (_, __) => Container( color: AppColors.background, alignment: Alignment.center, child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppColors.primary.withOpacity(0.5), ), ), ), ), errorWidget: (_, __, ___) => Container( color: AppColors.background, alignment: Alignment.center, child: Icon( Icons.broken_image_outlined, color: AppColors.textTertiary, size: 32, ), ), ), ), ), ), ), ], ), ); } void _openImageViewer(BuildContext context, String url, String heroTag) { Navigator.of(context).push( PageRouteBuilder( opaque: false, barrierColor: Colors.black87, transitionDuration: const Duration(milliseconds: 280), reverseTransitionDuration: const Duration(milliseconds: 220), pageBuilder: (_, __, ___) => _ImageViewer(url: url, heroTag: heroTag), ), ); } Widget _buildTimeline(BuildContext context, WorkOrder order, AppLocalizations l10n) { return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.timeline, size: 18, color: AppColors.primary), const SizedBox(width: 8), Text( l10n.progress, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ], ), const SizedBox(height: 20), _TimelineItem( isFirst: true, isActive: true, title: l10n.workOrderCreated, subtitle: '${order.creatorName} ${l10n.createdWorkOrder}', time: '${order.createdAt.month}/${order.createdAt.day} ${order.createdAt.hour.toString().padLeft(2, '0')}:${order.createdAt.minute.toString().padLeft(2, '0')}', ), if (order.assigneeName != null) _TimelineItem( isActive: true, title: l10n.workOrderAssigned, subtitle: '${l10n.assignee} ${order.assigneeName}', time: '${order.createdAt.month}/${order.createdAt.day} ${(order.createdAt.hour + 1).toString().padLeft(2, '0')}:${order.createdAt.minute.toString().padLeft(2, '0')}', ), if (order.status.index >= 2) _TimelineItem( isActive: true, title: l10n.startProcessing, subtitle: l10n.staffStartedProcessing, time: '${order.createdAt.month}/${order.createdAt.day} ${(order.createdAt.hour + 2).toString().padLeft(2, '0')}:${order.createdAt.minute.toString().padLeft(2, '0')}', ), if (order.completedAt != null) _TimelineItem( isLast: true, isActive: true, title: l10n.workOrderCompleted, subtitle: l10n.workOrderFinished, time: '${order.completedAt!.month}/${order.completedAt!.day} ${order.completedAt!.hour.toString().padLeft(2, '0')}:${order.completedAt!.minute.toString().padLeft(2, '0')}', ), ], ), ); } String _getPriorityText(BuildContext context, String priority) { final l10n = AppLocalizations.of(context)!; switch (priority) { case 'urgent': return l10n.urgent; case 'high': return l10n.high; default: return l10n.normal; } } WorkOrder _mockOrder(BuildContext context, AppLocalizations l10n) { return WorkOrder( id: widget.orderId, title: l10n.unknownWorkOrder, description: l10n.workOrderLoadFailed, status: WorkOrderStatus.pending, creatorName: l10n.system, createdAt: DateTime.now(), priority: 'normal', category: l10n.other, images: [], ); } } class _TimelineItem extends StatelessWidget { final bool isFirst; final bool isLast; final bool isActive; final String title; final String subtitle; final String time; const _TimelineItem({ this.isFirst = false, this.isLast = false, required this.isActive, required this.title, required this.subtitle, required this.time, }); @override Widget build(BuildContext context) { return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ if (!isFirst) Container(width: 2, height: 20, color: isActive ? AppColors.primary.withOpacity(0.3) : AppColors.divider), Container( width: 12, height: 12, decoration: BoxDecoration( color: isActive ? AppColors.primary : AppColors.divider, shape: BoxShape.circle, border: Border.all(color: AppColors.surface, width: 2), ), ), if (!isLast) Expanded(child: Container(width: 2, color: isActive ? AppColors.primary.withOpacity(0.3) : AppColors.divider)), ], ), const SizedBox(width: 16), Expanded( child: Padding( padding: EdgeInsets.only(bottom: isLast ? 0 : 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: isActive ? AppColors.textPrimary : AppColors.textTertiary, ), ), const SizedBox(height: 4), Text( subtitle, style: TextStyle( fontSize: 13, color: isActive ? AppColors.textSecondary : AppColors.textTertiary, ), ), const SizedBox(height: 4), Text( time, style: TextStyle( fontSize: 12, color: AppColors.textTertiary, ), ), ], ), ), ), ], ), ); } } class _ImageViewer extends StatelessWidget { final String url; final String heroTag; const _ImageViewer({required this.url, required this.heroTag}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.transparent, body: Stack( children: [ GestureDetector( onTap: () => Navigator.of(context).pop(), behavior: HitTestBehavior.opaque, child: Center( child: Hero( tag: heroTag, child: InteractiveViewer( minScale: 1.0, maxScale: 4.0, child: CachedNetworkImage( imageUrl: url, fit: BoxFit.contain, placeholder: (_, __) => const Center( child: SizedBox( width: 32, height: 32, child: CircularProgressIndicator( strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation(Colors.white), ), ), ), errorWidget: (_, __, ___) => const Icon( Icons.broken_image_outlined, color: Colors.white54, size: 48, ), ), ), ), ), ), Positioned( top: MediaQuery.of(context).padding.top + 8, right: 12, child: Material( color: Colors.black38, shape: const CircleBorder(), child: IconButton( icon: const Icon(Icons.close, color: Colors.white, size: 22), onPressed: () => Navigator.of(context).pop(), ), ), ), ], ), ); } } class _ActionBar extends StatelessWidget { final List children; const _ActionBar({required this.children}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: AppColors.surface, border: Border(top: BorderSide(color: AppColors.divider, width: 0.5)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 12, offset: const Offset(0, -2), ), ], ), child: SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Row(children: children), ), ), ); } } class _PrimaryButton extends StatelessWidget { final String label; final IconData icon; final bool loading; final VoidCallback? onPressed; const _PrimaryButton({ required this.label, required this.icon, this.loading = false, required this.onPressed, }); @override Widget build(BuildContext context) { final disabled = onPressed == null; return Material( borderRadius: BorderRadius.circular(14), color: Colors.transparent, child: Ink( decoration: BoxDecoration( color: disabled ? AppColors.divider : AppColors.primary, borderRadius: BorderRadius.circular(14), ), child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onPressed, child: Container( height: 48, alignment: Alignment.center, child: loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2.2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: Colors.white, size: 18), const SizedBox(width: 6), Text( label, style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600, ), ), ], ), ), ), ), ); } } class _SecondaryButton extends StatelessWidget { final String label; final IconData icon; final VoidCallback? onPressed; const _SecondaryButton({ required this.label, required this.icon, required this.onPressed, }); @override Widget build(BuildContext context) { final disabled = onPressed == null; return Material( color: AppColors.background, borderRadius: BorderRadius.circular(14), child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onPressed, child: Container( height: 48, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.divider), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, color: disabled ? AppColors.textTertiary : AppColors.primary, size: 18, ), const SizedBox(width: 6), Text( label, style: TextStyle( color: disabled ? AppColors.textTertiary : AppColors.primary, fontSize: 15, fontWeight: FontWeight.w600, ), ), ], ), ), ), ); } } class _DepartmentPicker extends StatelessWidget { final String? currentAssignee; const _DepartmentPicker({this.currentAssignee}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final departments = <_DeptOption>[ _DeptOption(label: l10n.deptMaintenance, icon: Icons.build_outlined), _DeptOption(label: l10n.deptCleaning, icon: Icons.cleaning_services_outlined), _DeptOption(label: l10n.deptFrontDesk, icon: Icons.support_agent_outlined), _DeptOption(label: l10n.deptSecurity, icon: Icons.shield_outlined), _DeptOption(label: l10n.deptAdmin, icon: Icons.badge_outlined), _DeptOption(label: l10n.deptKitchen, icon: Icons.restaurant_outlined), ]; return Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 40, height: 4, decoration: BoxDecoration( color: AppColors.divider, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.fromLTRB(20, 4, 20, 8), child: Row( children: [ Text( l10n.selectTransferDept, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const Spacer(), IconButton( icon: Icon(Icons.close, color: AppColors.textTertiary, size: 20), onPressed: () => Navigator.of(context).pop(), visualDensity: VisualDensity.compact, ), ], ), ), Divider(height: 1, color: AppColors.divider), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.symmetric(vertical: 4), itemCount: departments.length, separatorBuilder: (_, __) => Divider(height: 1, color: AppColors.divider.withOpacity(0.5), indent: 60), itemBuilder: (_, i) { final dept = departments[i]; final selected = dept.label == currentAssignee; return InkWell( onTap: () => Navigator.of(context).pop(dept.label), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(dept.icon, color: AppColors.primary, size: 18), ), const SizedBox(width: 12), Expanded( child: Text( dept.label, style: TextStyle( fontSize: 15, color: AppColors.textPrimary, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, ), ), ), if (selected) Icon(Icons.check, color: AppColors.primary, size: 20), ], ), ), ); }, ), const SizedBox(height: 8), ], ), ), ); } } class _DeptOption { final String label; final IconData icon; const _DeptOption({required this.label, required this.icon}); }