Files
AIOA/mobile/lib/features/tasks/presentation/tasks_page.dart
T
2026-07-18 19:20:07 +08:00

281 lines
8.7 KiB
Dart

import 'package:aioa_mobile/features/tasks/application/approval_task_controller.dart';
import 'package:aioa_mobile/features/tasks/application/notification_controller.dart';
import 'package:aioa_mobile/features/tasks/data/approval_task_repository.dart';
import 'package:aioa_mobile/features/tasks/data/notification_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
class TasksPage extends ConsumerWidget {
const TasksPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifications = ref.watch(notificationProvider);
final tasks = ref.watch(approvalTaskProvider);
final unread =
notifications.value?.where((item) => !item.isRead).length ?? 0;
final taskCount = tasks.value?.length ?? 0;
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('待办与通知'),
bottom: TabBar(
tabs: [
Tab(text: taskCount == 0 ? '待办' : '待办 ($taskCount)'),
Tab(text: unread == 0 ? '通知' : '通知 ($unread)'),
],
),
),
body: TabBarView(
children: [
_ApprovalTaskList(tasks: tasks),
_NotificationList(notifications: notifications),
],
),
),
);
}
}
class _ApprovalTaskList extends ConsumerWidget {
const _ApprovalTaskList({required this.tasks});
final AsyncValue<List<ApprovalTaskItem>> tasks;
@override
Widget build(BuildContext context, WidgetRef ref) => tasks.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('待办加载失败:$error'),
const SizedBox(height: 8),
FilledButton(
onPressed: ref.read(approvalTaskProvider.notifier).refresh,
child: const Text('重试'),
),
],
),
),
data: (items) {
if (items.isEmpty) return const Center(child: Text('暂无待办'));
return RefreshIndicator(
onRefresh: ref.read(approvalTaskProvider.notifier).refresh,
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) =>
_ApprovalTaskCard(task: items[index]),
),
);
},
);
}
class _ApprovalTaskCard extends ConsumerWidget {
const _ApprovalTaskCard({required this.task});
final ApprovalTaskItem task;
@override
Widget build(BuildContext context, WidgetRef ref) {
final request = task.leaveRequest;
final formatter = DateFormat('MM-dd HH:mm');
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const CircleAvatar(child: Icon(Icons.assignment_ind_outlined)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.name,
style: const TextStyle(fontWeight: FontWeight.w700),
),
Text(_typeLabel(request.type)),
],
),
),
],
),
const SizedBox(height: 12),
Text(
'${formatter.format(request.startsAt.toLocal())}${formatter.format(request.endsAt.toLocal())}',
),
const SizedBox(height: 6),
Text(request.reason),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => _decide(context, ref, approved: false),
child: const Text('驳回'),
),
),
const SizedBox(width: 10),
Expanded(
child: FilledButton(
onPressed: () => _decide(context, ref, approved: true),
child: const Text('批准'),
),
),
],
),
],
),
),
);
}
Future<void> _decide(
BuildContext context,
WidgetRef ref, {
required bool approved,
}) async {
final comment = await showDialog<String>(
context: context,
builder: (context) => _DecisionDialog(approved: approved),
);
if (comment == null || !context.mounted) return;
final error = await ref
.read(approvalTaskProvider.notifier)
.decide(task: task, approved: approved, comment: comment);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error ?? (approved ? '已批准' : '已驳回'))),
);
}
String _typeLabel(String type) => switch (type) {
'PERSONAL' => '事假',
'SICK' => '病假',
'ANNUAL' => '年假',
_ => type,
};
}
class _DecisionDialog extends StatefulWidget {
const _DecisionDialog({required this.approved});
final bool approved;
@override
State<_DecisionDialog> createState() => _DecisionDialogState();
}
class _DecisionDialogState extends State<_DecisionDialog> {
final controller = TextEditingController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => AlertDialog(
title: Text(widget.approved ? '批准申请' : '驳回申请'),
content: TextField(
controller: controller,
maxLength: 1000,
maxLines: 3,
decoration: InputDecoration(
labelText: widget.approved ? '审批意见(选填)' : '驳回原因',
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.pop(context, controller.text),
child: Text(widget.approved ? '确认批准' : '确认驳回'),
),
],
);
}
class _NotificationList extends ConsumerWidget {
const _NotificationList({required this.notifications});
final AsyncValue<List<AppNotification>> notifications;
@override
Widget build(BuildContext context, WidgetRef ref) => notifications.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('通知加载失败:$error'),
const SizedBox(height: 8),
FilledButton(
onPressed: ref.read(notificationProvider.notifier).refresh,
child: const Text('重试'),
),
],
),
),
data: (items) {
if (items.isEmpty) return const Center(child: Text('暂无通知'));
return RefreshIndicator(
onRefresh: ref.read(notificationProvider.notifier).refresh,
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final item = items[index];
return Card(
color: item.isRead
? null
: Theme.of(
context,
).colorScheme.primaryContainer.withValues(alpha: 0.35),
child: ListTile(
leading: Icon(_icon(item.type)),
title: Text(
item.title,
style: TextStyle(
fontWeight: item.isRead ? FontWeight.w500 : FontWeight.w700,
),
),
subtitle: Text(
'${item.body}\n${DateFormat('MM-dd HH:mm').format(item.createdAt.toLocal())}',
),
isThreeLine: true,
trailing: item.isRead ? null : const Badge(),
onTap: item.isRead
? null
: () => ref
.read(notificationProvider.notifier)
.markRead(item.id),
),
);
},
),
);
},
);
IconData _icon(String type) => switch (type) {
'APPROVAL_TASK_ASSIGNED' => Icons.assignment_outlined,
'LEAVE_APPROVED' => Icons.check_circle_outline,
'LEAVE_REJECTED' => Icons.cancel_outlined,
_ => Icons.notifications_none,
};
}