Files
lab-app/lib/features/lab/connections/lab_connections_screen.dart
T
Emre Emir 8bbc9dbff2 Initial commit: DLS - Dental Lab System
- Flutter + PocketBase dental lab management system
- Clinic & lab dashboards, job tracking, patient management
- Product catalog, finance tracking, multi-language support
- AI assistant integration, realtime notifications
- Windows installer (Inno Setup) included
- Developed by kovakyazilim.com
2026-06-11 15:57:31 +03:00

454 lines
15 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 '../../../core/widgets/gradient_app_bar.dart';
import '../../../models/connection.dart';
import 'connection_detail_screen.dart';
import 'lab_connections_repository.dart';
class LabConnectionsScreen extends ConsumerStatefulWidget {
const LabConnectionsScreen({super.key});
@override
ConsumerState<LabConnectionsScreen> createState() =>
_LabConnectionsScreenState();
}
class _LabConnectionsScreenState extends ConsumerState<LabConnectionsScreen> {
late Future<List<Connection>> _future;
final _searchController = TextEditingController();
String _searchQuery = '';
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _load() {
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
setState(() {
_future = LabConnectionsRepository.instance.listConnections(tenantId);
});
}
Future<void> _respond(String connectionId, bool approve) async {
try {
await LabConnectionsRepository.instance.respondToRequest(
connectionId: connectionId,
approve: approve,
);
_load();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
approve ? 'Bağlantı onaylandı' : 'Bağlantı reddedildi'),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hata: $e')),
);
}
}
}
Color _statusColor(ConnectionStatus status) {
return switch (status) {
ConnectionStatus.pending => AppColors.pending,
ConnectionStatus.approved => AppColors.success,
ConnectionStatus.rejected => AppColors.cancelled,
};
}
Color _statusBg(ConnectionStatus status) {
return switch (status) {
ConnectionStatus.pending => AppColors.pendingBg,
ConnectionStatus.approved => AppColors.successBg,
ConnectionStatus.rejected => AppColors.cancelledBg,
};
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: GradientAppBar(
title: 'Bağlantılar',
category: 'LABORATUVAR',
searchController: _searchController,
onSearchChanged: (v) => setState(() => _searchQuery = v),
searchHint: 'Klinik adı ara...',
),
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 allConnections = snap.data!;
final q = _searchQuery.toLowerCase().trim();
final connections = q.isEmpty
? allConnections
: allConnections.where((c) =>
(c.clinicName ?? '').toLowerCase().contains(q)).toList();
if (allConnections.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),
const Text(
'Kliniklerden gelen istekler burada görünür.',
style: TextStyle(
color: AppColors.textSecondary, fontSize: 13),
textAlign: TextAlign.center,
),
],
),
);
}
final pending = connections
.where((c) => c.status == ConnectionStatus.pending)
.toList();
final others = connections
.where((c) => c.status != ConnectionStatus.pending)
.toList();
return ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
children: [
if (pending.isNotEmpty) ...[
_SectionHeader(
label: 'Bekleyen İstekler',
count: pending.length,
countColor: AppColors.pending,
countBg: AppColors.pendingBg,
),
const SizedBox(height: 8),
...pending.map((c) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _ConnectionCard(
connection: c,
statusColor: _statusColor(c.status),
statusBg: _statusBg(c.status),
onApprove: () => _respond(c.id, true),
onReject: () => _respond(c.id, false),
),
)),
const SizedBox(height: 8),
],
if (others.isNotEmpty) ...[
_SectionHeader(
label: 'Bağlantılar',
count: others.length,
countColor: AppColors.textSecondary,
countBg: AppColors.surfaceVariant,
),
const SizedBox(height: 8),
...others.map((c) {
final tenantId = ref.read(authProvider).activeTenant!.tenant.id;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _ConnectionCard(
connection: c,
statusColor: _statusColor(c.status),
statusBg: _statusBg(c.status),
onTap: c.status == ConnectionStatus.approved
? () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ConnectionDetailScreen(
connection: c,
labTenantId: tenantId,
),
),
)
: null,
),
);
}),
],
],
);
},
),
),
);
}
}
class _SectionHeader extends StatelessWidget {
const _SectionHeader({
required this.label,
required this.count,
required this.countColor,
required this.countBg,
});
final String label;
final int count;
final Color countColor;
final Color countBg;
@override
Widget build(BuildContext context) {
return Row(
children: [
Text(
label,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.accent,
letterSpacing: 0.3,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: countBg,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$count',
style: TextStyle(
color: countColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
}
class _ConnectionCard extends StatefulWidget {
const _ConnectionCard({
required this.connection,
required this.statusColor,
required this.statusBg,
this.onApprove,
this.onReject,
this.onTap,
});
final Connection connection;
final Color statusColor;
final Color statusBg;
final VoidCallback? onApprove;
final VoidCallback? onReject;
final VoidCallback? onTap;
@override
State<_ConnectionCard> createState() => _ConnectionCardState();
}
class _ConnectionCardState extends State<_ConnectionCard> {
bool _loading = false;
Future<void> _act(VoidCallback? cb) async {
if (cb == null) return;
setState(() => _loading = true);
cb();
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) setState(() => _loading = false);
}
String _formatDate(String? raw) {
if (raw == null) return '';
try {
final dt = DateTime.parse(raw);
return '${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year}';
} catch (_) {
return '';
}
}
@override
Widget build(BuildContext context) {
final c = widget.connection;
final isPending = c.status == ConnectionStatus.pending;
return Material(
color: AppColors.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: widget.statusBg,
borderRadius: BorderRadius.circular(12)),
child: Icon(Icons.business_outlined,
color: widget.statusColor, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
c.clinicName ?? 'Klinik',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary),
),
if (c.dateCreated != null) ...[
const SizedBox(height: 2),
Text(
_formatDate(c.dateCreated),
style: const TextStyle(
color: AppColors.textMuted, fontSize: 12),
),
],
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: widget.statusBg,
borderRadius: BorderRadius.circular(8),
),
child: Text(
c.status.label,
style: TextStyle(
color: widget.statusColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
if (widget.onTap != null) ...[
const SizedBox(width: 4),
const Icon(Icons.chevron_right_rounded,
size: 18, color: AppColors.textMuted),
],
],
),
if (isPending) ...[
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed:
_loading ? null : () => _act(widget.onReject),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.cancelled,
side: const BorderSide(color: AppColors.cancelled)),
child: const Text('Reddet'),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton(
onPressed:
_loading ? null : () => _act(widget.onApprove),
style: FilledButton.styleFrom(
backgroundColor: AppColors.success),
child: _loading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white),
)
: const Text('Onayla'),
),
),
],
),
],
],
),
),
),
);
}
}