import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart' as ll; import 'package:maplibre_gl/maplibre_gl.dart'; import '../../../core/location/location_access_service.dart'; import '../../../core/maps/open_free_map.dart'; import '../../../core/providers/auth_provider.dart'; import '../../../core/theme/app_theme.dart'; import '../../../models/connection.dart'; import '../../../models/tenant.dart'; import 'clinic_connections_repository.dart'; class ClinicConnectionsScreen extends ConsumerStatefulWidget { const ClinicConnectionsScreen({super.key}); @override ConsumerState createState() => _ClinicConnectionsScreenState(); } class _ClinicConnectionsScreenState extends ConsumerState { late Future> _future; @override void initState() { super.initState(); _load(); } void _load() { final tenantId = ref.read(authProvider).activeTenant!.tenant.id; setState(() { _future = ClinicConnectionsRepository.instance.listConnections(tenantId); }); } void _showSearchDialog() { final clinicTenant = ref.read(authProvider).activeTenant!.tenant; showDialog( context: context, builder: (ctx) => _LabSearchDialog( clinicTenant: clinicTenant, onRequested: (labId, labName) async { Navigator.of(ctx).pop(); final tenantId = ref.read(authProvider).activeTenant!.tenant.id; try { await ClinicConnectionsRepository.instance.requestConnection( clinicTenantId: tenantId, labTenantId: labId, ); _load(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$labName\'a bağlantı talebi gönderildi.'), ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e')), ); } } }, ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bağlantılar'), actions: [ IconButton( icon: const Icon(Icons.add_link), tooltip: 'Laboratuvar Bul', onPressed: _showSearchDialog, ), ], ), body: RefreshIndicator( color: AppColors.accent, onRefresh: () async => _load(), child: FutureBuilder>( future: _future, builder: (ctx, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator(color: AppColors.accent), ); } if (snap.hasError) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: AppColors.cancelledBg, borderRadius: BorderRadius.circular(16), ), child: const Icon( Icons.wifi_off_rounded, color: AppColors.cancelled, size: 30, ), ), const SizedBox(height: 16), Text( 'Hata: ${snap.error}', style: const TextStyle(color: AppColors.textSecondary), ), const SizedBox(height: 12), FilledButton.icon( onPressed: _load, icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('Tekrar Dene'), ), ], ), ); } final connections = snap.data!; if (connections.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 72, height: 72, decoration: BoxDecoration( color: AppColors.inProgressBg, borderRadius: BorderRadius.circular(20), ), child: const Icon( Icons.link_off, color: AppColors.inProgress, size: 32, ), ), const SizedBox(height: 16), const Text( 'Henüz bağlantı yok', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), FilledButton.icon( onPressed: _showSearchDialog, icon: const Icon(Icons.search), label: const Text('Laboratuvar Bul'), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), itemCount: connections.length, itemBuilder: (context, index) { final conn = connections[index]; final statusColor = _statusColor(conn.status); final statusBg = _statusBg(conn.status); return Padding( padding: const EdgeInsets.only(bottom: 10), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: statusBg, borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.science_outlined, color: statusColor, size: 22, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( conn.labName ?? 'Laboratuvar', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), if (conn.dateCreated != null) ...[ const SizedBox(height: 2), Text( _formatDate(conn.dateCreated!), style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ], ), ), _StatusChip(status: conn.status), ], ), ), ); }, ); }, ), ), floatingActionButton: FloatingActionButton.extended( onPressed: _showSearchDialog, backgroundColor: AppColors.accent, foregroundColor: Colors.white, icon: const Icon(Icons.search), label: const Text('Laboratuvar Bul'), ), ); } Color _statusColor(ConnectionStatus s) { switch (s) { case ConnectionStatus.pending: return AppColors.pending; case ConnectionStatus.approved: return AppColors.success; case ConnectionStatus.rejected: return AppColors.cancelled; } } Color _statusBg(ConnectionStatus s) { switch (s) { case ConnectionStatus.pending: return AppColors.pendingBg; case ConnectionStatus.approved: return AppColors.successBg; case ConnectionStatus.rejected: return AppColors.cancelledBg; } } String _formatDate(String dateStr) { try { final d = DateTime.parse(dateStr); return '${d.day.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}.${d.year}'; } catch (_) { return dateStr; } } } class _StatusChip extends StatelessWidget { const _StatusChip({required this.status}); final ConnectionStatus status; @override Widget build(BuildContext context) { final color = _color(status); final bg = _bg(status); return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(8), ), child: Text( status.label, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.w600, ), ), ); } Color _color(ConnectionStatus s) { switch (s) { case ConnectionStatus.pending: return AppColors.pending; case ConnectionStatus.approved: return AppColors.success; case ConnectionStatus.rejected: return AppColors.cancelled; } } Color _bg(ConnectionStatus s) { switch (s) { case ConnectionStatus.pending: return AppColors.pendingBg; case ConnectionStatus.approved: return AppColors.successBg; case ConnectionStatus.rejected: return AppColors.cancelledBg; } } } class _LabSearchDialog extends StatefulWidget { const _LabSearchDialog({ required this.clinicTenant, required this.onRequested, }); final Tenant clinicTenant; final void Function(String labId, String labName) onRequested; @override State<_LabSearchDialog> createState() => _LabSearchDialogState(); } class _LabSearchDialogState extends State<_LabSearchDialog> { static const _fallbackCenter = LatLng(41.0082, 28.9784); static const _defaultZoom = 10.5; final _distance = const ll.Distance(); final _searchController = TextEditingController(); Timer? _searchDebounce; List<_LabSearchItem> _results = []; bool _isLoading = false; bool _searched = false; String? _error; String? _selectedLabId; LatLng? _devicePoint; bool _resolvingDeviceLocation = false; MapLibreMapController? _mapController; bool _styleReady = false; LatLng get _clinicPoint => LatLng( widget.clinicTenant.latitude ?? _fallbackCenter.latitude, widget.clinicTenant.longitude ?? _fallbackCenter.longitude, ); bool get _hasClinicLocation => widget.clinicTenant.hasLocation; LatLng? get _searchAnchorPoint => _devicePoint ?? (_hasClinicLocation ? _clinicPoint : null); _LabSearchItem? get _selectedLab { for (final item in _results) { if (item.tenant.id == _selectedLabId) return item; } return _results.isNotEmpty ? _results.first : null; } List<_LabSearchItem> get _mappedLabs => _results.where((item) => item.tenant.hasLocation).toList(); List<_LabSearchItem> get _legacyLabs => _results.where((item) => !item.tenant.hasLocation).toList(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _search()); } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); super.dispose(); } Future _search() async { final query = _searchController.text.trim(); setState(() { _isLoading = true; _searched = true; _error = null; }); try { final results = await ClinicConnectionsRepository.instance.searchLabs( query: query, city: widget.clinicTenant.city, ); final mapped = results .map( (lab) => _LabSearchItem( tenant: lab, distanceKm: _distanceFor(lab), ), ) .toList() ..sort((a, b) { final aDistance = a.distanceKm ?? double.infinity; final bDistance = b.distanceKm ?? double.infinity; final compareDistance = aDistance.compareTo(bDistance); if (compareDistance != 0) return compareDistance; return a.tenant.companyName.compareTo(b.tenant.companyName); }); setState(() { _results = mapped; _selectedLabId = mapped.isNotEmpty ? mapped.first.tenant.id : null; _isLoading = false; }); await _refreshMarkers(); await _moveMapToResults(); } catch (e) { setState(() { _isLoading = false; _error = e.toString(); }); } } double? _distanceFor(Tenant lab) { final anchor = _searchAnchorPoint; if (anchor == null || !lab.hasLocation) return null; final meters = _distance( ll.LatLng(anchor.latitude, anchor.longitude), ll.LatLng(lab.latitude!, lab.longitude!), ); return meters / 1000; } Future _moveMapToResults() async { final controller = _mapController; if (controller == null) return; final selected = _selectedLab; if (selected?.tenant.hasLocation == true) { await controller.animateCamera( CameraUpdate.newLatLngZoom( LatLng(selected!.tenant.latitude!, selected.tenant.longitude!), 12.8, ), ); return; } final fallbackPoint = _searchAnchorPoint ?? _clinicPoint; await controller.animateCamera( CameraUpdate.newLatLngZoom(fallbackPoint, _defaultZoom), ); } Future _refreshMarkers() async { final controller = _mapController; if (controller == null || !_styleReady) return; await controller.clearCircles(); final circles = [ if (_hasClinicLocation) CircleOptions( geometry: _clinicPoint, circleRadius: 7, circleColor: '#111827', circleStrokeWidth: 2, circleStrokeColor: '#FFFFFF', ), ..._mappedLabs.map( (item) => CircleOptions( geometry: LatLng( item.tenant.latitude!, item.tenant.longitude!, ), circleRadius: item.tenant.id == _selectedLabId ? 8 : 6, circleColor: item.tenant.id == _selectedLabId ? '#4F46E5' : '#0F766E', circleStrokeWidth: 2, circleStrokeColor: '#FFFFFF', ), ), ]; if (circles.isNotEmpty) { await controller.addCircles(circles); } } void _queueSearch(String _) { _searchDebounce?.cancel(); _searchDebounce = Timer(const Duration(milliseconds: 350), _search); } Future _useDeviceLocationForSearch() async { setState(() { _resolvingDeviceLocation = true; _error = null; }); try { final position = await LocationAccessService.getCurrentPosition(); _devicePoint = LatLng(position.latitude, position.longitude); await _search(); } catch (e) { setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _resolvingDeviceLocation = false); } } void _selectLab(_LabSearchItem item) { setState(() => _selectedLabId = item.tenant.id); unawaited(_refreshMarkers()); unawaited(_moveMapToResults()); } @override Widget build(BuildContext context) { final selectedLab = _selectedLab; return Dialog( insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), backgroundColor: Colors.transparent, child: Container( height: MediaQuery.sizeOf(context).height * 0.88, decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(24), ), child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 12), child: Row( children: [ const Expanded( child: Text( 'Laboratuvar Bul', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close_rounded), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( children: [ TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Lab adı, şehir veya ilçe ile arayın...', prefixIcon: const Icon(Icons.search_rounded), suffixIcon: _isLoading ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.accent, ), ), ) : (_searchController.text.isNotEmpty ? IconButton( onPressed: () { _searchController.clear(); _queueSearch(''); setState(() {}); }, icon: const Icon(Icons.close_rounded), ) : null), ), onChanged: (value) { setState(() {}); _queueSearch(value); }, ), const SizedBox(height: 10), Row( children: [ Icon( _searchAnchorPoint != null ? Icons.near_me_rounded : Icons.info_outline_rounded, size: 16, color: AppColors.textSecondary, ), const SizedBox(width: 8), Expanded( child: Text( _devicePoint != null ? 'Sonuçlar aktif konumunuza göre yakın olandan sıralanır.' : (_hasClinicLocation ? 'Sonuçlar kliniğin konumuna göre yakın olandan sıralanır.' : 'Aktif konumunuzu kullanarak yakın arama yapabilir veya isimle arayabilirsiniz.'), style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ), ], ), const SizedBox(height: 10), Row( children: [ OutlinedButton.icon( onPressed: _resolvingDeviceLocation ? null : _useDeviceLocationForSearch, icon: _resolvingDeviceLocation ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.my_location_rounded, size: 18), label: Text( _devicePoint == null ? 'Aktif Konumumla Ara' : 'Aktif Konumu Yenile', ), ), if (_devicePoint != null) ...[ const SizedBox(width: 8), TextButton( onPressed: () { setState(() => _devicePoint = null); unawaited(_search()); }, child: const Text('Klinik Konumuna Dön'), ), ], ], ), ], ), ), const SizedBox(height: 14), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Stack( children: [ MapLibreMap( styleString: OpenFreeMap.libertyStyle, initialCameraPosition: CameraPosition( target: _clinicPoint, zoom: _defaultZoom, ), onMapCreated: (controller) { _mapController = controller; }, onStyleLoadedCallback: () async { _styleReady = true; await _refreshMarkers(); }, compassEnabled: false, tiltGesturesEnabled: false, rotateGesturesEnabled: false, myLocationEnabled: false, myLocationTrackingMode: MyLocationTrackingMode.none, ), if (selectedLab != null) Positioned( left: 12, right: 12, bottom: 12, child: _SelectedLabCard( item: selectedLab, onRequest: () => widget.onRequested( selectedLab.tenant.id, selectedLab.tenant.companyName, ), ), ), ], ), ), ), ), const SizedBox(height: 14), Expanded( child: Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), child: _buildResultsList(), ), ), ], ), ), ); } Widget _buildResultsList() { if (_error != null) { return Center( child: Text( 'Hata: $_error', style: const TextStyle(color: AppColors.cancelled), textAlign: TextAlign.center, ), ); } if (_isLoading && !_searched) { return const Center( child: CircularProgressIndicator(color: AppColors.accent), ); } if (_searched && _results.isEmpty) { return const Center( child: Text( 'Bu kriterlerde laboratuvar bulunamadı.', style: TextStyle(color: AppColors.textSecondary), textAlign: TextAlign.center, ), ); } final children = [ if (_mappedLabs.isNotEmpty) ...[ const _ResultsSectionHeader( title: 'Haritadaki Laboratuvarlar', subtitle: 'Konumu tanımlı işletmeler', ), const SizedBox(height: 8), for (final item in _mappedLabs) Padding( padding: const EdgeInsets.only(bottom: 10), child: _LabResultTile( item: item, isSelected: item.tenant.id == _selectedLabId, badgeText: item.distanceKm != null ? '${item.distanceKm!.toStringAsFixed(1)} km' : 'Haritada', onTap: () => _selectLab(item), onRequest: () => widget.onRequested( item.tenant.id, item.tenant.companyName, ), ), ), ], if (_legacyLabs.isNotEmpty) ...[ if (_mappedLabs.isNotEmpty) const SizedBox(height: 8), const _ResultsSectionHeader( title: 'İsimle Bulunan İşletmeler', subtitle: 'Eski kayıtlar için konum zorunlu değil', ), const SizedBox(height: 8), for (final item in _legacyLabs) Padding( padding: const EdgeInsets.only(bottom: 10), child: _LabResultTile( item: item, isSelected: item.tenant.id == _selectedLabId, badgeText: 'Konum bekleniyor', onTap: () => _selectLab(item), onRequest: () => widget.onRequested( item.tenant.id, item.tenant.companyName, ), ), ), ], ]; return ListView(children: children); } } class _LabSearchItem { const _LabSearchItem({ required this.tenant, required this.distanceKm, }); final Tenant tenant; final double? distanceKm; } class _SelectedLabCard extends StatelessWidget { const _SelectedLabCard({ required this.item, required this.onRequest, }); final _LabSearchItem item; final VoidCallback onRequest; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.12), blurRadius: 16, offset: const Offset(0, 8), ), ], ), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: AppColors.inProgressBg, borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.science_outlined, color: AppColors.accent, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.tenant.companyName, style: const TextStyle( fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 4), Text( item.tenant.locationLabel.isNotEmpty ? item.tenant.locationLabel : 'Adres bilgisi henüz girilmemiş', style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ), const SizedBox(width: 12), FilledButton( onPressed: onRequest, child: const Text('Bağlan'), ), ], ), ); } } class _ResultsSectionHeader extends StatelessWidget { const _ResultsSectionHeader({ required this.title, required this.subtitle, }); final String title; final String subtitle; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 2), Text( subtitle, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary, ), ), ], ); } } class _LabResultTile extends StatelessWidget { const _LabResultTile({ required this.item, required this.isSelected, required this.badgeText, required this.onTap, required this.onRequest, }); final _LabSearchItem item; final bool isSelected; final String badgeText; final VoidCallback onTap; final VoidCallback onRequest; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(16), onTap: onTap, child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: isSelected ? AppColors.inProgressBg : AppColors.background, borderRadius: BorderRadius.circular(16), border: Border.all( color: isSelected ? AppColors.accent : AppColors.border, ), ), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), ), child: Icon( item.tenant.hasLocation ? Icons.location_searching_rounded : Icons.manage_search_rounded, color: AppColors.accent, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.tenant.companyName, style: const TextStyle( fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), if (item.tenant.locationLabel.isNotEmpty) ...[ const SizedBox(height: 4), Text( item.tenant.locationLabel, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], if (item.tenant.memberNumber.trim().isNotEmpty) ...[ const SizedBox(height: 4), Text( 'Üye No: ${item.tenant.memberNumber}', style: const TextStyle( fontSize: 12, color: AppColors.textMuted, ), ), ], ], ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( badgeText, style: const TextStyle( fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), OutlinedButton( onPressed: onRequest, child: const Text('Bağlan'), ), ], ), ], ), ), ); } }