import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'dart:async'; import './clock_in_model.dart'; import './clock_in_api.dart'; import 'package:intl/intl.dart'; class ClockInManager extends StatefulWidget { final String userId; const ClockInManager({required this.userId, super.key}); @override State createState() => _ClockInManagerState(); } class _ClockInManagerState extends State { final ClockInApiService _apiService = ClockInApiService(); late Future> _historyFuture; // --- 新增:店點相關變數 --- List _stores = []; ClockInStore? _selectedStore; bool _isLoadingStores = true; Position? _currentPosition; double _distanceInMeters = -1; String _currentTime = ""; late Timer _timer; @override void initState() { super.initState(); _initData(); _startClock(); } // 初始化資料:先抓店點,再抓歷史紀錄 Future _initData() async { try { final stores = await _apiService.fetchStores(); setState(() { _stores = stores; if (_stores.isNotEmpty) _selectedStore = _stores.first; _isLoadingStores = false; }); _refreshHistory(); _getCurrentLocation(); } catch (e) { _showMsg("初始化店點失敗: $e"); } } void _startClock() { _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (mounted) setState(() => _currentTime = DateFormat('HH:mm:ss').format(DateTime.now())); }); } // 修改:根據「目前選中店點」計算距離 Future _getCurrentLocation() 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; }); } } void _refreshHistory() { final future = _apiService.fetchHistory(widget.userId); setState(() => _historyFuture = future); } // 修改:打卡動作加入 StoreId 與動態距離判斷 Future _handleClockIn(String type) async { if (_selectedStore == null) { _showMsg("錯誤:請先選擇打卡店點"); 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), type: type, latitude: _currentPosition?.latitude, longitude: _currentPosition?.longitude, storeId: _selectedStore!.storeId, // 使用動態 ID ); bool success = await _apiService.postClockIn(newRecord); if (success) { _showMsg("[$type] 打卡成功!地點: ${_selectedStore!.storeName}"); _refreshHistory(); } } void _showMsg(String msg) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); } @override void dispose() { _timer.cancel(); super.dispose(); } @override Widget build(BuildContext context) { // 動態判斷是否在選中店點的範圍內 bool isWithinRange = _selectedStore != null && _distanceInMeters != -1 && _distanceInMeters <= _selectedStore!.distance; return Scaffold( appBar: AppBar(title: const Text('員工行動打卡')), body: _isLoadingStores ? const Center(child: CircularProgressIndicator()) : Column( children: [ // 新增:店點選擇下拉選單區 _buildStoreSelector(), _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)), ], ), ), Expanded( child: FutureBuilder>( 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('查無紀錄')); return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (ctx, i) => _buildHistoryItem(snapshot.data![i]), ); }, ), ), ], ), ); } // 新增:店點選擇器 UI Widget _buildStoreSelector() { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), color: Colors.white, child: DropdownButtonFormField( decoration: const InputDecoration( labelText: "選擇打卡店點", prefixIcon: Icon(Icons.store), border: OutlineInputBorder(), ), value: _selectedStore, items: _stores.map((s) => DropdownMenuItem( value: s, child: Text(s.storeName), )).toList(), onChanged: (val) { setState(() { _selectedStore = val; _distanceInMeters = -1; // 切換時重置距離,等待下次定位 }); _getCurrentLocation(); // 切換後立即重新計算距離 }, ), ); } // 修改:狀態卡片顯示 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)), 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), ), ], ), ], const SizedBox(height: 20), Row( children: [ Expanded(child: _actionBtn("上班打卡", Colors.blue, () => _handleClockIn("上班"))), const SizedBox(width: 15), Expanded(child: _actionBtn("下班打卡", Colors.orange, () => _handleClockIn("下班"))), ], ) ], ), ); } // _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) { return ListTile( leading: CircleAvatar(backgroundColor: record.typeColor, child: Text(record.type?[0] ?? '', style: const TextStyle(color: Colors.white))), title: Text("${record.type} - ${record.formattedTime}"), subtitle: Text(record.formattedDate), trailing: const Icon(Icons.check_circle, color: Colors.green, size: 16), ); } }