add chart code

This commit is contained in:
2026-01-13 23:11:25 +08:00
parent 0a9f0b8583
commit 6b4d98f5ce
14 changed files with 938 additions and 41 deletions
+68
View File
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
// 在 channel_sales_manager.dart 檔案下方或獨立檔案
Future<String?> showMonthPicker(BuildContext context, String currentYYMM) async {
int selectedYear = int.parse(currentYYMM.substring(0, 4));
int selectedMonth = int.parse(currentYYMM.substring(4, 6));
return showDialog<String>(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, setDialogState) {
return AlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => setDialogState(() => selectedYear--),
),
Text('$selectedYear'),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: () => setDialogState(() => selectedYear++),
),
],
),
content: SizedBox(
width: 300,
height: 200,
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 1.5,
),
itemCount: 12,
itemBuilder: (context, index) {
int month = index + 1;
bool isSelected = month == selectedMonth;
return InkWell(
onTap: () {
String result = '$selectedYear${month.toString().padLeft(2, '0')}';
Navigator.pop(context, result);
},
child: Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'$month月',
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
},
),
),
);
});
},
);
}
+61
View File
@@ -11,6 +11,67 @@ class GenericApiService {
// [修改] 移除 const COMMON_API_URL,改為方法獲取
static const String BASE_IP = "https://api.gex.com.tw:8033";
// 修改後的動態 URL 產生器,支援傳入不同的 store procedure 與回傳 dataset 數
String _getProcedureUrl(String endpoint, String version) {
final String db = AuthManager().currentCompany ?? "demo"; //
final String dbAll = "eis_$db"; //
return "$BASE_IP/xapi/v2/$dbAll/$endpoint/$version/"; //
}
/// 呼叫 Store Procedure 的通用方法
/// [T] : 目標資料模型
/// [procedureEndpoint] : 端點名稱 (例如: 'sp_get_channel_sales_stats')
/// [version] : 版本號,預設為 "2"
/// [params] : 傳遞給 SP 的參數 Map
/// [fromJson] : 將 Map 轉換為物件的工廠方法
Future<List<T>> fetchProcedure<T>({
required String procedureEndpoint,
String version = "2",
required Map<String, String> params,
required T Function(Map<String, dynamic>) fromJson,
}) async {
String? token = await AuthManager.getToken();
if (token == null) throw Exception('未登入:找不到有效 Token');
final Map<String, String> body = {"token": token, ...params};
final String url = _getProcedureUrl(procedureEndpoint, version);
try {
final response = await http.post(
Uri.parse(url),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body,
);
if (response.statusCode == 200) {
final Map<String, dynamic> responseData = json.decode(response.body);
final int code = responseData['code'] ?? responseData['Code'] ?? -1;
if (code == 0) {
final dynamic dataObject = responseData['data'];
if (dataObject is List) {
return dataObject.map((j) => fromJson(j as Map<String, dynamic>)).toList();
} else if (dataObject is Map<String, dynamic>) {
return [fromJson(dataObject)];
}
return []; // 數據格式不符回傳空清單
} else {
print("API 錯誤: ${responseData['msg'] ?? responseData['message']}");
return []; // Code 不為 0 回傳空清單
}
} else {
// [關鍵修正] statusCode 不為 200 時拋出異常或回傳空清單
throw Exception('HTTP 錯誤: ${response.statusCode}');
}
} catch (e) {
print("fetchProcedure 發生異常: $e");
rethrow; // 重新拋出異常讓呼叫端處理
}
// [關鍵修正] 確保函式末端一定有回傳值,解決截圖中的錯誤
}
// 以下是 orm_api
String _getDynamicUrl() {
// 從 AuthManager 獲取目前登入的公司別,若無則預設 eis_demo
final String db = AuthManager().currentCompany ?? "demo";
+100
View File
@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import '../leave/leave_api.dart'; // 確保路徑指向你的 LeaveApiService
class PersonPickerDialog extends StatefulWidget {
final String title;
const PersonPickerDialog({super.key, this.title = '人員查詢'});
@override
State<PersonPickerDialog> createState() => _PersonPickerDialogState();
}
class _PersonPickerDialogState extends State<PersonPickerDialog> {
final LeaveApiService _apiService = LeaveApiService();
final TextEditingController _controller = TextEditingController();
List<Map<String, dynamic>> _results = [];
bool _isLoading = false;
// 執行 API 搜尋
Future<void> _doSearch() async {
final keyword = _controller.text.trim();
if (keyword.isEmpty) return;
setState(() => _isLoading = true);
try {
// 呼叫我們先前在 leave_api 寫好的 fetchEmployees
// 該方法會從 basperson 抓取 personid, personcname, departmentid
final data = await _apiService.fetchEmployees(keyword);
setState(() {
_results = data;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('搜尋出錯: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.title),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
hintText: '輸入姓名或工號',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.send, color: Colors.blue),
onPressed: _doSearch,
),
border: const OutlineInputBorder(),
),
onSubmitted: (_) => _doSearch(),
),
const SizedBox(height: 15),
if (_isLoading)
const LinearProgressIndicator()
else
Flexible(
child: _results.isEmpty
? const Padding(
padding: EdgeInsets.all(20),
child: Text('請輸入關鍵字搜尋人員', style: TextStyle(color: Colors.grey)),
)
: ListView.separated(
shrinkWrap: true,
itemCount: _results.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (ctx, i) {
final p = _results[i];
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(p['name']), // personcname
subtitle: Text('工號: ${p['id']} | 部門: ${p['dept']}'),
onTap: () => Navigator.pop(context, p), // 回傳整筆資料
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
],
);
}
}