2025-12-29 First Commit

This commit is contained in:
DATAEXPRESS\4734
2025-12-29 15:20:40 +08:00
commit fb2603c6f0
92 changed files with 6006 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
import './leave_model.dart';
import '../services/generic_api_service.dart';
import 'package:intl/intl.dart';
class LeaveApiService {
final GenericApiService _apiService = GenericApiService();
// 獲取個人請假紀錄
Future<List<Leave>> fetchLeaves(String personId) async {
// 排序:按單據日期降冪
String queryFilter = "1^100^billdate^*^personid^=^$personId";
return await _apiService.fetchList<Leave>(
tableName: "hrs_leave",
pk: "billno",
queryFilter: queryFilter,
fromJson: (json) => Leave.fromJson(json),
);
}
// 提交請假單 (新增)
Future<bool> createLeave(Leave leave) async {
final Map<String, dynamic> data = {
"billdate": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
"billno": "LV${DateTime.now().millisecondsSinceEpoch}", // 範例編號
"personid": leave.personId,
"agentid": leave.agentId,
"leavetype": leave.leaveType,
"starttime": DateFormat('yyyy-MM-dd HH:mm:ss').format(leave.startTime!),
"endtime": DateFormat('yyyy-MM-dd HH:mm:ss').format(leave.endTime!),
"days": leave.days,
"hours": leave.hours,
"leave_note": leave.leaveNote,
"flow_status": "1", // 提交即進入審核中
"create_user": leave.personId,
"create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
};
// 呼叫底層 saveData 實作
// return await _apiService.saveData("hrs_leave", data);
print("提交假單: $data");
return true;
}
}
+149
View File
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import './leave_model.dart';
class LeaveDetail extends StatelessWidget {
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,
),
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),
),
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),
),
),
const SizedBox(height: 32),
// 底部操作按鈕 (例如:若為草稿可編輯,或撤回)
if (leave.flowStatus == '0' || leave.flowStatus == '1')
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
// 實作撤回或取消邏輯
},
icon: const Icon(Icons.history_outlined),
label: const Text('撤回申請'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
),
);
}
// 頂部狀態顯示:呈現單號與醒目的狀態標籤
Widget _buildHeaderStatus() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'單號: ${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),
),
],
),
Chip(
backgroundColor: leave.statusColor.withOpacity(0.1),
side: BorderSide(color: leave.statusColor),
label: Text(
leave.statusText,
style: TextStyle(color: leave.statusColor, fontWeight: FontWeight.bold),
),
),
],
);
}
// 核心資訊卡片
Widget _buildInfoCard(BuildContext context) {
return Card(
elevation: 0,
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),
const Divider(height: 30),
_buildDetailRow(Icons.timer_outlined, '請假時數', '${leave.days}${leave.hours} 小時'),
const Divider(height: 30),
_buildDetailRow(Icons.person_outline, '代理人', leave.agentId),
const Divider(height: 30),
_buildDetailRow(Icons.edit_calendar, '申請日期',
leave.billDate != null ? DateFormat('yyyy-MM-dd').format(leave.billDate!) : 'N/A'),
],
),
),
);
}
// 輔助元件:建立細節列
Widget _buildDetailRow(IconData icon, String label, String value) {
return Row(
children: [
Icon(icon, size: 20, color: Colors.blueAccent),
const SizedBox(width: 12),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
const Spacer(),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
),
],
);
}
}
+94
View File
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import './leave_model.dart';
import './leave_api.dart';
import 'package:intl/intl.dart';
class LeaveForm extends StatefulWidget {
final String userId;
const LeaveForm({required this.userId, super.key});
@override
State<LeaveForm> createState() => _LeaveFormState();
}
class _LeaveFormState extends State<LeaveForm> {
final _formKey = GlobalKey<FormState>();
String _selectedType = '事假';
String _agentId = '';
String _note = '';
DateTime _start = DateTime.now();
DateTime _end = DateTime.now().add(const Duration(hours: 8));
final List<String> _types = ['事假', '病假', '特休', '婚假', '喪假'];
void _submit() async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
final newLeave = Leave(
billNo: '', // API 端生成
personId: widget.userId,
agentId: _agentId,
leaveType: _selectedType,
startTime: _start,
endTime: _end,
days: 1.0, // 簡化處理,實際可依 start/end 計算
hours: 8.0,
leaveNote: _note,
);
final success = await LeaveApiService().createLeave(newLeave);
if (success && mounted) {
Navigator.pop(context, true);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('新增請假申請')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
DropdownButtonFormField<String>(
value: _selectedType,
decoration: const InputDecoration(labelText: '請假類別'),
items: _types.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
onChanged: (v) => setState(() => _selectedType = v!),
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(labelText: '代理人工號'),
validator: (v) => v!.isEmpty ? '必填' : null,
onSaved: (v) => _agentId = v!,
),
const SizedBox(height: 16),
ListTile(
title: const Text('開始時間'),
subtitle: Text(DateFormat('yyyy/MM/dd HH:mm').format(_start)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
// 這裡簡化,實務上可串接 showDatePicker + showTimePicker
},
),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(labelText: '事由說明'),
maxLines: 3,
onSaved: (v) => _note = v!,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
child: const Text('提交申請'),
),
],
),
),
);
}
}
+85
View File
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import './leave_model.dart';
import './leave_api.dart';
import './leave_form.dart'; // 稍後定義的新增頁面
import './leave_detail.dart';
class LeaveManager extends StatefulWidget {
final String currentUserId;
const LeaveManager({required this.currentUserId, super.key});
@override
State<LeaveManager> createState() => _LeaveManagerState();
}
class _LeaveManagerState extends State<LeaveManager> {
late LeaveApiService _apiService;
late Future<List<Leave>> _leaveFuture;
@override
void initState() {
super.initState();
_apiService = LeaveApiService();
_refreshList();
}
void _refreshList() {
setState(() {
_leaveFuture = _apiService.fetchLeaves(widget.currentUserId);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('請假紀錄')),
body: FutureBuilder<List<Leave>>(
future: _leaveFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('尚無請假紀錄'));
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (ctx, i) => _buildLeaveCard(snapshot.data![i]),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => LeaveForm(userId: widget.currentUserId)),
);
if (result == true) _refreshList();
},
label: const Text('申請請假'),
icon: const Icon(Icons.add),
),
);
}
Widget _buildLeaveCard(Leave item) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => LeaveDetail(leave: item)),
);
},
title: Text('${item.leaveType} (${item.days}${item.hours}時)'),
subtitle: Text(item.formattedRange),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: item.statusColor, borderRadius: BorderRadius.circular(5)),
child: Text(item.statusText, style: const TextStyle(color: Colors.white, fontSize: 12)),
),
),
);
}
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class Leave {
final String billNo; // billno (Primary Key)
final DateTime? billDate; // billdate
final String personId; // personid
final String agentId; // agentid (代理人)
final String leaveType; // leavetype (假別:事假、病假等)
final DateTime? startTime; // starttime
final DateTime? endTime; // endtime
final double days; // days
final double hours; // hours
final String? leaveNote; // leave_note
final String? flowStatus; // flow_status (0:草稿, 1:審核中, 2:已核准, X:駁回)
Leave({
required this.billNo,
this.billDate,
required this.personId,
required this.agentId,
required this.leaveType,
this.startTime,
this.endTime,
this.days = 0,
this.hours = 0,
this.leaveNote,
this.flowStatus,
});
factory Leave.fromJson(Map<String, dynamic> json) {
return Leave(
billNo: json['billno'] as String? ?? '',
billDate: json['billdate'] != null ? DateTime.tryParse(json['billdate']) : null,
personId: json['personid'] as String? ?? '',
agentId: json['agentid'] as String? ?? '',
leaveType: json['leavetype'] as String? ?? '',
startTime: json['starttime'] != null ? DateTime.tryParse(json['starttime']) : null,
endTime: json['endtime'] != null ? DateTime.tryParse(json['endtime']) : null,
days: double.tryParse(json['days']?.toString() ?? '0') ?? 0,
hours: double.tryParse(json['hours']?.toString() ?? '0') ?? 0,
leaveNote: json['leave_note'] as String?,
flowStatus: json['flow_status'] as String?,
);
}
// 格式化顯示
String get formattedRange {
if (startTime == null || endTime == null) return '時間未定';
final df = DateFormat('yyyy/MM/dd HH:mm');
return '${df.format(startTime!)} ~ ${df.format(endTime!)}';
}
// 狀態顏色映射
Color get statusColor {
switch (flowStatus) {
case '1': return Colors.orange; // 審核中
case '2': return Colors.green; // 已核准
case 'X': return Colors.red; // 駁回
default: return Colors.grey; // 草稿
}
}
String get statusText {
switch (flowStatus) {
case '1': return '審核中';
case '2': return '已核准';
case 'X': return '已駁回';
default: return '草稿';
}
}
}