From af27b77ad7b2fd6fa24085035554bfc0c73c5e74 Mon Sep 17 00:00:00 2001 From: "LAPTOP-EJ2PF6VA\\genie" Date: Sat, 21 Mar 2026 21:23:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=B9=BE=E5=80=8B=E5=B0=8F?= =?UTF-8?q?=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/AnnouncementManager/announcement_api.dart | 2 +- .../announcement_detail.dart | 2 +- .../announcement_model.dart | 3 + lib/calendar/calendar_api.dart | 55 ++- lib/calendar/calendar_form.dart | 252 ++++++++++++ lib/calendar/calendar_manager.dart | 368 ++++++++++++++---- lib/leave/leave_api.dart | 4 +- lib/leave/leave_model.dart | 22 +- lib/todo/todo_api.dart | 39 +- lib/todo/todo_form.dart | 39 +- lib/todo/todo_model.dart | 8 +- 11 files changed, 674 insertions(+), 120 deletions(-) create mode 100644 lib/calendar/calendar_form.dart diff --git a/lib/AnnouncementManager/announcement_api.dart b/lib/AnnouncementManager/announcement_api.dart index ba380fc..8457c33 100644 --- a/lib/AnnouncementManager/announcement_api.dart +++ b/lib/AnnouncementManager/announcement_api.dart @@ -10,7 +10,7 @@ class AnnouncementApiService { return await _apiService.fetchList( tableName: "eipbbs_m", pk: "uniqueno", - queryFilter: "1^10^uniqueno^*^^pmsm02^^", + queryFilter: "1^10^uniqueno^*^^eipm11^^", // 將 Announcement 的轉換方法傳進去 fromJson: (json) => Announcement.fromJson(json), ); diff --git a/lib/AnnouncementManager/announcement_detail.dart b/lib/AnnouncementManager/announcement_detail.dart index ed17d3d..de73204 100644 --- a/lib/AnnouncementManager/announcement_detail.dart +++ b/lib/AnnouncementManager/announcement_detail.dart @@ -48,7 +48,7 @@ class AnnouncementDetail extends StatelessWidget { children: [ Text('發布日期: ${announcement.formattedBillDate}', style: const TextStyle(color: Colors.grey, fontSize: 14)), - Text('作者: ${announcement.createdBy}', + Text('作者: ${announcement.personCName}', style: const TextStyle(color: Colors.grey, fontSize: 14)), ], ), diff --git a/lib/AnnouncementManager/announcement_model.dart b/lib/AnnouncementManager/announcement_model.dart index ab723b1..5d046dc 100644 --- a/lib/AnnouncementManager/announcement_model.dart +++ b/lib/AnnouncementManager/announcement_model.dart @@ -9,6 +9,7 @@ class Announcement { final DateTime startDate; // startdate final DateTime endDate; // end_date final String? attachment; // bbs_attach + final String personCName; // 姓名 Announcement({ required this.uniqueNo, @@ -19,6 +20,7 @@ class Announcement { required this.startDate, required this.endDate, this.attachment, + required this.personCName, }); // Factory 構造函數:從 API 返回的 JSON (Map) 創建 Announcement 物件 @@ -41,6 +43,7 @@ class Announcement { startDate: parseDate(json['startdate'])!, endDate: parseDate(json['end_date'])!, attachment: json['bbs_attach'] as String?, + personCName: json['personCName'] as String, ); } diff --git a/lib/calendar/calendar_api.dart b/lib/calendar/calendar_api.dart index c2ddcf5..2fddeac 100644 --- a/lib/calendar/calendar_api.dart +++ b/lib/calendar/calendar_api.dart @@ -1,3 +1,5 @@ +// calendar_api.dart + import './calendar_model.dart'; import '../services/generic_api_service.dart'; import 'package:intl/intl.dart'; @@ -5,13 +7,19 @@ import 'package:intl/intl.dart'; class CalendarApiService { final GenericApiService _apiService = GenericApiService(); - // 獲取指定月份或範圍的行程 - Future> fetchEvents(String userId, DateTime month) async { - // 企業實作建議:僅抓取特定月份資料,減少手機負載 - final sDate = DateTime(month.year, month.month, 1); - final eDate = DateTime(month.year, month.month + 1, 0); + // 獲取指定「日期區間」的行程 + Future> fetchEvents(String userId, DateTime startDate, DateTime endDate) async { + // 格式化查詢的起迄日期 + final sDate = DateFormat('yyyy-MM-dd').format(startDate); + // 結束日期加 1 天,用小於 (<) 來涵蓋 endDate 當天的所有時間 (23:59:59) + final eDate = DateFormat('yyyy-MM-dd').format(endDate.add(const Duration(days: 1))); - String queryFilter = "1^500^start_time^*^personid^=^$userId"; + // 1. 組合 wheresql_org + // 條件:大於等於開始日期,小於結束日期的隔天,且人員代號相符 + String whereSql = "start_time >= '$sDate' AND start_time < '$eDate' AND personid = '$userId'"; + + // 2. 嚴格依照 API 規範組裝 10 個參數 + String queryFilter = "1^500^start_time^*^$whereSql^^^^^"; return await _apiService.fetchList( tableName: "eip_new_calendar", @@ -21,19 +29,36 @@ class CalendarApiService { ); } - // 新增/更新行程 - Future saveEvent(CalendarEvent event) async { - final data = { - "uuid": event.uuid, + // 新增行程至資料庫 + Future createEvent(CalendarEvent event) async { + final Map data = { + // uuid 通常由後端資料庫生成,新增時可以不傳或傳空值 "title": event.title, "cal_description": event.description, - "start_time": event.startTime?.toIso8601String(), - "end_time": event.endTime?.toIso8601String(), + "start_time": event.startTime != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(event.startTime!) : null, + "end_time": event.endTime != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(event.endTime!) : null, + "cal_type": event.type, + "cal_level": event.level, "personid": event.personId, "cal_finished": event.isFinished ? 'Y' : 'N', - "update_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), + "projectid": event.projectId, // 關聯專案 + "create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), }; - // return await _apiService.saveData("eip_new_calendar", data); - return true; + + try { + // 根據 Generic API 規範,使用 action: "C" 進行新增 + await _apiService.fetchList( + tableName: "eip_new_calendar", + pk: "uuid", + queryFilter: "", + action: "C", + data: data, + fromJson: (json) => json, + ); + return true; + } catch (e) { + print("Create Calendar Event Error: $e"); + return false; + } } } \ No newline at end of file diff --git a/lib/calendar/calendar_form.dart b/lib/calendar/calendar_form.dart new file mode 100644 index 0000000..f8f1dad --- /dev/null +++ b/lib/calendar/calendar_form.dart @@ -0,0 +1,252 @@ +// 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 createState() => _CalendarFormState(); +} + +class _CalendarFormState extends State { + final _formKey = GlobalKey(); + 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 _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 items, ValueChanged onChanged, {Map? 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( + 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), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/calendar/calendar_manager.dart b/lib/calendar/calendar_manager.dart index 54a1b6e..ca643db 100644 --- a/lib/calendar/calendar_manager.dart +++ b/lib/calendar/calendar_manager.dart @@ -1,9 +1,12 @@ +// calendar_manager.dart + import 'package:flutter/material.dart'; -import 'package:table_calendar/table_calendar.dart'; // 建議引入此套件 +import 'package:table_calendar/table_calendar.dart'; import 'package:intl/intl.dart'; import './calendar_model.dart'; import './calendar_api.dart'; import './calendar_detail.dart'; +import './calendar_form.dart'; class CalendarManager extends StatefulWidget { final String userId; @@ -16,33 +19,47 @@ class CalendarManager extends StatefulWidget { class _CalendarManagerState extends State { final CalendarApiService _apiService = CalendarApiService(); - // 狀態控制 + // ==== 日曆模式狀態 ==== CalendarFormat _calendarFormat = CalendarFormat.month; DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay; + Map> _eventsMap = {}; // 供日曆標點使用 - // 資料儲存:將事件按日期分類,方便日曆標點 - Map> _eventsMap = {}; + // ==== 列表模式狀態 ==== + late DateTime _listStartDate; + late DateTime _listEndDate; + List _listEvents = []; // 供列表模式獨立顯示使用 + + // 控制目前是日曆還是列表 + String _viewMode = '日曆模式'; bool _isLoading = false; @override void initState() { super.initState(); _selectedDay = _focusedDay; + + // 初始化列表模式的預設區間為「本月 1 日」到「本月底」 + _listStartDate = DateTime(DateTime.now().year, DateTime.now().month, 1); + _listEndDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0); + + // 預設載入日曆資料 _fetchMonthEvents(_focusedDay); } - // 核心:抓取整個月的資料並進行分類 + // ============== API 抓取邏輯 ============== + + // 1. 日曆模式:抓取整個月並轉換為 Map 標點 Future _fetchMonthEvents(DateTime month) async { setState(() => _isLoading = true); try { - final events = await _apiService.fetchEvents(widget.userId, month); + final sDate = DateTime(month.year, month.month, 1); + final eDate = DateTime(month.year, month.month + 1, 0); // 當月最後一天 + final events = await _apiService.fetchEvents(widget.userId, sDate, eDate); - // 將 List 轉換為 Map> Map> newMap = {}; for (var event in events) { if (event.startTime != null) { - // 只取日期部分作為 Key final dateKey = DateTime(event.startTime!.year, event.startTime!.month, event.startTime!.day); if (newMap[dateKey] == null) newMap[dateKey] = []; newMap[dateKey]!.add(event); @@ -59,7 +76,65 @@ class _CalendarManagerState extends State { } } - // 獲取選定日期的行程 + // 2. 列表模式:抓取特定日期區間,單純存成 List + Future _fetchListEvents() async { + setState(() => _isLoading = true); + try { + final events = await _apiService.fetchEvents(widget.userId, _listStartDate, _listEndDate); + + // 依開始時間排序 + events.sort((a, b) => (a.startTime ?? DateTime.now()).compareTo(b.startTime ?? DateTime.now())); + + setState(() { + _listEvents = events; + _isLoading = false; + }); + } catch (e) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("列表載入失敗: $e"))); + } + } + + // 簡單式:獨立選擇開始或結束日期 + Future _selectDate(bool isStart) async { + final DateTime initialDate = isStart ? _listStartDate : _listEndDate; + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime(2020), + lastDate: DateTime(2030), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light(primary: Colors.blueAccent), + ), + child: child!, + ); + }, + ); + + if (picked != null) { + setState(() { + if (isStart) { + _listStartDate = picked; + // 防呆:如果開始日期大於結束日期,讓結束日期自動等於開始日期 + if (_listStartDate.isAfter(_listEndDate)) { + _listEndDate = _listStartDate; + } + } else { + _listEndDate = picked; + // 防呆:如果結束日期小於開始日期,讓開始日期自動等於結束日期 + if (_listEndDate.isBefore(_listStartDate)) { + _listStartDate = _listEndDate; + } + } + }); + _fetchListEvents(); // 選定後立刻觸發 API 重新查詢 + } + } + + // 獲取日曆選定日期的行程 List _getEventsForDay(DateTime day) { final dateKey = DateTime(day.year, day.month, day.day); return _eventsMap[dateKey] ?? []; @@ -68,102 +143,257 @@ class _CalendarManagerState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFFF8F9FA), appBar: AppBar( - title: const Text('企業行事曆'), + title: const Text('行事曆', style: TextStyle(fontWeight: FontWeight.bold)), + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + elevation: 0.5, actions: [ - IconButton(icon: const Icon(Icons.today), onPressed: () => setState(() => _focusedDay = DateTime.now())), + // 視圖切換下拉選單 + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _viewMode, + icon: const Icon(Icons.arrow_drop_down, color: Colors.blueAccent), + items: ['日曆模式', '列表模式'].map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), + ); + }).toList(), + onChanged: (newValue) { + if (newValue != null && newValue != _viewMode) { + setState(() => _viewMode = newValue); + // 切換模式時,呼叫對應的 API + if (newValue == '列表模式') { + _fetchListEvents(); + } else { + _fetchMonthEvents(_focusedDay); + } + } + }, + ), + ), + ), + IconButton( + icon: const Icon(Icons.today, color: Colors.blueAccent), + onPressed: () { + setState(() { + _focusedDay = DateTime.now(); + _selectedDay = DateTime.now(); + + // 如果是列表模式,也順便重置回當月區間 + _listStartDate = DateTime(DateTime.now().year, DateTime.now().month, 1); + _listEndDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0); + }); + + _viewMode == '日曆模式' + ? _fetchMonthEvents(_focusedDay) + : _fetchListEvents(); + } + ), ], ), - body: Column( - children: [ - // 1. 日曆組件 - TableCalendar( + body: _viewMode == '日曆模式' ? _buildCalendarLayout() : _buildListLayout(), + floatingActionButton: FloatingActionButton( + backgroundColor: Colors.blueAccent, + child: const Icon(Icons.add, color: Colors.white), + onPressed: () async { + // 跳轉至新增表單,並將目前選擇的日期傳入作為預設值 + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CalendarForm( + userId: widget.userId, + initialDate: _selectedDay, // 貼心設計:自動帶入你在日曆上點擊的日期 + ), + ), + ); + + // 如果表單回傳 true (代表新增成功),就重新 Call API 抓取最新資料! + if (result == true) { + if (_viewMode == '日曆模式') { + _fetchMonthEvents(_focusedDay); + } else { + _fetchListEvents(); + } + } + }, + ), + ); + } + + // ============== 視圖 1:日曆模式 ============== + Widget _buildCalendarLayout() { + return Column( + children: [ + Container( + color: Colors.white, + child: TableCalendar( firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: _focusedDay, calendarFormat: _calendarFormat, selectedDayPredicate: (day) => isSameDay(_selectedDay, day), - eventLoader: _getEventsForDay, // 在有行程的日期顯示小點點 - - // 樣式設定 + eventLoader: _getEventsForDay, calendarStyle: CalendarStyle( - todayDecoration: BoxDecoration(color: Colors.blue.withOpacity(0.5), shape: BoxShape.circle), - selectedDecoration: const BoxDecoration(color: Colors.blue, shape: BoxShape.circle), + todayDecoration: BoxDecoration(color: Colors.blue.withOpacity(0.3), shape: BoxShape.circle), + selectedDecoration: const BoxDecoration(color: Colors.blueAccent, shape: BoxShape.circle), markerDecoration: const BoxDecoration(color: Colors.orange, shape: BoxShape.circle), ), - - // 互動事件 onDaySelected: (selectedDay, focusedDay) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); }, - onFormatChanged: (format) { - setState(() => _calendarFormat = format); - }, + onFormatChanged: (format) => setState(() => _calendarFormat = format), onPageChanged: (focusedDay) { _focusedDay = focusedDay; - _fetchMonthEvents(focusedDay); // 切換月份時自動重新抓取 API + _fetchMonthEvents(focusedDay); }, ), - - const Divider(height: 1), - - // 2. 下方行程列表 - Expanded( - child: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _buildEventList(), - ), - ], - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () { /* 實重新增行程邏輯 */ }, - ), + ), + const SizedBox(height: 8), + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildEventListForDay(), + ), + ], ); } - Widget _buildEventList() { + Widget _buildEventListForDay() { final dayEvents = _getEventsForDay(_selectedDay!); if (dayEvents.isEmpty) { return Center( - child: Text( - "${DateFormat('MM/dd').format(_selectedDay!)} 沒有行程", - style: const TextStyle(color: Colors.grey), - ), + child: Text("${DateFormat('MM/dd').format(_selectedDay!)} 沒有行程", style: const TextStyle(color: Colors.grey, fontSize: 16)), ); } return ListView.builder( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: dayEvents.length, - itemBuilder: (context, index) { - final event = dayEvents[index]; - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 10), - child: ListTile( - leading: Container( - width: 4, - height: 40, - decoration: BoxDecoration( - color: event.levelColor, - borderRadius: BorderRadius.circular(2), + itemBuilder: (context, index) => _buildEventCard(dayEvents[index]), + ); + } + + // ============== 視圖 2:列表模式 (簡單式日期篩選器) ============== + Widget _buildListLayout() { + return Column( + children: [ + // 頂部日期區間選擇器 (簡單式左右分割) + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Expanded(child: _buildDateSelectButton('開始日期', _listStartDate, true)), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Text('至', style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)), ), - ), - title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold)), - subtitle: Text(event.timeRangeText), - trailing: const Icon(Icons.chevron_right), - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => CalendarDetail(event: event)) - ), + Expanded(child: _buildDateSelectButton('結束日期', _listEndDate, false)), + ], ), - ); - }, + ), + + // 列表結果 + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _listEvents.isEmpty + ? const Center(child: Text("該區間尚無任何行程", style: TextStyle(color: Colors.grey, fontSize: 16))) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + itemCount: _listEvents.length, + itemBuilder: (context, index) { + return _buildEventCard(_listEvents[index], showDate: true); + }, + ), + ), + ], + ); + } + + // 獨立的日期選擇按鈕 UI + Widget _buildDateSelectButton(String label, DateTime date, bool isStart) { + return InkWell( + onTap: () => _selectDate(isStart), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.05), + border: Border.all(color: Colors.blueAccent.withOpacity(0.3)), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.calendar_today, size: 14, color: Colors.blueAccent), + const SizedBox(width: 6), + Text( + DateFormat('yyyy/MM/dd').format(date), + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.blueAccent, fontSize: 14) + ), + ], + ), + ], + ), + ), + ); + } + + // ============== 共用元件:行程 Card ============== + Widget _buildEventCard(CalendarEvent event, {bool showDate = false}) { + return Card( + elevation: 0, + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.grey.shade200), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + leading: Container( + width: 5, + height: 40, + decoration: BoxDecoration( + color: event.levelColor, + borderRadius: BorderRadius.circular(4), + ), + ), + title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + subtitle: Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Row( + children: [ + Icon(Icons.access_time, size: 14, color: Colors.grey.shade600), + const SizedBox(width: 4), + Text( + showDate && event.startTime != null + ? "${DateFormat('MM/dd').format(event.startTime!)} ${event.timeRangeText}" + : event.timeRangeText, + style: TextStyle(color: Colors.grey.shade700), + ), + ], + ), + ), + trailing: const Icon(Icons.chevron_right, color: Colors.grey), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => CalendarDetail(event: event)) + ), + ), ); } } \ No newline at end of file diff --git a/lib/leave/leave_api.dart b/lib/leave/leave_api.dart index bbd591e..04e4c5f 100644 --- a/lib/leave/leave_api.dart +++ b/lib/leave/leave_api.dart @@ -154,7 +154,7 @@ class LeaveApiService { Future promoteLeave(String personId, String billNo) async { // 依據標準規範,參數一律使用 para0x final Map params = { - "para01": "eipm21", + "para01": "hrsm11", "para02": personId, "para03": billNo, }; @@ -182,7 +182,7 @@ class LeaveApiService { /// 對應 API: bpm_sign_history/2/ Future>> fetchSignHistory(String billNo) async { final Map params = { - "para01": "eipm21", // 來源單別 functiontag + "para01": "hrsm11", // 來源單別 functiontag "para02": billNo, // 原單單號 query_id }; diff --git a/lib/leave/leave_model.dart b/lib/leave/leave_model.dart index a826383..c24076a 100644 --- a/lib/leave/leave_model.dart +++ b/lib/leave/leave_model.dart @@ -90,21 +90,31 @@ class Leave { return '${df.format(startTime!)} ~ ${df.format(endTime!)}'; } + /* + -- [sign_status] [varchar](2) not null, -- N:待簽核 P:簽核中 R:拒絶 A:同意 C:作廢 + -- [flow_status] [varchar](2) not null, -- N:待簽核 P:簽核中 Z:結案 C:作廢 + */ // 狀態顏色映射 Color get statusColor { switch (flowStatus) { - case '1': return Colors.orange; // 審核中 - case '2': return Colors.green; // 已核准 - case 'X': return Colors.red; // 駁回 + case 'N': return Colors.blue; // 待簽核 + case 'P': return Colors.orange; // 審核中 + case 'A': return Colors.green; // 同意 + case 'R': return Colors.red; // 駁回 + case 'C': return Colors.black; // 作廢 + case 'Z': return Colors.purple; // 結案 default: return Colors.grey; // 草稿 } } String get statusText { switch (flowStatus) { - case '1': return '審核中'; - case '2': return '已核准'; - case 'X': return '已駁回'; + case 'N': return '待簽核'; + case 'P': return '審核中'; + case 'A': return '同意'; + case 'R': return '駁回'; + case 'C': return '作廢'; + case 'Z': return '結案'; default: return '草稿'; } } diff --git a/lib/todo/todo_api.dart b/lib/todo/todo_api.dart index 0ea6eb2..ebc9d18 100644 --- a/lib/todo/todo_api.dart +++ b/lib/todo/todo_api.dart @@ -1,4 +1,4 @@ -// todo_api.dart (新增 Create 功能) +// todo_api.dart import './todo_model.dart'; import '../services/generic_api_service.dart'; @@ -10,16 +10,31 @@ class TodoApiService { TodoApiService({this.currentUserId = 'admin'}); - // 原始的查詢功能... /// 獲取指定日期的任務清單 - Future> fetchTodos({DateTime? selectedDate}) async { + Future> fetchTodos({DateTime? selectedDate, String? statusFilter}) async { // 預設查詢今天的資料 DateTime dateToQuery = selectedDate ?? DateTime.now(); String formattedDate = DateFormat('yyyy-MM-dd').format(dateToQuery); - // 構建 queryFilter:過濾特定的 end_date 並根據 id 排序 - // 格式範例: 1^100^id^*^^end_date^2024-05-01 - String queryFilter = "1^100^id^*^^end_date^$formattedDate"; + // 1. 組合 wheresql_org (第 5 個參數) + // 使用 LIKE 來比對 DATETIME 欄位,確保抓到該日期的所有時段 + String whereSql = "end_date LIKE '$formattedDate%'"; + + // 2. 如果有傳入狀態過濾條件,動態加上 AND 語法 + if (statusFilter != null && statusFilter != 'All') { + String dbStatus = ''; + if (statusFilter == 'Done') dbStatus = 'DONE'; + else if (statusFilter == 'In Progress') dbStatus = 'WIP'; + else if (statusFilter == 'To do') dbStatus = 'TODO'; // 假設你的待辦狀態是 TODO + + if (dbStatus.isNotEmpty) { + whereSql += " AND pbi_status = '$dbStatus'"; + } + } + + // 3. 嚴格依照 API 規範組裝 10 個參數 (共 9 個 ^ 分隔符) + // 格式:pageno^pagerec^orderby^udf_fields^wheresql_org^menuid^where_fields^where_value^where_field^where_idvalue + String queryFilter = "1^100^id^*^$whereSql^^^^^"; return await _apiService.fetchList( tableName: "eip_todolist", @@ -36,14 +51,14 @@ class TodoApiService { "task_name": todo.taskName, "task_desc": todo.description, "issue_priority": todo.priority, - "pbi_status": todo.status ?? 'WIP', // 預設為進行中 + "pbi_status": todo.status ?? 'WIP', "end_date": todo.endDate != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(todo.endDate!) : null, "create_user": currentUserId, "create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), + // 新增時先不寫入 update_user / update_date }; try { - // 根據 Generic API 規範,使用 action: "C" 進行新增 await _apiService.fetchList( tableName: "eip_todolist", pk: "id", @@ -63,13 +78,13 @@ class TodoApiService { Future updateTodoStatus(int id, String status) async { final Map data = { "id": id, // 必填 PK - "pbi_status": status, // 更新狀態為 'DONE' - "modify_user": currentUserId, - "modify_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), + "pbi_status": status, // 更新狀態 + // 修正為 Schema 正確的欄位名稱 update_user / update_date + "update_user": currentUserId, + "update_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), }; try { - // 使用 Action "U" 代表 Update await _apiService.fetchList( tableName: "eip_todolist", pk: "id", diff --git a/lib/todo/todo_form.dart b/lib/todo/todo_form.dart index 28c8506..8da2411 100644 --- a/lib/todo/todo_form.dart +++ b/lib/todo/todo_form.dart @@ -1,4 +1,4 @@ -// todo_form.dart (適配 Todo Model) +// todo_form.dart (適配 Todo Model - 中文化與選項調整) import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -21,8 +21,10 @@ class _TodoFormState extends State { final TextEditingController _nameController = TextEditingController(); final TextEditingController _descController = TextEditingController(); - String _selectedClass = 'Work'; - String _selectedPriority = 'Medium'; + // 根據需求更新預設值與選項 + String _selectedClass = '工作'; + String _selectedPriority = 'Middle'; + String _selectedStatus = 'ToDo'; // 新增狀態變數 DateTime _endDate = DateTime.now().add(const Duration(days: 7)); final Color primaryPurple = const Color(0xFF6542D0); @@ -53,7 +55,7 @@ class _TodoFormState extends State { className: _selectedClass, description: _descController.text, priority: _selectedPriority, - status: 'WIP', + status: _selectedStatus, // 這裡改為帶入表單選擇的狀態 endDate: _endDate, createdBy: widget.userId, ); @@ -77,7 +79,7 @@ class _TodoFormState extends State { appBar: AppBar( backgroundColor: bgLight, elevation: 0, leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87), onPressed: () => Navigator.pop(context)), - title: const Text('New Task', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)), + title: const Text('新增任務', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)), centerTitle: true, ), body: SingleChildScrollView( @@ -87,15 +89,25 @@ class _TodoFormState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildDropdownField('Task Category', _selectedClass, ['Work', 'Personal', 'Urgent'], (val) => setState(() => _selectedClass = val!)), + // 類別:改為 固定三個選項 + _buildDropdownField('任務類別', _selectedClass, ['工作', '個人行程', '其他'], (val) => setState(() => _selectedClass = val!)), const SizedBox(height: 20), - _buildTextField('Task Name', _nameController, 'e.g. Design UI Mockup', Icons.edit_note), + + // 名稱與說明:加上中文提示 + _buildTextField('任務名稱', _nameController, '例如:撰寫系統分析報告', Icons.edit_note), const SizedBox(height: 20), - _buildTextField('Description', _descController, 'Enter details here...', Icons.description, isMultiline: true), + _buildTextField('任務說明', _descController, '請輸入任務詳細說明...', Icons.description, isMultiline: true), const SizedBox(height: 20), - _buildDropdownField('Priority', _selectedPriority, ['High', 'Medium', 'Low'], (val) => setState(() => _selectedPriority = val!)), + + // 優先級:改為 High, Middle, Low + _buildDropdownField('優先級', _selectedPriority, ['High', 'Middle', 'Low'], (val) => setState(() => _selectedPriority = val!)), const SizedBox(height: 20), - _buildDatePicker('Due Date', _endDate), + + // 新增狀態下拉選單 + _buildDropdownField('目前狀態', _selectedStatus, ['ToDo', 'WIP', 'Hold', 'Done'], (val) => setState(() => _selectedStatus = val!)), + const SizedBox(height: 20), + + _buildDatePicker('截止日期', _endDate), const SizedBox(height: 40), _buildSubmitButton(), ], @@ -119,7 +131,7 @@ class _TodoFormState extends State { controller: controller, maxLines: isMultiline ? 3 : 1, decoration: InputDecoration(hintText: hint, border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.only(top: 8)), - validator: (v) => v == null || v.isEmpty ? 'Cannot be empty' : null, + validator: (v) => v == null || v.isEmpty ? '此欄位不能為空' : null, // 必填防呆中文化 ) ], ), @@ -155,7 +167,8 @@ class _TodoFormState extends State { children: [ Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), const SizedBox(height: 4), - Text(DateFormat('dd MMM, yyyy').format(date), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + // 日期格式也順便調整成台灣習慣的 YYYY/MM/DD + Text(DateFormat('yyyy/MM/dd').format(date), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ], ), Icon(Icons.calendar_today, color: primaryPurple, size: 20), @@ -172,7 +185,7 @@ class _TodoFormState extends State { child: ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: primaryPurple, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 5), onPressed: _handleSubmit, - child: const Text('Create Task', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), + child: const Text('建立任務', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), ), ); } diff --git a/lib/todo/todo_model.dart b/lib/todo/todo_model.dart index ff4a0a3..540a24c 100644 --- a/lib/todo/todo_model.dart +++ b/lib/todo/todo_model.dart @@ -1,3 +1,4 @@ +// todo_model.dart import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -11,6 +12,8 @@ class Todo { final DateTime? endDate; // end_date (預計完成日期) final String? createdBy; // create_user (建立者) final DateTime? createDate; // create_date (建立日期) + final String? updatedBy; // update_user (更新者) - 配合新 Schema 新增 + final DateTime? updateDate; // update_date (更新日期) - 配合新 Schema 新增 Todo({ required this.id, @@ -22,6 +25,8 @@ class Todo { this.endDate, this.createdBy, this.createDate, + this.updatedBy, + this.updateDate, }); // Factory 構造函數:從 API 返回的 JSON (Map) 創建 Todo 物件 @@ -29,7 +34,6 @@ class Todo { // 輔助函數:安全解析日期字串 DateTime? parseDate(dynamic date) { if (date is String && date.isNotEmpty) { - // 假設日期格式為 YYYY-MM-DD HH:mm:ss.sss 或 YYYY-MM-DD return DateTime.tryParse(date); } return null; @@ -45,6 +49,8 @@ class Todo { endDate: parseDate(json['end_date']), createdBy: json['create_user'] as String?, createDate: parseDate(json['create_date']), + updatedBy: json['update_user'] as String?, + updateDate: parseDate(json['update_date']), ); }