Simplify lab trial dispatch and fix per-jaw pricing

This commit is contained in:
egecankomur
2026-10-10 14:52:05 +03:00
parent f44e07f378
commit d20d44a1f3
6 changed files with 204 additions and 74 deletions
@@ -112,7 +112,11 @@ class ClinicJobsRepository {
}
Future<Job> approveAtClinic(String jobId, Job job, {String? note}) async {
final nextStep = job.nextStep;
if (job.status != JobStatus.inProgress ||
job.location != JobLocation.atClinic) {
throw StateError('Yalnızca klinikteki üretim işleri onaylanabilir.');
}
final nextStep = job.provaRequired ? job.currentStep : job.nextStep;
if (nextStep == null) throw Exception('Bu aşamadan ileri gidilemez.');
final record = await _pb.collection('jobs').update(jobId, body: {
@@ -189,15 +189,19 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
}
}
void _showHandToClinicSheet(Job job) {
void _showHandToClinicSheet(Job job, {required bool finish}) {
_showAdaptive(
context,
_HandToClinicSheet(
job: job,
finish: finish,
onDone: (Job updated) {
if (mounted) {
setState(() => _job = updated.copyWith(
clinicName: job.clinicName, labName: job.labName));
setState(() {
_job = updated.copyWith(
clinicName: job.clinicName, labName: job.labName);
_historyFuture = JobHistoryService.instance.listForJob(job.id);
});
}
},
),
@@ -525,20 +529,26 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
backgroundColor: AppColors.success,
),
),
if (canSendToClinic)
if (canSendToClinic && job.provaRequired) ...[
FilledButton.icon(
onPressed: () => _showHandToClinicSheet(job),
onPressed: () => _showHandToClinicSheet(job, finish: false),
icon: const Icon(Icons.send_outlined),
label: Text(
(job.isLastStep)
? 'Son Prova - Teslime Gönder'
: 'Prova için Kliniğe Gönder',
),
label: const Text('Provaya Gönder'),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
backgroundColor: (job.isLastStep)
? AppColors.success
: AppColors.inProgress,
backgroundColor: AppColors.inProgress,
),
),
const SizedBox(height: 12),
],
if (canSendToClinic)
FilledButton.icon(
onPressed: () => _showHandToClinicSheet(job, finish: true),
icon: const Icon(Icons.check_circle_outline),
label: const Text('Bitime Gönder'),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(52),
backgroundColor: AppColors.success,
),
),
if (canCancelJobs && job.status == JobStatus.pending) ...[
@@ -674,8 +684,13 @@ class _LatestRevisionBanner extends StatelessWidget {
// ── Hand to Clinic Sheet ─────────────────────────────────────────────────────
class _HandToClinicSheet extends StatefulWidget {
const _HandToClinicSheet({required this.job, required this.onDone});
const _HandToClinicSheet({
required this.job,
required this.finish,
required this.onDone,
});
final Job job;
final bool finish;
final void Function(Job updatedJob) onDone;
@override
@@ -685,6 +700,7 @@ class _HandToClinicSheet extends StatefulWidget {
class _HandToClinicSheetState extends State<_HandToClinicSheet> {
final _noteController = TextEditingController();
bool _sending = false;
String? _noteError;
@override
void dispose() {
@@ -696,20 +712,8 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
Widget build(BuildContext context) {
final isDesktop =
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
final currentStep = widget.job.currentStep;
final isLast = widget.job.isLastStep;
final stepLabel = currentStep?.label ?? '';
final requiresClinicApproval = currentStep?.requiresClinicApproval ?? true;
final continuesInLab = !widget.job.provaRequired &&
currentStep == JobStep.olcuKontrol &&
widget.job.nextStep == JobStep.cilaBitim;
final buttonLabel = isLast
? (widget.job.provaRequired
? 'Son Prova · Teslime Gönder'
: 'Teslime Gönder')
: requiresClinicApproval && !continuesInLab
? '$stepLabel için Kliniğe Gönder'
: '$stepLabel tamamlandı, sonraki adıma geç';
final isLast = widget.finish;
final buttonLabel = isLast ? 'Bitime Gönder' : 'Provaya Gönder';
final buttonColor = isLast ? AppColors.success : AppColors.inProgress;
return Container(
@@ -737,18 +741,17 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
const SizedBox(height: 8),
Text(
isLast
? 'İş teslim edilecek olarak işaretlenecek.'
: requiresClinicApproval && !continuesInLab
? 'İş klinikteki prova veya onay için gönderilecek.'
: 'Bu iç adım tamamlanacak ve iş laboratuvarda ilerleyecek.',
? 'Cila / bitim tamamlandıktan sonra seçin. İş teslim için gönderildi olarak işaretlenecek.'
: 'İş üretim aşamasında kalır. Klinik onayından sonra gerektiği kadar tekrar provaya gönderebilirsiniz.',
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...',
decoration: InputDecoration(
labelText: 'Aşama açıklaması',
hintText: 'Yapılan işlemi ve klinik için açıklamanızı yazın...',
errorText: _noteError,
),
maxLines: 3,
),
@@ -757,6 +760,10 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
onPressed: _sending
? null
: () async {
if (_noteController.text.trim().isEmpty) {
setState(() => _noteError = 'Aşama açıklaması yazın.');
return;
}
setState(() => _sending = true);
final navigator = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
@@ -765,20 +772,17 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
await LabJobsRepository.instance.handToClinic(
widget.job.id,
widget.job,
note: _noteController.text.trim().isEmpty
? null
: _noteController.text.trim(),
finish: widget.finish,
note: _noteController.text.trim(),
);
widget.onDone(updated);
navigator.pop();
messenger.showSnackBar(
SnackBar(
content: Text(isLast
? 'İş teslim için gönderildi'
: requiresClinicApproval && !continuesInLab
? 'Onay için kliniğe gönderildi'
: 'İş bir sonraki iç adıma geçirildi')),
: 'İş provaya gönderildi')),
);
if (context.mounted) widget.onDone(updated);
} catch (e) {
if (context.mounted) {
setState(() => _sending = false);
+21 -27
View File
@@ -83,40 +83,34 @@ class LabJobsRepository {
return job;
}
Future<Job> handToClinic(String jobId, Job job, {String? note}) async {
final currentStep = job.currentStep;
if (currentStep == null) {
throw Exception('Geçerli bir iş adımı bulunamadı.');
Future<Job> handToClinic(String jobId, Job job,
{required bool finish, required String note}) async {
if (note.trim().isEmpty) {
throw ArgumentError('Aşama açıklaması gereklidir.');
}
final isFinal = currentStep == JobStep.cilaBitim;
final nextStep = job.nextStep;
final shouldStayAtLabAfterMeasure = !job.provaRequired &&
currentStep == JobStep.olcuKontrol &&
nextStep == JobStep.cilaBitim;
final patch = isFinal
? {'status': 'sent', 'location': 'at_clinic'}
: currentStep.requiresClinicApproval && !shouldStayAtLabAfterMeasure
? {'location': 'at_clinic'}
: {
'current_step': nextStep?.value,
'location': 'at_lab',
};
final dispatchStep = job.labDispatchStep(finish: finish);
final steps = [...job.stepTemplate];
if (!steps.contains(dispatchStep)) {
steps.insert(0, dispatchStep);
}
final patch = {
'status': finish ? 'sent' : 'in_progress',
'location': 'at_clinic',
'current_step': dispatchStep.value,
'workflow_steps': steps.map((step) => step.value).toList(),
};
final record = await _pb.collection('jobs').update(jobId, body: patch);
final updated = Job.fromJson(record.toJson());
unawaited(JobHistoryService.instance.append(
await JobHistoryService.instance.append(
jobId: jobId,
clinicTenantId: job.clinicTenantId,
labTenantId: job.labTenantId,
action: (currentStep.requiresClinicApproval &&
!shouldStayAtLabAfterMeasure) ||
isFinal
? JobHistoryAction.handedToClinic
: JobHistoryAction.stepCompleted,
step: currentStep,
note: note,
));
action: JobHistoryAction.handedToClinic,
step: dispatchStep,
note:
'${finish ? "Bitime gönderildi" : "Provaya gönderildi"}: ${note.trim()}',
);
return updated;
}