modify clockin & leave
This commit is contained in:
@@ -25,7 +25,7 @@ class ClockInApiService {
|
||||
return await _apiService.fetchList<ClockInRecord>(
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ class _ClockInManagerState extends State<ClockInManager> {
|
||||
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 ?? ""),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Leave>(
|
||||
@@ -82,4 +87,42 @@ class LeaveApiService {
|
||||
fromJson: (json) => Leave.fromJson(json), // 這裡填入模型的解析工廠
|
||||
);
|
||||
}
|
||||
|
||||
// 修正 5: 實作撤回請假單 API
|
||||
Future<bool> withdrawLeave(String billNo) async {
|
||||
try {
|
||||
await _apiService.fetchList<dynamic>(
|
||||
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<List<FlowLog>> fetchFlowLogs(String billNo) async {
|
||||
// 假設有一張表 flow_log 記錄簽核歷程
|
||||
return await _apiService.fetchList<FlowLog>(
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+98
-72
@@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,18 @@ class _LeaveFormState extends State<LeaveForm> {
|
||||
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<LeaveForm> {
|
||||
leaveType: _selectedType!.id, // 傳送 ID 給後端
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
days: 1.0, // 簡化處理,實際可依 start/end 計算
|
||||
hours: 8.0,
|
||||
days: calcDays, // 寫入計算後的天數
|
||||
hours: calcHours, // 寫入計算後的小時
|
||||
leaveNote: _note,
|
||||
);
|
||||
|
||||
|
||||
@@ -72,7 +72,8 @@ class _LeaveManagerState extends State<LeaveManager> {
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user