feat: complete leave approval MVP
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import 'package:aioa_mobile/features/requests/application/leave_request_controller.dart';
|
||||
import 'package:aioa_mobile/features/requests/data/leave_request_repository.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class LeaveRequestDetailPage extends ConsumerWidget {
|
||||
const LeaveRequestDetailPage({required this.id, super.key});
|
||||
final String id;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final detail = ref.watch(leaveRequestDetailProvider(id));
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('申请详情')),
|
||||
body: detail.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) => Center(child: Text('详情加载失败:$error')),
|
||||
data: (value) => _DetailBody(id: id, detail: value),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DetailBody extends ConsumerWidget {
|
||||
const _DetailBody({required this.id, required this.detail});
|
||||
final String id;
|
||||
final LeaveRequestDetail detail;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final request = detail.request;
|
||||
final formatter = DateFormat('yyyy-MM-dd HH:mm');
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_statusLabel(request.status),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_row('请假类型', _typeLabel(request.type)),
|
||||
_row('开始时间', formatter.format(request.startsAt.toLocal())),
|
||||
_row('结束时间', formatter.format(request.endsAt.toLocal())),
|
||||
_row('请假原因', request.reason),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'流程时间线',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (detail.timeline.isEmpty)
|
||||
const Card(
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit_note),
|
||||
title: Text('草稿已创建'),
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final event in detail.timeline) _TimelineTile(event: event),
|
||||
const SizedBox(height: 16),
|
||||
if (request.status == 'DRAFT')
|
||||
FilledButton.icon(
|
||||
onPressed: () => _transition(context, ref, 'submit'),
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
label: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text('提交审批'),
|
||||
),
|
||||
),
|
||||
if (request.status == 'PENDING')
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _transition(context, ref, 'withdraw'),
|
||||
icon: const Icon(Icons.undo),
|
||||
label: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text('撤回申请'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _transition(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String action,
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(action == 'submit' ? '提交审批' : '撤回申请'),
|
||||
content: Text(action == 'submit' ? '提交后将进入审批流程,确认继续?' : '确认撤回当前申请?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
final error = await ref
|
||||
.read(leaveRequestDetailProvider(id).notifier)
|
||||
.transition(action);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error ?? (action == 'submit' ? '已提交审批' : '已撤回'))),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _row(String label, String value) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(width: 78, child: Text(label)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
String _typeLabel(String value) => switch (value) {
|
||||
'PERSONAL' => '事假',
|
||||
'SICK' => '病假',
|
||||
'ANNUAL' => '年假',
|
||||
_ => value,
|
||||
};
|
||||
String _statusLabel(String value) => switch (value) {
|
||||
'DRAFT' => '草稿',
|
||||
'PENDING' => '审批中',
|
||||
'APPROVED' => '已通过',
|
||||
'REJECTED' => '已驳回',
|
||||
'WITHDRAWN' => '已撤回',
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
|
||||
class _TimelineTile extends StatelessWidget {
|
||||
const _TimelineTile({required this.event});
|
||||
final LeaveTimelineEvent event;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.radio_button_checked),
|
||||
title: Text(_eventLabel(event.eventType)),
|
||||
subtitle: Text(
|
||||
'${event.fromStatus} → ${event.toStatus}\n${DateFormat('MM-dd HH:mm').format(event.occurredAt.toLocal())}',
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
);
|
||||
|
||||
String _eventLabel(String value) => switch (value) {
|
||||
'LEAVE_REQUEST_SUBMITTED' => '申请已提交',
|
||||
'LEAVE_REQUEST_WITHDRAWN' => '申请已撤回',
|
||||
'LEAVE_REQUEST_APPROVED' => '申请已通过',
|
||||
'LEAVE_REQUEST_REJECTED' => '申请已驳回',
|
||||
'LEAVE_APPROVAL_TASK_APPROVED' => '审批节点已通过',
|
||||
'LEAVE_APPROVAL_TASK_REJECTED' => '审批节点已驳回',
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user