2025-12-29 First Commit

This commit is contained in:
DATAEXPRESS\4734
2025-12-29 15:20:40 +08:00
commit fb2603c6f0
92 changed files with 6006 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import './issue_model.dart';
import '../services/generic_api_service.dart'; // 引入共用服務
import '../auth_manager.dart'; // 確保引入 AuthManager
class IssueApiService {
final GenericApiService _apiService = GenericApiService();
final String currentUserId; // 應由登入頁面傳入
IssueApiService({this.currentUserId = 'admin'});
Future<List<Issue>> fetchIssues() async {
// 篩選條件:指派給當前使用者 (responsible = currentUserId) 且狀態非 '已結案' (Status != 4)
// 格式: 1^100^raised_date^*^responsible^=^$currentUserId^issue_status^!=^4
// 讀取 user_id
String? user_id = await AuthManager.getUserId();
String filterPart = "responsible^$user_id";
// 完整的 queryFilter 格式:Page^PageSize^SortColumn^SortOrder^Filter...
String queryFilter = "1^100^issueid^*^^^$filterPart";
return await _apiService.fetchList<Issue>(
tableName: "pms_issuelog", // 對應到問題追蹤表格
pk: "issueid", // 主鍵為 issueid
queryFilter: queryFilter,
fromJson: (json) => Issue.fromJson(json),
);
}
}
+156
View File
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import './issue_model.dart';
import 'package:intl/intl.dart';
class IssueDetail extends StatelessWidget {
final Issue issue;
const IssueDetail({required this.issue, super.key});
// 輔助函式:建立屬性列
Widget _buildAttributeRow(BuildContext context, String label, String? value, {Color color = Colors.black}) {
// 處理日期欄位
String displayValue = value ?? 'N/A';
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black54),
),
),
Expanded(
child: Text(
displayValue,
style: TextStyle(fontWeight: FontWeight.w600, color: color),
),
),
],
),
);
}
// 輔助函式:格式化日期時間
String _formatDateTime(DateTime? dateTime) {
if (dateTime == null) return 'N/A';
return DateFormat('yyyy/MM/dd HH:mm').format(dateTime);
}
@override
Widget build(BuildContext context) {
// 完整描述 (Model中可能被截斷,這裡使用原始欄位)
final fullDescription = issue.description ?? '無詳細描述。';
final hasSolution = issue.solution != null && issue.solution!.isNotEmpty;
return Scaffold(
appBar: AppBar(
title: Text('問題 #${issue.issueId}'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 標題與 ID
Text(
'問題 ID: #${issue.issueId}',
style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold, color: Colors.black87),
),
const Divider(height: 24.0),
// 核心屬性
_buildAttributeRow(context, '狀態', issue.statusText, color: issue.statusColor),
_buildAttributeRow(context, '優先級', issue.priority, color: issue.priorityColor),
_buildAttributeRow(context, '預計完成日', issue.formattedExpectedDate, color: Colors.blue),
_buildAttributeRow(context, '指派對象', issue.responsible),
_buildAttributeRow(context, '提出人', issue.raisedBy),
_buildAttributeRow(context, '專案 ID', issue.projectId),
_buildAttributeRow(context, '功能碼', issue.functionCode),
_buildAttributeRow(context, '提出時間', _formatDateTime(issue.raisedDate)),
_buildAttributeRow(context, '解決說明', hasSolution ? '詳見下方' : '尚未解決', color: hasSolution ? Colors.green : Colors.red),
const Divider(height: 32.0),
// 問題詳細描述
const Text(
'問題描述 (Issue Description):',
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
fullDescription,
style: const TextStyle(fontSize: 16.0, height: 1.5, color: Colors.black87),
textAlign: TextAlign.justify,
),
),
const Divider(height: 32.0),
// 解決說明/備註
Text(
'解決說明 (Solution Explanation):',
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w600,
color: hasSolution ? Colors.black87 : Colors.grey.shade500
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
color: hasSolution ? Colors.lightGreen.shade50 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: hasSolution ? Colors.green.shade200 : Colors.transparent)
),
child: Text(
issue.solution ?? '尚無解決說明或備註。',
style: TextStyle(
fontSize: 16.0,
height: 1.5,
color: hasSolution ? Colors.black87 : Colors.grey.shade600
),
textAlign: TextAlign.justify,
),
),
const SizedBox(height: 40),
// 底部按鈕
Center(
child: ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('待實作處理問題流程(例如:變更狀態)。')),
);
},
icon: const Icon(Icons.build),
label: const Text('處理問題'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
textStyle: const TextStyle(fontSize: 18),
),
),
),
],
),
),
);
}
}
+156
View File
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import './issue_api.dart';
import './issue_model.dart';
import './issue_detail.dart';
class IssueManager extends StatefulWidget {
// 實際應用中,這裡應該傳入當前用戶 ID
final String currentUserId;
const IssueManager({this.currentUserId = 'admin', super.key});
@override
State<StatefulWidget> createState() {
return _IssueManagerState();
}
}
class _IssueManagerState extends State<IssueManager> {
late IssueApiService _apiService;
late Future<List<Issue>> _issuesFuture;
@override
void initState() {
super.initState();
_apiService = IssueApiService(currentUserId: widget.currentUserId);
_issuesFuture = _apiService.fetchIssues();
}
void _refreshIssues() {
setState(() {
_issuesFuture = _apiService.fetchIssues();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('指派給我的問題清單 (${widget.currentUserId})'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.refresh),
tooltip: '刷新列表',
onPressed: _refreshIssues,
),
IconButton(
icon: const Icon(Icons.home),
tooltip: '返回主頁',
onPressed: () => Navigator.pop(context),
),
],
),
body: FutureBuilder<List<Issue>>(
future: _issuesFuture,
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: _refreshIssues, child: const Text('重試')),
],
),
);
} else if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return IssueList(issues: snapshot.data!);
} else {
return const Center(child: Text('目前沒有指派給您的問題。'));
}
},
),
// 底部浮動按鈕:新增問題
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('待實作新增問題頁面。')),
);
},
icon: const Icon(Icons.add),
label: const Text('新增問題'),
backgroundColor: Colors.redAccent,
),
);
}
}
// -----------------------------------------------------------
// 列表顯示小部件 (IssueList)
// -----------------------------------------------------------
class IssueList extends StatelessWidget {
final List<Issue> issues;
const IssueList({required this.issues, super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: issues.length,
itemBuilder: (BuildContext context, int index) {
final item = issues[index];
return Card(
elevation: 3,
margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 16.0),
child: InkWell(
onTap: () {
// 點擊項目:導航到詳細頁面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => IssueDetail(issue: item),
),
);
},
child: ListTile(
// 左側顯示優先級
leading: Icon(
item.priorityIcon,
color: item.priorityColor,
size: 30,
),
title: Text(
'#${item.issueId} ${item.description ?? '無描述'}',
style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'專案: ${item.projectId ?? 'N/A'} | 提出人: ${item.raisedBy ?? 'N/A'}',
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
item.statusText,
style: TextStyle(fontSize: 12, color: item.statusColor, fontWeight: FontWeight.bold),
),
Text(
item.formattedExpectedDate,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
],
),
),
),
);
},
);
}
}
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class Issue {
final int issueId; // issueid (Primary Key)
final String? projectId; // projectid
final String? functionCode; // functioncode
final String? description; // issue_description
final String? priority; // issue_priority (High, Medium, Low)
final int? status; // issue_status (1=New, 2=InProgress, 3=Resolved, 4=Closed)
final String? raisedBy; // raised_by
final String? responsible; // responsible
final DateTime? raisedDate; // raised_date
final DateTime? expectedDate; // excepted_date
final String? solution; // solution_explanation
Issue({
required this.issueId,
this.projectId,
this.functionCode,
this.description,
this.priority,
this.status,
this.raisedBy,
this.responsible,
this.raisedDate,
this.expectedDate,
this.solution,
});
factory Issue.fromJson(Map<String, dynamic> json) {
DateTime? parseDate(dynamic date) {
if (date is String && date.isNotEmpty) {
return DateTime.tryParse(date);
}
return null;
}
// 由於 issue_priority 是 VARCHAR,我們假設它直接就是 'High', 'Medium', 'Low'
// 或是一個代號,此處保留為 String
final rawPriority = json['issue_priority'] as String?;
// 假設 issue_description 欄位是問題標題/簡述
final rawDescription = json['issue_description'] as String?;
final descriptionLength = rawDescription?.length ?? 0;
final truncatedDescription = (descriptionLength > 50)
? rawDescription!.substring(0, 50) + '...' // 列表截斷
: rawDescription;
return Issue(
issueId: json['issueid'] as int? ?? 0,
projectId: json['projectid'] as String?,
functionCode: json['functioncode'] as String?,
description: truncatedDescription, // 使用修正後的變數
priority: rawPriority,
status: json['issue_status'] as int?,
raisedBy: json['raised_by'] as String?,
responsible: json['responsible'] as String?,
raisedDate: parseDate(json['raised_date']),
expectedDate: parseDate(json['excepted_date']),
solution: json['solution_explanation'] as String?,
);
}
// Helper: 格式化預計完成日期
String get formattedExpectedDate {
if (expectedDate == null) return 'N/A';
return DateFormat('yyyy/MM/dd').format(expectedDate!);
}
// Helper: 獲取狀態文字
String get statusText {
switch (status) {
case 1:
return '新建';
case 2:
return '進行中';
case 3:
return '已解決';
case 4:
return '已結案';
default:
return '未知';
}
}
// Helper: 獲取狀態顏色
Color get statusColor {
switch (status) {
case 1:
return Colors.red; // 新建
case 2:
return Colors.orange; // 進行中
case 3:
return Colors.blue; // 已解決
case 4:
return Colors.green; // 已結案
default:
return Colors.grey;
}
}
// Helper: 獲取優先級圖示
IconData get priorityIcon {
switch (priority?.toLowerCase()) {
case 'high':
return Icons.arrow_upward;
case 'medium':
return Icons.remove;
case 'low':
return Icons.arrow_downward;
default:
return Icons.sort;
}
}
// Helper: 獲取優先級顏色
Color get priorityColor {
switch (priority?.toLowerCase()) {
case 'high':
return Colors.red.shade700;
case 'medium':
return Colors.orange.shade700;
case 'low':
return Colors.blue.shade700;
default:
return Colors.grey;
}
}
}