modify icon & add notify function
This commit is contained in:
+253
-97
@@ -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<StatefulWidget> createState() {
|
||||
return _TodoManagerState();
|
||||
}
|
||||
State<StatefulWidget> createState() => _TodoManagerState();
|
||||
}
|
||||
|
||||
class _TodoManagerState extends State<TodoManager> {
|
||||
late TodoApiService _apiService;
|
||||
late Future<List<Todo>> _todosFuture;
|
||||
|
||||
// UI 狀態控制
|
||||
int _selectedDateIndex = 3; // 預設選中 index 3 (即「今天」)
|
||||
String _selectedFilter = 'All'; // All, To do, In Progress, Done
|
||||
late List<DateTime> _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<TodoManager> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('我的待辦事項'),
|
||||
actions: <Widget>[
|
||||
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<List<Todo>>(
|
||||
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<Todo> 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<List<Todo>>(
|
||||
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<Todo> 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 空間
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user