2025-12-29 First Commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import './todo_model.dart';
|
||||
import '../services/generic_api_service.dart'; // 引入共用服務
|
||||
|
||||
class TodoApiService {
|
||||
final GenericApiService _apiService = GenericApiService();
|
||||
final String currentUserId;
|
||||
|
||||
TodoApiService({this.currentUserId = 'admin'});
|
||||
|
||||
Future<List<Todo>> fetchTodos() async {
|
||||
return await _apiService.fetchList<Todo>(
|
||||
tableName: "eip_todolist",
|
||||
pk: "id",
|
||||
// 如果 queryFilter 需要動態包含使用者 ID,可以在這裡字串插值
|
||||
// 假設原程式碼邏輯是 "1^10^id^*^^pmsm02^^"
|
||||
queryFilter: "1^10^id^*^^pmsm02^^",
|
||||
fromJson: (json) => Todo.fromJson(json),
|
||||
// 如果未來需要傳遞其他參數 (如 userID),可以用 additionalParams
|
||||
// additionalParams: { "userId": currentUserId },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import './todo_model.dart';
|
||||
|
||||
class TodoDetail extends StatelessWidget {
|
||||
final Todo todo;
|
||||
|
||||
const TodoDetail({required this.todo, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(todo.taskName, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// 任務標題
|
||||
Text(
|
||||
todo.taskName,
|
||||
style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
),
|
||||
const Divider(height: 24.0),
|
||||
|
||||
// 任務屬性表格 (簡潔顯示)
|
||||
_buildAttributeRow(context, '狀態', todo.status ?? 'N/A', todo.statusColor),
|
||||
_buildAttributeRow(context, '優先級', todo.priority ?? 'N/A', Colors.red),
|
||||
_buildAttributeRow(context, '截止日期', todo.formattedEndDate, Colors.blue),
|
||||
_buildAttributeRow(context, '建立者', todo.createdBy ?? 'N/A', Colors.grey),
|
||||
_buildAttributeRow(context, '分類', todo.className, Colors.purple),
|
||||
|
||||
const Divider(height: 32.0),
|
||||
|
||||
// 詳細說明
|
||||
const Text(
|
||||
'詳細說明:',
|
||||
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
todo.description ?? '無詳細說明。',
|
||||
style: const TextStyle(fontSize: 16.0, height: 1.5, color: Colors.black54),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// 底部按鈕 (例如:標記完成)
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('已標記任務 "${todo.taskName}" 待實作更新狀態。')),
|
||||
);
|
||||
// 實際應用中,這裡會呼叫 API 更新 pbi_status 為 'DONE'
|
||||
},
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
label: const Text('標記為已完成'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
textStyle: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttributeRow(BuildContext context, String label, String value, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: color),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import './todo_api.dart';
|
||||
import './todo_model.dart';
|
||||
import './todo_detail.dart'; // 稍後創建
|
||||
// import './main.dart'; // 確保可以訪問 MainMenu
|
||||
|
||||
class TodoManager extends StatefulWidget {
|
||||
// 實際應用中,這裡應該傳入當前用戶 ID
|
||||
final String currentUserId;
|
||||
const TodoManager({this.currentUserId = 'admin', super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _TodoManagerState();
|
||||
}
|
||||
}
|
||||
|
||||
class _TodoManagerState extends State<TodoManager> {
|
||||
late TodoApiService _apiService;
|
||||
late Future<List<Todo>> _todosFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_apiService = TodoApiService(currentUserId: widget.currentUserId);
|
||||
// 頁面加載時自動開始獲取資料
|
||||
_todosFuture = _apiService.fetchTodos();
|
||||
}
|
||||
|
||||
// 刷新資料的函數
|
||||
void _refreshTodos() {
|
||||
setState(() {
|
||||
_todosFuture = _apiService.fetchTodos();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('我的待辦事項'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: '刷新列表',
|
||||
onPressed: _refreshTodos,
|
||||
),
|
||||
// 假設這裡使用 Navigator.pop(context) 即可返回 MainMenu
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home),
|
||||
tooltip: '返回主頁',
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('載入失敗: ${snapshot.error}', textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: _refreshTodos, child: const Text('重試')),
|
||||
],
|
||||
),
|
||||
);
|
||||
} 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class Todo {
|
||||
final int id;
|
||||
final String className; // todolist_class (類別/分類)
|
||||
final String taskName; // task_name (任務名稱/標題)
|
||||
final String? description; // task_desc (詳細說明)
|
||||
final String? priority; // issue_priority (優先級)
|
||||
final String? status; // pbi_status (狀態)
|
||||
final DateTime? endDate; // end_date (預計完成日期)
|
||||
final String? createdBy; // create_user (建立者)
|
||||
final DateTime? createDate; // create_date (建立日期)
|
||||
|
||||
Todo({
|
||||
required this.id,
|
||||
required this.className,
|
||||
required this.taskName,
|
||||
this.description,
|
||||
this.priority,
|
||||
this.status,
|
||||
this.endDate,
|
||||
this.createdBy,
|
||||
this.createDate,
|
||||
});
|
||||
|
||||
// Factory 構造函數:從 API 返回的 JSON (Map) 創建 Todo 物件
|
||||
factory Todo.fromJson(Map<String, dynamic> json) {
|
||||
// 輔助函數:安全解析日期字串
|
||||
DateTime? parseDate(dynamic date) {
|
||||
if (date is String && date.isNotEmpty) {
|
||||
// 假設日期格式為 YYYY-MM-DD HH:mm:ss.sss 或 YYYY-MM-DD
|
||||
return DateTime.tryParse(date);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return Todo(
|
||||
id: json['id'] as int? ?? 0,
|
||||
className: json['todolist_class'] as String? ?? '未分類',
|
||||
taskName: json['task_name'] as String? ?? '無任務標題',
|
||||
description: json['task_desc'] as String?,
|
||||
priority: json['issue_priority'] as String?,
|
||||
status: json['pbi_status'] as String?,
|
||||
endDate: parseDate(json['end_date']),
|
||||
createdBy: json['create_user'] as String?,
|
||||
createDate: parseDate(json['create_date']),
|
||||
);
|
||||
}
|
||||
|
||||
// 格式化日期,用於列表顯示
|
||||
String get formattedEndDate {
|
||||
if (endDate == null) return 'N/A';
|
||||
return DateFormat('yyyy/MM/dd').format(endDate!);
|
||||
}
|
||||
|
||||
// 根據狀態獲取顏色 (例如:已完成/進行中)
|
||||
Color get statusColor {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'WIP': // Work In Progress
|
||||
return Colors.blue;
|
||||
case 'DONE': // Completed
|
||||
return Colors.green;
|
||||
case 'HOLD': // On Hold
|
||||
return Colors.orange;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user