2025-12-29 First Commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import './calendar_model.dart';
|
||||
import '../services/generic_api_service.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class CalendarApiService {
|
||||
final GenericApiService _apiService = GenericApiService();
|
||||
|
||||
// 獲取指定月份或範圍的行程
|
||||
Future<List<CalendarEvent>> fetchEvents(String userId, DateTime month) async {
|
||||
// 企業實作建議:僅抓取特定月份資料,減少手機負載
|
||||
final sDate = DateTime(month.year, month.month, 1);
|
||||
final eDate = DateTime(month.year, month.month + 1, 0);
|
||||
|
||||
String queryFilter = "1^500^start_time^*^personid^=^$userId";
|
||||
|
||||
return await _apiService.fetchList<CalendarEvent>(
|
||||
tableName: "eip_new_calendar",
|
||||
pk: "uuid",
|
||||
queryFilter: queryFilter,
|
||||
fromJson: (json) => CalendarEvent.fromJson(json),
|
||||
);
|
||||
}
|
||||
|
||||
// 新增/更新行程
|
||||
Future<bool> saveEvent(CalendarEvent event) async {
|
||||
final data = {
|
||||
"uuid": event.uuid,
|
||||
"title": event.title,
|
||||
"cal_description": event.description,
|
||||
"start_time": event.startTime?.toIso8601String(),
|
||||
"end_time": event.endTime?.toIso8601String(),
|
||||
"personid": event.personId,
|
||||
"cal_finished": event.isFinished ? 'Y' : 'N',
|
||||
"update_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
||||
};
|
||||
// return await _apiService.saveData("eip_new_calendar", data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import './calendar_model.dart';
|
||||
|
||||
class CalendarDetail extends StatelessWidget {
|
||||
final CalendarEvent event;
|
||||
|
||||
const CalendarDetail({required this.event, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('行程詳情')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(backgroundColor: event.levelColor, radius: 8),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(event.title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 40),
|
||||
_buildDetailItem(Icons.access_time, '時間範圍', event.timeRangeText),
|
||||
_buildDetailItem(Icons.repeat, '重複設定', event.repeatedFlag ?? '無重複'),
|
||||
_buildDetailItem(Icons.folder_open, '關聯專案', event.projectId ?? '無相關專案'),
|
||||
_buildDetailItem(Icons.category_outlined, '類型', event.type ?? '一般行程'),
|
||||
const SizedBox(height: 24),
|
||||
const Text('行程描述', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(event.description ?? '無詳細描述內容', style: const TextStyle(height: 1.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailItem(IconData icon, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.blueGrey, size: 20),
|
||||
const SizedBox(width: 15),
|
||||
Text(label, style: const TextStyle(color: Colors.grey)),
|
||||
const Spacer(),
|
||||
Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:table_calendar/table_calendar.dart'; // 建議引入此套件
|
||||
import 'package:intl/intl.dart';
|
||||
import './calendar_model.dart';
|
||||
import './calendar_api.dart';
|
||||
import './calendar_detail.dart';
|
||||
|
||||
class CalendarManager extends StatefulWidget {
|
||||
final String userId;
|
||||
const CalendarManager({required this.userId, super.key});
|
||||
|
||||
@override
|
||||
State<CalendarManager> createState() => _CalendarManagerState();
|
||||
}
|
||||
|
||||
class _CalendarManagerState extends State<CalendarManager> {
|
||||
final CalendarApiService _apiService = CalendarApiService();
|
||||
|
||||
// 狀態控制
|
||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||
DateTime _focusedDay = DateTime.now();
|
||||
DateTime? _selectedDay;
|
||||
|
||||
// 資料儲存:將事件按日期分類,方便日曆標點
|
||||
Map<DateTime, List<CalendarEvent>> _eventsMap = {};
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDay = _focusedDay;
|
||||
_fetchMonthEvents(_focusedDay);
|
||||
}
|
||||
|
||||
// 核心:抓取整個月的資料並進行分類
|
||||
Future<void> _fetchMonthEvents(DateTime month) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final events = await _apiService.fetchEvents(widget.userId, month);
|
||||
|
||||
// 將 List 轉換為 Map<DateTime, List<Event>>
|
||||
Map<DateTime, List<CalendarEvent>> newMap = {};
|
||||
for (var event in events) {
|
||||
if (event.startTime != null) {
|
||||
// 只取日期部分作為 Key
|
||||
final dateKey = DateTime(event.startTime!.year, event.startTime!.month, event.startTime!.day);
|
||||
if (newMap[dateKey] == null) newMap[dateKey] = [];
|
||||
newMap[dateKey]!.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_eventsMap = newMap;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("載入失敗: $e")));
|
||||
}
|
||||
}
|
||||
|
||||
// 獲取選定日期的行程
|
||||
List<CalendarEvent> _getEventsForDay(DateTime day) {
|
||||
final dateKey = DateTime(day.year, day.month, day.day);
|
||||
return _eventsMap[dateKey] ?? [];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('企業行事曆'),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.today), onPressed: () => setState(() => _focusedDay = DateTime.now())),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 1. 日曆組件
|
||||
TableCalendar<CalendarEvent>(
|
||||
firstDay: DateTime.utc(2020, 1, 1),
|
||||
lastDay: DateTime.utc(2030, 12, 31),
|
||||
focusedDay: _focusedDay,
|
||||
calendarFormat: _calendarFormat,
|
||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
||||
eventLoader: _getEventsForDay, // 在有行程的日期顯示小點點
|
||||
|
||||
// 樣式設定
|
||||
calendarStyle: CalendarStyle(
|
||||
todayDecoration: BoxDecoration(color: Colors.blue.withOpacity(0.5), shape: BoxShape.circle),
|
||||
selectedDecoration: const BoxDecoration(color: Colors.blue, shape: BoxShape.circle),
|
||||
markerDecoration: const BoxDecoration(color: Colors.orange, shape: BoxShape.circle),
|
||||
),
|
||||
|
||||
// 互動事件
|
||||
onDaySelected: (selectedDay, focusedDay) {
|
||||
setState(() {
|
||||
_selectedDay = selectedDay;
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
setState(() => _calendarFormat = format);
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_focusedDay = focusedDay;
|
||||
_fetchMonthEvents(focusedDay); // 切換月份時自動重新抓取 API
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// 2. 下方行程列表
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _buildEventList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () { /* 實重新增行程邏輯 */ },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventList() {
|
||||
final dayEvents = _getEventsForDay(_selectedDay!);
|
||||
|
||||
if (dayEvents.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"${DateFormat('MM/dd').format(_selectedDay!)} 沒有行程",
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: dayEvents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final event = dayEvents[index];
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 4,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: event.levelColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(event.timeRangeText),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => CalendarDetail(event: event))
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class CalendarEvent {
|
||||
final String uuid;
|
||||
final String title;
|
||||
final String? description;
|
||||
final DateTime? startTime;
|
||||
final DateTime? endTime;
|
||||
final String? type; // cal_type
|
||||
final String? level; // cal_level (重要程度)
|
||||
final String? personId;
|
||||
final bool isFinished; // cal_finished: 'Y' or 'N'
|
||||
final String? repeatedFlag; // cal_repeated_flag
|
||||
final String? projectId;
|
||||
|
||||
CalendarEvent({
|
||||
required this.uuid,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.type,
|
||||
this.level,
|
||||
this.personId,
|
||||
this.isFinished = false,
|
||||
this.repeatedFlag,
|
||||
this.projectId,
|
||||
});
|
||||
|
||||
factory CalendarEvent.fromJson(Map<String, dynamic> json) {
|
||||
return CalendarEvent(
|
||||
uuid: json['uuid'] as String,
|
||||
title: json['title'] as String? ?? '未命名行程',
|
||||
description: json['cal_description'] as String?,
|
||||
startTime: json['start_time'] != null ? DateTime.tryParse(json['start_time']) : null,
|
||||
endTime: json['end_time'] != null ? DateTime.tryParse(json['end_time']) : null,
|
||||
type: json['cal_type'] as String?,
|
||||
level: json['cal_level'] as String?,
|
||||
personId: json['personid'] as String?,
|
||||
isFinished: json['cal_finished'] == 'Y',
|
||||
repeatedFlag: json['cal_repeated_flag'] as String?,
|
||||
projectId: json['projectid'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
// UI 輔助屬性
|
||||
Color get levelColor {
|
||||
switch (level?.toUpperCase()) {
|
||||
case 'URGENT': return Colors.red;
|
||||
case 'HIGH': return Colors.orange;
|
||||
case 'NORMAL': return Colors.blue;
|
||||
default: return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String get timeRangeText {
|
||||
if (startTime == null) return "未定時";
|
||||
final df = DateFormat('HH:mm');
|
||||
return "${df.format(startTime!)}${endTime != null ? ' - ${df.format(endTime!)}' : ''}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user