修改幾個小 issue
This commit is contained in:
@@ -10,7 +10,7 @@ class AnnouncementApiService {
|
|||||||
return await _apiService.fetchList<Announcement>(
|
return await _apiService.fetchList<Announcement>(
|
||||||
tableName: "eipbbs_m",
|
tableName: "eipbbs_m",
|
||||||
pk: "uniqueno",
|
pk: "uniqueno",
|
||||||
queryFilter: "1^10^uniqueno^*^^pmsm02^^",
|
queryFilter: "1^10^uniqueno^*^^eipm11^^",
|
||||||
// 將 Announcement 的轉換方法傳進去
|
// 將 Announcement 的轉換方法傳進去
|
||||||
fromJson: (json) => Announcement.fromJson(json),
|
fromJson: (json) => Announcement.fromJson(json),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class AnnouncementDetail extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text('發布日期: ${announcement.formattedBillDate}',
|
Text('發布日期: ${announcement.formattedBillDate}',
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||||
Text('作者: ${announcement.createdBy}',
|
Text('作者: ${announcement.personCName}',
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
style: const TextStyle(color: Colors.grey, fontSize: 14)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class Announcement {
|
|||||||
final DateTime startDate; // startdate
|
final DateTime startDate; // startdate
|
||||||
final DateTime endDate; // end_date
|
final DateTime endDate; // end_date
|
||||||
final String? attachment; // bbs_attach
|
final String? attachment; // bbs_attach
|
||||||
|
final String personCName; // 姓名
|
||||||
|
|
||||||
Announcement({
|
Announcement({
|
||||||
required this.uniqueNo,
|
required this.uniqueNo,
|
||||||
@@ -19,6 +20,7 @@ class Announcement {
|
|||||||
required this.startDate,
|
required this.startDate,
|
||||||
required this.endDate,
|
required this.endDate,
|
||||||
this.attachment,
|
this.attachment,
|
||||||
|
required this.personCName,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Factory 構造函數:從 API 返回的 JSON (Map) 創建 Announcement 物件
|
// Factory 構造函數:從 API 返回的 JSON (Map) 創建 Announcement 物件
|
||||||
@@ -41,6 +43,7 @@ class Announcement {
|
|||||||
startDate: parseDate(json['startdate'])!,
|
startDate: parseDate(json['startdate'])!,
|
||||||
endDate: parseDate(json['end_date'])!,
|
endDate: parseDate(json['end_date'])!,
|
||||||
attachment: json['bbs_attach'] as String?,
|
attachment: json['bbs_attach'] as String?,
|
||||||
|
personCName: json['personCName'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// calendar_api.dart
|
||||||
|
|
||||||
import './calendar_model.dart';
|
import './calendar_model.dart';
|
||||||
import '../services/generic_api_service.dart';
|
import '../services/generic_api_service.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
@@ -5,13 +7,19 @@ import 'package:intl/intl.dart';
|
|||||||
class CalendarApiService {
|
class CalendarApiService {
|
||||||
final GenericApiService _apiService = GenericApiService();
|
final GenericApiService _apiService = GenericApiService();
|
||||||
|
|
||||||
// 獲取指定月份或範圍的行程
|
// 獲取指定「日期區間」的行程
|
||||||
Future<List<CalendarEvent>> fetchEvents(String userId, DateTime month) async {
|
Future<List<CalendarEvent>> fetchEvents(String userId, DateTime startDate, DateTime endDate) async {
|
||||||
// 企業實作建議:僅抓取特定月份資料,減少手機負載
|
// 格式化查詢的起迄日期
|
||||||
final sDate = DateTime(month.year, month.month, 1);
|
final sDate = DateFormat('yyyy-MM-dd').format(startDate);
|
||||||
final eDate = DateTime(month.year, month.month + 1, 0);
|
// 結束日期加 1 天,用小於 (<) 來涵蓋 endDate 當天的所有時間 (23:59:59)
|
||||||
|
final eDate = DateFormat('yyyy-MM-dd').format(endDate.add(const Duration(days: 1)));
|
||||||
|
|
||||||
String queryFilter = "1^500^start_time^*^personid^=^$userId";
|
// 1. 組合 wheresql_org
|
||||||
|
// 條件:大於等於開始日期,小於結束日期的隔天,且人員代號相符
|
||||||
|
String whereSql = "start_time >= '$sDate' AND start_time < '$eDate' AND personid = '$userId'";
|
||||||
|
|
||||||
|
// 2. 嚴格依照 API 規範組裝 10 個參數
|
||||||
|
String queryFilter = "1^500^start_time^*^$whereSql^^^^^";
|
||||||
|
|
||||||
return await _apiService.fetchList<CalendarEvent>(
|
return await _apiService.fetchList<CalendarEvent>(
|
||||||
tableName: "eip_new_calendar",
|
tableName: "eip_new_calendar",
|
||||||
@@ -21,19 +29,36 @@ class CalendarApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增/更新行程
|
// 新增行程至資料庫
|
||||||
Future<bool> saveEvent(CalendarEvent event) async {
|
Future<bool> createEvent(CalendarEvent event) async {
|
||||||
final data = {
|
final Map<String, dynamic> data = {
|
||||||
"uuid": event.uuid,
|
// uuid 通常由後端資料庫生成,新增時可以不傳或傳空值
|
||||||
"title": event.title,
|
"title": event.title,
|
||||||
"cal_description": event.description,
|
"cal_description": event.description,
|
||||||
"start_time": event.startTime?.toIso8601String(),
|
"start_time": event.startTime != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(event.startTime!) : null,
|
||||||
"end_time": event.endTime?.toIso8601String(),
|
"end_time": event.endTime != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(event.endTime!) : null,
|
||||||
|
"cal_type": event.type,
|
||||||
|
"cal_level": event.level,
|
||||||
"personid": event.personId,
|
"personid": event.personId,
|
||||||
"cal_finished": event.isFinished ? 'Y' : 'N',
|
"cal_finished": event.isFinished ? 'Y' : 'N',
|
||||||
"update_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
"projectid": event.projectId, // 關聯專案
|
||||||
|
"create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
||||||
};
|
};
|
||||||
// return await _apiService.saveData("eip_new_calendar", data);
|
|
||||||
|
try {
|
||||||
|
// 根據 Generic API 規範,使用 action: "C" 進行新增
|
||||||
|
await _apiService.fetchList<dynamic>(
|
||||||
|
tableName: "eip_new_calendar",
|
||||||
|
pk: "uuid",
|
||||||
|
queryFilter: "",
|
||||||
|
action: "C",
|
||||||
|
data: data,
|
||||||
|
fromJson: (json) => json,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print("Create Calendar Event Error: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// calendar_form.dart
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import './calendar_model.dart';
|
||||||
|
import './calendar_api.dart';
|
||||||
|
|
||||||
|
class CalendarForm extends StatefulWidget {
|
||||||
|
final String userId;
|
||||||
|
final DateTime? initialDate; // 接收從行事曆首頁傳來的預設日期
|
||||||
|
|
||||||
|
const CalendarForm({required this.userId, this.initialDate, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CalendarForm> createState() => _CalendarFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalendarFormState extends State<CalendarForm> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final CalendarApiService _apiService = CalendarApiService();
|
||||||
|
|
||||||
|
final TextEditingController _titleController = TextEditingController();
|
||||||
|
final TextEditingController _descController = TextEditingController();
|
||||||
|
final TextEditingController _projectController = TextEditingController();
|
||||||
|
|
||||||
|
String _selectedType = '會議';
|
||||||
|
String _selectedLevel = 'NORMAL';
|
||||||
|
|
||||||
|
late DateTime _startDate;
|
||||||
|
late TimeOfDay _startTime;
|
||||||
|
late DateTime _endDate;
|
||||||
|
late TimeOfDay _endTime;
|
||||||
|
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 如果有傳入初始日期,就用它;否則用今天
|
||||||
|
DateTime baseDate = widget.initialDate ?? DateTime.now();
|
||||||
|
_startDate = baseDate;
|
||||||
|
_endDate = baseDate;
|
||||||
|
|
||||||
|
// 預設時間:目前時間的下一個整點,例如現在 10:25,預設 11:00 ~ 12:00
|
||||||
|
int nextHour = TimeOfDay.now().hour + 1;
|
||||||
|
_startTime = TimeOfDay(hour: nextHour > 23 ? 23 : nextHour, minute: 0);
|
||||||
|
_endTime = TimeOfDay(hour: nextHour + 1 > 23 ? 23 : nextHour + 1, minute: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 結合日期與時間的選擇器
|
||||||
|
Future<void> _pickDateTime(bool isStart) async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: isStart ? _startDate : _endDate,
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2030),
|
||||||
|
builder: (context, child) => Theme(
|
||||||
|
data: ThemeData.light().copyWith(colorScheme: const ColorScheme.light(primary: Colors.blueAccent)),
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (date == null) return;
|
||||||
|
|
||||||
|
final time = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: isStart ? _startTime : _endTime,
|
||||||
|
builder: (context, child) => Theme(
|
||||||
|
data: ThemeData.light().copyWith(colorScheme: const ColorScheme.light(primary: Colors.blueAccent)),
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (time == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (isStart) {
|
||||||
|
_startDate = date; _startTime = time;
|
||||||
|
// 防呆:如果結束日期早於開始日期,自動順延
|
||||||
|
if (_endDate.isBefore(_startDate)) _endDate = _startDate;
|
||||||
|
} else {
|
||||||
|
_endDate = date; _endTime = time;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubmit() async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
setState(() => _isSubmitting = true);
|
||||||
|
|
||||||
|
// 合併 Date 與 Time 成為完整的 DateTime
|
||||||
|
final start = DateTime(_startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute);
|
||||||
|
final end = DateTime(_endDate.year, _endDate.month, _endDate.day, _endTime.hour, _endTime.minute);
|
||||||
|
|
||||||
|
final newEvent = CalendarEvent(
|
||||||
|
uuid: '', // 由後端生成
|
||||||
|
title: _titleController.text,
|
||||||
|
description: _descController.text,
|
||||||
|
startTime: start,
|
||||||
|
endTime: end,
|
||||||
|
type: _selectedType,
|
||||||
|
level: _selectedLevel,
|
||||||
|
personId: widget.userId,
|
||||||
|
projectId: _projectController.text.isNotEmpty ? _projectController.text : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool success = await _apiService.createEvent(newEvent);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
if (success) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('行程新增成功!')));
|
||||||
|
Navigator.pop(context, true); // 回傳 true 通知列表刷新
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('新增失敗,請檢查網路連線。', style: TextStyle(color: Colors.white)), backgroundColor: Colors.red));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF8F9FA),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('新增行程', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
elevation: 0.5,
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildTextField('行程標題 *', _titleController, '例如:部門月會', Icons.title, isRequired: true),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDropdownField('類型', _selectedType, ['會議', '拜訪客戶', '專案開發', '私人行程', '其他'], (v) => setState(() => _selectedType = v!))),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildDropdownField('重要性', _selectedLevel,
|
||||||
|
['URGENT', 'HIGH', 'NORMAL'],
|
||||||
|
(v) => setState(() => _selectedLevel = v!),
|
||||||
|
displayNames: {'URGENT': '緊急', 'HIGH': '高', 'NORMAL': '一般'}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
const Text('時間設定', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blueGrey)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildTimePickerTile('開始時間', _startDate, _startTime, true),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildTimePickerTile('結束時間', _endDate, _endTime, false),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
_buildTextField('專案關聯 (選填)', _projectController, '輸入專案代號或名稱', Icons.folder_open),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
_buildTextField('行程詳細說明', _descController, '記錄會議大綱或行程備註...', Icons.notes, isMultiline: true),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isSubmitting ? null : _handleSubmit,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: _isSubmitting
|
||||||
|
? const CircularProgressIndicator(color: Colors.white)
|
||||||
|
: const Text('儲存行程', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 共用輸入框元件
|
||||||
|
Widget _buildTextField(String label, TextEditingController controller, String hint, IconData icon, {bool isMultiline = false, bool isRequired = false}) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
maxLines: isMultiline ? 4 : 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
icon: Icon(icon, color: Colors.grey.shade400, size: 20),
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: TextStyle(color: Colors.grey.shade600, fontSize: 14),
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: TextStyle(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
validator: isRequired ? (v) => v == null || v.isEmpty ? '此欄位為必填' : null : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 共用下拉選單元件 (支援實際值與顯示名稱不同)
|
||||||
|
Widget _buildDropdownField(String label, String value, List<String> items, ValueChanged<String?> onChanged, {Map<String, String>? displayNames}) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
decoration: InputDecoration(labelText: label, border: InputBorder.none, labelStyle: TextStyle(color: Colors.grey.shade600, fontSize: 14)),
|
||||||
|
items: items.map((s) => DropdownMenuItem(value: s, child: Text(displayNames != null ? displayNames[s]! : s, style: const TextStyle(fontSize: 15)))).toList(),
|
||||||
|
onChanged: onChanged,
|
||||||
|
icon: Icon(Icons.keyboard_arrow_down, color: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 時間選擇按鈕元件
|
||||||
|
Widget _buildTimePickerTile(String label, DateTime date, TimeOfDay time, bool isStart) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => _pickDateTime(isStart),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade200)),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.access_time, color: isStart ? Colors.blueAccent : Colors.orangeAccent, size: 22),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label, style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text("${DateFormat('yyyy/MM/dd').format(date)} ${time.format(context)}", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Icon(Icons.edit_calendar, color: Colors.grey.shade300, size: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
|
// calendar_manager.dart
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:table_calendar/table_calendar.dart'; // 建議引入此套件
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import './calendar_model.dart';
|
import './calendar_model.dart';
|
||||||
import './calendar_api.dart';
|
import './calendar_api.dart';
|
||||||
import './calendar_detail.dart';
|
import './calendar_detail.dart';
|
||||||
|
import './calendar_form.dart';
|
||||||
|
|
||||||
class CalendarManager extends StatefulWidget {
|
class CalendarManager extends StatefulWidget {
|
||||||
final String userId;
|
final String userId;
|
||||||
@@ -16,33 +19,47 @@ class CalendarManager extends StatefulWidget {
|
|||||||
class _CalendarManagerState extends State<CalendarManager> {
|
class _CalendarManagerState extends State<CalendarManager> {
|
||||||
final CalendarApiService _apiService = CalendarApiService();
|
final CalendarApiService _apiService = CalendarApiService();
|
||||||
|
|
||||||
// 狀態控制
|
// ==== 日曆模式狀態 ====
|
||||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||||
DateTime _focusedDay = DateTime.now();
|
DateTime _focusedDay = DateTime.now();
|
||||||
DateTime? _selectedDay;
|
DateTime? _selectedDay;
|
||||||
|
Map<DateTime, List<CalendarEvent>> _eventsMap = {}; // 供日曆標點使用
|
||||||
|
|
||||||
// 資料儲存:將事件按日期分類,方便日曆標點
|
// ==== 列表模式狀態 ====
|
||||||
Map<DateTime, List<CalendarEvent>> _eventsMap = {};
|
late DateTime _listStartDate;
|
||||||
|
late DateTime _listEndDate;
|
||||||
|
List<CalendarEvent> _listEvents = []; // 供列表模式獨立顯示使用
|
||||||
|
|
||||||
|
// 控制目前是日曆還是列表
|
||||||
|
String _viewMode = '日曆模式';
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedDay = _focusedDay;
|
_selectedDay = _focusedDay;
|
||||||
|
|
||||||
|
// 初始化列表模式的預設區間為「本月 1 日」到「本月底」
|
||||||
|
_listStartDate = DateTime(DateTime.now().year, DateTime.now().month, 1);
|
||||||
|
_listEndDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0);
|
||||||
|
|
||||||
|
// 預設載入日曆資料
|
||||||
_fetchMonthEvents(_focusedDay);
|
_fetchMonthEvents(_focusedDay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 核心:抓取整個月的資料並進行分類
|
// ============== API 抓取邏輯 ==============
|
||||||
|
|
||||||
|
// 1. 日曆模式:抓取整個月並轉換為 Map 標點
|
||||||
Future<void> _fetchMonthEvents(DateTime month) async {
|
Future<void> _fetchMonthEvents(DateTime month) async {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
final events = await _apiService.fetchEvents(widget.userId, month);
|
final sDate = DateTime(month.year, month.month, 1);
|
||||||
|
final eDate = DateTime(month.year, month.month + 1, 0); // 當月最後一天
|
||||||
|
final events = await _apiService.fetchEvents(widget.userId, sDate, eDate);
|
||||||
|
|
||||||
// 將 List 轉換為 Map<DateTime, List<Event>>
|
|
||||||
Map<DateTime, List<CalendarEvent>> newMap = {};
|
Map<DateTime, List<CalendarEvent>> newMap = {};
|
||||||
for (var event in events) {
|
for (var event in events) {
|
||||||
if (event.startTime != null) {
|
if (event.startTime != null) {
|
||||||
// 只取日期部分作為 Key
|
|
||||||
final dateKey = DateTime(event.startTime!.year, event.startTime!.month, event.startTime!.day);
|
final dateKey = DateTime(event.startTime!.year, event.startTime!.month, event.startTime!.day);
|
||||||
if (newMap[dateKey] == null) newMap[dateKey] = [];
|
if (newMap[dateKey] == null) newMap[dateKey] = [];
|
||||||
newMap[dateKey]!.add(event);
|
newMap[dateKey]!.add(event);
|
||||||
@@ -59,7 +76,65 @@ class _CalendarManagerState extends State<CalendarManager> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 獲取選定日期的行程
|
// 2. 列表模式:抓取特定日期區間,單純存成 List
|
||||||
|
Future<void> _fetchListEvents() async {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
try {
|
||||||
|
final events = await _apiService.fetchEvents(widget.userId, _listStartDate, _listEndDate);
|
||||||
|
|
||||||
|
// 依開始時間排序
|
||||||
|
events.sort((a, b) => (a.startTime ?? DateTime.now()).compareTo(b.startTime ?? DateTime.now()));
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_listEvents = events;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("列表載入失敗: $e")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 簡單式:獨立選擇開始或結束日期
|
||||||
|
Future<void> _selectDate(bool isStart) async {
|
||||||
|
final DateTime initialDate = isStart ? _listStartDate : _listEndDate;
|
||||||
|
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: DateTime(2020),
|
||||||
|
lastDate: DateTime(2030),
|
||||||
|
builder: (context, child) {
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
colorScheme: const ColorScheme.light(primary: Colors.blueAccent),
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
if (isStart) {
|
||||||
|
_listStartDate = picked;
|
||||||
|
// 防呆:如果開始日期大於結束日期,讓結束日期自動等於開始日期
|
||||||
|
if (_listStartDate.isAfter(_listEndDate)) {
|
||||||
|
_listEndDate = _listStartDate;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_listEndDate = picked;
|
||||||
|
// 防呆:如果結束日期小於開始日期,讓開始日期自動等於結束日期
|
||||||
|
if (_listEndDate.isBefore(_listStartDate)) {
|
||||||
|
_listStartDate = _listEndDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_fetchListEvents(); // 選定後立刻觸發 API 重新查詢
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取日曆選定日期的行程
|
||||||
List<CalendarEvent> _getEventsForDay(DateTime day) {
|
List<CalendarEvent> _getEventsForDay(DateTime day) {
|
||||||
final dateKey = DateTime(day.year, day.month, day.day);
|
final dateKey = DateTime(day.year, day.month, day.day);
|
||||||
return _eventsMap[dateKey] ?? [];
|
return _eventsMap[dateKey] ?? [];
|
||||||
@@ -68,102 +143,257 @@ class _CalendarManagerState extends State<CalendarManager> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF8F9FA),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('企業行事曆'),
|
title: const Text('行事曆', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
elevation: 0.5,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(icon: const Icon(Icons.today), onPressed: () => setState(() => _focusedDay = DateTime.now())),
|
// 視圖切換下拉選單
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
value: _viewMode,
|
||||||
|
icon: const Icon(Icons.arrow_drop_down, color: Colors.blueAccent),
|
||||||
|
items: ['日曆模式', '列表模式'].map((String value) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newValue) {
|
||||||
|
if (newValue != null && newValue != _viewMode) {
|
||||||
|
setState(() => _viewMode = newValue);
|
||||||
|
// 切換模式時,呼叫對應的 API
|
||||||
|
if (newValue == '列表模式') {
|
||||||
|
_fetchListEvents();
|
||||||
|
} else {
|
||||||
|
_fetchMonthEvents(_focusedDay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.today, color: Colors.blueAccent),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_focusedDay = DateTime.now();
|
||||||
|
_selectedDay = DateTime.now();
|
||||||
|
|
||||||
|
// 如果是列表模式,也順便重置回當月區間
|
||||||
|
_listStartDate = DateTime(DateTime.now().year, DateTime.now().month, 1);
|
||||||
|
_listEndDate = DateTime(DateTime.now().year, DateTime.now().month + 1, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
_viewMode == '日曆模式'
|
||||||
|
? _fetchMonthEvents(_focusedDay)
|
||||||
|
: _fetchListEvents();
|
||||||
|
}
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: _viewMode == '日曆模式' ? _buildCalendarLayout() : _buildListLayout(),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
child: const Icon(Icons.add, color: Colors.white),
|
||||||
|
onPressed: () async {
|
||||||
|
// 跳轉至新增表單,並將目前選擇的日期傳入作為預設值
|
||||||
|
final result = await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => CalendarForm(
|
||||||
|
userId: widget.userId,
|
||||||
|
initialDate: _selectedDay, // 貼心設計:自動帶入你在日曆上點擊的日期
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果表單回傳 true (代表新增成功),就重新 Call API 抓取最新資料!
|
||||||
|
if (result == true) {
|
||||||
|
if (_viewMode == '日曆模式') {
|
||||||
|
_fetchMonthEvents(_focusedDay);
|
||||||
|
} else {
|
||||||
|
_fetchListEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 視圖 1:日曆模式 ==============
|
||||||
|
Widget _buildCalendarLayout() {
|
||||||
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// 1. 日曆組件
|
Container(
|
||||||
TableCalendar<CalendarEvent>(
|
color: Colors.white,
|
||||||
|
child: TableCalendar<CalendarEvent>(
|
||||||
firstDay: DateTime.utc(2020, 1, 1),
|
firstDay: DateTime.utc(2020, 1, 1),
|
||||||
lastDay: DateTime.utc(2030, 12, 31),
|
lastDay: DateTime.utc(2030, 12, 31),
|
||||||
focusedDay: _focusedDay,
|
focusedDay: _focusedDay,
|
||||||
calendarFormat: _calendarFormat,
|
calendarFormat: _calendarFormat,
|
||||||
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
||||||
eventLoader: _getEventsForDay, // 在有行程的日期顯示小點點
|
eventLoader: _getEventsForDay,
|
||||||
|
|
||||||
// 樣式設定
|
|
||||||
calendarStyle: CalendarStyle(
|
calendarStyle: CalendarStyle(
|
||||||
todayDecoration: BoxDecoration(color: Colors.blue.withOpacity(0.5), shape: BoxShape.circle),
|
todayDecoration: BoxDecoration(color: Colors.blue.withOpacity(0.3), shape: BoxShape.circle),
|
||||||
selectedDecoration: const BoxDecoration(color: Colors.blue, shape: BoxShape.circle),
|
selectedDecoration: const BoxDecoration(color: Colors.blueAccent, shape: BoxShape.circle),
|
||||||
markerDecoration: const BoxDecoration(color: Colors.orange, shape: BoxShape.circle),
|
markerDecoration: const BoxDecoration(color: Colors.orange, shape: BoxShape.circle),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 互動事件
|
|
||||||
onDaySelected: (selectedDay, focusedDay) {
|
onDaySelected: (selectedDay, focusedDay) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedDay = selectedDay;
|
_selectedDay = selectedDay;
|
||||||
_focusedDay = focusedDay;
|
_focusedDay = focusedDay;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onFormatChanged: (format) {
|
onFormatChanged: (format) => setState(() => _calendarFormat = format),
|
||||||
setState(() => _calendarFormat = format);
|
|
||||||
},
|
|
||||||
onPageChanged: (focusedDay) {
|
onPageChanged: (focusedDay) {
|
||||||
_focusedDay = focusedDay;
|
_focusedDay = focusedDay;
|
||||||
_fetchMonthEvents(focusedDay); // 切換月份時自動重新抓取 API
|
_fetchMonthEvents(focusedDay);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const Divider(height: 1),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 2. 下方行程列表
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _buildEventList(),
|
: _buildEventListForDay(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
onPressed: () { /* 實重新增行程邏輯 */ },
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEventList() {
|
Widget _buildEventListForDay() {
|
||||||
final dayEvents = _getEventsForDay(_selectedDay!);
|
final dayEvents = _getEventsForDay(_selectedDay!);
|
||||||
|
|
||||||
if (dayEvents.isEmpty) {
|
if (dayEvents.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text("${DateFormat('MM/dd').format(_selectedDay!)} 沒有行程", style: const TextStyle(color: Colors.grey, fontSize: 16)),
|
||||||
"${DateFormat('MM/dd').format(_selectedDay!)} 沒有行程",
|
|
||||||
style: const TextStyle(color: Colors.grey),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
itemCount: dayEvents.length,
|
itemCount: dayEvents.length,
|
||||||
|
itemBuilder: (context, index) => _buildEventCard(dayEvents[index]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 視圖 2:列表模式 (簡單式日期篩選器) ==============
|
||||||
|
Widget _buildListLayout() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// 頂部日期區間選擇器 (簡單式左右分割)
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildDateSelectButton('開始日期', _listStartDate, true)),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
||||||
|
child: Text('至', style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
Expanded(child: _buildDateSelectButton('結束日期', _listEndDate, false)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 列表結果
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _listEvents.isEmpty
|
||||||
|
? const Center(child: Text("該區間尚無任何行程", style: TextStyle(color: Colors.grey, fontSize: 16)))
|
||||||
|
: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
itemCount: _listEvents.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final event = dayEvents[index];
|
return _buildEventCard(_listEvents[index], showDate: true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獨立的日期選擇按鈕 UI
|
||||||
|
Widget _buildDateSelectButton(String label, DateTime date, bool isStart) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => _selectDate(isStart),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(0.05),
|
||||||
|
border: Border.all(color: Colors.blueAccent.withOpacity(0.3)),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.calendar_today, size: 14, color: Colors.blueAccent),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
DateFormat('yyyy/MM/dd').format(date),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.blueAccent, fontSize: 14)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== 共用元件:行程 Card ==============
|
||||||
|
Widget _buildEventCard(CalendarEvent event, {bool showDate = false}) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 0,
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
leading: Container(
|
leading: Container(
|
||||||
width: 4,
|
width: 5,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: event.levelColor,
|
color: event.levelColor,
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
title: Text(event.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
subtitle: Text(event.timeRangeText),
|
subtitle: Padding(
|
||||||
trailing: const Icon(Icons.chevron_right),
|
padding: const EdgeInsets.only(top: 6.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.access_time, size: 14, color: Colors.grey.shade600),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
showDate && event.startTime != null
|
||||||
|
? "${DateFormat('MM/dd').format(event.startTime!)} ${event.timeRangeText}"
|
||||||
|
: event.timeRangeText,
|
||||||
|
style: TextStyle(color: Colors.grey.shade700),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right, color: Colors.grey),
|
||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => CalendarDetail(event: event))
|
MaterialPageRoute(builder: (_) => CalendarDetail(event: event))
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ class LeaveApiService {
|
|||||||
Future<bool> promoteLeave(String personId, String billNo) async {
|
Future<bool> promoteLeave(String personId, String billNo) async {
|
||||||
// 依據標準規範,參數一律使用 para0x
|
// 依據標準規範,參數一律使用 para0x
|
||||||
final Map<String, String> params = {
|
final Map<String, String> params = {
|
||||||
"para01": "eipm21",
|
"para01": "hrsm11",
|
||||||
"para02": personId,
|
"para02": personId,
|
||||||
"para03": billNo,
|
"para03": billNo,
|
||||||
};
|
};
|
||||||
@@ -182,7 +182,7 @@ class LeaveApiService {
|
|||||||
/// 對應 API: bpm_sign_history/2/
|
/// 對應 API: bpm_sign_history/2/
|
||||||
Future<List<Map<String, dynamic>>> fetchSignHistory(String billNo) async {
|
Future<List<Map<String, dynamic>>> fetchSignHistory(String billNo) async {
|
||||||
final Map<String, String> params = {
|
final Map<String, String> params = {
|
||||||
"para01": "eipm21", // 來源單別 functiontag
|
"para01": "hrsm11", // 來源單別 functiontag
|
||||||
"para02": billNo, // 原單單號 query_id
|
"para02": billNo, // 原單單號 query_id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,21 +90,31 @@ class Leave {
|
|||||||
return '${df.format(startTime!)} ~ ${df.format(endTime!)}';
|
return '${df.format(startTime!)} ~ ${df.format(endTime!)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
-- [sign_status] [varchar](2) not null, -- N:待簽核 P:簽核中 R:拒絶 A:同意 C:作廢
|
||||||
|
-- [flow_status] [varchar](2) not null, -- N:待簽核 P:簽核中 Z:結案 C:作廢
|
||||||
|
*/
|
||||||
// 狀態顏色映射
|
// 狀態顏色映射
|
||||||
Color get statusColor {
|
Color get statusColor {
|
||||||
switch (flowStatus) {
|
switch (flowStatus) {
|
||||||
case '1': return Colors.orange; // 審核中
|
case 'N': return Colors.blue; // 待簽核
|
||||||
case '2': return Colors.green; // 已核准
|
case 'P': return Colors.orange; // 審核中
|
||||||
case 'X': return Colors.red; // 駁回
|
case 'A': return Colors.green; // 同意
|
||||||
|
case 'R': return Colors.red; // 駁回
|
||||||
|
case 'C': return Colors.black; // 作廢
|
||||||
|
case 'Z': return Colors.purple; // 結案
|
||||||
default: return Colors.grey; // 草稿
|
default: return Colors.grey; // 草稿
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String get statusText {
|
String get statusText {
|
||||||
switch (flowStatus) {
|
switch (flowStatus) {
|
||||||
case '1': return '審核中';
|
case 'N': return '待簽核';
|
||||||
case '2': return '已核准';
|
case 'P': return '審核中';
|
||||||
case 'X': return '已駁回';
|
case 'A': return '同意';
|
||||||
|
case 'R': return '駁回';
|
||||||
|
case 'C': return '作廢';
|
||||||
|
case 'Z': return '結案';
|
||||||
default: return '草稿';
|
default: return '草稿';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-12
@@ -1,4 +1,4 @@
|
|||||||
// todo_api.dart (新增 Create 功能)
|
// todo_api.dart
|
||||||
|
|
||||||
import './todo_model.dart';
|
import './todo_model.dart';
|
||||||
import '../services/generic_api_service.dart';
|
import '../services/generic_api_service.dart';
|
||||||
@@ -10,16 +10,31 @@ class TodoApiService {
|
|||||||
|
|
||||||
TodoApiService({this.currentUserId = 'admin'});
|
TodoApiService({this.currentUserId = 'admin'});
|
||||||
|
|
||||||
// 原始的查詢功能...
|
|
||||||
/// 獲取指定日期的任務清單
|
/// 獲取指定日期的任務清單
|
||||||
Future<List<Todo>> fetchTodos({DateTime? selectedDate}) async {
|
Future<List<Todo>> fetchTodos({DateTime? selectedDate, String? statusFilter}) async {
|
||||||
// 預設查詢今天的資料
|
// 預設查詢今天的資料
|
||||||
DateTime dateToQuery = selectedDate ?? DateTime.now();
|
DateTime dateToQuery = selectedDate ?? DateTime.now();
|
||||||
String formattedDate = DateFormat('yyyy-MM-dd').format(dateToQuery);
|
String formattedDate = DateFormat('yyyy-MM-dd').format(dateToQuery);
|
||||||
|
|
||||||
// 構建 queryFilter:過濾特定的 end_date 並根據 id 排序
|
// 1. 組合 wheresql_org (第 5 個參數)
|
||||||
// 格式範例: 1^100^id^*^^end_date^2024-05-01
|
// 使用 LIKE 來比對 DATETIME 欄位,確保抓到該日期的所有時段
|
||||||
String queryFilter = "1^100^id^*^^end_date^$formattedDate";
|
String whereSql = "end_date LIKE '$formattedDate%'";
|
||||||
|
|
||||||
|
// 2. 如果有傳入狀態過濾條件,動態加上 AND 語法
|
||||||
|
if (statusFilter != null && statusFilter != 'All') {
|
||||||
|
String dbStatus = '';
|
||||||
|
if (statusFilter == 'Done') dbStatus = 'DONE';
|
||||||
|
else if (statusFilter == 'In Progress') dbStatus = 'WIP';
|
||||||
|
else if (statusFilter == 'To do') dbStatus = 'TODO'; // 假設你的待辦狀態是 TODO
|
||||||
|
|
||||||
|
if (dbStatus.isNotEmpty) {
|
||||||
|
whereSql += " AND pbi_status = '$dbStatus'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 嚴格依照 API 規範組裝 10 個參數 (共 9 個 ^ 分隔符)
|
||||||
|
// 格式:pageno^pagerec^orderby^udf_fields^wheresql_org^menuid^where_fields^where_value^where_field^where_idvalue
|
||||||
|
String queryFilter = "1^100^id^*^$whereSql^^^^^";
|
||||||
|
|
||||||
return await _apiService.fetchList<Todo>(
|
return await _apiService.fetchList<Todo>(
|
||||||
tableName: "eip_todolist",
|
tableName: "eip_todolist",
|
||||||
@@ -36,14 +51,14 @@ class TodoApiService {
|
|||||||
"task_name": todo.taskName,
|
"task_name": todo.taskName,
|
||||||
"task_desc": todo.description,
|
"task_desc": todo.description,
|
||||||
"issue_priority": todo.priority,
|
"issue_priority": todo.priority,
|
||||||
"pbi_status": todo.status ?? 'WIP', // 預設為進行中
|
"pbi_status": todo.status ?? 'WIP',
|
||||||
"end_date": todo.endDate != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(todo.endDate!) : null,
|
"end_date": todo.endDate != null ? DateFormat('yyyy-MM-dd HH:mm:ss').format(todo.endDate!) : null,
|
||||||
"create_user": currentUserId,
|
"create_user": currentUserId,
|
||||||
"create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
"create_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
||||||
|
// 新增時先不寫入 update_user / update_date
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 根據 Generic API 規範,使用 action: "C" 進行新增
|
|
||||||
await _apiService.fetchList<dynamic>(
|
await _apiService.fetchList<dynamic>(
|
||||||
tableName: "eip_todolist",
|
tableName: "eip_todolist",
|
||||||
pk: "id",
|
pk: "id",
|
||||||
@@ -63,13 +78,13 @@ class TodoApiService {
|
|||||||
Future<bool> updateTodoStatus(int id, String status) async {
|
Future<bool> updateTodoStatus(int id, String status) async {
|
||||||
final Map<String, dynamic> data = {
|
final Map<String, dynamic> data = {
|
||||||
"id": id, // 必填 PK
|
"id": id, // 必填 PK
|
||||||
"pbi_status": status, // 更新狀態為 'DONE'
|
"pbi_status": status, // 更新狀態
|
||||||
"modify_user": currentUserId,
|
// 修正為 Schema 正確的欄位名稱 update_user / update_date
|
||||||
"modify_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
"update_user": currentUserId,
|
||||||
|
"update_date": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用 Action "U" 代表 Update
|
|
||||||
await _apiService.fetchList<dynamic>(
|
await _apiService.fetchList<dynamic>(
|
||||||
tableName: "eip_todolist",
|
tableName: "eip_todolist",
|
||||||
pk: "id",
|
pk: "id",
|
||||||
|
|||||||
+26
-13
@@ -1,4 +1,4 @@
|
|||||||
// todo_form.dart (適配 Todo Model)
|
// todo_form.dart (適配 Todo Model - 中文化與選項調整)
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
@@ -21,8 +21,10 @@ class _TodoFormState extends State<TodoForm> {
|
|||||||
final TextEditingController _nameController = TextEditingController();
|
final TextEditingController _nameController = TextEditingController();
|
||||||
final TextEditingController _descController = TextEditingController();
|
final TextEditingController _descController = TextEditingController();
|
||||||
|
|
||||||
String _selectedClass = 'Work';
|
// 根據需求更新預設值與選項
|
||||||
String _selectedPriority = 'Medium';
|
String _selectedClass = '工作';
|
||||||
|
String _selectedPriority = 'Middle';
|
||||||
|
String _selectedStatus = 'ToDo'; // 新增狀態變數
|
||||||
DateTime _endDate = DateTime.now().add(const Duration(days: 7));
|
DateTime _endDate = DateTime.now().add(const Duration(days: 7));
|
||||||
|
|
||||||
final Color primaryPurple = const Color(0xFF6542D0);
|
final Color primaryPurple = const Color(0xFF6542D0);
|
||||||
@@ -53,7 +55,7 @@ class _TodoFormState extends State<TodoForm> {
|
|||||||
className: _selectedClass,
|
className: _selectedClass,
|
||||||
description: _descController.text,
|
description: _descController.text,
|
||||||
priority: _selectedPriority,
|
priority: _selectedPriority,
|
||||||
status: 'WIP',
|
status: _selectedStatus, // 這裡改為帶入表單選擇的狀態
|
||||||
endDate: _endDate,
|
endDate: _endDate,
|
||||||
createdBy: widget.userId,
|
createdBy: widget.userId,
|
||||||
);
|
);
|
||||||
@@ -77,7 +79,7 @@ class _TodoFormState extends State<TodoForm> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: bgLight, elevation: 0,
|
backgroundColor: bgLight, elevation: 0,
|
||||||
leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87), onPressed: () => Navigator.pop(context)),
|
leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black87), onPressed: () => Navigator.pop(context)),
|
||||||
title: const Text('New Task', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)),
|
title: const Text('新增任務', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
@@ -87,15 +89,25 @@ class _TodoFormState extends State<TodoForm> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildDropdownField('Task Category', _selectedClass, ['Work', 'Personal', 'Urgent'], (val) => setState(() => _selectedClass = val!)),
|
// 類別:改為 固定三個選項
|
||||||
|
_buildDropdownField('任務類別', _selectedClass, ['工作', '個人行程', '其他'], (val) => setState(() => _selectedClass = val!)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildTextField('Task Name', _nameController, 'e.g. Design UI Mockup', Icons.edit_note),
|
|
||||||
|
// 名稱與說明:加上中文提示
|
||||||
|
_buildTextField('任務名稱', _nameController, '例如:撰寫系統分析報告', Icons.edit_note),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildTextField('Description', _descController, 'Enter details here...', Icons.description, isMultiline: true),
|
_buildTextField('任務說明', _descController, '請輸入任務詳細說明...', Icons.description, isMultiline: true),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildDropdownField('Priority', _selectedPriority, ['High', 'Medium', 'Low'], (val) => setState(() => _selectedPriority = val!)),
|
|
||||||
|
// 優先級:改為 High, Middle, Low
|
||||||
|
_buildDropdownField('優先級', _selectedPriority, ['High', 'Middle', 'Low'], (val) => setState(() => _selectedPriority = val!)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildDatePicker('Due Date', _endDate),
|
|
||||||
|
// 新增狀態下拉選單
|
||||||
|
_buildDropdownField('目前狀態', _selectedStatus, ['ToDo', 'WIP', 'Hold', 'Done'], (val) => setState(() => _selectedStatus = val!)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_buildDatePicker('截止日期', _endDate),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
_buildSubmitButton(),
|
_buildSubmitButton(),
|
||||||
],
|
],
|
||||||
@@ -119,7 +131,7 @@ class _TodoFormState extends State<TodoForm> {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: isMultiline ? 3 : 1,
|
maxLines: isMultiline ? 3 : 1,
|
||||||
decoration: InputDecoration(hintText: hint, border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.only(top: 8)),
|
decoration: InputDecoration(hintText: hint, border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.only(top: 8)),
|
||||||
validator: (v) => v == null || v.isEmpty ? 'Cannot be empty' : null,
|
validator: (v) => v == null || v.isEmpty ? '此欄位不能為空' : null, // 必填防呆中文化
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -155,7 +167,8 @@ class _TodoFormState extends State<TodoForm> {
|
|||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(DateFormat('dd MMM, yyyy').format(date), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
// 日期格式也順便調整成台灣習慣的 YYYY/MM/DD
|
||||||
|
Text(DateFormat('yyyy/MM/dd').format(date), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Icon(Icons.calendar_today, color: primaryPurple, size: 20),
|
Icon(Icons.calendar_today, color: primaryPurple, size: 20),
|
||||||
@@ -172,7 +185,7 @@ class _TodoFormState extends State<TodoForm> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: primaryPurple, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 5),
|
style: ElevatedButton.styleFrom(backgroundColor: primaryPurple, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 5),
|
||||||
onPressed: _handleSubmit,
|
onPressed: _handleSubmit,
|
||||||
child: const Text('Create Task', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
|
child: const Text('建立任務', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// todo_model.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ class Todo {
|
|||||||
final DateTime? endDate; // end_date (預計完成日期)
|
final DateTime? endDate; // end_date (預計完成日期)
|
||||||
final String? createdBy; // create_user (建立者)
|
final String? createdBy; // create_user (建立者)
|
||||||
final DateTime? createDate; // create_date (建立日期)
|
final DateTime? createDate; // create_date (建立日期)
|
||||||
|
final String? updatedBy; // update_user (更新者) - 配合新 Schema 新增
|
||||||
|
final DateTime? updateDate; // update_date (更新日期) - 配合新 Schema 新增
|
||||||
|
|
||||||
Todo({
|
Todo({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -22,6 +25,8 @@ class Todo {
|
|||||||
this.endDate,
|
this.endDate,
|
||||||
this.createdBy,
|
this.createdBy,
|
||||||
this.createDate,
|
this.createDate,
|
||||||
|
this.updatedBy,
|
||||||
|
this.updateDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Factory 構造函數:從 API 返回的 JSON (Map) 創建 Todo 物件
|
// Factory 構造函數:從 API 返回的 JSON (Map) 創建 Todo 物件
|
||||||
@@ -29,7 +34,6 @@ class Todo {
|
|||||||
// 輔助函數:安全解析日期字串
|
// 輔助函數:安全解析日期字串
|
||||||
DateTime? parseDate(dynamic date) {
|
DateTime? parseDate(dynamic date) {
|
||||||
if (date is String && date.isNotEmpty) {
|
if (date is String && date.isNotEmpty) {
|
||||||
// 假設日期格式為 YYYY-MM-DD HH:mm:ss.sss 或 YYYY-MM-DD
|
|
||||||
return DateTime.tryParse(date);
|
return DateTime.tryParse(date);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -45,6 +49,8 @@ class Todo {
|
|||||||
endDate: parseDate(json['end_date']),
|
endDate: parseDate(json['end_date']),
|
||||||
createdBy: json['create_user'] as String?,
|
createdBy: json['create_user'] as String?,
|
||||||
createDate: parseDate(json['create_date']),
|
createDate: parseDate(json['create_date']),
|
||||||
|
updatedBy: json['update_user'] as String?,
|
||||||
|
updateDate: parseDate(json['update_date']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user