Add pricing entry flow and platform admin foundations
This commit is contained in:
@@ -14,13 +14,13 @@ import 'lab_jobs_repository.dart';
|
||||
// ── Adaptive sheet helper ────────────────────────────────────────────────────
|
||||
|
||||
void _showAdaptive(BuildContext context, Widget content) {
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
final isDesktop =
|
||||
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
if (isDesktop) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: content,
|
||||
@@ -51,33 +51,66 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
String? _loadError;
|
||||
bool _isActing = false;
|
||||
late Future<List<JobFile>> _filesFuture;
|
||||
late UnsubFn _unsub;
|
||||
late Future<List<JobHistoryEntry>> _historyFuture;
|
||||
final List<UnsubFn> _unsubs = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
_loadFiles();
|
||||
_unsub = RealtimeService.instance.watch(
|
||||
_loadHistory();
|
||||
_unsubs.add(RealtimeService.instance.watch(
|
||||
'jobs',
|
||||
topic: widget.jobId,
|
||||
onEvent: (_) { if (mounted && !_isActing) _load(); },
|
||||
);
|
||||
onEvent: (_) {
|
||||
if (mounted && !_isActing) _load();
|
||||
},
|
||||
));
|
||||
_unsubs.add(RealtimeService.instance.watch(
|
||||
'job_files',
|
||||
filter: 'job_id="${widget.jobId}"',
|
||||
onEvent: (_) {
|
||||
if (mounted) _loadFiles();
|
||||
},
|
||||
));
|
||||
_unsubs.add(RealtimeService.instance.watch(
|
||||
'job_status_history',
|
||||
filter: 'job_id="${widget.jobId}"',
|
||||
onEvent: (_) {
|
||||
if (mounted) _loadHistory();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unsub();
|
||||
for (final unsub in _unsubs) {
|
||||
unsub();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() { _loadingJob = true; _loadError = null; });
|
||||
setState(() {
|
||||
_loadingJob = true;
|
||||
_loadError = null;
|
||||
});
|
||||
try {
|
||||
final job = await LabJobsRepository.instance.getJob(widget.jobId);
|
||||
if (mounted) setState(() { _job = job; _loadingJob = false; });
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_job = job;
|
||||
_loadingJob = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() { _loadError = e.toString(); _loadingJob = false; });
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loadError = e.toString();
|
||||
_loadingJob = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,14 +120,23 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _loadHistory() {
|
||||
setState(() {
|
||||
_historyFuture = JobHistoryService.instance.listForJob(widget.jobId);
|
||||
});
|
||||
}
|
||||
|
||||
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?'),
|
||||
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ç')),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Vazgeç')),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(backgroundColor: AppColors.cancelled),
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
@@ -108,13 +150,18 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
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.')));
|
||||
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')));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Hata: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +171,11 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
try {
|
||||
final updated = await LabJobsRepository.instance.acceptJob(job);
|
||||
if (mounted) {
|
||||
setState(() { _job = updated.copyWith(clinicName: job.clinicName, labName: job.labName); _isActing = false; });
|
||||
setState(() {
|
||||
_job = updated.copyWith(
|
||||
clinicName: job.clinicName, labName: job.labName);
|
||||
_isActing = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('İş kabul edildi')),
|
||||
);
|
||||
@@ -132,7 +183,8 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isActing = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Hata: $e')));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Hata: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +195,10 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
_HandToClinicSheet(
|
||||
job: job,
|
||||
onDone: (Job updated) {
|
||||
if (mounted) setState(() => _job = updated.copyWith(clinicName: job.clinicName, labName: job.labName));
|
||||
if (mounted) {
|
||||
setState(() => _job = updated.copyWith(
|
||||
clinicName: job.clinicName, labName: job.labName));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -170,7 +225,8 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
}
|
||||
|
||||
String _formatDate(DateTime dt, {bool withTime = false}) {
|
||||
final d = '${dt.day.toString().padLeft(2, '0')}.${dt.month.toString().padLeft(2, '0')}.${dt.year}';
|
||||
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')}';
|
||||
}
|
||||
@@ -234,260 +290,261 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
||||
job.location == JobLocation.atLab;
|
||||
final canAccept = !isDeliveryOnly && job.status == JobStatus.pending;
|
||||
|
||||
return ListView(
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Header card
|
||||
Container(
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
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.route_outlined,
|
||||
label: 'Akış',
|
||||
value: job.workflowPreset.title,
|
||||
),
|
||||
_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: 20),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
JobFilesPanel(
|
||||
job: job,
|
||||
filesFuture: _filesFuture,
|
||||
onRefresh: _loadFiles,
|
||||
// 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,
|
||||
isDelivered: job.status == JobStatus.delivered,
|
||||
historyFuture: _historyFuture,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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: 16),
|
||||
],
|
||||
);
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
JobFilesPanel(
|
||||
job: job,
|
||||
filesFuture: _filesFuture,
|
||||
onRefresh: _loadFiles,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,12 +572,19 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
final isDesktop =
|
||||
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||
final currentStep = widget.job.currentStep;
|
||||
final isLast = widget.job.isLastStep;
|
||||
final stepLabel = widget.job.currentStep?.label ?? '';
|
||||
final stepLabel = currentStep?.label ?? '';
|
||||
final requiresClinicApproval = currentStep?.requiresClinicApproval ?? true;
|
||||
final buttonLabel = isLast
|
||||
? (widget.job.provaRequired ? 'Son Prova · Teslime Gönder' : 'Teslime Gönder')
|
||||
: '$stepLabel için Kliniğe Gönder';
|
||||
? (widget.job.provaRequired
|
||||
? 'Son Prova · Teslime Gönder'
|
||||
: 'Teslime Gönder')
|
||||
: requiresClinicApproval
|
||||
? '$stepLabel için Kliniğe Gönder'
|
||||
: '$stepLabel tamamlandı, sonraki adıma geç';
|
||||
final buttonColor = isLast ? AppColors.success : AppColors.inProgress;
|
||||
|
||||
return Container(
|
||||
@@ -534,9 +598,7 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 24,
|
||||
bottom: isDesktop
|
||||
? 24
|
||||
: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
bottom: isDesktop ? 24 : MediaQuery.of(context).viewInsets.bottom + 24,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -544,17 +606,16 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
||||
children: [
|
||||
Text(
|
||||
buttonLabel,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary),
|
||||
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.',
|
||||
: requiresClinicApproval
|
||||
? 'İş klinikteki prova veya onay için gönderilecek.'
|
||||
: 'Bu iç adım tamamlanacak ve iş laboratuvarda ilerleyecek.',
|
||||
style: const TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -575,7 +636,8 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
||||
final navigator = Navigator.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
final updated = await LabJobsRepository.instance.handToClinic(
|
||||
final updated =
|
||||
await LabJobsRepository.instance.handToClinic(
|
||||
widget.job.id,
|
||||
widget.job,
|
||||
note: _noteController.text.trim().isEmpty
|
||||
@@ -587,7 +649,9 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
||||
SnackBar(
|
||||
content: Text(isLast
|
||||
? 'İş teslim için gönderildi'
|
||||
: 'Prova için klinik\'e gönderildi')),
|
||||
: requiresClinicApproval
|
||||
? 'Onay için kliniğe gönderildi'
|
||||
: 'İş bir sonraki iç adıma geçirildi')),
|
||||
);
|
||||
if (context.mounted) widget.onDone(updated);
|
||||
} catch (e) {
|
||||
@@ -645,7 +709,8 @@ class _InfoRow extends StatelessWidget {
|
||||
width: 110,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 13),
|
||||
style:
|
||||
const TextStyle(color: AppColors.textSecondary, fontSize: 13),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -670,10 +735,12 @@ class _JobStepper extends StatelessWidget {
|
||||
const _JobStepper({
|
||||
required this.steps,
|
||||
required this.currentStep,
|
||||
required this.isDelivered,
|
||||
required this.historyFuture,
|
||||
});
|
||||
final List<JobStep> steps;
|
||||
final JobStep? currentStep;
|
||||
final bool isDelivered;
|
||||
final Future<List<JobHistoryEntry>> historyFuture;
|
||||
|
||||
@override
|
||||
@@ -686,7 +753,8 @@ class _JobStepper extends StatelessWidget {
|
||||
final Map<JobStep, int> revisionCounts = {};
|
||||
final Map<JobStep, List<JobHistoryEntry>> notesByStep = {};
|
||||
for (final e in history) {
|
||||
if (e.action == JobHistoryAction.revisionRequested && e.step != null) {
|
||||
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) {
|
||||
@@ -699,8 +767,8 @@ class _JobStepper extends StatelessWidget {
|
||||
return Column(
|
||||
children: List.generate(steps.length, (i) {
|
||||
final step = steps[i];
|
||||
final isCompleted = i < currentIndex;
|
||||
final isCurrent = i == currentIndex;
|
||||
final isCompleted = isDelivered || i < currentIndex;
|
||||
final isCurrent = !isDelivered && i == currentIndex;
|
||||
final isLastItem = i == steps.length - 1;
|
||||
final revCount = revisionCounts[step] ?? 0;
|
||||
final stepNotes = notesByStep[step] ?? const <JobHistoryEntry>[];
|
||||
@@ -728,7 +796,7 @@ class _JobStepper extends StatelessWidget {
|
||||
Container(
|
||||
width: 2,
|
||||
height: 44,
|
||||
color: i < currentIndex
|
||||
color: isDelivered || i < currentIndex
|
||||
? AppColors.success.withValues(alpha: 0.35)
|
||||
: AppColors.border,
|
||||
),
|
||||
@@ -788,7 +856,8 @@ class _JobStepper extends StatelessWidget {
|
||||
),
|
||||
if (stepNotes.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
...stepNotes.map((entry) => _StepNoteCard(entry: entry)),
|
||||
...stepNotes
|
||||
.map((entry) => _StepNoteCard(entry: entry)),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -846,6 +915,7 @@ class _StepNoteCard extends StatelessWidget {
|
||||
String _label(JobHistoryAction action) {
|
||||
return switch (action) {
|
||||
JobHistoryAction.revisionRequested => 'Revizyon Notu',
|
||||
JobHistoryAction.stepCompleted => 'İç Adım Notu',
|
||||
JobHistoryAction.handedToClinic => 'Laboratuvar Notu',
|
||||
JobHistoryAction.approved => 'Onay Notu',
|
||||
JobHistoryAction.delivered => 'Teslim Notu',
|
||||
|
||||
Reference in New Issue
Block a user