add chart code
This commit is contained in:
@@ -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 {
|
||||
// 排序:按單據日期降冪
|
||||
|
||||
@@ -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
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user