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/router/app_router.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 'clinic_jobs_repository.dart'; enum _JobSort { newestFirst, oldestFirst, byDueDate, byType } const _kSortLabels = [ 'Yeniden Eskiye', 'Eskiden Yeniye', 'Vade Tarihine Göre', 'Türe Göre', ]; class ClinicJobsScreen extends ConsumerStatefulWidget { const ClinicJobsScreen({super.key}); @override ConsumerState createState() => _ClinicJobsScreenState(); } class _ClinicJobsScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final _searchController = TextEditingController(); String _searchQuery = ''; _JobSort _sort = _JobSort.newestFirst; @override void initState() { super.initState(); _tabController = TabController(length: 5, vsync: this); _tabController.addListener(() { if (mounted) setState(() {}); }); } @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]); } } @override Widget build(BuildContext context) { final isSortActive = _sort != _JobSort.newestFirst; return Scaffold( backgroundColor: AppColors.background, appBar: GradientAppBar( title: 'İşlerim', category: 'KLİNİK', searchController: _searchController, onSearchChanged: _onSearchChanged, searchHint: 'Protokol, laboratuvar veya tür ara...', actions: [ IconButton( onPressed: _showSortOptions, tooltip: 'Sırala', icon: Badge( isLabelVisible: isSortActive, smallSize: 8, backgroundColor: AppColors.accent, child: const Icon(Icons.sort_rounded), ), ), if (ref.watch(authProvider).activeTenant?.canCreateJobs ?? true) IconButton( onPressed: () => context.push(routeClinicJobNew), tooltip: 'Yeni İş', icon: const Icon(Icons.add_rounded), ), ], ), body: Column( children: [ PillTabs( tabs: const ['Tümü', 'Onay Bekleyen', 'Lab\'da', 'Teslimat', 'Teslim Alındı'], selected: _tabController.index, onSelect: (i) => _tabController.animateTo(i), ), Expanded( child: TabBarView( controller: _tabController, children: [ _JobsTab( statuses: const ['pending', 'in_progress', 'sent', 'delivered'], searchQuery: _searchQuery, sort: _sort, ), _JobsTab( statuses: const ['in_progress'], location: 'at_clinic', searchQuery: _searchQuery, sort: _sort, ), _JobsTab( filterExtra: 'status = "pending" || (status = "in_progress" && location = "at_lab")', searchQuery: _searchQuery, sort: _sort, ), _JobsTab( statuses: const ['sent'], searchQuery: _searchQuery, sort: _sort, ), _JobsTab( statuses: const ['delivered'], searchQuery: _searchQuery, sort: _sort, ), ], ), ), ], ), ); } } class _JobsTab extends ConsumerStatefulWidget { const _JobsTab({ this.statuses, this.location, this.filterExtra, required this.searchQuery, required this.sort, }); final List? statuses; final String? location; final String? filterExtra; final String searchQuery; final _JobSort sort; @override ConsumerState<_JobsTab> createState() => _JobsTabState(); } class _JobsTabState extends ConsumerState<_JobsTab> { final List _jobs = []; bool _isLoading = false; bool _hasMore = true; int _page = 1; static const _limit = 20; String? _error; late UnsubFn _unsub; final _scrollController = ScrollController(); @override void initState() { super.initState(); _load(); _scrollController.addListener(_onScroll); final tenantId = ref.read(authProvider).activeTenant!.tenant.id; _unsub = RealtimeService.instance.watch( 'jobs', filter: 'clinic_tenant_id="$tenantId"', onEvent: (_) { if (mounted) _load(); }, ); } @override void dispose() { _unsub(); _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 && !_isLoading && _hasMore) { _loadMore(); } } Future _load() async { if (_isLoading) return; setState(() { _isLoading = true; _error = null; _page = 1; _jobs.clear(); _hasMore = true; }); await _fetch(); } Future _loadMore() async { if (_isLoading || !_hasMore) return; _page++; await _fetch(); } Future _fetch() async { setState(() => _isLoading = true); try { final tenantId = ref.read(authProvider).activeTenant!.tenant.id; final results = await ClinicJobsRepository.instance.listOutbound( tenantId, statuses: widget.statuses, location: widget.location, filterExtra: widget.filterExtra, page: _page, limit: _limit, ); setState(() { _jobs.addAll(results); _hasMore = results.length == _limit; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } List get _filtered { var list = _jobs.toList(); final q = widget.searchQuery.toLowerCase().trim(); if (q.isNotEmpty) { list = list.where((j) { return j.patientCode.toLowerCase().contains(q) || (j.labName?.toLowerCase().contains(q) ?? false) || j.prostheticType.label.toLowerCase().contains(q); }).toList(); } switch (widget.sort) { case _JobSort.newestFirst: list.sort((a, b) => b.dateCreated.compareTo(a.dateCreated)); case _JobSort.oldestFirst: list.sort((a, b) => a.dateCreated.compareTo(b.dateCreated)); case _JobSort.byDueDate: list.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: list.sort( (a, b) => a.prostheticType.label.compareTo(b.prostheticType.label)); } return list; } @override Widget build(BuildContext context) { if (_error != null && _jobs.isEmpty) { 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: $_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'), ), ], ), ); } if (_isLoading && _jobs.isEmpty) { return const Center( child: CircularProgressIndicator(color: AppColors.accent)); } final filtered = _filtered; if (filtered.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 RefreshIndicator( color: AppColors.accent, onRefresh: _load, child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), itemCount: filtered.length + (_hasMore && widget.searchQuery.isEmpty ? 1 : 0), itemBuilder: (context, index) { if (index == filtered.length) { return const Padding( padding: EdgeInsets.all(16), child: Center( child: CircularProgressIndicator(color: AppColors.accent)), ); } final job = filtered[index]; return Padding( padding: const EdgeInsets.only(bottom: 10), child: _JobListCard( job: job, onTap: () => context.push('/clinic/jobs/${job.id}'), ), ); }, ), ); } } class _JobListCard extends StatelessWidget { const _JobListCard({required this.job, required this.onTap}); final Job job; final VoidCallback onTap; @override Widget build(BuildContext context) { final statusColor = _statusColor(job); final statusBg = _statusBg(job); final isOverdue = job.dueDate != null && job.dueDate!.isBefore(DateTime.now()); 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( 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: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: statusBg, borderRadius: BorderRadius.circular(12)), child: Icon(Icons.medical_services_outlined, color: statusColor, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( job.patientCode, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppColors.textPrimary), ), ), _StatusBadge(status: job.status, location: job.location), ], ), const SizedBox(height: 3), Text(job.prostheticType.label, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary)), if (job.labName != null) ...[ const SizedBox(height: 2), Text( job.labName!, style: const TextStyle( fontSize: 12, color: AppColors.textMuted), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], if (job.dueDate != null) ...[ const SizedBox(height: 2), Row( children: [ Icon(Icons.calendar_today_outlined, size: 11, color: isOverdue ? AppColors.cancelled : AppColors.textMuted), const SizedBox(width: 3), Text( _fmt(job.dueDate!), style: TextStyle( fontSize: 12, color: isOverdue ? AppColors.cancelled : AppColors.textMuted, fontWeight: isOverdue ? FontWeight.w600 : FontWeight.normal, ), ), ], ), ], ], ), ), const SizedBox(width: 8), const 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(Job job) { if (job.status == JobStatus.inProgress && job.location == JobLocation.atClinic) return AppColors.pending; switch (job.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; } } Color _statusBg(Job job) { if (job.status == JobStatus.inProgress && job.location == JobLocation.atClinic) return AppColors.pendingBg; switch (job.status) { case JobStatus.pending: return AppColors.pendingBg; case JobStatus.inProgress: return AppColors.inProgressBg; case JobStatus.sent: return AppColors.inProgressBg; case JobStatus.delivered: return AppColors.successBg; case JobStatus.cancelled: return AppColors.cancelledBg; } } } class _StatusBadge extends StatelessWidget { const _StatusBadge({required this.status, required this.location}); final JobStatus status; final JobLocation location; String get _label { if (status == JobStatus.inProgress && location == JobLocation.atClinic) return 'Onay Bekliyor'; if (status == JobStatus.sent) return 'Teslimat Bekliyor'; return status.label; } Color get _color { if (status == JobStatus.inProgress && location == JobLocation.atClinic) return AppColors.pending; 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; } } Color get _bg { if (status == JobStatus.inProgress && location == JobLocation.atClinic) return AppColors.pendingBg; switch (status) { case JobStatus.pending: return AppColors.pendingBg; case JobStatus.inProgress: return AppColors.inProgressBg; case JobStatus.sent: return AppColors.inProgressBg; case JobStatus.delivered: return AppColors.successBg; case JobStatus.cancelled: return AppColors.cancelledBg; } } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: _bg, borderRadius: BorderRadius.circular(8), ), child: Text( _label, style: TextStyle( color: _color, fontSize: 11, fontWeight: FontWeight.w600, ), ), ); } }