2025-12-29 First Commit
This commit is contained in:
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 '待開始';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user