305 lines
11 KiB
Dart
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 空間
|
|
),
|
|
);
|
|
}
|
|
} |