add chart code

This commit is contained in:
2026-01-13 23:11:25 +08:00
parent 0a9f0b8583
commit 6b4d98f5ce
14 changed files with 938 additions and 41 deletions
+33
View File
@@ -6,6 +6,39 @@ import '../auth_manager.dart'; // 確保引入 AuthManager
class LeaveApiService {
final GenericApiService _apiService = GenericApiService();
// 新增:獲取所有啟用的假別清單
Future<List<LeaveType>> fetchLeaveTypes() async {
// 這裡通常不需要特別的 filter,或可根據公司邏輯過濾性別等
return await _apiService.fetchList<LeaveType>(
tableName: "hrs_leavetype",
pk: "leavetype_id",
queryFilter: "1^100^leavetype_id^*^^^", // 取得前100筆
fromJson: (json) => LeaveType.fromJson(json),
);
}
// 查詢員工清單 (代理人)
Future<List<Map<String, dynamic>>> fetchEmployees(String keyword) async {
// 1. 設定查詢過濾器 (依據 Generic API 規範)
// 格式:當前頁^每頁筆數^排序欄位^關鍵字欄位^關鍵字內容
// 我們同時搜尋姓名(personcname)或工號(personid)
String queryFilter = "1^50^personid^*^^^personcname^$keyword";
final List<dynamic> result = await _apiService.fetchList<dynamic>(
tableName: "basperson", // 指向員工主檔
pk: "personid",
queryFilter: queryFilter,
fromJson: (json) => json,
);
// 2. 轉換並確保回傳正確的欄位映射
return result.map((e) => {
'id': e['personid'] ?? '',
'name': e['personcname'] ?? '',
'dept': e['departmentid'] ?? '',
}).toList();
}
// 獲取個人請假紀錄
Future<List<Leave>> fetchLeaves(String personId) async {
// 排序:按單據日期降冪
+8 -3
View File
@@ -139,9 +139,14 @@ class LeaveDetail extends StatelessWidget {
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),
// 使用 Flexible 限制文字寬度並允許換行
Flexible(
child: Text(
value,
textAlign: TextAlign.end, // 靠右對齊
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15),
softWrap: true, // 允許自動換行
),
),
],
);
+143 -29
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import './leave_model.dart';
import './leave_api.dart';
import 'package:intl/intl.dart';
import '../services/person_picker_dialog.dart';
class LeaveForm extends StatefulWidget {
final String userId;
@@ -13,34 +14,92 @@ class LeaveForm extends StatefulWidget {
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 LeaveApiService _leaveApi = LeaveApiService();
final List<String> _types = ['事假', '病假', '特休', '婚假', '喪假'];
// 狀態變數
List<LeaveType> _dbLeaveTypes = [];
LeaveType? _selectedType;
bool _isLoadingTypes = true;
Map<String, dynamic>? _selectedAgent; // 改為這個
DateTime _startDate = DateTime.now();
TimeOfDay _startTime = const TimeOfDay(hour: 09, minute: 00);
DateTime _endDate = DateTime.now();
TimeOfDay _endTime = const TimeOfDay(hour: 18, minute: 00);
String _note = '';
Future<void> _pickDateTime(bool isStart) async {
final date = await showDatePicker(
context: context,
initialDate: isStart ? _startDate : _endDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (date == null) return;
final time = await showTimePicker(
context: context,
initialTime: isStart ? _startTime : _endTime,
);
if (time == null) return;
setState(() {
if (isStart) {
_startDate = date; _startTime = time;
} else {
_endDate = date; _endTime = time;
}
});
}
void _submit() async {
if (_formKey.currentState!.validate()) {
if (_formKey.currentState!.validate() && _selectedType != null && _selectedAgent != null) {
_formKey.currentState!.save();
// 合併日期時間
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);
final newLeave = Leave(
billNo: '', // API 端生成
personId: widget.userId,
agentId: _agentId,
leaveType: _selectedType,
startTime: _start,
endTime: _end,
agentId: _selectedAgent!['id'].toString(), // 確保轉為 String
leaveType: _selectedType!.id, // 傳送 ID 給後端
startTime: start,
endTime: end,
days: 1.0, // 簡化處理,實際可依 start/end 計算
hours: 8.0,
leaveNote: _note,
);
await LeaveApiService().createLeave(newLeave);
//if (success && mounted) {
// Navigator.pop(context, true);
//}
if (mounted) {
Navigator.pop(context, true);
}
}
}
@override
void initState() {
super.initState();
_loadInitialData();
}
// 從 API 載入假別
Future<void> _loadInitialData() async {
try {
final types = await _leaveApi.fetchLeaveTypes();
setState(() {
_dbLeaveTypes = types;
// 預設選取第一筆(如有資料)
if (_dbLeaveTypes.isNotEmpty) {
_selectedType = _dbLeaveTypes.first;
}
_isLoadingTypes = false;
});
} catch (e) {
setState(() => _isLoadingTypes = false);
// 實務上應加入錯誤處理提示
}
}
@@ -48,32 +107,62 @@ class _LeaveFormState extends State<LeaveForm> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('新增請假申請')),
body: Form(
body: _isLoadingTypes
? const Center(child: CircularProgressIndicator()) // 載入中顯示轉圈
:Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
DropdownButtonFormField<String>(
// 假別選擇 (顯示名稱,存入代碼)
DropdownButtonFormField<LeaveType>(
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!,
decoration: const InputDecoration(labelText: '請假類別', border: OutlineInputBorder()),
// 將 API 取得的資料轉換為選單項目
items: _dbLeaveTypes.map((t) => DropdownMenuItem(
value: t,
child: Text(t.name) // 顯示 leavetype_name
)).toList(),
onChanged: (v) => setState(() => _selectedType = v),
validator: (v) => v == null ? '請選擇假別' : null,
),
const SizedBox(height: 16),
// 代理人開窗
// 在 _LeaveFormState 內部的 Widget Tree 中
ListTile(
title: const Text('開始時間'),
subtitle: Text(DateFormat('yyyy/MM/dd HH:mm').format(_start)),
trailing: const Icon(Icons.calendar_today),
title: const Text('代理人'),
// 顯示已選擇的代理人姓名與 ID,若無則顯示提示
subtitle: Text(_selectedAgent == null
? '請點擊選擇代理人'
: '${_selectedAgent!['name']} (${_selectedAgent!['id']})'),
trailing: const Icon(Icons.person_add_alt_1, color: Colors.blue),
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
onTap: () async {
// 這裡簡化,實務上可串接 showDatePicker + showTimePicker
// 呼叫獨立的彈窗組件
final Map<String, dynamic>? result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => const PersonPickerDialog(title: '查詢代理人'),
);
// 如果使用者有選取人員(result 不為 null),則更新 UI 狀態
if (result != null) {
setState(() {
_selectedAgent = result;
// 這裡 result 的內容為 {'id': '...', 'name': '...', 'dept': '...'}
});
}
},
),
const SizedBox(height: 16),
// 起訖時間選擇區
_buildTimePickerTile('開始時間', _startDate, _startTime, true),
const SizedBox(height: 10),
_buildTimePickerTile('結束時間', _endDate, _endTime, false),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(labelText: '事由說明'),
@@ -91,4 +180,29 @@ class _LeaveFormState extends State<LeaveForm> {
),
);
}
Widget _buildTimePickerTile(String label, DateTime date, TimeOfDay time, bool isStart) {
final format = DateFormat('yyyy/MM/dd');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 14, color: Colors.blueGrey)),
const SizedBox(height: 5),
InkWell(
onTap: () => _pickDateTime(isStart),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
decoration: BoxDecoration(border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4)),
child: Row(
children: [
const Icon(Icons.access_time, size: 20, color: Colors.blue),
const SizedBox(width: 10),
Text('${format.format(date)} ${time.format(context)}', style: const TextStyle(fontSize: 16)),
],
),
),
),
],
);
}
}
+31
View File
@@ -1,12 +1,39 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
// 假別模型 260110
class LeaveType {
final String id;
final String name;
LeaveType({required this.id, required this.name});
factory LeaveType.fromJson(Map<String, dynamic> json) {
return LeaveType(
id: json['leavetype_id'] as String,
name: json['leavetype_name'] as String,
);
}
}
// 簽核進度模型
class FlowLog {
final String stepName;
final String approverName;
final String status; // 1:同意, X:駁回, 0:待審
final DateTime? time;
FlowLog({required this.stepName, required this.approverName, required this.status, this.time});
}
class Leave {
final String billNo; // billno (Primary Key)
final DateTime? billDate; // billdate
final String personId; // personid
final String agentId; // agentid (代理人)
final String agentName; /// 關聯顯示
final String leaveType; // leavetype (假別:事假、病假等)
final String leaveTypeName; /// 顯示名稱
final DateTime? startTime; // starttime
final DateTime? endTime; // endtime
final double days; // days
@@ -19,7 +46,9 @@ class Leave {
this.billDate,
required this.personId,
required this.agentId,
this.agentName = '',
required this.leaveType,
this.leaveTypeName = '',
this.startTime,
this.endTime,
this.days = 0,
@@ -34,7 +63,9 @@ class Leave {
billDate: json['billdate'] != null ? DateTime.tryParse(json['billdate']) : null,
personId: json['personid'] as String? ?? '',
agentId: json['agentid'] as String? ?? '',
agentName: json['agentname'] ?? '', /// 假設 API 會 Join 姓名
leaveType: json['leavetype'] as String? ?? '',
leaveTypeName: json['leavetype_name'] ?? json['leavetype'] ?? '',
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,