update new version

This commit is contained in:
2025-12-29 22:19:48 +08:00
parent fb2603c6f0
commit b26a339d5c
18 changed files with 1113 additions and 293 deletions
+113 -122
View File
@@ -17,7 +17,6 @@ class _ClockInManagerState extends State<ClockInManager> {
final ClockInApiService _apiService = ClockInApiService();
late Future<List<ClockInRecord>> _historyFuture;
// --- 新增:店點相關變數 ---
List<ClockInStore> _stores = [];
ClockInStore? _selectedStore;
bool _isLoadingStores = true;
@@ -25,28 +24,33 @@ class _ClockInManagerState extends State<ClockInManager> {
Position? _currentPosition;
double _distanceInMeters = -1;
String _currentTime = "";
late Timer _timer;
Timer? _timer;
@override
void initState() {
super.initState();
_initData();
_initAllData();
_startClock();
}
// 初始化資料:先抓店點,再抓歷史紀錄
Future<void> _initData() async {
Future<void> _initAllData() async {
// 1. 先抓店點
try {
final stores = await _apiService.fetchStores();
setState(() {
_stores = stores;
if (_stores.isNotEmpty) _selectedStore = _stores.first;
_isLoadingStores = false;
});
_refreshHistory();
_getCurrentLocation();
if (mounted) {
setState(() {
_stores = stores;
if (_stores.isNotEmpty) _selectedStore = _stores.first;
_isLoadingStores = false;
});
// 2. 抓歷史紀錄
_refreshHistory();
// 3. 初次定位
_handleLocationPermission();
_updateLocation();
}
} catch (e) {
_showMsg("初始化店點失敗: $e");
_showMsg("讀取店點資訊失敗");
}
}
@@ -56,61 +60,85 @@ class _ClockInManagerState extends State<ClockInManager> {
});
}
// 修改:根據「目前選中店點」計算距離
Future<void> _getCurrentLocation() async {
Future<bool> _handleLocationPermission() async {
bool serviceEnabled;
LocationPermission permission;
// 檢查定位服務是否開啟
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
_showMsg('手機定位服務已關閉,請開啟。');
return false;
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
_showMsg('定位權限被拒絕。');
return false;
}
}
if (permission == LocationPermission.deniedForever) {
_showMsg('定位權限被永久拒絕,請至系統設定開啟。');
return false;
}
return true;
}
Future<void> _updateLocation() async {
if (_selectedStore == null) return;
final position = await Geolocator.getCurrentPosition();
// 使用所選店點的經緯度進行計算
final distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
_selectedStore!.latitude,
_selectedStore!.longitude
);
if (mounted) {
setState(() {
_currentPosition = position;
_distanceInMeters = distance;
});
try {
Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
double distance = Geolocator.distanceBetween(
position.latitude, position.longitude,
_selectedStore!.latitude, _selectedStore!.longitude
);
if (mounted) {
setState(() {
_currentPosition = position;
_distanceInMeters = distance;
});
}
} catch (e) {
print("定位失敗: $e");
}
}
void _refreshHistory() {
final future = _apiService.fetchHistory(widget.userId);
setState(() => _historyFuture = future);
setState(() {
_historyFuture = _apiService.fetchHistory(widget.userId);
});
}
// 修改:打卡動作加入 StoreId 與動態距離判斷
Future<void> _handleClockIn(String type) async {
if (_selectedStore == null) {
_showMsg("錯誤:請先選擇打卡店點");
if (_selectedStore == null) return;
// 再次確認位置
await _updateLocation();
if (_distanceInMeters > _selectedStore!.distance) {
_showMsg("打卡失敗:距離 ${_selectedStore!.storeName} 過遠 (${_distanceInMeters.toInt()}m)");
return;
}
await _getCurrentLocation();
// 使用資料表中的 Distance 欄位作為判斷標準
if (_distanceInMeters > _selectedStore!.distance || _distanceInMeters == -1) {
_showMsg("打卡失敗:距離 ${_selectedStore!.storeName} 已超過 ${_selectedStore!.distance} 公尺 (目前: ${_distanceInMeters.toInt()}m)");
return;
}
final newRecord = ClockInRecord(
userId: int.tryParse(widget.userId),
final record = ClockInRecord(
userId: widget.userId,
type: type,
latitude: _currentPosition?.latitude,
longitude: _currentPosition?.longitude,
storeId: _selectedStore!.storeId, // 使用動態 ID
storeId: _selectedStore!.storeId,
);
bool success = await _apiService.postClockIn(newRecord);
await _apiService.postClockIn(record);
/*
final success = await _apiService.postClockIn(record);
if (success) {
_showMsg("[$type] 打卡成功!地點: ${_selectedStore!.storeName}");
_showMsg("$type 打卡成功");
_refreshHistory();
}
*/
}
void _showMsg(String msg) {
@@ -119,49 +147,40 @@ class _ClockInManagerState extends State<ClockInManager> {
@override
void dispose() {
_timer.cancel();
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 動態判斷是否在選中店點的範圍內
bool isWithinRange = _selectedStore != null &&
_distanceInMeters != -1 &&
_distanceInMeters >= 0 &&
_distanceInMeters <= _selectedStore!.distance;
return Scaffold(
appBar: AppBar(title: const Text('員工行動打卡')),
appBar: AppBar(title: const Text('行動打卡系統')),
body: _isLoadingStores
? const Center(child: CircularProgressIndicator())
: Column(
children: [
// 新增:店點選擇下拉選單區
_buildStoreSelector(),
// 店點切換
_buildStorePicker(),
// 打卡狀態區
_buildStatusCard(isWithinRange),
const Divider(height: 1),
// ... 歷史紀錄標題與清單部分維持不變 (同附件)
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('今日打卡紀錄', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
IconButton(onPressed: _refreshHistory, icon: const Icon(Icons.refresh)),
],
),
// 歷史紀錄清單
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Align(alignment: Alignment.centerLeft, child: Text("今日紀錄", style: TextStyle(fontWeight: FontWeight.bold))),
),
Expanded(
child: FutureBuilder<List<ClockInRecord>>(
future: _historyFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
if (!snapshot.hasData || snapshot.data!.isEmpty) return const Center(child: Text('查無紀錄'));
final list = snapshot.data ?? [];
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (ctx, i) => _buildHistoryItem(snapshot.data![i]),
itemCount: list.length,
itemBuilder: (ctx, i) => _buildHistoryTile(list[i]),
);
},
),
@@ -171,64 +190,46 @@ class _ClockInManagerState extends State<ClockInManager> {
);
}
// 新增:店點選擇器 UI
Widget _buildStoreSelector() {
Widget _buildStorePicker() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
padding: const EdgeInsets.all(12),
color: Colors.white,
child: DropdownButtonFormField<ClockInStore>(
decoration: const InputDecoration(
labelText: "選擇打卡店點",
prefixIcon: Icon(Icons.store),
border: OutlineInputBorder(),
),
value: _selectedStore,
items: _stores.map((s) => DropdownMenuItem(
value: s,
child: Text(s.storeName),
)).toList(),
decoration: const InputDecoration(labelText: "目前打卡店點", border: OutlineInputBorder()),
items: _stores.map((s) => DropdownMenuItem(value: s, child: Text(s.storeName))).toList(),
onChanged: (val) {
setState(() {
_selectedStore = val;
_distanceInMeters = -1; // 切換時重置距離,等待下次定位
});
_getCurrentLocation(); // 切換後立即重新計算距離
setState(() => _selectedStore = val);
_updateLocation();
},
),
);
}
// 修改:狀態卡片顯示
Widget _buildStatusCard(bool isWithinRange) {
return Container(
padding: const EdgeInsets.all(20),
color: Colors.blue.shade50,
child: Column(
children: [
Text(_currentTime, style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.blue)),
Text(_currentTime, style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
if (_selectedStore != null) ...[
Text(_selectedStore!.storeAddress ?? "", style: const TextStyle(color: Colors.blueGrey)),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, color: isWithinRange ? Colors.green : Colors.red),
Text(
isWithinRange
? "已進入 ${_selectedStore!.storeName} 範圍"
: "距離過遠: ${_distanceInMeters.toInt()}m (限制: ${_selectedStore!.distance}m)",
style: TextStyle(color: isWithinRange ? Colors.green : Colors.red, fontWeight: FontWeight.bold),
),
],
),
],
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, color: isWithinRange ? Colors.green : Colors.red),
Text(
isWithinRange ? "進入打卡範圍" : "超出範圍 (${_distanceInMeters.toInt()}m / ${_selectedStore?.distance}m)",
style: TextStyle(color: isWithinRange ? Colors.green : Colors.red, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(child: _actionBtn("上班打卡", Colors.blue, () => _handleClockIn("上班"))),
const SizedBox(width: 15),
Expanded(child: _actionBtn("下班打卡", Colors.orange, () => _handleClockIn("下班"))),
Expanded(child: ElevatedButton(onPressed: () => _handleClockIn("上班"), child: const Text("上班"))),
const SizedBox(width: 10),
Expanded(child: ElevatedButton(onPressed: () => _handleClockIn("下班"), child: const Text("下班"))),
],
)
],
@@ -236,21 +237,11 @@ class _ClockInManagerState extends State<ClockInManager> {
);
}
// _actionBtn 與 _buildHistoryItem 維持與附件原程式相同...
Widget _actionBtn(String label, Color color, VoidCallback onPressed) {
return ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: color, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15)),
onPressed: onPressed,
child: Text(label, style: const TextStyle(fontSize: 18)),
);
}
Widget _buildHistoryItem(ClockInRecord record) {
Widget _buildHistoryTile(ClockInRecord record) {
return ListTile(
leading: CircleAvatar(backgroundColor: record.typeColor, child: Text(record.type?[0] ?? '', style: const TextStyle(color: Colors.white))),
leading: Icon(Icons.access_time, color: record.typeColor),
title: Text("${record.type} - ${record.formattedTime}"),
subtitle: Text(record.formattedDate),
trailing: const Icon(Icons.check_circle, color: Colors.green, size: 16),
subtitle: Text(record.storeId ?? ""),
);
}
}