From 0516db16901a2a9e6ef85a38ca2a035b9c29eaba Mon Sep 17 00:00:00 2001 From: "LAPTOP-EJ2PF6VA\\genie" Date: Wed, 4 Mar 2026 23:45:26 +0800 Subject: [PATCH] modify clockin & leave --- lib/clockin/clock_in_api.dart | 2 +- lib/clockin/clock_in_manager.dart | 2 +- lib/clockin/clock_in_model.dart | 4 + lib/leave/leave_api.dart | 43 ++++++++ lib/leave/leave_detail.dart | 170 +++++++++++++++++------------- lib/leave/leave_form.dart | 16 ++- lib/leave/leave_manager.dart | 3 +- 7 files changed, 163 insertions(+), 77 deletions(-) diff --git a/lib/clockin/clock_in_api.dart b/lib/clockin/clock_in_api.dart index 336552f..5490081 100644 --- a/lib/clockin/clock_in_api.dart +++ b/lib/clockin/clock_in_api.dart @@ -25,7 +25,7 @@ class ClockInApiService { return await _apiService.fetchList( tableName: "hrs_ClockInRecord", pk: "ClockInId", - queryFilter: "1^100^ClockInDateTime^*^^^ClockInUserId^$userId", + queryFilter: "1^100^ClockInDateTime^*^ClockInDateTime >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)^^ClockInUserId^$userId", fromJson: (json) => ClockInRecord.fromJson(json), ); } diff --git a/lib/clockin/clock_in_manager.dart b/lib/clockin/clock_in_manager.dart index db6a95a..3f097b0 100644 --- a/lib/clockin/clock_in_manager.dart +++ b/lib/clockin/clock_in_manager.dart @@ -240,7 +240,7 @@ class _ClockInManagerState extends State { Widget _buildHistoryTile(ClockInRecord record) { return ListTile( leading: Icon(Icons.access_time, color: record.typeColor), - title: Text("${record.type} - ${record.formattedTime}"), + title: Text("${record.type} - ${record.formattedDateTime}"), subtitle: Text(record.storeId ?? ""), ); } diff --git a/lib/clockin/clock_in_model.dart b/lib/clockin/clock_in_model.dart index 1b4affb..18667cd 100644 --- a/lib/clockin/clock_in_model.dart +++ b/lib/clockin/clock_in_model.dart @@ -34,6 +34,10 @@ class ClockInRecord { String get formattedTime => dateTime != null ? DateFormat('HH:mm:ss').format(dateTime!) : '--:--'; String get formattedDate => dateTime != null ? DateFormat('yyyy-MM-dd').format(dateTime!) : 'N/A'; + // 新增:滿足列表顯示「日期 + 時間」的需求 + String get formattedDateTime => dateTime != null + ? DateFormat('yyyy-MM-dd HH:mm').format(dateTime!) + : 'N/A'; Color get typeColor { if (type == '上班') return Colors.blue; diff --git a/lib/leave/leave_api.dart b/lib/leave/leave_api.dart index 194d56e..288c2b0 100644 --- a/lib/leave/leave_api.dart +++ b/lib/leave/leave_api.dart @@ -71,6 +71,11 @@ class LeaveApiService { "flow_status": "1", // 提交即進入審核中 "create_user": currentUid, "create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), + // Sync calc + "start_date": DateFormat('yyyy-MM-dd').format(leave.startTime!), + "end_date": DateFormat('yyyy-MM-dd').format(leave.endTime!), + "start_time": DateFormat('HH:mm').format(leave.startTime!), + "end_time": DateFormat('HH:mm').format(leave.endTime!), }; return await _apiService.fetchList( @@ -82,4 +87,42 @@ class LeaveApiService { fromJson: (json) => Leave.fromJson(json), // 這裡填入模型的解析工廠 ); } + + // 修正 5: 實作撤回請假單 API + Future withdrawLeave(String billNo) async { + try { + await _apiService.fetchList( + tableName: "hrs_leave", + pk: "billno", + queryFilter: "billno^$billNo", // 指定該單號 + action: "U", // U 代表 Update + data: { + "flow_status": "0", // 0: 退回草稿/已撤回 + "update_user": AuthManager().currentUserId, + "update_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), + }, + fromJson: (json) => json, + ); + return true; + } catch (e) { + print("撤回失敗: $e"); + return false; + } + } + + // 修正 3: 查詢簽核進度 (FlowLog) + Future> fetchFlowLogs(String billNo) async { + // 假設有一張表 flow_log 記錄簽核歷程 + return await _apiService.fetchList( + tableName: "flow_log", + pk: "log_id", + queryFilter: "1^50^create_date^*^^^billno^$billNo", + fromJson: (json) => FlowLog( + stepName: json['step_name'] ?? '簽核節點', + approverName: json['approver_name'] ?? '系統/主管', + status: json['status'] ?? '0', + time: json['create_date'] != null ? DateTime.tryParse(json['create_date']) : null, + ), + ); + } } \ No newline at end of file diff --git a/lib/leave/leave_detail.dart b/lib/leave/leave_detail.dart index dde9e74..14176e4 100644 --- a/lib/leave/leave_detail.dart +++ b/lib/leave/leave_detail.dart @@ -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 createState() => _LeaveDetailState(); +} + +class _LeaveDetailState extends State { + final LeaveApiService _apiService = LeaveApiService(); + bool _isWithdrawing = false; + + // 實作撤回邏輯 + Future _handleWithdraw() async { + final confirm = await showDialog( + 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>( + 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(), + ), + ); + }, + ); + } } \ No newline at end of file diff --git a/lib/leave/leave_form.dart b/lib/leave/leave_form.dart index 33966bb..21c935d 100644 --- a/lib/leave/leave_form.dart +++ b/lib/leave/leave_form.dart @@ -60,6 +60,18 @@ class _LeaveFormState extends State { final start = DateTime(_startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute); final end = DateTime(_endDate.year, _endDate.month, _endDate.day, _endTime.hour, _endTime.minute); + // 檢查結束時間是否大於開始時間 + if (end.isBefore(start) || end.isAtSameMomentAs(start)) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('結束時間必須大於開始時間'))); + return; + } + + // 修正 2: 自動推算天數與小時 (簡易版:假設一天 8 小時工作制) + final duration = end.difference(start); + final double totalHours = duration.inMinutes / 60.0; + final double calcDays = (totalHours / 8).floorToDouble(); + final double calcHours = totalHours % 8; + final newLeave = Leave( billNo: '', // API 端生成 personId: widget.userId, @@ -67,8 +79,8 @@ class _LeaveFormState extends State { leaveType: _selectedType!.id, // 傳送 ID 給後端 startTime: start, endTime: end, - days: 1.0, // 簡化處理,實際可依 start/end 計算 - hours: 8.0, + days: calcDays, // 寫入計算後的天數 + hours: calcHours, // 寫入計算後的小時 leaveNote: _note, ); diff --git a/lib/leave/leave_manager.dart b/lib/leave/leave_manager.dart index d837866..292a53d 100644 --- a/lib/leave/leave_manager.dart +++ b/lib/leave/leave_manager.dart @@ -72,7 +72,8 @@ class _LeaveManagerState extends State { MaterialPageRoute(builder: (context) => LeaveDetail(leave: item)), ); }, - title: Text('${item.leaveType} (${item.days}天 ${item.hours}時)'), + // title: Text('${item.leaveType} (${item.days}天 ${item.hours}時)'), + title: Text('${item.leaveTypeName} (${item.days}天 ${item.hours}時)'), // 使用 leaveTypeName subtitle: Text(item.formattedRange), trailing: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),