554 lines
18 KiB
Dart
554 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
import '../providers/work_order_provider.dart';
|
|
import '../providers/order_provider.dart';
|
|
import '../models/work_order.dart';
|
|
import '../models/order.dart';
|
|
import '../theme.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
import '../widgets/skeleton.dart';
|
|
|
|
class OrderWorkPage extends ConsumerStatefulWidget {
|
|
final String initialTab;
|
|
const OrderWorkPage({super.key, this.initialTab = 'work'});
|
|
|
|
@override
|
|
ConsumerState<OrderWorkPage> createState() => _OrderWorkPageState();
|
|
}
|
|
|
|
class _OrderWorkPageState extends ConsumerState<OrderWorkPage>
|
|
with SingleTickerProviderStateMixin {
|
|
late TabController _tabController;
|
|
WorkOrderStatus _workStatus = WorkOrderStatus.all;
|
|
OrderStatus _orderStatus = OrderStatus.all;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tabController = TabController(
|
|
length: 2,
|
|
vsync: this,
|
|
initialIndex: widget.initialTab == 'order' ? 1 : 0,
|
|
);
|
|
_tabController.addListener(_onTabChanged);
|
|
Future.microtask(() {
|
|
ref.read(workOrderProvider.notifier).loadOrders(WorkOrderStatus.all);
|
|
ref.read(orderProvider.notifier).loadOrders(OrderStatus.all);
|
|
});
|
|
}
|
|
|
|
void _onTabChanged() {
|
|
if (!_tabController.indexIsChanging) {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
return Scaffold(
|
|
backgroundColor: AppColors.background,
|
|
appBar: AppBar(
|
|
title: Text(l10n.ordersAndWorkOrders),
|
|
backgroundColor: AppColors.surface,
|
|
elevation: 0,
|
|
scrolledUnderElevation: 0,
|
|
surfaceTintColor: Colors.transparent,
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(52),
|
|
child: Container(
|
|
color: AppColors.surface,
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
|
child: Container(
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.background,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: TabBar(
|
|
controller: _tabController,
|
|
tabs: [
|
|
Tab(text: l10n.workOrder),
|
|
Tab(text: l10n.order),
|
|
],
|
|
indicator: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: AppColors.surface,
|
|
),
|
|
indicatorSize: TabBarIndicatorSize.tab,
|
|
indicatorPadding: const EdgeInsets.all(3),
|
|
labelColor: AppColors.textPrimary,
|
|
unselectedLabelColor: AppColors.textSecondary,
|
|
labelStyle: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
unselectedLabelStyle: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
dividerColor: Colors.transparent,
|
|
dividerHeight: 0,
|
|
splashFactory: NoSplash.splashFactory,
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
body: TabBarView(
|
|
controller: _tabController,
|
|
children: [
|
|
_WorkOrderTab(
|
|
status: _workStatus,
|
|
onStatusChanged: (s) {
|
|
setState(() => _workStatus = s);
|
|
ref.read(workOrderProvider.notifier).loadOrders(s);
|
|
},
|
|
),
|
|
_OrderTab(
|
|
status: _orderStatus,
|
|
onStatusChanged: (s) {
|
|
setState(() => _orderStatus = s);
|
|
ref.read(orderProvider.notifier).loadOrders(s);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _WorkOrderTab extends ConsumerWidget {
|
|
final WorkOrderStatus status;
|
|
final ValueChanged<WorkOrderStatus> onStatusChanged;
|
|
|
|
const _WorkOrderTab({required this.status, required this.onStatusChanged});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final state = ref.watch(workOrderProvider);
|
|
|
|
return Column(
|
|
children: [
|
|
_buildWorkFilterChips(context, l10n),
|
|
Expanded(
|
|
child: state.isLoading
|
|
? SkeletonList(
|
|
count: 5,
|
|
itemBuilder: (_) => const WorkOrderCardSkeleton(),
|
|
)
|
|
: state.orders.isEmpty
|
|
? _buildEmptyState(l10n.noWorkOrders)
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: state.orders.length,
|
|
itemBuilder: (context, index) {
|
|
return _buildWorkOrderCard(context, state.orders[index], index);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildWorkFilterChips(BuildContext context, AppLocalizations l10n) {
|
|
final filters = [
|
|
(WorkOrderStatus.all, l10n.all),
|
|
(WorkOrderStatus.pending, l10n.pending),
|
|
(WorkOrderStatus.processing, l10n.processing),
|
|
(WorkOrderStatus.completed, l10n.completed),
|
|
];
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
color: AppColors.surface,
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: filters.map((f) {
|
|
final isSelected = status == f.$1;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: FilterChip(
|
|
selected: isSelected,
|
|
onSelected: (_) => onStatusChanged(f.$1),
|
|
backgroundColor: AppColors.background,
|
|
selectedColor: AppColors.primary.withOpacity(0.1),
|
|
checkmarkColor: AppColors.primary,
|
|
label: Text(f.$2),
|
|
labelStyle: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
color: isSelected ? AppColors.primary : AppColors.textSecondary,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
side: BorderSide(
|
|
color: isSelected ? AppColors.primary : Colors.transparent,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWorkOrderCard(BuildContext context, WorkOrder order, int index) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
return GestureDetector(
|
|
onTap: () => context.push('/work-order/${order.id}'),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
order.title,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: order.statusColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
order.statusText,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: order.statusColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
order.description,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: AppColors.textSecondary,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.person_outline, size: 14, color: AppColors.textTertiary),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'${l10n.creator}: ${order.creatorName}',
|
|
style: TextStyle(fontSize: 12, color: AppColors.textTertiary),
|
|
),
|
|
if (order.assigneeName != null) ...[
|
|
const SizedBox(width: 12),
|
|
Icon(Icons.assignment_ind_outlined, size: 14, color: AppColors.textTertiary),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'${order.status == WorkOrderStatus.pending ? l10n.transferDept : l10n.assignee}: ${order.assigneeName}',
|
|
style: TextStyle(fontSize: 12, color: AppColors.textTertiary),
|
|
),
|
|
],
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: _getPriorityColor(order.priority).withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
_getPriorityText(context, order.priority),
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: _getPriorityColor(order.priority),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
).animate().fadeIn(duration: 300.ms, delay: (index * 50).ms).slideY(begin: 0.1, end: 0, duration: 300.ms);
|
|
}
|
|
|
|
Color _getPriorityColor(String priority) {
|
|
switch (priority) {
|
|
case 'urgent':
|
|
return AppColors.error;
|
|
case 'high':
|
|
return AppColors.warning;
|
|
default:
|
|
return AppColors.textTertiary;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
Widget _buildEmptyState(String text) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.inbox_outlined, size: 64, color: AppColors.textTertiary.withOpacity(0.5)),
|
|
const SizedBox(height: 16),
|
|
Text(text, style: TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OrderTab extends ConsumerWidget {
|
|
final OrderStatus status;
|
|
final ValueChanged<OrderStatus> onStatusChanged;
|
|
|
|
const _OrderTab({required this.status, required this.onStatusChanged});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final state = ref.watch(orderProvider);
|
|
|
|
return Column(
|
|
children: [
|
|
_buildOrderFilterChips(context, l10n),
|
|
Expanded(
|
|
child: state.isLoading
|
|
? SkeletonList(
|
|
count: 5,
|
|
itemBuilder: (_) => const OrderCardSkeleton(),
|
|
)
|
|
: state.orders.isEmpty
|
|
? _buildEmptyState(l10n.noOrders)
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: state.orders.length,
|
|
itemBuilder: (context, index) {
|
|
return _buildOrderCard(context, state.orders[index], index);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildOrderFilterChips(BuildContext context, AppLocalizations l10n) {
|
|
final filters = [
|
|
(OrderStatus.all, l10n.all),
|
|
(OrderStatus.pendingPayment, l10n.pendingPayment),
|
|
(OrderStatus.pendingVerification, l10n.pendingVerify),
|
|
(OrderStatus.verified, l10n.verified),
|
|
(OrderStatus.pendingRefund, l10n.pendingRefund),
|
|
(OrderStatus.refunded, l10n.refunded),
|
|
];
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
color: AppColors.surface,
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: filters.map((f) {
|
|
final isSelected = status == f.$1;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: FilterChip(
|
|
selected: isSelected,
|
|
onSelected: (_) => onStatusChanged(f.$1),
|
|
backgroundColor: AppColors.background,
|
|
selectedColor: AppColors.primary.withOpacity(0.1),
|
|
checkmarkColor: AppColors.primary,
|
|
label: Text(f.$2),
|
|
labelStyle: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
color: isSelected ? AppColors.primary : AppColors.textSecondary,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
side: BorderSide(
|
|
color: isSelected ? AppColors.primary : Colors.transparent,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOrderCard(BuildContext context, OrderItem order, int index) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
return GestureDetector(
|
|
onTap: () => context.push('/order/${order.id}'),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
order.orderNo,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: order.statusColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
order.statusText,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: order.statusColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
order.productName,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.person_outline, size: 14, color: AppColors.textTertiary),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
order.customerName,
|
|
style: TextStyle(fontSize: 13, color: AppColors.textSecondary),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Icon(Icons.confirmation_number_outlined, size: 14, color: AppColors.textTertiary),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'x${order.quantity}',
|
|
style: TextStyle(fontSize: 13, color: AppColors.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'¥${order.amount.toStringAsFixed(2)}',
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.primary,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
if (order.verifyCode != null)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.background,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
'${l10n.verifyCode}: ${order.verifyCode}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontFamily: 'monospace',
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
).animate().fadeIn(duration: 300.ms, delay: (index * 50).ms).slideY(begin: 0.1, end: 0, duration: 300.ms);
|
|
}
|
|
|
|
Widget _buildEmptyState(String text) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.receipt_long_outlined, size: 64, color: AppColors.textTertiary.withOpacity(0.5)),
|
|
const SizedBox(height: 16),
|
|
Text(text, style: TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|