diff --git a/lib/main.dart b/lib/main.dart index 094b643..35c286a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -126,7 +126,7 @@ class MainMenu extends StatelessWidget { MenuItem( title: '待辦事項', icon: Icons.checklist, - targetScreen: TodoManager(), + targetScreen: TodoManager(currentUserId: uid), ), // 5. 行事曆 MenuItem( diff --git a/lib/todo/todo_api.dart b/lib/todo/todo_api.dart index ca53424..0ea6eb2 100644 --- a/lib/todo/todo_api.dart +++ b/lib/todo/todo_api.dart @@ -1,5 +1,8 @@ +// todo_api.dart (新增 Create 功能) + import './todo_model.dart'; -import '../services/generic_api_service.dart'; // 引入共用服務 +import '../services/generic_api_service.dart'; +import 'package:intl/intl.dart'; class TodoApiService { final GenericApiService _apiService = GenericApiService(); @@ -7,16 +10,78 @@ class TodoApiService { TodoApiService({this.currentUserId = 'admin'}); - Future> fetchTodos() async { + // 原始的查詢功能... + /// 獲取指定日期的任務清單 + Future> fetchTodos({DateTime? selectedDate}) 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"; + return await _apiService.fetchList( tableName: "eip_todolist", pk: "id", - // 如果 queryFilter 需要動態包含使用者 ID,可以在這裡字串插值 - // 假設原程式碼邏輯是 "1^10^id^*^^pmsm02^^" - queryFilter: "1^10^id^*^^pmsm02^^", + queryFilter: queryFilter, fromJson: (json) => Todo.fromJson(json), - // 如果未來需要傳遞其他參數 (如 userID),可以用 additionalParams - // additionalParams: { "userId": currentUserId }, ); } + + // 新增:提交任務至資料庫 + Future createTodo(Todo todo) async { + final Map data = { + "todolist_class": todo.className, + "task_name": todo.taskName, + "task_desc": todo.description, + "issue_priority": todo.priority, + "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()), + }; + + try { + // 根據 Generic API 規範,使用 action: "C" 進行新增 + await _apiService.fetchList( + tableName: "eip_todolist", + pk: "id", + queryFilter: "", + action: "C", + data: data, + fromJson: (json) => json, + ); + return true; + } catch (e) { + print("Create Todo Error: $e"); + return false; + } + } + + /// 更新任務狀態為已完成 + 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()), + }; + + try { + // 使用 Action "U" 代表 Update + await _apiService.fetchList( + tableName: "eip_todolist", + pk: "id", + queryFilter: "", + action: "U", + data: data, + fromJson: (json) => json, + ); + return true; + } catch (e) { + print("Update Todo Status Error: $e"); + return false; + } + } } \ No newline at end of file diff --git a/lib/todo/todo_detail.dart b/lib/todo/todo_detail.dart index dd1e7a0..4b164d2 100644 --- a/lib/todo/todo_detail.dart +++ b/lib/todo/todo_detail.dart @@ -1,97 +1,189 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import './todo_model.dart'; +import './todo_api.dart'; -class TodoDetail extends StatelessWidget { +class TodoDetail extends StatefulWidget { final Todo todo; const TodoDetail({required this.todo, super.key}); + @override + State createState() => _TodoDetailState(); +} + +class _TodoDetailState extends State { + final TodoApiService _apiService = TodoApiService(); + bool _isUpdating = false; // 控制按鈕 Loading 狀態 + + // 執行「標記為完成」的 API 邏輯 + Future _handleMarkAsDone() async { + setState(() => _isUpdating = true); + + // 呼叫 API 更新 pbi_status 為 'DONE' + bool success = await _apiService.updateTodoStatus(widget.todo.id, 'DONE'); + + if (mounted) { + setState(() => _isUpdating = false); + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('任務 "${widget.todo.taskName}" 已完成!')), + ); + // 重要:返回 true 告知列表頁 (TodoManager) 執行 _refreshTodos() + Navigator.pop(context, true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('狀態更新失敗,請檢查網路連線。'), backgroundColor: Colors.red), + ); + } + } + } + @override Widget build(BuildContext context) { + // 檢查目前狀態是否已為 DONE + bool isCompleted = widget.todo.status?.toUpperCase() == 'DONE'; + return Scaffold( + backgroundColor: const Color(0xFFF8F9FA), appBar: AppBar( - title: Text(todo.taskName, overflow: TextOverflow.ellipsis), + title: const Text('任務詳情', style: TextStyle(fontWeight: FontWeight.bold)), + centerTitle: true, + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, ), body: SingleChildScrollView( - padding: const EdgeInsets.all(20.0), + padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 任務標題 - Text( - todo.taskName, - style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold, color: Colors.black87), - ), - const Divider(height: 24.0), + children: [ + // 標題與分類卡片 + _buildHeaderCard(), + const SizedBox(height: 24), - // 任務屬性表格 (簡潔顯示) - _buildAttributeRow(context, '狀態', todo.status ?? 'N/A', todo.statusColor), - _buildAttributeRow(context, '優先級', todo.priority ?? 'N/A', Colors.red), - _buildAttributeRow(context, '截止日期', todo.formattedEndDate, Colors.blue), - _buildAttributeRow(context, '建立者', todo.createdBy ?? 'N/A', Colors.grey), - _buildAttributeRow(context, '分類', todo.className, Colors.purple), + // 詳細屬性清單 + const Text('詳細資訊', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey)), + const SizedBox(height: 12), + _buildInfoCard(), + const SizedBox(height: 24), - const Divider(height: 32.0), - - // 詳細說明 - const Text( - '詳細說明:', - style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600, color: Colors.black87), - ), - const SizedBox(height: 8), - Text( - todo.description ?? '無詳細說明。', - style: const TextStyle(fontSize: 16.0, height: 1.5, color: Colors.black54), - textAlign: TextAlign.justify, - ), + // 任務描述 + const Text('任務說明', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey)), + const SizedBox(height: 12), + _buildDescriptionBox(), const SizedBox(height: 40), - // 底部按鈕 (例如:標記完成) - Center( - child: ElevatedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('已標記任務 "${todo.taskName}" 待實作更新狀態。')), - ); - // 實際應用中,這裡會呼叫 API 更新 pbi_status 為 'DONE' - }, - icon: const Icon(Icons.check_circle_outline), - label: const Text('標記為已完成'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), - backgroundColor: Colors.green, - foregroundColor: Colors.white, - textStyle: const TextStyle(fontSize: 18), - ), - ), - ), + // 操作按鈕 + _buildActionButton(isCompleted), ], ), ), ); } - Widget _buildAttributeRow(BuildContext context, String label, String value, Color color) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( + Widget _buildHeaderCard() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10)], + ), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 100, - child: Text( - '$label:', - style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black), - ), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration(color: const Color(0xFFF0EFFF), borderRadius: BorderRadius.circular(8)), + child: Text(widget.todo.className, style: const TextStyle(color: Color(0xFF6542D0), fontWeight: FontWeight.bold, fontSize: 12)), + ), + const Spacer(), + Text(widget.todo.priority ?? '一般', style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.w600)), + ], ), - Expanded( - child: Text( - value, - style: TextStyle(fontWeight: FontWeight.w600, color: color), - ), + const SizedBox(height: 16), + Text( + widget.todo.taskName, + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87), ), ], ), ); } + + Widget _buildInfoCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), + child: Column( + children: [ + _buildDetailRow(Icons.calendar_today, '截止日期', widget.todo.formattedEndDate), + const Divider(height: 32), + _buildDetailRow(Icons.person_outline, '建立者', widget.todo.createdBy ?? '系統'), + const Divider(height: 32), + _buildDetailRow(Icons.info_outline, '目前狀態', widget.todo.status ?? 'WIP', color: widget.todo.statusColor), + ], + ), + ); + } + + Widget _buildDetailRow(IconData icon, String label, String value, {Color? color}) { + return Row( + children: [ + Icon(icon, size: 20, color: Colors.grey), + const SizedBox(width: 12), + Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)), + const Spacer(), + Text(value, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: color ?? Colors.black87)), + ], + ); + } + + Widget _buildDescriptionBox() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), + child: Text( + widget.todo.description ?? '尚無詳細說明。', + style: const TextStyle(fontSize: 15, color: Colors.black54, height: 1.6), + ), + ); + } + + Widget _buildActionButton(bool isCompleted) { + if (isCompleted) { + return Center( + child: Column( + children: const [ + Icon(Icons.check_circle, color: Colors.green, size: 64), + SizedBox(height: 8), + Text('此任務已圓滿完成', style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold, fontSize: 16)), + ], + ), + ); + } + + return SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton.icon( + onPressed: _isUpdating ? null : _handleMarkAsDone, + icon: _isUpdating + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) + : const Icon(Icons.check_circle_outline), + label: Text(_isUpdating ? '正在更新狀態...' : '標記為已完成', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 4, + ), + ), + ); + } } \ No newline at end of file diff --git a/lib/todo/todo_form.dart b/lib/todo/todo_form.dart index 03c1630..28c8506 100644 --- a/lib/todo/todo_form.dart +++ b/lib/todo/todo_form.dart @@ -1,206 +1,178 @@ +// todo_form.dart (適配 Todo Model) + import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import './todo_model.dart'; +import './todo_api.dart'; class TodoForm extends StatefulWidget { - const TodoForm({super.key}); + final String userId; + const TodoForm({required this.userId, super.key}); @override State createState() => _TodoFormState(); } class _TodoFormState extends State { + final _formKey = GlobalKey(); + late TodoApiService _apiService; + + // 控制器與狀態 + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _descController = TextEditingController(); + + String _selectedClass = 'Work'; + String _selectedPriority = 'Medium'; + DateTime _endDate = DateTime.now().add(const Duration(days: 7)); + final Color primaryPurple = const Color(0xFF6542D0); final Color bgLight = const Color(0xFFF8F9FA); - // 表單資料狀態 (可視需求綁定至 API) - String _selectedGroup = 'Work'; - DateTime _startDate = DateTime.now(); - DateTime _endDate = DateTime.now().add(const Duration(days: 30)); + @override + void initState() { + super.initState(); + _apiService = TodoApiService(currentUserId: widget.userId); + } + + Future _selectEndDate() async { + final picked = await showDatePicker( + context: context, + initialDate: _endDate, + firstDate: DateTime.now(), + lastDate: DateTime(2030), + ); + if (picked != null) setState(() => _endDate = picked); + } + + void _handleSubmit() async { + if (_formKey.currentState!.validate()) { + // 構建 Todo 物件 + final newTodo = Todo( + id: 0, // 由後端生成 + taskName: _nameController.text, + className: _selectedClass, + description: _descController.text, + priority: _selectedPriority, + status: 'WIP', + endDate: _endDate, + createdBy: widget.userId, + ); + + bool success = await _apiService.createTodo(newTodo); + if (mounted) { + if (success) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('任務新增成功!'))); + Navigator.pop(context, true); // 回傳 true 告知列表頁刷新 + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('新增失敗,請檢查網路連線。'))); + } + } + } + } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: bgLight, 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('Add Project', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)), + 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)), centerTitle: true, - actions: [ - IconButton(icon: const Icon(Icons.notifications_outlined, color: Colors.black87), onPressed: () {}) - ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDropdownField('Task Group', _selectedGroup, Icons.work_outline), - const SizedBox(height: 24), - _buildTextField('Project Name', 'Grocery Shopping App', isMultiline: false), - const SizedBox(height: 24), - _buildTextField('Description', 'This application is designed for super shops...', isMultiline: true), - const SizedBox(height: 24), - _buildDatePicker('Start Date', _startDate, true), - const SizedBox(height: 24), - _buildDatePicker('End Date', _endDate, false), - const SizedBox(height: 24), - _buildLogoSelector(), - const SizedBox(height: 40), - - // 底部大型送出按鈕 - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: primaryPurple, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - elevation: 5, - shadowColor: primaryPurple.withOpacity(0.5) - ), - onPressed: () { - // 觸發 API 儲存邏輯 - Navigator.pop(context); - }, - child: const Text('Add Project', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), - ), - ) - ], + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDropdownField('Task Category', _selectedClass, ['Work', 'Personal', 'Urgent'], (val) => setState(() => _selectedClass = val!)), + const SizedBox(height: 20), + _buildTextField('Task Name', _nameController, 'e.g. Design UI Mockup', Icons.edit_note), + const SizedBox(height: 20), + _buildTextField('Description', _descController, 'Enter details here...', Icons.description, isMultiline: true), + const SizedBox(height: 20), + _buildDropdownField('Priority', _selectedPriority, ['High', 'Medium', 'Low'], (val) => setState(() => _selectedPriority = val!)), + const SizedBox(height: 20), + _buildDatePicker('Due Date', _endDate), + const SizedBox(height: 40), + _buildSubmitButton(), + ], + ), ), ), ); } - // --- 輔助表單元件 --- + // --- 重構後的 UI 元件 --- - Widget _buildDropdownField(String label, String value, IconData icon) { + Widget _buildTextField(String label, TextEditingController controller, String hint, IconData icon, {bool isMultiline = false}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 5))]), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: value, - isExpanded: true, - icon: const Icon(Icons.arrow_drop_down, color: Colors.black54), - items: ['Work', 'Personal', 'Study'].map((String val) { - return DropdownMenuItem( - value: val, - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(color: Colors.pink.shade50, borderRadius: BorderRadius.circular(10)), - child: Icon(icon, color: Colors.pinkAccent, size: 20), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), - Text(val, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87)), - ], - ) - ], - ), - ); - }).toList(), - onChanged: (newValue) { - if (newValue != null) setState(() => _selectedGroup = newValue); - }, - ), - ), - ); - } - - Widget _buildTextField(String label, String placeholder, {bool isMultiline = false}) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 5))]), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), TextFormField( - initialValue: placeholder, - maxLines: isMultiline ? 4 : 1, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black87), - decoration: const InputDecoration( - border: InputBorder.none, - isDense: true, - contentPadding: EdgeInsets.only(top: 8), - ), + 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, ) ], ), ); } - Widget _buildDatePicker(String label, DateTime date, bool isStart) { - // 實務上這裡會加上 onTap 呼叫 showDatePicker + Widget _buildDropdownField(String label, String value, List items, ValueChanged onChanged) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 5))]), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(color: const Color(0xFFF0EFFF), borderRadius: BorderRadius.circular(10)), - child: Icon(Icons.calendar_month, color: primaryPurple, size: 20), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)), - const SizedBox(height: 4), - Text('01 May, 2022', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black87)), // Mock 字串 - ], - ), - ], - ), - const Icon(Icons.arrow_drop_down, color: Colors.black54) - ], + child: DropdownButtonHideUnderline( + child: DropdownButtonFormField( + value: value, + decoration: InputDecoration(labelText: label, labelStyle: const TextStyle(fontSize: 14, color: Colors.grey), border: InputBorder.none), + items: items.map((s) => DropdownMenuItem(value: s, child: Text(s, style: const TextStyle(fontWeight: FontWeight.bold)))).toList(), + onChanged: onChanged, + ), ), ); } - Widget _buildLogoSelector() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 5))]), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CircleAvatar(backgroundColor: Colors.teal, radius: 24, child: Text('GS', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text('Grocery', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.teal)), - Text('shop', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orange)), - ], - ) - ], - ), - TextButton( - style: TextButton.styleFrom( - backgroundColor: const Color(0xFFF0EFFF), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)) + Widget _buildDatePicker(String label, DateTime date) { + return InkWell( + onTap: _selectEndDate, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 5))]), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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)), + ], ), - onPressed: () {}, - child: Text('Change Logo', style: TextStyle(color: primaryPurple, fontWeight: FontWeight.bold)), - ) - ], + Icon(Icons.calendar_today, color: primaryPurple, size: 20), + ], + ), + ), + ); + } + + Widget _buildSubmitButton() { + return SizedBox( + width: double.infinity, + height: 56, + 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)), ), ); } diff --git a/lib/todo/todo_manager.dart b/lib/todo/todo_manager.dart index 22e35df..65cf48e 100644 --- a/lib/todo/todo_manager.dart +++ b/lib/todo/todo_manager.dart @@ -17,10 +17,9 @@ class _TodoManagerState extends State { late TodoApiService _apiService; late Future> _todosFuture; - // UI 狀態控制 - int _selectedDateIndex = 3; // 預設選中 index 3 (即「今天」) - String _selectedFilter = 'All'; // All, To do, In Progress, Done - late List _dynamicDates; // 儲存動態產生的日期 + int _selectedDateIndex = 2; // 預設中間為「今天」 + String _selectedFilter = 'All'; + late List _dynamicDates; final Color primaryPurple = const Color(0xFF6542D0); final Color bgLight = const Color(0xFFF8F9FA); @@ -29,21 +28,24 @@ class _TodoManagerState extends State { void initState() { super.initState(); _apiService = TodoApiService(currentUserId: widget.currentUserId); - _todosFuture = _apiService.fetchTodos(); _generateDates(); + _refreshTodos(); } - // 產生前3天到後1天的日期區間 void _generateDates() { DateTime today = DateTime.now(); + // 產生 前2天 到 後2天,讓今天置中 _dynamicDates = List.generate(5, (index) { - return today.subtract(Duration(days: 3 - index)); + return today.subtract(Duration(days: 2 - index)); }); } void _refreshTodos() { setState(() { - _todosFuture = _apiService.fetchTodos(); + // 根據選中的日期去後端抓資料 + _todosFuture = _apiService.fetchTodos( + selectedDate: _dynamicDates[_selectedDateIndex] + ); }); } @@ -57,15 +59,20 @@ class _TodoManagerState extends State { _buildCustomHeader(), _buildDateSelector(), _buildFilterChips(), - Expanded( - child: _buildTodoListBody(), - ), + Expanded(child: _buildTodoListBody()), ], ), ), floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (_) => const TodoForm())); + onPressed: () async { + // 修正:加上 async 並移除 const,傳入 userId 並等待回傳結果 + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => TodoForm(userId: widget.currentUserId), + ), + ); + if (result == true) _refreshTodos(); }, backgroundColor: primaryPurple, elevation: 4, @@ -73,12 +80,15 @@ class _TodoManagerState extends State { child: const Icon(Icons.add, color: Colors.white, size: 28), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, - bottomNavigationBar: _buildBottomNav(), + bottomNavigationBar: const BottomAppBar( + shape: CircularNotchedRectangle(), + notchMargin: 8.0, + color: Color(0xFFF0EFFF), + child: SizedBox(height: 60), + ), ); } - // --- UI 元件區塊 --- - Widget _buildCustomHeader() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), @@ -90,10 +100,9 @@ class _TodoManagerState extends State { onPressed: () => Navigator.pop(context), ), const Text( - "Today's Tasks", - style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + "Tasks", + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), ), - // 移除小鈴鐺圖示後,保留一個佔位符號以確保標題能完美置中 const SizedBox(width: 48), ], ), @@ -102,41 +111,61 @@ class _TodoManagerState extends State { Widget _buildDateSelector() { return SizedBox( - height: 90, - child: ListView.builder( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: _dynamicDates.length, - itemBuilder: (context, index) { - final date = _dynamicDates[index]; - final isSelected = index == _selectedDateIndex; + height: 95, + child: Center( // 加入 Center 讓日期列表盡量置中 + child: ListView.builder( + shrinkWrap: true, // 讓內容寬度根據子項決定 + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 10), + itemCount: _dynamicDates.length, + itemBuilder: (context, index) { + final date = _dynamicDates[index]; + final isSelected = index == _selectedDateIndex; - return GestureDetector( - onTap: () => setState(() => _selectedDateIndex = index), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - margin: const EdgeInsets.symmetric(horizontal: 8), - width: 65, - decoration: BoxDecoration( - color: isSelected ? primaryPurple : Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - if (!isSelected) BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5)) - ], + return GestureDetector( + onTap: () { + setState(() => _selectedDateIndex = index); + _refreshTodos(); // 點擊連動 API 查詢 + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 6), + width: 62, + decoration: BoxDecoration( + color: isSelected ? primaryPurple : Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + if (!isSelected) + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 5) + ) + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + DateFormat('MMM').format(date), + style: TextStyle(color: isSelected ? Colors.white70 : Colors.grey, fontSize: 12) + ), + const SizedBox(height: 4), + Text( + DateFormat('dd').format(date), + style: TextStyle(color: isSelected ? Colors.white : Colors.black, fontSize: 20, fontWeight: FontWeight.bold) + ), + const SizedBox(height: 4), + Text( + DateFormat('EEE').format(date), + style: TextStyle(color: isSelected ? Colors.white70 : Colors.grey, fontSize: 12) + ), + ], + ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(DateFormat('MMM').format(date), style: TextStyle(color: isSelected ? Colors.white70 : Colors.grey, fontSize: 12)), - const SizedBox(height: 4), - Text(DateFormat('dd').format(date), style: TextStyle(color: isSelected ? Colors.white : Colors.black, fontSize: 20, fontWeight: FontWeight.bold)), - const SizedBox(height: 4), - Text(DateFormat('EEE').format(date), style: TextStyle(color: isSelected ? Colors.white70 : Colors.grey, fontSize: 12)), - ], - ), - ), - ); - }, + ); + }, + ), ), ); } @@ -147,7 +176,7 @@ class _TodoManagerState extends State { height: 60, child: ListView.builder( scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), itemCount: filters.length, itemBuilder: (context, index) { final isSelected = _selectedFilter == filters[index]; @@ -181,124 +210,47 @@ class _TodoManagerState extends State { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center(child: Text('載入失敗: ${snapshot.error}')); - } else if (snapshot.hasData) { - - // 取得目前選取的時間 - DateTime selectedDate = _dynamicDates[_selectedDateIndex]; - - List filteredTodos = snapshot.data!.where((todo) { - // 1. 狀態過濾邏輯 - bool statusMatch = true; - if (_selectedFilter != 'All') { - String status = (todo.status ?? '').toUpperCase(); - if (_selectedFilter == 'Done') statusMatch = status == 'DONE'; - else if (_selectedFilter == 'In Progress') statusMatch = status == 'WIP'; - else if (_selectedFilter == 'To do') statusMatch = status != 'DONE' && status != 'WIP'; - } - - // 2. 日期過濾邏輯 (比對 年、月、日) - bool dateMatch = false; - if (todo.endDate != null) { - dateMatch = (todo.endDate!.year == selectedDate.year) && - (todo.endDate!.month == selectedDate.month) && - (todo.endDate!.day == selectedDate.day); - } - - return statusMatch && dateMatch; - }).toList(); - - if (filteredTodos.isEmpty) { - return Center( - child: Text('此日期沒有「$_selectedFilter」狀態的任務。', - style: const TextStyle(color: Colors.grey, fontSize: 16)), - ); - } - - return RefreshIndicator( - onRefresh: () async => _refreshTodos(), - child: ListView.builder( - padding: const EdgeInsets.fromLTRB(20, 10, 20, 80), - itemCount: filteredTodos.length, - itemBuilder: (context, index) => _buildTaskCard(filteredTodos[index]), - ), - ); - } else { - return const Center(child: Text('目前沒有待辦事項。工作很輕鬆!')); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('此日期沒有任務,放鬆一下吧!')); } + + List filtered = snapshot.data!.where((todo) { + if (_selectedFilter == 'All') return true; + String status = (todo.status ?? '').toUpperCase(); + if (_selectedFilter == 'Done') return status == 'DONE'; + if (_selectedFilter == 'In Progress') return status == 'WIP'; + return status != 'DONE' && status != 'WIP'; + }).toList(); + + return RefreshIndicator( + onRefresh: () async => _refreshTodos(), + child: ListView.builder( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 80), + itemCount: filtered.length, + itemBuilder: (context, index) => _buildTaskCard(filtered[index]), + ), + ); }, ); } Widget _buildTaskCard(Todo item) { - String displayStatus = 'To-do'; - Color chipColor = Colors.blueGrey; - Color chipBg = Colors.blueGrey.withOpacity(0.1); - - if (item.status?.toUpperCase() == 'DONE') { - displayStatus = 'Done'; - chipColor = primaryPurple; - chipBg = primaryPurple.withOpacity(0.1); - } else if (item.status?.toUpperCase() == 'WIP') { - displayStatus = 'In Progress'; - chipColor = Colors.orange; - chipBg = Colors.orange.withOpacity(0.1); - } - return Container( margin: const EdgeInsets.only(bottom: 16), - padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), - boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.08), blurRadius: 15, offset: const Offset(0, 5))], + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 15, offset: const Offset(0, 5))], ), - child: InkWell( + child: ListTile( + contentPadding: const EdgeInsets.all(16), onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => TodoDetail(todo: item))), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(item.className, style: const TextStyle(color: Colors.grey, fontSize: 13, fontWeight: FontWeight.w500)), - Icon(Icons.work_outline, color: primaryPurple.withOpacity(0.5), size: 18), - ], - ), - const SizedBox(height: 8), - Text(item.taskName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87)), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const Icon(Icons.access_time_filled, color: Colors.grey, size: 16), - const SizedBox(width: 6), - Text(item.formattedEndDate, style: const TextStyle(color: Colors.grey, fontSize: 13, fontWeight: FontWeight.w500)), - ], - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration(color: chipBg, borderRadius: BorderRadius.circular(20)), - child: Text(displayStatus, style: TextStyle(color: chipColor, fontSize: 12, fontWeight: FontWeight.w700)), - ) - ], - ) - ], + title: Text(item.taskName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(item.className, style: const TextStyle(color: Colors.grey)), ), - ), - ); - } - - // 移除了內部的圖示,但保留了 BottomAppBar 的挖空設計與高度 - Widget _buildBottomNav() { - return const BottomAppBar( - shape: CircularNotchedRectangle(), - notchMargin: 8.0, - color: Color(0xFFF0EFFF), - elevation: 0, - child: SizedBox( - height: 60, // 維持底部導覽列的高度,留給 FAB 空間 + trailing: Icon(Icons.chevron_right, color: primaryPurple), ), ); }