modify clockin & leave

This commit is contained in:
2026-03-04 23:45:26 +08:00
parent ce706694ef
commit 0516db1690
7 changed files with 163 additions and 77 deletions
+98 -72
View File
@@ -1,66 +1,75 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import './leave_model.dart';
import './leave_api.dart';
class LeaveDetail extends StatelessWidget {
class LeaveDetail extends StatefulWidget {
final Leave leave;
const LeaveDetail({required this.leave, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('請假單詳情'),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0.5,
State<LeaveDetail> createState() => _LeaveDetailState();
}
class _LeaveDetailState extends State<LeaveDetail> {
final LeaveApiService _apiService = LeaveApiService();
bool _isWithdrawing = false;
// 實作撤回邏輯
Future<void> _handleWithdraw() async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('確認撤回'),
content: const Text('您確定要撤回此張請假單嗎?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('確定', style: TextStyle(color: Colors.red))),
],
),
);
if (confirm == true) {
setState(() => _isWithdrawing = true);
final success = await _apiService.withdrawLeave(widget.leave.billNo);
setState(() => _isWithdrawing = false);
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已成功撤回')));
Navigator.pop(context, true); // 回傳 true 讓前一頁 (Manager) 重新整理列表
}
}
}
@override
Widget build(BuildContext context) {
final leave = widget.leave;
return Scaffold(
appBar: AppBar(title: const Text('請假單詳情')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 頂部狀態區塊
_buildHeaderStatus(),
const SizedBox(height: 24),
// 主要資訊區塊 (使用卡片包裝)
_buildInfoCard(context),
const SizedBox(height: 24),
// 請假事由區塊
const Text(
'請假事由',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
// 修正 3: 新增簽核進度區塊
const Text('簽核進度', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey)),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
),
child: Text(
leave.leaveNote ?? '未填寫事由',
style: const TextStyle(fontSize: 15, height: 1.5),
),
),
_buildFlowTimeline(),
const SizedBox(height: 24),
const SizedBox(height: 32),
// 底部操作按鈕 (例如:若為草稿可編輯,或撤回)
// 修正 5: 實作撤回按鈕邏輯
if (leave.flowStatus == '0' || leave.flowStatus == '1')
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
// 實作撤回或取消邏輯
},
icon: const Icon(Icons.history_outlined),
label: const Text('撤回申請'),
onPressed: _isWithdrawing ? null : _handleWithdraw,
icon: _isWithdrawing ? const CircularProgressIndicator(strokeWidth: 2) : const Icon(Icons.history_outlined),
label: Text(_isWithdrawing ? '處理中...' : '撤回申請'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
@@ -74,7 +83,6 @@ class LeaveDetail extends StatelessWidget {
);
}
// 頂部狀態顯示:呈現單號與醒目的狀態標籤
Widget _buildHeaderStatus() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -82,73 +90,91 @@ class LeaveDetail extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'單號: ${leave.billNo}',
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
Text('單號: ${widget.leave.billNo}', style: const TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 4),
Text(
leave.leaveType,
style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
// 修正 4: 顯示 leaveTypeName
Text(widget.leave.leaveTypeName, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
],
),
Chip(
backgroundColor: leave.statusColor.withOpacity(0.1),
side: BorderSide(color: leave.statusColor),
label: Text(
leave.statusText,
style: TextStyle(color: leave.statusColor, fontWeight: FontWeight.bold),
),
backgroundColor: widget.leave.statusColor.withOpacity(0.1),
side: BorderSide(color: widget.leave.statusColor),
label: Text(widget.leave.statusText, style: TextStyle(color: widget.leave.statusColor, fontWeight: FontWeight.bold)),
),
],
);
}
// 核心資訊卡片
Widget _buildInfoCard(BuildContext context) {
// 處理代理人顯示文字 (防呆處理)
String displayAgent = widget.leave.agentName.isNotEmpty
? '${widget.leave.agentName} (${widget.leave.agentId})'
: widget.leave.agentId;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(color: Colors.grey.shade200),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15), side: BorderSide(color: Colors.grey.shade200)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
_buildDetailRow(Icons.calendar_month, '請假期間', leave.formattedRange),
_buildDetailRow(Icons.calendar_month, '請假期間', widget.leave.formattedRange),
const Divider(height: 30),
_buildDetailRow(Icons.timer_outlined, '請假時數', '${leave.days}${leave.hours} 小時'),
_buildDetailRow(Icons.timer_outlined, '請假時數', '${widget.leave.days}${widget.leave.hours} 小時'),
const Divider(height: 30),
_buildDetailRow(Icons.person_outline, '代理人', leave.agentId),
// 修正 6: 顯示帶有姓名的代理人資訊
_buildDetailRow(Icons.person_outline, '代理人', displayAgent),
const Divider(height: 30),
_buildDetailRow(Icons.edit_calendar, '申請日期',
leave.billDate != null ? DateFormat('yyyy-MM-dd').format(leave.billDate!) : 'N/A'),
_buildDetailRow(Icons.edit_note, '事由', widget.leave.leaveNote ?? '未填寫'),
],
),
),
);
}
// 輔助元件:建立細節列
Widget _buildDetailRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.blueAccent),
const SizedBox(width: 12),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
const Spacer(),
// 使用 Flexible 限制文字寬度並允許換行
Flexible(
child: Text(
value,
textAlign: TextAlign.end, // 靠右對齊
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
softWrap: true, // 允許自動換行
),
Expanded(
flex: 2,
child: Text(value, textAlign: TextAlign.end, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15)),
),
],
);
}
// 實作簽核時間軸 (搭配 FutureBuilder)
Widget _buildFlowTimeline() {
return FutureBuilder<List<FlowLog>>(
future: _apiService.fetchFlowLogs(widget.leave.billNo),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
if (!snapshot.hasData || snapshot.data!.isEmpty) return const Text('尚無簽核歷程', style: TextStyle(color: Colors.grey));
final logs = snapshot.data!;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.grey.shade50, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
child: Column(
children: logs.map((log) {
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
log.status == '1' ? Icons.check_circle : (log.status == 'X' ? Icons.cancel : Icons.pending),
color: log.status == '1' ? Colors.green : (log.status == 'X' ? Colors.red : Colors.orange),
),
title: Text('${log.stepName} - ${log.approverName}'),
subtitle: Text(log.time != null ? DateFormat('MM/dd HH:mm').format(log.time!) : ''),
);
}).toList(),
),
);
},
);
}
}