Files
lab-app/lib/features/clinic/connections/clinic_connections_screen.dart
T
2026-06-10 23:22:15 +03:00

442 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/providers/auth_provider.dart';
import '../../../core/theme/app_theme.dart';
import '../../../models/connection.dart';
import 'clinic_connections_repository.dart';
class ClinicConnectionsScreen extends ConsumerStatefulWidget {
const ClinicConnectionsScreen({super.key});
@override
ConsumerState<ClinicConnectionsScreen> createState() =>
_ClinicConnectionsScreenState();
}
class _ClinicConnectionsScreenState
extends ConsumerState<ClinicConnectionsScreen> {
late Future<List<Connection>> _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() {
showDialog(
context: context,
builder: (ctx) => _LabSearchDialog(
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<List<Connection>>(
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.onRequested});
final void Function(String labId, String labName) onRequested;
@override
State<_LabSearchDialog> createState() => _LabSearchDialogState();
}
class _LabSearchDialogState extends State<_LabSearchDialog> {
final _searchController = TextEditingController();
List<Map<String, dynamic>> _results = [];
bool _isLoading = false;
bool _searched = false;
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _search() async {
final query = _searchController.text.trim();
if (query.isEmpty) return;
setState(() {
_isLoading = true;
_searched = true;
});
try {
final results =
await ClinicConnectionsRepository.instance.searchLabs(query);
setState(() {
_results = results;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Laboratuvar Bul'),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: 'Lab adı ile arayın...',
contentPadding: EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
onSubmitted: (_) => _search(),
),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _search,
child: const Text('Ara'),
),
],
),
const SizedBox(height: 12),
if (_isLoading)
const Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(color: AppColors.accent),
)
else if (_searched && _results.isEmpty)
const Padding(
padding: EdgeInsets.all(16),
child: Text('Sonuç bulunamadı',
style: TextStyle(color: AppColors.textSecondary)),
)
else if (_results.isNotEmpty)
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 240),
child: ListView.builder(
shrinkWrap: true,
itemCount: _results.length,
itemBuilder: (context, index) {
final lab = _results[index];
final name =
lab['company_name'] as String? ?? 'Lab';
return ListTile(
dense: true,
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.inProgressBg,
borderRadius: BorderRadius.circular(8)),
child: const Icon(Icons.science_outlined,
color: AppColors.inProgress, size: 18),
),
title: Text(name,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.textPrimary)),
subtitle: lab['member_number'] != null
? Text('No: ${lab['member_number']}',
style: const TextStyle(
color: AppColors.textSecondary))
: null,
onTap: () =>
widget.onRequested(lab['id'] as String, name),
);
},
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('İptal'),
),
],
);
}
}