import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/providers/auth_provider.dart'; import '../../../core/services/realtime_service.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/gradient_app_bar.dart'; import '../../../core/widgets/pill_tabs.dart'; import '../../../models/job.dart'; import 'lab_jobs_repository.dart'; enum _JobSort { newestFirst, oldestFirst, byDueDate, byType } const _kSortLabels = [ 'Yeniden Eskiye', 'Eskiden Yeniye', 'Vade Tarihine Göre', 'Türe Göre', ]; class LabAllJobsScreen extends ConsumerStatefulWidget { const LabAllJobsScreen({super.key}); @override ConsumerState createState() => _LabAllJobsScreenState(); } class _LabAllJobsScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final _searchController = TextEditingController(); String _searchQuery = ''; _JobSort _sort = _JobSort.newestFirst; bool _bulkAccepting = false; final Map _counts = { 'all': null, 'pending': null, 'in_progress': null, 'sent': null, 'delivered': null, }; final _pendingTabKey = GlobalKey<_PendingJobsTabState>(); // null entry = Tümü (bütün statüsler) static const List _statuses = [null, 'pending', 'in_progress', 'sent', 'delivered']; static const _tabLabels = ['Tümü', 'Onay Bekleyen', 'Devam Eden', 'Gönderildi', 'Teslim Edildi']; String _countKey(String? s) => s ?? 'all'; @override void initState() { super.initState(); final isDelivery = ref.read(authProvider).activeTenant?.isDeliveryOnly ?? false; _tabController = TabController(length: 5, vsync: this, initialIndex: isDelivery ? 3 : 0); _tabController.addListener(() { if (mounted) setState(() {}); }); _fetchAllCounts(); } Future _fetchAllCounts() async { final tenantId = ref.read(authProvider).activeTenant?.tenant.id; if (tenantId == null) return; final results = await Future.wait( _statuses.map((s) => LabJobsRepository.instance.countByStatus(tenantId, s)), ); if (!mounted) return; setState(() { for (var i = 0; i < _statuses.length; i++) { _counts[_countKey(_statuses[i])] = results[i]; } }); } @override void dispose() { _tabController.dispose(); _searchController.dispose(); super.dispose(); } void _onSearchChanged(String value) { setState(() => _searchQuery = value); } Future _showSortOptions() async { final result = await showSortSheet( context, title: 'Sıralama', options: _kSortLabels, current: _sort.index, ); if (result != null) { setState(() => _sort = _JobSort.values[result]); } } Future _bulkAccept() async { final tenantId = ref.read(authProvider).activeTenant!.tenant.id; setState(() => _bulkAccepting = true); try { await LabJobsRepository.instance.bulkAcceptPending(tenantId); _pendingTabKey.currentState?._load(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Tüm işler kabul edildi')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e')), ); } } finally { if (mounted) setState(() => _bulkAccepting = false); } } @override Widget build(BuildContext context) { final isSortActive = _sort != _JobSort.newestFirst; final onPendingTab = _tabController.index == 1; final pendingCount = _counts['pending']; return Scaffold( backgroundColor: AppColors.background, appBar: GradientAppBar( title: 'İşler', category: 'LABORATUVAR', searchController: _searchController, onSearchChanged: _onSearchChanged, searchHint: 'Protokol, klinik veya tür ara...', actions: [ if (!onPendingTab) IconButton( onPressed: _showSortOptions, tooltip: 'Sırala', icon: Badge( isLabelVisible: isSortActive, smallSize: 8, backgroundColor: AppColors.accent, child: const Icon(Icons.sort_rounded), ), ), ], ), floatingActionButton: onPendingTab && (pendingCount == null || pendingCount > 0) ? FloatingActionButton.extended( onPressed: _bulkAccepting ? null : _bulkAccept, icon: _bulkAccepting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.done_all), label: Text(_bulkAccepting ? 'Kabul ediliyor...' : 'Tümünü Kabul Et'), backgroundColor: AppColors.pending, foregroundColor: Colors.white, ) : null, body: Column( children: [ PillTabs( tabs: _tabLabels, selected: _tabController.index, onSelect: (i) => _tabController.animateTo(i), counts: _statuses.map((s) => _counts[_countKey(s)]).toList(), ), Expanded( child: TabBarView( controller: _tabController, children: [ _LabJobsTab( status: null, searchQuery: _searchQuery, sort: _sort, onCountLoaded: (c) => setState(() => _counts['all'] = c), ), _PendingJobsTab( key: _pendingTabKey, searchQuery: _searchQuery, onCountLoaded: (c) => setState(() => _counts['pending'] = c), ), _LabJobsTab( status: 'in_progress', searchQuery: _searchQuery, sort: _sort, onCountLoaded: (c) => setState(() => _counts['in_progress'] = c), ), _LabJobsTab( status: 'sent', searchQuery: _searchQuery, sort: _sort, onCountLoaded: (c) => setState(() => _counts['sent'] = c), ), _LabJobsTab( status: 'delivered', searchQuery: _searchQuery, sort: _sort, onCountLoaded: (c) => setState(() => _counts['delivered'] = c), ), ], ), ), ], ), ); } } // ── Pending (Onay Bekleyen) tab ─────────────────────────────────────────────── class _PendingJobsTab extends ConsumerStatefulWidget { const _PendingJobsTab({super.key, required this.searchQuery, this.onCountLoaded}); final String searchQuery; final void Function(int)? onCountLoaded; @override ConsumerState<_PendingJobsTab> createState() => _PendingJobsTabState(); } class _PendingJobsTabState extends ConsumerState<_PendingJobsTab> { late Future> _future; late UnsubFn _unsub; @override void initState() { super.initState(); _load(); final tenantId = ref.read(authProvider).activeTenant!.tenant.id; _unsub = RealtimeService.instance.watch( 'jobs', filter: 'lab_tenant_id="$tenantId" && status="pending"', onEvent: (_) { if (mounted) _load(); }, ); } @override void dispose() { _unsub(); super.dispose(); } void _load() { final tenantId = ref.read(authProvider).activeTenant!.tenant.id; setState(() { _future = LabJobsRepository.instance.listInbound(tenantId, status: 'pending', limit: 50); }); } Future _acceptJob(Job job) async { try { await LabJobsRepository.instance.acceptJob(job); _load(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('İş kabul edildi')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e')), ); } } } List _filtered(List jobs) { final q = widget.searchQuery.toLowerCase().trim(); if (q.isEmpty) return jobs; return jobs.where((j) => j.patientCode.toLowerCase().contains(q) || (j.clinicName?.toLowerCase().contains(q) ?? false) || j.prostheticType.label.toLowerCase().contains(q) ).toList(); } @override Widget build(BuildContext context) { return 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 all = snap.data!; WidgetsBinding.instance.addPostFrameCallback((_) { widget.onCountLoaded?.call(all.length); }); final jobs = _filtered(all); if (jobs.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 72, height: 72, decoration: BoxDecoration( color: AppColors.successBg, borderRadius: BorderRadius.circular(20), ), child: const Icon(Icons.inbox_outlined, color: AppColors.success, size: 32), ), const SizedBox(height: 16), Text( widget.searchQuery.isNotEmpty ? 'Sonuç bulunamadı' : 'Onay bekleyen iş yok', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary), ), if (widget.searchQuery.isEmpty) ...[ const SizedBox(height: 6), const Text('Tüm işler kabul edildi', style: TextStyle(color: AppColors.textSecondary, fontSize: 13)), ], ], ), ); } return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 12, 16, 100), itemCount: jobs.length, itemBuilder: (ctx, i) { final job = jobs[i]; return Padding( padding: const EdgeInsets.only(bottom: 10), child: _PendingJobCard( job: job, onAccept: () => _acceptJob(job), ), ); }, ); }, ), ); } } class _PendingJobCard extends StatefulWidget { const _PendingJobCard({required this.job, required this.onAccept}); final Job job; final VoidCallback onAccept; @override State<_PendingJobCard> createState() => _PendingJobCardState(); } class _PendingJobCardState extends State<_PendingJobCard> { bool _accepting = false; @override Widget build(BuildContext context) { final job = widget.job; return Dismissible( key: ValueKey(job.id), direction: DismissDirection.endToStart, background: Container( alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 20), decoration: BoxDecoration( color: AppColors.success, borderRadius: BorderRadius.circular(14), ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_rounded, color: Colors.white, size: 28), SizedBox(height: 4), Text('Kabul Et', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600)), ], ), ), confirmDismiss: (_) async { setState(() => _accepting = true); try { await LabJobsRepository.instance.acceptJob(job); return true; } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e')), ); } return false; } finally { if (mounted) setState(() => _accepting = false); } }, child: Material( color: AppColors.surface, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: () => context.push('/lab/jobs/${job.id}'), borderRadius: BorderRadius.circular(14), child: Container( 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: IntrinsicHeight( child: Row( children: [ Container( width: 4, decoration: const BoxDecoration( color: AppColors.pending, borderRadius: BorderRadius.only( topLeft: Radius.circular(14), bottomLeft: Radius.circular(14), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( job.patientCode, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.textPrimary), ), const SizedBox(height: 4), Row( children: [ const Icon(Icons.local_hospital_outlined, size: 12, color: AppColors.textMuted), const SizedBox(width: 4), Expanded( child: Text( job.clinicName ?? 'Klinik', style: const TextStyle(fontSize: 12, color: AppColors.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 6), Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), decoration: BoxDecoration( color: AppColors.pendingBg, borderRadius: BorderRadius.circular(6), ), child: Text( job.prostheticType.label, style: const TextStyle(fontSize: 11, color: AppColors.pending, fontWeight: FontWeight.w600), ), ), if (job.dueDate != null) ...[ const SizedBox(width: 6), const Icon(Icons.calendar_today_outlined, size: 11, color: AppColors.textMuted), const SizedBox(width: 3), Text( '${job.dueDate!.day.toString().padLeft(2, '0')}.${job.dueDate!.month.toString().padLeft(2, '0')}.${job.dueDate!.year}', style: const TextStyle(fontSize: 11, color: AppColors.textMuted), ), ], ], ), ], ), ), const SizedBox(width: 8), _accepting ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: AppColors.success), ) : Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), decoration: BoxDecoration( color: AppColors.successBg, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.success.withValues(alpha: 0.3)), ), child: const Text( 'Kabul Et', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.success), ), ), ], ), ), ), ], ), ), ), ), ), ); } } class _LabJobsTab extends ConsumerStatefulWidget { const _LabJobsTab({ required this.status, required this.searchQuery, required this.sort, this.onCountLoaded, }); final String? status; // null = tüm statüsler final String searchQuery; final _JobSort sort; final void Function(int)? onCountLoaded; @override ConsumerState<_LabJobsTab> createState() => _LabJobsTabState(); } class _LabJobsTabState extends ConsumerState<_LabJobsTab> { late Future> _future; late UnsubFn _unsub; @override void initState() { super.initState(); _load(); final tenantId = ref.read(authProvider).activeTenant!.tenant.id; _unsub = RealtimeService.instance.watch( 'jobs', filter: 'lab_tenant_id="$tenantId"', onEvent: (_) { if (mounted) _load(); }, ); } @override void dispose() { _unsub(); super.dispose(); } void _load() { final tenantId = ref.read(authProvider).activeTenant!.tenant.id; setState(() { _future = LabJobsRepository.instance .listInbound(tenantId, status: widget.status, limit: 50); }); } List _applyFilters(List jobs) { var list = jobs; final q = widget.searchQuery.toLowerCase().trim(); if (q.isNotEmpty) { list = list.where((j) { return j.patientCode.toLowerCase().contains(q) || (j.clinicName?.toLowerCase().contains(q) ?? false) || j.prostheticType.label.toLowerCase().contains(q) || (j.currentStep?.label.toLowerCase().contains(q) ?? false); }).toList(); } final sorted = List.from(list); switch (widget.sort) { case _JobSort.newestFirst: sorted.sort((a, b) => b.dateCreated.compareTo(a.dateCreated)); case _JobSort.oldestFirst: sorted.sort((a, b) => a.dateCreated.compareTo(b.dateCreated)); case _JobSort.byDueDate: sorted.sort((a, b) { if (a.dueDate == null && b.dueDate == null) return 0; if (a.dueDate == null) return 1; if (b.dueDate == null) return -1; return a.dueDate!.compareTo(b.dueDate!); }); case _JobSort.byType: sorted.sort( (a, b) => a.prostheticType.label.compareTo(b.prostheticType.label)); } return sorted; } @override Widget build(BuildContext context) { return 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 all = snap.data!; WidgetsBinding.instance.addPostFrameCallback((_) { widget.onCountLoaded?.call(all.length); }); final jobs = _applyFilters(all); if (jobs.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.work_off_outlined, color: AppColors.inProgress, size: 32), ), const SizedBox(height: 16), Text( widget.searchQuery.isNotEmpty ? 'Sonuç bulunamadı' : 'Henüz iş yok', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), itemCount: jobs.length, itemBuilder: (ctx, i) { final job = jobs[i]; return Padding( padding: const EdgeInsets.only(bottom: 10), child: _LabJobCard( job: job, onTap: () => context.push('/lab/jobs/${job.id}'), ), ); }, ); }, ), ); } } class _LabJobCard extends StatelessWidget { const _LabJobCard({required this.job, required this.onTap}); final Job job; final VoidCallback onTap; @override Widget build(BuildContext context) { final isOverdue = job.dueDate != null && job.dueDate!.isBefore(DateTime.now()); final accentColor = _statusColor(job.status); return Semantics( label: job.patientCode, button: true, excludeSemantics: true, child: Material( color: AppColors.surface, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(14), child: Container( 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: IntrinsicHeight( child: Row( children: [ Container( width: 4, decoration: BoxDecoration( color: accentColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(14), bottomLeft: Radius.circular(14), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( job.patientCode, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), ), if (job.currentStep != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.inProgressBg, borderRadius: BorderRadius.circular(8), ), child: Text( job.currentStep!.label, style: const TextStyle( color: AppColors.inProgress, fontSize: 11, fontWeight: FontWeight.w600, ), ), ), ], ), const SizedBox(height: 5), Row( children: [ const Icon(Icons.local_hospital_outlined, size: 12, color: AppColors.textMuted), const SizedBox(width: 4), Expanded( child: Text( job.clinicName ?? 'Klinik', style: const TextStyle( fontSize: 12, color: AppColors.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 5), Row( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.surfaceVariant, borderRadius: BorderRadius.circular(6), ), child: Text( job.prostheticType.label, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary, fontWeight: FontWeight.w500, ), ), ), if (job.dueDate != null) ...[ const SizedBox(width: 8), Icon(Icons.calendar_today_outlined, size: 11, color: isOverdue ? AppColors.cancelled : AppColors.textMuted), const SizedBox(width: 3), Text( _fmt(job.dueDate!), style: TextStyle( fontSize: 11, color: isOverdue ? AppColors.cancelled : AppColors.textMuted, fontWeight: isOverdue ? FontWeight.w600 : FontWeight.normal, ), ), ], ], ), ], ), ), ), const Padding( padding: EdgeInsets.only(right: 10), child: Icon(Icons.chevron_right, color: AppColors.textMuted, size: 20), ), ], ), ), ), ), ), ); } String _fmt(DateTime d) => '${d.day.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}.${d.year}'; Color _statusColor(JobStatus status) { switch (status) { case JobStatus.pending: return AppColors.pending; case JobStatus.inProgress: return AppColors.inProgress; case JobStatus.sent: return AppColors.accent; case JobStatus.delivered: return AppColors.success; case JobStatus.cancelled: return AppColors.cancelled; } } }