222 lines
7.7 KiB
Dart
222 lines
7.7 KiB
Dart
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;
|
|
const LeaveForm({required this.userId, super.key});
|
|
|
|
@override
|
|
State<LeaveForm> createState() => _LeaveFormState();
|
|
}
|
|
|
|
class _LeaveFormState extends State<LeaveForm> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final LeaveApiService _leaveApi = LeaveApiService();
|
|
|
|
// 狀態變數
|
|
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() && _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);
|
|
|
|
// 檢查結束時間是否大於開始時間
|
|
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,
|
|
agentId: _selectedAgent!['id'].toString(), // 確保轉為 String
|
|
agentName: '',
|
|
leaveType: _selectedType!.id, // 傳送 ID 給後端
|
|
leaveTypeName: '',
|
|
startTime: start,
|
|
endTime: end,
|
|
days: calcDays, // 寫入計算後的天數
|
|
hours: calcHours, // 寫入計算後的小時
|
|
leaveNote: _note,
|
|
);
|
|
|
|
await LeaveApiService().createLeave(newLeave);
|
|
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);
|
|
// 實務上應加入錯誤處理提示
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('新增請假申請')),
|
|
body: _isLoadingTypes
|
|
? const Center(child: CircularProgressIndicator()) // 載入中顯示轉圈
|
|
:Form(
|
|
key: _formKey,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
// 假別選擇 (顯示名稱,存入代碼)
|
|
DropdownButtonFormField<LeaveType>(
|
|
value: _selectedType,
|
|
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('代理人'),
|
|
// 顯示已選擇的代理人姓名與 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 {
|
|
// 呼叫獨立的彈窗組件
|
|
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: '事由說明'),
|
|
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('提交申請'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
} |