diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 48e93bd..3e74f88 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ > fetchMessages(String userId) async { + // 計算 30 天前的日期,作為查詢參數 + final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30)); + final dateParam = DateFormat('yyyy-MM-dd').format(thirtyDaysAgo); + + // 格式:當前頁^每頁筆數^排序欄位^關鍵字欄位^關鍵字內容 + // 排序:按通知日期 (create_date) 降冪排列 + // 實務上可根據您的 Generic API 支援度,將 dateParam 傳入作為過濾條件 + // String queryFilter = "1^100^create_date^*^^^receivers^$userId^after^$dateParam"; + String queryFilter = "1^100^create_date^*^^^receivers^$userId^^"; + + return await _apiService.fetchList( + tableName: "eip_message", // 指向訊息資料表 + pk: "id", // + queryFilter: queryFilter, + fromJson: (json) => EipMessage.fromJson(json), + ); + } + + // (選用) 更新訊息為已讀狀態 + Future markAsRead(int messageId) async { + // 實作呼叫更新 msg_status 的 API 邏輯 + // ... + return true; + } +} \ No newline at end of file diff --git a/lib/message/message_detail.dart b/lib/message/message_detail.dart new file mode 100644 index 0000000..562481f --- /dev/null +++ b/lib/message/message_detail.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import './message_model.dart'; + +class MessageDetail extends StatelessWidget { + final EipMessage message; + + const MessageDetail({required this.message, super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('訊息內容'), + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0.5, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 主旨區塊 + Text( + message.subjectLine, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, height: 1.4), + ), + const SizedBox(height: 16), + + // 寄件資訊區塊 + Row( + children: [ + CircleAvatar( + backgroundColor: Colors.blueGrey.shade100, + child: const Icon(Icons.person, color: Colors.blueGrey), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message.sender, // + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + Text( + message.formattedDate, + style: const TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + ], + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 20.0), + child: Divider(), + ), + + // 訊息內文區塊 + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + message.msgContent.isNotEmpty ? message.msgContent : '無內容', // + style: const TextStyle(fontSize: 16, height: 1.6, color: Colors.black87), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/message/message_manager.dart b/lib/message/message_manager.dart new file mode 100644 index 0000000..a4705f7 --- /dev/null +++ b/lib/message/message_manager.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import './message_model.dart'; +import './message_api.dart'; +import './message_detail.dart'; + +class MessageManager extends StatefulWidget { + final String currentUserId; + const MessageManager({required this.currentUserId, super.key}); + + @override + State createState() => _MessageManagerState(); +} + +class _MessageManagerState extends State { + late MessageApiService _apiService; + late Future> _messageFuture; + + @override + void initState() { + super.initState(); + _apiService = MessageApiService(); + _refreshList(); + } + + void _refreshList() { + setState(() { + _messageFuture = _apiService.fetchMessages(widget.currentUserId); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('通知訊息'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _refreshList, + ) + ], + ), + body: FutureBuilder>( + future: _messageFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('近 30 天內無任何通知訊息')); + } + return ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (ctx, i) => _buildMessageCard(snapshot.data![i]), + ); + }, + ), + // 此為唯讀功能,因此移除 FloatingActionButton + ); + } + + Widget _buildMessageCard(EipMessage item) { + return Card( + elevation: 0.5, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + side: BorderSide(color: Colors.grey.shade200), + borderRadius: BorderRadius.circular(10), + ), + child: ListTile( + onTap: () async { + // 點擊進入詳情 + await Navigator.push( + context, + MaterialPageRoute(builder: (context) => MessageDetail(message: item)), + ); + // 若在詳情頁有觸發「已讀」,返回時可重新整理列表 + _refreshList(); + }, + leading: CircleAvatar( + backgroundColor: item.statusColor.withOpacity(0.1), + child: Icon( + item.isRead ? Icons.mark_email_read : Icons.mark_email_unread, + color: item.statusColor, + ), + ), + title: Text( + item.subjectLine, // + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: item.isRead ? FontWeight.normal : FontWeight.bold, + ), + ), + subtitle: Text(item.formattedDate), + ), + ); + } +} \ No newline at end of file diff --git a/lib/message/message_model.dart b/lib/message/message_model.dart new file mode 100644 index 0000000..be7e350 --- /dev/null +++ b/lib/message/message_model.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class EipMessage { + final int id; // id (Primary Key) + final String sender; // sender (發件人) + final String receivers; // receivers (收件人) + final String subjectLine; // subject_line (主旨) + final String msgContent; // msg_content (內容) + final String msgStatus; // msg_status (閱讀狀態) + final DateTime? createDate; // create_date (通知日期) + + EipMessage({ + required this.id, + this.sender = '', + this.receivers = '', + this.subjectLine = '', + this.msgContent = '', + this.msgStatus = '0', + this.createDate, + }); + + factory EipMessage.fromJson(Map json) { + return EipMessage( + id: int.tryParse(json['id']?.toString() ?? '0') ?? 0, + sender: json['sender'] as String? ?? '系統通知', + receivers: json['receivers'] as String? ?? '', + subjectLine: json['subject_line'] as String? ?? '無主旨', + msgContent: json['msg_content'] as String? ?? '', + msgStatus: json['msg_status'] as String? ?? '0', + createDate: json['create_date'] != null ? DateTime.tryParse(json['create_date']) : null, + ); + } + + // 格式化顯示通知日期 + String get formattedDate { + if (createDate == null) return '未知時間'; + final df = DateFormat('yyyy/MM/dd HH:mm'); + return df.format(createDate!); + } + + // 判斷是否已讀 (假設 '1' 為已讀,'0' 為未讀) + bool get isRead => msgStatus == '1'; + + // 狀態視覺呈現 + Color get statusColor => isRead ? Colors.grey : Colors.blueAccent; + String get statusText => isRead ? '已讀' : '未讀'; +} \ No newline at end of file diff --git a/lib/todo/todo_form.dart b/lib/todo/todo_form.dart new file mode 100644 index 0000000..03c1630 --- /dev/null +++ b/lib/todo/todo_form.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; + +class TodoForm extends StatefulWidget { + const TodoForm({super.key}); + + @override + State createState() => _TodoFormState(); +} + +class _TodoFormState extends State { + 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 + 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)), + 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)), + ), + ) + ], + ), + ), + ); + } + + // --- 輔助表單元件 --- + + Widget _buildDropdownField(String label, String value, IconData icon) { + 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), + ), + ) + ], + ), + ); + } + + Widget _buildDatePicker(String label, DateTime date, bool isStart) { + // 實務上這裡會加上 onTap 呼叫 showDatePicker + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 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: [ + 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) + ], + ), + ); + } + + 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)) + ), + onPressed: () {}, + child: Text('Change Logo', style: TextStyle(color: primaryPurple, fontWeight: FontWeight.bold)), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/todo/todo_manager.dart b/lib/todo/todo_manager.dart index 4d52fee..22e35df 100644 --- a/lib/todo/todo_manager.dart +++ b/lib/todo/todo_manager.dart @@ -1,33 +1,46 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import './todo_api.dart'; import './todo_model.dart'; -import './todo_detail.dart'; // 稍後創建 -// import './main.dart'; // 確保可以訪問 MainMenu +import './todo_detail.dart'; +import './todo_form.dart'; class TodoManager extends StatefulWidget { - // 實際應用中,這裡應該傳入當前用戶 ID final String currentUserId; const TodoManager({this.currentUserId = 'admin', super.key}); @override - State createState() { - return _TodoManagerState(); - } + State createState() => _TodoManagerState(); } 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; // 儲存動態產生的日期 + + final Color primaryPurple = const Color(0xFF6542D0); + final Color bgLight = const Color(0xFFF8F9FA); + @override void initState() { super.initState(); _apiService = TodoApiService(currentUserId: widget.currentUserId); - // 頁面加載時自動開始獲取資料 _todosFuture = _apiService.fetchTodos(); + _generateDates(); + } + + // 產生前3天到後1天的日期區間 + void _generateDates() { + DateTime today = DateTime.now(); + _dynamicDates = List.generate(5, (index) { + return today.subtract(Duration(days: 3 - index)); + }); } - // 刷新資料的函數 void _refreshTodos() { setState(() { _todosFuture = _apiService.fetchTodos(); @@ -37,113 +50,256 @@ class _TodoManagerState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('我的待辦事項'), - actions: [ + backgroundColor: bgLight, + body: SafeArea( + child: Column( + children: [ + _buildCustomHeader(), + _buildDateSelector(), + _buildFilterChips(), + Expanded( + child: _buildTodoListBody(), + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (_) => const TodoForm())); + }, + backgroundColor: primaryPurple, + elevation: 4, + shape: const CircleBorder(), + child: const Icon(Icons.add, color: Colors.white, size: 28), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: _buildBottomNav(), + ); + } + + // --- UI 元件區塊 --- + + Widget _buildCustomHeader() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ IconButton( - icon: const Icon(Icons.refresh), - tooltip: '刷新列表', - onPressed: _refreshTodos, - ), - // 假設這裡使用 Navigator.pop(context) 即可返回 MainMenu - IconButton( - icon: const Icon(Icons.home), - tooltip: '返回主頁', + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87), onPressed: () => Navigator.pop(context), ), + const Text( + "Today's Tasks", + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + ), + // 移除小鈴鐺圖示後,保留一個佔位符號以確保標題能完美置中 + const SizedBox(width: 48), ], ), - body: FutureBuilder>( - future: _todosFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center( + ); + } + + 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; + + 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)) + ], + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('載入失敗: ${snapshot.error}', textAlign: TextAlign.center), - const SizedBox(height: 16), - ElevatedButton(onPressed: _refreshTodos, child: const Text('重試')), + 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)), ], ), - ); - } else if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return TodoList(todos: snapshot.data!); - } else { - return const Center(child: Text('目前沒有待辦事項。工作很輕鬆!')); - } + ), + ); }, ), ); } -} -// ----------------------------------------------------------- -// 列表顯示小部件 (TodoList) -// ----------------------------------------------------------- - -class TodoList extends StatelessWidget { - final List todos; - - const TodoList({required this.todos, super.key}); - - @override - Widget build(BuildContext context) { - return ListView.builder( - itemCount: todos.length, - itemBuilder: (BuildContext context, int index) { - final item = todos[index]; - - return Card( - elevation: 3, - margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 16.0), - child: InkWell( - onTap: () { - // 點擊項目:導航到詳細頁面 - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TodoDetail(todo: item), - ), - ); - }, - child: ListTile( - // 左側狀態指示器 - leading: Container( - width: 10, - decoration: BoxDecoration( - color: item.statusColor, - borderRadius: BorderRadius.circular(5), - ), - ), - title: Text( - item.taskName, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - '分類: ${item.className} | 優先級: ${item.priority ?? '一般'}', - style: const TextStyle(fontSize: 12), - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '截止日', - style: TextStyle(fontSize: 10, color: item.statusColor), - ), - Text( - item.formattedEndDate, - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600), - ), - ], + Widget _buildFilterChips() { + final filters = ['All', 'To do', 'In Progress', 'Done']; + return SizedBox( + height: 60, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + itemCount: filters.length, + itemBuilder: (context, index) { + final isSelected = _selectedFilter == filters[index]; + return Padding( + padding: const EdgeInsets.only(right: 12), + child: ChoiceChip( + label: Text(filters[index]), + selected: isSelected, + onSelected: (bool selected) { + if (selected) setState(() => _selectedFilter = filters[index]); + }, + selectedColor: primaryPurple, + backgroundColor: const Color(0xFFF0EFFF), + labelStyle: TextStyle( + color: isSelected ? Colors.white : primaryPurple, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide.none), ), - ), - ); + ); + }, + ), + ); + } + + Widget _buildTodoListBody() { + return FutureBuilder>( + future: _todosFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + 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('目前沒有待辦事項。工作很輕鬆!')); + } }, ); } + + 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))], + ), + child: InkWell( + 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)), + ) + ], + ) + ], + ), + ), + ); + } + + // 移除了內部的圖示,但保留了 BottomAppBar 的挖空設計與高度 + Widget _buildBottomNav() { + return const BottomAppBar( + shape: CircularNotchedRectangle(), + notchMargin: 8.0, + color: Color(0xFFF0EFFF), + elevation: 0, + child: SizedBox( + height: 60, // 維持底部導覽列的高度,留給 FAB 空間 + ), + ); + } } \ No newline at end of file diff --git a/lib/todo/ux/ai-prompt.txt b/lib/todo/ux/ai-prompt.txt new file mode 100644 index 0000000..aae9d99 --- /dev/null +++ b/lib/todo/ux/ai-prompt.txt @@ -0,0 +1,7 @@ + +1.W 3iϡAbWǭ}o{ +MAIs Gemini 3.0 U prompt + +бN쥻}on ux אּsWǪ˦Aapi 줣ܡAuOѦ ux ק + +>> N|ͷs{ \ No newline at end of file diff --git a/lib/todo/ux/todo-01.png b/lib/todo/ux/todo-01.png new file mode 100644 index 0000000..916923d Binary files /dev/null and b/lib/todo/ux/todo-01.png differ diff --git a/lib/todo/ux/todo-02.png b/lib/todo/ux/todo-02.png new file mode 100644 index 0000000..8bdd010 Binary files /dev/null and b/lib/todo/ux/todo-02.png differ diff --git a/lib/todo/ux/todo-03.png b/lib/todo/ux/todo-03.png new file mode 100644 index 0000000..97f71d3 Binary files /dev/null and b/lib/todo/ux/todo-03.png differ diff --git a/pubspec.lock b/pubspec.lock index 1f99d86..2d558ec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -21,10 +37,26 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -150,6 +182,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -240,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" image_picker: dependency: "direct main" description: @@ -312,6 +360,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" leak_tracker: dependency: transitive description: @@ -348,18 +404,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -408,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -424,6 +488,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" shared_preferences: dependency: "direct main" description: @@ -545,10 +617,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" typed_data: dependency: transitive description: @@ -661,6 +733,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.10.0 <4.0.0" flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4560382..7dcac64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,17 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 + flutter_launcher_icons: ^0.13.1 # 請檢查最新版本 + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/images/app_icon.png" + min_sdk_android: 21 # android support for adaptive icons + +# 如果需要 Android 自適應圖示 (Adaptive Icons),可額外配置: +# adaptive_icon_background: "#FFFFFF" +# adaptive_icon_foreground: "assets/images/app_icon_foreground.png" # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec