modify todo function (processing)
This commit is contained in:
+109
-157
@@ -17,10 +17,9 @@ 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; // 儲存動態產生的日期
|
||||
int _selectedDateIndex = 2; // 預設中間為「今天」
|
||||
String _selectedFilter = 'All';
|
||||
late List<DateTime> _dynamicDates;
|
||||
|
||||
final Color primaryPurple = const Color(0xFF6542D0);
|
||||
final Color bgLight = const Color(0xFFF8F9FA);
|
||||
@@ -29,21 +28,24 @@ class _TodoManagerState extends State<TodoManager> {
|
||||
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<TodoManager> {
|
||||
_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<TodoManager> {
|
||||
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<TodoManager> {
|
||||
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<TodoManager> {
|
||||
|
||||
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<TodoManager> {
|
||||
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<TodoManager> {
|
||||
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('目前沒有待辦事項。工作很輕鬆!'));
|
||||
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const Center(child: Text('此日期沒有任務,放鬆一下吧!'));
|
||||
}
|
||||
|
||||
List<Todo> 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user