Files
flutter-learn/lib/todo/todo_manager.dart
T

305 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import './todo_api.dart';
import './todo_model.dart';
import './todo_detail.dart';
import './todo_form.dart';
class TodoManager extends StatefulWidget {
final String currentUserId;
const TodoManager({this.currentUserId = 'admin', super.key});
@override
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();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
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.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),
],
),
);
}
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(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)),
],
),
),
);
},
),
);
}
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 空間
),
);
}
}