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 './meeting_model.dart';
import '../services/generic_api_service.dart'; // 引入共用服務
class MeetingApiService {
final GenericApiService _apiService = GenericApiService();
final String currentUserId; // 應由登入頁面傳入
MeetingApiService({this.currentUserId = 'admin'});
Future<List<Meeting>> fetchMeetings() async {
// 目標:獲取與當前用戶 (currentUserId) 相關的會議通知。
// 採用過濾邏輯:篩選參與者名單 (meeting_users) 中包含 currentUserId 的記錄。
// 預設參數:頁碼1,每頁100筆,按 startdate 降冪排序 (*表示降冪)
// 格式: 1^100^startdate^*^meeting_users^LIKE^%currentUserId%
String filterPart = "startdate^*^meeting_users^LIKE^%$currentUserId%";
// 完整的 queryFilter 格式:Page^PageSize^SortColumn^SortOrder^FilterColumn^Operator^FilterValue...
String queryFilter = "1^100^$filterPart";
return await _apiService.fetchList<Meeting>(
tableName: "eipmeetingrec_m", // 對應到會議通知表格
pk: "uniqueno", // 主鍵為 uniqueno
queryFilter: queryFilter,
fromJson: (json) => Meeting.fromJson(json),
);
}
}
+138
View File
@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import './meeting_model.dart';
import 'package:intl/intl.dart';
class MeetingDetail extends StatelessWidget {
final Meeting meeting;
const MeetingDetail({required this.meeting, super.key});
// 輔助函式:建立屬性列 (從 todo_detail.dart 最佳化而來)
Widget _buildAttributeRow(BuildContext context, String label, String value, {Color color = Colors.black}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black54),
),
),
Expanded(
child: Text(
value,
style: TextStyle(fontWeight: FontWeight.w600, color: color),
),
),
],
),
);
}
// 輔助函式:格式化日期時間範圍
String _formatDateRange() {
final dateFormat = DateFormat('yyyy/MM/dd');
final startDateStr = meeting.startDate != null ? dateFormat.format(meeting.startDate!) : 'N/A';
final endDateStr = meeting.endDate != null ? dateFormat.format(meeting.endDate!) : startDateStr;
// 檢查日期是否相同
final isSameDate = meeting.startDate != null && meeting.endDate != null &&
meeting.startDate!.day == meeting.endDate!.day &&
meeting.startDate!.month == meeting.endDate!.month &&
meeting.startDate!.year == meeting.endDate!.year;
String dateRange;
if (isSameDate) {
dateRange = startDateStr;
} else {
dateRange = '$startDateStr$endDateStr';
}
final timeRange = (meeting.startTime ?? '') + ' ~ ' + (meeting.endTime ?? '');
return '$dateRange ${timeRange.trim()}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(meeting.meetingTitle, overflow: TextOverflow.ellipsis),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// 會議標題
Text(
meeting.meetingTitle,
style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold, color: Colors.black87),
),
const Divider(height: 24.0),
// 會議屬性表格
_buildAttributeRow(context, '狀態', meeting.statusText, color: meeting.statusColor),
_buildAttributeRow(context, '地點', meeting.meetingPlace ?? '待定'),
_buildAttributeRow(context, '時間範圍', _formatDateRange(), color: Colors.blue),
_buildAttributeRow(context, '主持人 ID', meeting.bossPersonId ?? 'N/A'),
_buildAttributeRow(context, '記錄人 ID', meeting.recPersonId ?? 'N/A'),
_buildAttributeRow(context, '參與者名單', meeting.meetingUsers ?? ''),
const Divider(height: 32.0),
// 會議說明
const Text(
'會議說明:',
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 8),
Text(
meeting.meetingDesc ?? '無詳細說明。',
style: const TextStyle(fontSize: 16.0, height: 1.5, color: Colors.black54),
textAlign: TextAlign.justify,
),
const Divider(height: 32.0),
// 會議決議
const Text(
'會議決議:',
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 8),
Text(
meeting.resolution ?? '無決議事項。',
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(
const SnackBar(content: Text('待實作下載會議文件功能。')),
);
},
icon: const Icon(Icons.file_download),
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),
),
),
),
],
),
),
);
}
}
+147
View File
@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import './meeting_api.dart';
import './meeting_model.dart';
import './meeting_detail.dart';
class MeetingManager extends StatefulWidget {
// 實際應用中,這裡應該傳入當前用戶 ID
final String currentUserId;
const MeetingManager({this.currentUserId = 'admin', super.key});
@override
State<StatefulWidget> createState() {
return _MeetingManagerState();
}
}
class _MeetingManagerState extends State<MeetingManager> {
late MeetingApiService _apiService;
late Future<List<Meeting>> _meetingsFuture;
@override
void initState() {
super.initState();
// 服務初始化時傳入當前用戶 ID
_apiService = MeetingApiService(currentUserId: widget.currentUserId);
_meetingsFuture = _apiService.fetchMeetings();
}
// 刷新資料的函數
void _refreshMeetings() {
setState(() {
_meetingsFuture = _apiService.fetchMeetings();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('我的會議通知'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.refresh),
tooltip: '刷新列表',
onPressed: _refreshMeetings,
),
IconButton(
icon: const Icon(Icons.home),
tooltip: '返回主頁',
onPressed: () => Navigator.pop(context),
),
],
),
body: FutureBuilder<List<Meeting>>(
future: _meetingsFuture,
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: _refreshMeetings, child: const Text('重試')),
],
),
);
} else if (snapshot.hasData && snapshot.data!.isNotEmpty) {
return MeetingList(meetings: snapshot.data!);
} else {
return const Center(child: Text('目前沒有相關會議通知。'));
}
},
),
);
}
}
// -----------------------------------------------------------
// 列表顯示小部件 (MeetingList)
// -----------------------------------------------------------
class MeetingList extends StatelessWidget {
final List<Meeting> meetings;
const MeetingList({required this.meetings, super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: meetings.length,
itemBuilder: (BuildContext context, int index) {
final item = meetings[index];
return Card(
elevation: 3,
margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 16.0),
child: InkWell(
onTap: () {
// 點擊項目:導航到詳細頁面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MeetingDetail(meeting: item),
),
);
},
child: ListTile(
// 左側圖示顯示
leading: Icon(
Icons.calendar_month,
color: item.statusColor,
size: 32,
),
title: Text(
item.meetingTitle,
style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'地點: ${item.meetingPlace ?? '待定'} | 主持: ${item.bossPersonId ?? '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: 10, color: item.statusColor),
),
Text(
item.formattedStartDateTime,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
),
],
),
),
),
);
},
);
}
}
+94
View File
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class Meeting {
final int uniqueNo; // uniqueno (Primary Key)
final String meetingTitle; // meeting_title (會議標題)
final String? meetingPlace; // meeting_place (會議地點)
final String? meetingDesc; // meeting_desc (會議說明)
final String? bossPersonId; // boss_personid (主持人 ID)
final String? recPersonId; // rec_personid (記錄人 ID)
final DateTime? startDate; // startdate (開始日期)
final String? startTime; // starttime (開始時間, e.g., '14:00')
final DateTime? endDate; // end_date (結束日期)
final String? endTime; // end_time (結束時間, e.g., '16:00')
final String? resolution; // meeting_resolution (會議決議)
final String? meetingUsers; // meeting_users (參與者名單)
final String? flowFlag; // flowflag (狀態/流程旗標)
Meeting({
required this.uniqueNo,
required this.meetingTitle,
this.meetingPlace,
this.meetingDesc,
this.bossPersonId,
this.recPersonId,
this.startDate,
this.startTime,
this.endDate,
this.endTime,
this.resolution,
this.meetingUsers,
this.flowFlag,
});
// Factory 構造函數:從 API 返回的 JSON (Map) 創建 Meeting 物件
factory Meeting.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 Meeting(
uniqueNo: json['uniqueno'] as int? ?? 0,
meetingTitle: json['meeting_title'] as String? ?? '無標題會議',
meetingPlace: json['meeting_place'] as String?,
meetingDesc: json['meeting_desc'] as String?,
bossPersonId: json['boss_personid'] as String?,
recPersonId: json['rec_personid'] as String?,
startDate: parseDate(json['startdate']),
startTime: json['starttime'] as String?,
endDate: parseDate(json['end_date']),
endTime: json['end_time'] as String?,
resolution: json['meeting_resolution'] as String?,
meetingUsers: json['meeting_users'] as String?,
flowFlag: json['flowflag'] as String?,
);
}
// Helper: 格式化開始日期和時間
String get formattedStartDateTime {
if (startDate == null) return 'N/A';
final datePart = DateFormat('MM/dd').format(startDate!);
final timePart = startTime ?? '';
return '$datePart ${timePart.isNotEmpty ? timePart : ''}'.trim();
}
// Helper: 獲取會議狀態顏色
Color get statusColor {
// 假設 '1' 表示已定案/已批准, '0' 表示審核中, 其他表示草稿/待開始
switch (flowFlag) {
case '1':
return Colors.green; // 已定案
case '0':
return Colors.orange; // 審核中
default:
return Colors.blue; // 待開始
}
}
// Helper: 獲取會議狀態文字
String get statusText {
switch (flowFlag) {
case '1':
return '已定案';
case '0':
return '審核中';
default:
return '待開始';
}
}
}