422 lines
14 KiB
Dart
422 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../models/work_order.dart';
|
|
import '../providers/work_order_provider.dart';
|
|
import '../theme.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
|
|
class WorkOrderDetailPage extends ConsumerStatefulWidget {
|
|
final String orderId;
|
|
const WorkOrderDetailPage({super.key, required this.orderId});
|
|
|
|
@override
|
|
ConsumerState<WorkOrderDetailPage> createState() => _WorkOrderDetailPageState();
|
|
}
|
|
|
|
class _WorkOrderDetailPageState extends ConsumerState<WorkOrderDetailPage> {
|
|
@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 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),
|
|
_buildInfoCard(context, order, l10n),
|
|
const SizedBox(height: 20),
|
|
_buildDescriptionCard(context, order, l10n),
|
|
if (order.images.isNotEmpty) ...[
|
|
const SizedBox(height: 20),
|
|
_buildImagesCard(context, order, l10n),
|
|
],
|
|
const SizedBox(height: 20),
|
|
_buildTimeline(context, order, l10n),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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(l10n.assignee, order.assigneeName ?? l10n.notAssigned, Icons.assignment_ind_outlined),
|
|
const Divider(height: 24),
|
|
_buildInfoRow(l10n.category, order.category, Icons.folder_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: const TextStyle(fontSize: 12, color: AppColors.textTertiary),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDescriptionCard(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.description_outlined, size: 18, color: AppColors.primary),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
l10n.problemDesc,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
order.description,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
color: AppColors.textSecondary,
|
|
height: 1.6,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildImagesCard(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.image_outlined, size: 18, color: AppColors.primary),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
l10n.attachments,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 12,
|
|
children: order.images.map((img) {
|
|
return Container(
|
|
width: 100,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.background,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Icon(Icons.image, color: AppColors.textTertiary),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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: const 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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|