761 lines
25 KiB
Dart
761 lines
25 KiB
Dart
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<ClinicJobDetailScreen> createState() =>
|
||
_ClinicJobDetailScreenState();
|
||
}
|
||
|
||
class _ClinicJobDetailScreenState
|
||
extends ConsumerState<ClinicJobDetailScreen> {
|
||
Job? _job;
|
||
String? _loadError;
|
||
late Future<List<JobFile>> _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<void> _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<void> _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<void> _cancelJob(Job job) async {
|
||
final confirmed = await showDialog<bool>(
|
||
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<void> _requestRevision(Job job) async {
|
||
final noteController = TextEditingController();
|
||
final confirmed = await showDialog<bool>(
|
||
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<void> _markDelivered(Job job) async {
|
||
final noteCtrl = TextEditingController();
|
||
final confirmed = await showDialog<bool>(
|
||
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<List<JobFile>> 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<JobStep> steps;
|
||
final int currentStepIndex;
|
||
final Future<List<JobHistoryEntry>> historyFuture;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return FutureBuilder<List<JobHistoryEntry>>(
|
||
future: historyFuture,
|
||
builder: (ctx, snap) {
|
||
final history = snap.data ?? [];
|
||
final Map<JobStep, int> revisionCounts = {};
|
||
for (final e in history) {
|
||
if (e.action == JobHistoryAction.revisionRequested && e.step != null) {
|
||
revisionCounts[e.step!] = (revisionCounts[e.step!] ?? 0) + 1;
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}).toList(),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|