import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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 'clinic_jobs_repository.dart'; class ClinicJobDetailScreen extends ConsumerStatefulWidget { const ClinicJobDetailScreen({super.key, required this.jobId}); final String jobId; @override ConsumerState createState() => _ClinicJobDetailScreenState(); } class _ClinicJobDetailScreenState extends ConsumerState { Job? _job; String? _loadError; late Future> _filesFuture; bool _isActing = false; 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 { if (mounted) setState(() { _loadError = null; }); try { final job = await ClinicJobsRepository.instance.getJob(widget.jobId); if (mounted) setState(() { _job = job; }); } catch (e) { if (mounted) setState(() { _loadError = e.toString(); }); } } void _loadFiles() { setState(() { _filesFuture = JobFilesRepository.instance.listForJob(widget.jobId); }); } Future _approve(Job job) async { setState(() => _isActing = true); try { final updated = await ClinicJobsRepository.instance.approveAtClinic(job.id, job); if (mounted) { setState(() { _job = updated.copyWith(clinicName: job.clinicName, labName: job.labName); _isActing = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('İş onaylandı.')), ); } } catch (e) { if (mounted) { setState(() => _isActing = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e')), ); } } } 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 ClinicJobsRepository.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 _requestRevision(Job job) async { final noteController = TextEditingController(); final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Revizyon Talebi'), content: TextField( controller: noteController, decoration: const InputDecoration( labelText: 'Açıklama', hintText: 'Revizyon sebebini belirtin...', ), minLines: 3, maxLines: 5, ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Gönder'), ), ], ), ); if (confirmed != true || !mounted) return; if (noteController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Lütfen bir açıklama girin.')), ); return; } setState(() => _isActing = true); try { final updated = await ClinicJobsRepository.instance.requestRevision( job.id, job, note: noteController.text.trim(), ); if (mounted) { setState(() { _job = updated.copyWith(clinicName: job.clinicName, labName: job.labName); _isActing = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Revizyon talebi gönderildi.')), ); } } catch (e) { if (mounted) { setState(() => _isActing = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e')), ); } } } Future _markDelivered(Job job) async { final noteCtrl = TextEditingController(); final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Teslim Alındı'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Bu işi teslim aldığınızı onaylıyor musunuz?'), const SizedBox(height: 12), TextField( controller: noteCtrl, decoration: const InputDecoration( labelText: 'Teslimat notu (isteğe bağlı)', hintText: 'Teslim eden kişi, durum vb...', isDense: true, ), maxLines: 2, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('İptal'), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: AppColors.success), onPressed: () => Navigator.pop(ctx, true), child: const Text('Teslim Alındı'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _isActing = true); try { final note = noteCtrl.text.trim().isNotEmpty ? noteCtrl.text.trim() : null; final updated = await ClinicJobsRepository.instance.markDelivered(job.id, job, note: note); if (mounted) { setState(() { _job = updated.copyWith(clinicName: job.clinicName, labName: job.labName); _isActing = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('İş teslim alındı olarak işaretlendi.')), ); } } catch (e) { if (mounted) { setState(() => _isActing = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Hata: $e')), ); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(title: const Text('İş Detayı')), body: _buildBody(), ); } Widget _buildBody() { if (_job == null && _loadError == 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 Center(child: CircularProgressIndicator(color: AppColors.accent)); final job = _job!; final membership = ref.read(authProvider).activeTenant; final canDeliver = membership?.canDeliverJobs ?? true; final canCancel = membership?.canCancelJobs ?? true; final canManage = !(membership?.isDeliveryOnly ?? false); return _JobDetailBody( job: job, filesFuture: _filesFuture, isActing: _isActing, canDeliver: canDeliver, canManage: canManage, onApprove: canManage ? () => _approve(job) : () {}, onRevision: canManage ? () => _requestRevision(job) : () {}, onDelivered: () => _markDelivered(job), onCancel: (canCancel && job.status == JobStatus.pending) ? () => _cancelJob(job) : null, onFilesRefresh: _loadFiles, ); } } class _JobDetailBody extends StatelessWidget { const _JobDetailBody({ required this.job, required this.filesFuture, required this.isActing, required this.canDeliver, required this.canManage, required this.onApprove, required this.onRevision, required this.onDelivered, required this.onFilesRefresh, this.onCancel, }); final Job job; final Future> filesFuture; final bool isActing; final bool canDeliver; final bool canManage; final VoidCallback onApprove; final VoidCallback onRevision; final VoidCallback onDelivered; final VoidCallback? onCancel; final VoidCallback onFilesRefresh; @override Widget build(BuildContext context) { final steps = job.stepTemplate; final currentStepIndex = job.currentStep != null ? steps.indexOf(job.currentStep!) : -1; final canApproveOrRevise = canManage && job.location == JobLocation.atClinic && job.status == JobStatus.inProgress; final canMarkDelivered = canDeliver && job.status == JobStatus.sent; return ListView( padding: const EdgeInsets.all(16), children: [ // Info 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: [ // Patient code + status 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), ), ), _StatusBadge(status: job.status), ], ), const SizedBox(height: 16), const Divider(height: 1, color: AppColors.border), const SizedBox(height: 12), // Patient + Lab _SectionLabel(title: 'Hasta & Laboratuvar'), if (job.patientName != null && job.patientName!.isNotEmpty) _InfoRow(label: 'Hasta', value: job.patientName!), _InfoRow(label: 'Protokol No', value: job.patientCode), if (job.patientId != null) _InfoRow(label: 'Hasta ID', value: job.patientId!), _InfoRow( label: 'Laboratuvar', value: job.labName ?? 'Bilinmiyor'), const SizedBox(height: 12), // Prosthetic _SectionLabel(title: 'Protez Bilgisi'), _InfoRow(label: 'Tür', value: job.prostheticType.label), if (job.prostheticName != null && job.prostheticName!.isNotEmpty) _InfoRow(label: 'Ürün', value: job.prostheticName!), if (job.workflowType != null) _InfoRow(label: 'İş Tipi', value: job.workflowType!.label), _InfoRow( label: 'Prova', value: job.provaRequired ? 'Provalı' : 'Provasız', ), _InfoRow(label: 'Üye Sayısı', value: '${job.memberCount}'), if (job.teeth.isNotEmpty) _InfoRow(label: 'Dişler', value: job.teeth.join(', ')), if (job.color != null && job.color!.isNotEmpty) _InfoRow(label: 'Renk', value: job.color!), if (job.description != null && job.description!.isNotEmpty) _InfoRow(label: 'Açıklama', value: job.description!), if (job.dueDate != null) _InfoRow(label: 'Son Tarih', value: _formatDate(job.dueDate!, withTime: true)), if (job.price != null) _InfoRow( label: 'Fiyat', value: '${job.price!.toStringAsFixed(2)} ${job.currency ?? 'TRY'}'), ], ), ), const SizedBox(height: 16), // Stepper 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: [ const Text( 'İş Adımları', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textPrimary), ), const SizedBox(height: 16), _StepperWidget( steps: steps, currentStepIndex: currentStepIndex, historyFuture: JobHistoryService.instance.listForJob(job.id), ), ], ), ), const SizedBox(height: 24), // Action buttons if (isActing) const Center( child: CircularProgressIndicator(color: AppColors.accent)) else if (canApproveOrRevise) ...[ FilledButton.icon( onPressed: onApprove, icon: const Icon(Icons.check_circle_outline), label: const Text('Onayla'), style: FilledButton.styleFrom( minimumSize: const Size(double.infinity, 50), backgroundColor: AppColors.success, ), ), const SizedBox(height: 12), OutlinedButton.icon( onPressed: onRevision, icon: const Icon(Icons.replay_outlined), label: const Text('Revizyon İste'), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 50), foregroundColor: AppColors.pending, side: const BorderSide(color: AppColors.pending), ), ), ] else if (canMarkDelivered) FilledButton.icon( onPressed: onDelivered, icon: const Icon(Icons.inventory_2_outlined), label: const Text('Teslim Aldım'), style: FilledButton.styleFrom( minimumSize: const Size(double.infinity, 50), ), ), if (onCancel != null) ...[ const SizedBox(height: 12), OutlinedButton.icon( onPressed: onCancel, icon: const Icon(Icons.close_rounded), label: const Text('İşi İptal Et'), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 50), foregroundColor: AppColors.cancelled, side: const BorderSide(color: AppColors.cancelled), ), ), ], const SizedBox(height: 20), // Files panel JobFilesPanel( job: job, filesFuture: filesFuture, onRefresh: onFilesRefresh, ), const SizedBox(height: 12), Text( 'Oluşturulma: ${_formatDate(job.dateCreated)}', style: const TextStyle(fontSize: 12, color: AppColors.textMuted), textAlign: TextAlign.center, ), const SizedBox(height: 8), ], ); } String _formatDate(DateTime d, {bool withTime = false}) { final s = '${d.day.toString().padLeft(2, '0')}.${d.month.toString().padLeft(2, '0')}.${d.year}'; if (!withTime || (d.hour == 0 && d.minute == 0)) return s; return '$s ${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}'; } } class _StepperWidget extends StatelessWidget { const _StepperWidget({ required this.steps, required this.currentStepIndex, required this.historyFuture, }); final List steps; final int currentStepIndex; final Future> historyFuture; @override Widget build(BuildContext context) { return FutureBuilder>( future: historyFuture, builder: (ctx, snap) { final history = snap.data ?? []; 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); } } return Column( children: steps.asMap().entries.map((entry) { final index = entry.key; final step = entry.value; final isCompleted = index < currentStepIndex; final isCurrent = index == currentStepIndex; 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 (index < steps.length - 1) Container( width: 2, height: 44, color: index < currentStepIndex ? 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)), ], ], ), ), ), ], ); }).toList(), ); }, ); } } 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', }; } } class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.title}); final String title; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Text( title, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.accent, letterSpacing: 0.5), ), ); } } class _InfoRow extends StatelessWidget { const _InfoRow({required this.label, required this.value}); final String label; final String value; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 110, child: Text( label, style: const TextStyle( fontSize: 13, color: AppColors.textSecondary), ), ), Expanded( child: Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textPrimary), ), ), ], ), ); } } class _StatusBadge extends StatelessWidget { const _StatusBadge({required this.status}); final JobStatus status; @override Widget build(BuildContext context) { final color = _color(status); final bg = _bg(status); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(10), ), child: Text( status.label, style: TextStyle( color: color, fontSize: 13, fontWeight: FontWeight.w600, ), ), ); } Color _color(JobStatus s) { switch (s) { 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 _bg(JobStatus s) { switch (s) { 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; } } }