2025-12-29 First Commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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('提交申請'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 '草稿';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user