252 lines
10 KiB
Dart
252 lines
10 KiB
Dart
// calendar_form.dart
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import './calendar_model.dart';
|
|
import './calendar_api.dart';
|
|
|
|
class CalendarForm extends StatefulWidget {
|
|
final String userId;
|
|
final DateTime? initialDate; // 接收從行事曆首頁傳來的預設日期
|
|
|
|
const CalendarForm({required this.userId, this.initialDate, super.key});
|
|
|
|
@override
|
|
State<CalendarForm> createState() => _CalendarFormState();
|
|
}
|
|
|
|
class _CalendarFormState extends State<CalendarForm> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final CalendarApiService _apiService = CalendarApiService();
|
|
|
|
final TextEditingController _titleController = TextEditingController();
|
|
final TextEditingController _descController = TextEditingController();
|
|
final TextEditingController _projectController = TextEditingController();
|
|
|
|
String _selectedType = '會議';
|
|
String _selectedLevel = 'NORMAL';
|
|
|
|
late DateTime _startDate;
|
|
late TimeOfDay _startTime;
|
|
late DateTime _endDate;
|
|
late TimeOfDay _endTime;
|
|
|
|
bool _isSubmitting = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 如果有傳入初始日期,就用它;否則用今天
|
|
DateTime baseDate = widget.initialDate ?? DateTime.now();
|
|
_startDate = baseDate;
|
|
_endDate = baseDate;
|
|
|
|
// 預設時間:目前時間的下一個整點,例如現在 10:25,預設 11:00 ~ 12:00
|
|
int nextHour = TimeOfDay.now().hour + 1;
|
|
_startTime = TimeOfDay(hour: nextHour > 23 ? 23 : nextHour, minute: 0);
|
|
_endTime = TimeOfDay(hour: nextHour + 1 > 23 ? 23 : nextHour + 1, minute: 0);
|
|
}
|
|
|
|
// 結合日期與時間的選擇器
|
|
Future<void> _pickDateTime(bool isStart) async {
|
|
final date = await showDatePicker(
|
|
context: context,
|
|
initialDate: isStart ? _startDate : _endDate,
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime(2030),
|
|
builder: (context, child) => Theme(
|
|
data: ThemeData.light().copyWith(colorScheme: const ColorScheme.light(primary: Colors.blueAccent)),
|
|
child: child!,
|
|
),
|
|
);
|
|
if (date == null) return;
|
|
|
|
final time = await showTimePicker(
|
|
context: context,
|
|
initialTime: isStart ? _startTime : _endTime,
|
|
builder: (context, child) => Theme(
|
|
data: ThemeData.light().copyWith(colorScheme: const ColorScheme.light(primary: Colors.blueAccent)),
|
|
child: child!,
|
|
),
|
|
);
|
|
if (time == null) return;
|
|
|
|
setState(() {
|
|
if (isStart) {
|
|
_startDate = date; _startTime = time;
|
|
// 防呆:如果結束日期早於開始日期,自動順延
|
|
if (_endDate.isBefore(_startDate)) _endDate = _startDate;
|
|
} else {
|
|
_endDate = date; _endTime = time;
|
|
}
|
|
});
|
|
}
|
|
|
|
void _handleSubmit() async {
|
|
if (_formKey.currentState!.validate()) {
|
|
setState(() => _isSubmitting = true);
|
|
|
|
// 合併 Date 與 Time 成為完整的 DateTime
|
|
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 newEvent = CalendarEvent(
|
|
uuid: '', // 由後端生成
|
|
title: _titleController.text,
|
|
description: _descController.text,
|
|
startTime: start,
|
|
endTime: end,
|
|
type: _selectedType,
|
|
level: _selectedLevel,
|
|
personId: widget.userId,
|
|
projectId: _projectController.text.isNotEmpty ? _projectController.text : null,
|
|
);
|
|
|
|
bool success = await _apiService.createEvent(newEvent);
|
|
|
|
if (mounted) {
|
|
setState(() => _isSubmitting = false);
|
|
if (success) {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('行程新增成功!')));
|
|
Navigator.pop(context, true); // 回傳 true 通知列表刷新
|
|
} else {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('新增失敗,請檢查網路連線。', style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF8F9FA),
|
|
appBar: AppBar(
|
|
title: const Text('新增行程', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: Colors.black87,
|
|
elevation: 0.5,
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildTextField('行程標題 *', _titleController, '例如:部門月會', Icons.title, isRequired: true),
|
|
const SizedBox(height: 16),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(child: _buildDropdownField('類型', _selectedType, ['會議', '拜訪客戶', '專案開發', '私人行程', '其他'], (v) => setState(() => _selectedType = v!))),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: _buildDropdownField('重要性', _selectedLevel,
|
|
['URGENT', 'HIGH', 'NORMAL'],
|
|
(v) => setState(() => _selectedLevel = v!),
|
|
displayNames: {'URGENT': '緊急', 'HIGH': '高', 'NORMAL': '一般'}
|
|
)
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
const Text('時間設定', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey)),
|
|
const SizedBox(height: 12),
|
|
_buildTimePickerTile('開始時間', _startDate, _startTime, true),
|
|
const SizedBox(height: 12),
|
|
_buildTimePickerTile('結束時間', _endDate, _endTime, false),
|
|
const SizedBox(height: 24),
|
|
|
|
_buildTextField('專案關聯 (選填)', _projectController, '輸入專案代號或名稱', Icons.folder_open),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildTextField('行程詳細說明', _descController, '記錄會議大綱或行程備註...', Icons.notes, isMultiline: true),
|
|
const SizedBox(height: 40),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: ElevatedButton(
|
|
onPressed: _isSubmitting ? null : _handleSubmit,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blueAccent,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: _isSubmitting
|
|
? const CircularProgressIndicator(color: Colors.white)
|
|
: const Text('儲存行程', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 共用輸入框元件
|
|
Widget _buildTextField(String label, TextEditingController controller, String hint, IconData icon, {bool isMultiline = false, bool isRequired = false}) {
|
|
return Container(
|
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
child: TextFormField(
|
|
controller: controller,
|
|
maxLines: isMultiline ? 4 : 1,
|
|
decoration: InputDecoration(
|
|
icon: Icon(icon, color: Colors.grey.shade400, size: 20),
|
|
labelText: label,
|
|
labelStyle: TextStyle(color: Colors.grey.shade600, fontSize: 14),
|
|
border: InputBorder.none,
|
|
hintText: hint,
|
|
hintStyle: TextStyle(color: Colors.grey.shade300),
|
|
),
|
|
validator: isRequired ? (v) => v == null || v.isEmpty ? '此欄位為必填' : null : null,
|
|
),
|
|
);
|
|
}
|
|
|
|
// 共用下拉選單元件 (支援實際值與顯示名稱不同)
|
|
Widget _buildDropdownField(String label, String value, List<String> items, ValueChanged<String?> onChanged, {Map<String, String>? displayNames}) {
|
|
return Container(
|
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
child: DropdownButtonFormField<String>(
|
|
value: value,
|
|
decoration: InputDecoration(labelText: label, border: InputBorder.none, labelStyle: TextStyle(color: Colors.grey.shade600, fontSize: 14)),
|
|
items: items.map((s) => DropdownMenuItem(value: s, child: Text(displayNames != null ? displayNames[s]! : s, style: const TextStyle(fontSize: 15)))).toList(),
|
|
onChanged: onChanged,
|
|
icon: Icon(Icons.keyboard_arrow_down, color: Colors.grey.shade400),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 時間選擇按鈕元件
|
|
Widget _buildTimePickerTile(String label, DateTime date, TimeOfDay time, bool isStart) {
|
|
return InkWell(
|
|
onTap: () => _pickDateTime(isStart),
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.access_time, color: isStart ? Colors.blueAccent : Colors.orangeAccent, size: 22),
|
|
const SizedBox(width: 16),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
|
|
const SizedBox(height: 4),
|
|
Text("${DateFormat('yyyy/MM/dd').format(date)} ${time.format(context)}", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
Icon(Icons.edit_calendar, color: Colors.grey.shade300, size: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |