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 '../../../models/job.dart'; import '../../../models/job_file.dart'; import '../../../features/shared/job_files_repository.dart'; import '../../../features/shared/job_files_panel.dart'; import '../../../core/services/job_history_service.dart'; import 'lab_jobs_repository.dart'; // ── Adaptive sheet helper ──────────────────────────────────────────────────── void _showAdaptive(BuildContext context, Widget content) { final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint; if (isDesktop) { showDialog( context: context, builder: (_) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 560), child: content, ), ), ); } else { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => content, ); } } class LabJobDetailScreen extends ConsumerStatefulWidget { const LabJobDetailScreen({super.key, required this.jobId}); final String jobId; @override ConsumerState createState() => _LabJobDetailScreenState(); } class _LabJobDetailScreenState extends ConsumerState { Job? _job; bool _loadingJob = false; String? _loadError; bool _isActing = false; late Future> _filesFuture; late UnsubFn _unsub; @override void initState() { super.initState(); _load(); _loadFiles(); _unsub = RealtimeService.instance.watch( 'jobs', topic: widget.jobId, onEvent: (_) { if (mounted && !_isActing) _load(); }, ); } @override void dispose() { _unsub(); super.dispose(); } Future _load() async { setState(() { _loadingJob = true; _loadError = null; }); try { final job = await LabJobsRepository.instance.getJob(widget.jobId); if (mounted) setState(() { _job = job; _loadingJob = false; }); } catch (e) { if (mounted) setState(() { _loadError = e.toString(); _loadingJob = false; }); } } void _loadFiles() { setState(() { _filesFuture = JobFilesRepository.instance.listForJob(widget.jobId); }); } Future _cancelJob(Job job) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('İşi İptal Et'), content: const Text('Bu iş geri alınamaz şekilde iptal edilir. Onaylıyor musunuz?'), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Vazgeç')), FilledButton( style: FilledButton.styleFrom(backgroundColor: AppColors.cancelled), onPressed: () => Navigator.pop(ctx, true), child: const Text('İptal Et'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _isActing = true); try { final updated = await LabJobsRepository.instance.cancelJob(job.id, job); if (mounted) { setState(() { _job = _job!.copyWith(status: updated.status); _isActing = false; }); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('İş iptal edildi.'))); } } catch (e) { if (mounted) { setState(() => _isActing = false); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Hata: $e'))); } } } Future _acceptJob(Job job) async { setState(() => _isActing = true); try { final updated = await LabJobsRepository.instance.acceptJob(job); if (mounted) { setState(() { _job = updated.copyWith(clinicName: job.clinicName, labName: job.labName); _isActing = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('İş kabul edildi')), ); } } catch (e) { if (mounted) { setState(() => _isActing = false); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Hata: $e'))); } } } void _showHandToClinicSheet(Job job) { _showAdaptive( context, _HandToClinicSheet( job: job, onDone: (Job updated) { if (mounted) setState(() => _job = updated.copyWith(clinicName: job.clinicName, labName: job.labName)); }, ), ); } Color _statusColor(JobStatus status) { return switch (status) { JobStatus.pending => AppColors.pending, JobStatus.inProgress => AppColors.inProgress, JobStatus.sent => AppColors.accent, JobStatus.delivered => AppColors.success, JobStatus.cancelled => AppColors.cancelled, }; } Color _statusBg(JobStatus status) { return switch (status) { JobStatus.pending => AppColors.pendingBg, JobStatus.inProgress => AppColors.inProgressBg, JobStatus.sent => AppColors.inProgressBg, JobStatus.delivered => AppColors.successBg, JobStatus.cancelled => AppColors.cancelledBg, }; } String _formatDate(DateTime dt, {bool withTime = false}) { final d = '${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year}'; if (!withTime || (dt.hour == 0 && dt.minute == 0)) return d; return '$d ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('İş Detayı'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.pop(), ), ), body: _buildBody(), ); } Widget _buildBody() { if (_loadingJob && _job == null) { return const Center( child: CircularProgressIndicator(color: AppColors.accent)); } if (_loadError != null && _job == null) { 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: $_loadError', 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 (_job == null) return const SizedBox.shrink(); { final job = _job!; final membership = ref.read(authProvider).activeTenant; final isDeliveryOnly = membership?.isDeliveryOnly ?? false; final canCancelJobs = membership?.canCancelJobs ?? true; final canSendToClinic = !isDeliveryOnly && job.status == JobStatus.inProgress && job.location == JobLocation.atLab; final canAccept = !isDeliveryOnly && job.status == JobStatus.pending; return ListView( padding: const EdgeInsets.all(16), children: [ // Header card Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 12, offset: const Offset(0, 4)) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( job.patientName?.isNotEmpty == true ? job.patientName! : job.patientCode, style: Theme.of(context) .textTheme .headlineSmall ?.copyWith(fontWeight: FontWeight.bold, color: AppColors.textPrimary), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 5), decoration: BoxDecoration( color: _statusBg(job.status), borderRadius: BorderRadius.circular(10), ), child: Text( job.status.label, style: TextStyle( color: _statusColor(job.status), fontWeight: FontWeight.w600, fontSize: 13, ), ), ), ], ), const SizedBox(height: 12), _InfoRow( icon: Icons.business, label: 'Klinik', value: job.clinicName ?? '-'), if (job.patientName != null && job.patientName!.isNotEmpty) _InfoRow( icon: Icons.person_outline, label: 'Hasta', value: job.patientName!, ), _InfoRow( icon: Icons.tag_outlined, label: 'Protokol No', value: job.patientCode, ), _InfoRow( icon: Icons.medical_services_outlined, label: 'Protez Tipi', value: job.prostheticType.label), if (job.prostheticName != null && job.prostheticName!.isNotEmpty) _InfoRow( icon: Icons.category_outlined, label: 'Ürün', value: job.prostheticName!, ), if (job.workflowType != null) _InfoRow( icon: Icons.tune_rounded, label: 'İş Tipi', value: job.workflowType!.label, ), _InfoRow( icon: Icons.fact_check_outlined, label: 'Prova', value: job.provaRequired ? 'Provalı' : 'Provasız', ), _InfoRow( icon: Icons.format_list_numbered, label: 'Üye Sayısı', value: '${job.memberCount} üye'), if (job.color != null) _InfoRow( icon: Icons.color_lens_outlined, label: 'Renk', value: job.color!), if (job.dueDate != null) _InfoRow( icon: Icons.calendar_today, label: 'Teslim Tarihi', value: _formatDate(job.dueDate!, withTime: true), valueColor: job.dueDate!.isBefore(DateTime.now()) ? AppColors.cancelled : null), _InfoRow( icon: Icons.add_circle_outline, label: 'Oluşturulma', value: _formatDate(job.dateCreated)), if (job.price != null && job.currency != null) _InfoRow( icon: Icons.attach_money, label: 'Fiyat', value: '${job.price!.toStringAsFixed(2)} ${job.currency}'), if (job.description != null && job.description!.isNotEmpty) _InfoRow( icon: Icons.notes, label: 'Açıklama', value: job.description!), ], ), ), const SizedBox(height: 16), // Stepper Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 12, offset: const Offset(0, 4)) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'İş Adımları', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.w600, color: AppColors.textPrimary), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: job.provaRequired ? AppColors.inProgressBg : AppColors.successBg, borderRadius: BorderRadius.circular(6), ), child: Text( job.provaRequired ? 'Provalı' : 'Provasız', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: job.provaRequired ? AppColors.inProgress : AppColors.success, ), ), ), ], ), const SizedBox(height: 16), _JobStepper( steps: job.stepTemplate, currentStep: job.currentStep, historyFuture: JobHistoryService.instance .listForJob(job.id), ), ], ), ), const SizedBox(height: 24), // Action buttons if (_isActing) const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: Center(child: CircularProgressIndicator(color: AppColors.accent)), ) else ...[ if (canAccept) FilledButton.icon( onPressed: () => _acceptJob(job), icon: const Icon(Icons.check_circle_outline), label: const Text('Kabul Et'), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), backgroundColor: AppColors.success, ), ), if (canSendToClinic) FilledButton.icon( onPressed: () => _showHandToClinicSheet(job), icon: const Icon(Icons.send_outlined), label: Text( (job.isLastStep) ? 'Son Prova - Teslime Gönder' : 'Prova için Kliniğe Gönder', ), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), backgroundColor: (job.isLastStep) ? AppColors.success : AppColors.inProgress, ), ), if (canCancelJobs && job.status == JobStatus.pending) ...[ const SizedBox(height: 12), OutlinedButton.icon( onPressed: () => _cancelJob(job), icon: const Icon(Icons.close_rounded), label: const Text('İşi İptal Et'), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(50), foregroundColor: AppColors.cancelled, side: const BorderSide(color: AppColors.cancelled), ), ), ], ], const SizedBox(height: 20), JobFilesPanel( job: job, filesFuture: _filesFuture, onRefresh: _loadFiles, ), const SizedBox(height: 16), ], ); } } } // ── Hand to Clinic Sheet ───────────────────────────────────────────────────── class _HandToClinicSheet extends StatefulWidget { const _HandToClinicSheet({required this.job, required this.onDone}); final Job job; final void Function(Job updatedJob) onDone; @override State<_HandToClinicSheet> createState() => _HandToClinicSheetState(); } class _HandToClinicSheetState extends State<_HandToClinicSheet> { final _noteController = TextEditingController(); bool _sending = false; @override void dispose() { _noteController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint; final isLast = widget.job.isLastStep; final stepLabel = widget.job.currentStep?.label ?? ''; final buttonLabel = isLast ? (widget.job.provaRequired ? 'Son Prova · Teslime Gönder' : 'Teslime Gönder') : '$stepLabel için Kliniğe Gönder'; final buttonColor = isLast ? AppColors.success : AppColors.inProgress; return Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.vertical( top: isDesktop ? Radius.zero : const Radius.circular(20), ), ), padding: EdgeInsets.only( left: 20, right: 20, top: 24, bottom: isDesktop ? 24 : MediaQuery.of(context).viewInsets.bottom + 24, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( buttonLabel, style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.bold, color: AppColors.textPrimary), ), const SizedBox(height: 8), Text( isLast ? 'İş teslim edilecek olarak işaretlenecek.' : 'İş klinikteki prova için gönderilecek.', style: const TextStyle(color: AppColors.textSecondary), ), const SizedBox(height: 16), TextField( controller: _noteController, decoration: const InputDecoration( labelText: 'Not (isteğe bağlı)', hintText: 'Klinik için not ekleyin...', ), maxLines: 3, ), const SizedBox(height: 16), FilledButton( onPressed: _sending ? null : () async { setState(() => _sending = true); final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); try { final updated = await LabJobsRepository.instance.handToClinic( widget.job.id, widget.job, note: _noteController.text.trim().isEmpty ? null : _noteController.text.trim(), ); navigator.pop(); messenger.showSnackBar( SnackBar( content: Text(isLast ? 'İş teslim için gönderildi' : 'Prova için klinik\'e gönderildi')), ); if (context.mounted) widget.onDone(updated); } catch (e) { if (context.mounted) { setState(() => _sending = false); messenger.showSnackBar( SnackBar(content: Text('Hata: $e')), ); } } }, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), backgroundColor: buttonColor, ), child: _sending ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : Text(buttonLabel), ), ], ), ); } } // ── Info Row ───────────────────────────────────────────────────────────────── class _InfoRow extends StatelessWidget { const _InfoRow({ required this.icon, required this.label, required this.value, this.valueColor, }); final IconData icon; final String label; final String value; final Color? valueColor; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 18, color: AppColors.textMuted), const SizedBox(width: 10), SizedBox( width: 110, child: Text( label, style: const TextStyle(color: AppColors.textSecondary, fontSize: 13), ), ), Expanded( child: Text( value, style: TextStyle( fontWeight: FontWeight.w500, color: valueColor ?? AppColors.textPrimary, fontSize: 14, ), ), ), ], ), ); } } // ── Job Stepper ─────────────────────────────────────────────────────────────── class _JobStepper extends StatelessWidget { const _JobStepper({ required this.steps, required this.currentStep, required this.historyFuture, }); final List steps; final JobStep? currentStep; final Future> historyFuture; @override Widget build(BuildContext context) { return FutureBuilder>( future: historyFuture, builder: (ctx, snap) { final history = snap.data ?? []; // Revizyon sayısı per adım final Map revisionCounts = {}; final Map> notesByStep = {}; for (final e in history) { if (e.action == JobHistoryAction.revisionRequested && e.step != null) { revisionCounts[e.step!] = (revisionCounts[e.step!] ?? 0) + 1; } if (e.step != null && e.note != null && e.note!.trim().isNotEmpty) { notesByStep.putIfAbsent(e.step!, () => []).add(e); } } final currentIndex = currentStep != null ? steps.indexOf(currentStep!) : -1; return Column( children: List.generate(steps.length, (i) { final step = steps[i]; final isCompleted = i < currentIndex; final isCurrent = i == currentIndex; final isLastItem = i == steps.length - 1; final revCount = revisionCounts[step] ?? 0; final stepNotes = notesByStep[step] ?? const []; Color dotColor; IconData dotIcon; if (isCompleted) { dotColor = AppColors.success; dotIcon = Icons.check_circle; } else if (isCurrent) { dotColor = AppColors.inProgress; dotIcon = Icons.radio_button_checked; } else { dotColor = AppColors.muted; dotIcon = Icons.radio_button_unchecked; } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ Icon(dotIcon, color: dotColor, size: 24), if (!isLastItem) Container( width: 2, height: 44, color: i < currentIndex ? AppColors.success.withValues(alpha: 0.35) : AppColors.border, ), ], ), const SizedBox(width: 12), Expanded( child: Padding( padding: const EdgeInsets.only(top: 2, bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( step.label, style: TextStyle( fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal, color: isCompleted ? AppColors.success : isCurrent ? AppColors.inProgress : AppColors.textMuted, fontSize: 15, ), ), if (revCount > 0) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 1), decoration: BoxDecoration( color: AppColors.cancelledBg, borderRadius: BorderRadius.circular(6), ), child: Text( '$revCount revizyon', style: const TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.cancelled, ), ), ), ], ], ), if (isCurrent) Text( step.description, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), if (stepNotes.isNotEmpty) ...[ const SizedBox(height: 8), ...stepNotes.map((entry) => _StepNoteCard(entry: entry)), ], ], ), ), ), ], ); }), ); }, ); } } class _StepNoteCard extends StatelessWidget { const _StepNoteCard({required this.entry}); final JobHistoryEntry entry; @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.only(top: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: AppColors.surfaceVariant, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _label(entry.action), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: AppColors.textSecondary, ), ), const SizedBox(height: 4), Text( entry.note!, style: const TextStyle( fontSize: 12, color: AppColors.textPrimary, ), ), ], ), ); } String _label(JobHistoryAction action) { return switch (action) { JobHistoryAction.revisionRequested => 'Revizyon Notu', JobHistoryAction.handedToClinic => 'Laboratuvar Notu', JobHistoryAction.approved => 'Onay Notu', JobHistoryAction.delivered => 'Teslim Notu', JobHistoryAction.accepted => 'Kabul Notu', JobHistoryAction.cancelled => 'İptal Notu', }; } }