281 lines
8.7 KiB
Dart
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,
|
|
};
|
|
}
|