Initial commit
This commit is contained in:
421
lib/pages/work_order_detail_page.dart
Normal file
421
lib/pages/work_order_detail_page.dart
Normal file
@@ -0,0 +1,421 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user