enum JobStatus { pending, inProgress, sent, delivered, cancelled } enum JobStep { olcu, // legacy fallback olcuKontrol, // geleneksel/arjinat ölçü veya model kontrolü dijitalTasarim, // dijital tasarım klinik onayı modelHazirlik, // internal hazırlık/model döküm altYapiProva, // sabit seramik/metal — alt yapı (coping) ustYapiProva, // sabit seramik — bisküvi prova mumProva, // hareketli protez — mum prova dislerProva, // hareketli protez — dişler prova dayanakProva, // implant — dayanak prova kronProva, // implant — kron prova fotografOnay, // foto/mockup ile klinik onayı kaliteKontrol, // internal kalite kontrol teslimOncesiKontrol, // internal final kontrol cilaBitim, // son cila / bitim (her şablonda son adım) } enum JobLocation { atClinic, atLab } enum JobWorkflowType { arjinat, geleneksel, dijital } enum ProstheticFamily { sabit, implant, hareketli, gecici, ozel } enum ProstheticType { metalPorselen, zirkonyum, implantUstuZirkonyum, gecici, eMax, tamProtez, parsiyel, diger, } // ── Status ──────────────────────────────────────────────────────────────────── extension JobStatusExt on JobStatus { String get label => switch (this) { JobStatus.pending => 'Bekliyor', JobStatus.inProgress => 'İşlemde', JobStatus.sent => 'Gönderildi', JobStatus.delivered => 'Teslim Alındı', JobStatus.cancelled => 'İptal', }; String get value => switch (this) { JobStatus.pending => 'pending', JobStatus.inProgress => 'in_progress', JobStatus.sent => 'sent', JobStatus.delivered => 'delivered', JobStatus.cancelled => 'cancelled', }; } // ── Step ────────────────────────────────────────────────────────────────────── extension JobStepExt on JobStep { String get label => switch (this) { JobStep.olcu => 'Ölçü', JobStep.olcuKontrol => 'Ölçü / Model Kontrol', JobStep.dijitalTasarim => 'Dijital Tasarım Onayı', JobStep.modelHazirlik => 'Model Hazırlık', JobStep.altYapiProva => 'Alt Yapı Prova', JobStep.ustYapiProva => 'Üst Yapı Prova', JobStep.mumProva => 'Mum Prova', JobStep.dislerProva => 'Dişler Prova', JobStep.dayanakProva => 'Dayanak Prova', JobStep.kronProva => 'Kron Prova', JobStep.fotografOnay => 'Fotoğraf / Mockup Onayı', JobStep.kaliteKontrol => 'Kalite Kontrol', JobStep.teslimOncesiKontrol => 'Teslim Öncesi Kontrol', JobStep.cilaBitim => 'Cila / Bitim', }; /// One-liner shown under the step on the stepper String get description => switch (this) { JobStep.olcu => 'İlk ölçü alındı', JobStep.olcuKontrol => 'Ölçü, model veya kapanış kaydı kontrolü', JobStep.dijitalTasarim => 'Dijital tasarım ekranı veya mockup klinik onayı', JobStep.modelHazirlik => 'Model hazırlık, döküm veya artikülatör aşaması', JobStep.altYapiProva => 'Metal/zirkonyum coping klinik onayı', JobStep.ustYapiProva => 'Bisküvi pişirimi sonrası klinik onayı', JobStep.mumProva => 'Mum prova klinik onayı', JobStep.dislerProva => 'Diş dizimi klinik onayı', JobStep.dayanakProva => 'Dayanak klinik onayı', JobStep.kronProva => 'Kron klinik onayı', JobStep.fotografOnay => 'Fotoğraf veya mockup üzerinden klinik teyidi', JobStep.kaliteKontrol => 'Laboratuvar iç kalite kontrol aşaması', JobStep.teslimOncesiKontrol => 'Teslimat öncesi son iç kontrol', JobStep.cilaBitim => 'Son cila ve teslim hazırlığı', }; String get value => switch (this) { JobStep.olcu => 'olcu', JobStep.olcuKontrol => 'olcu_kontrol', JobStep.dijitalTasarim => 'dijital_tasarim', JobStep.modelHazirlik => 'model_hazirlik', JobStep.altYapiProva => 'alt_yapi_prova', JobStep.ustYapiProva => 'ust_yapi_prova', JobStep.mumProva => 'mum_prova', JobStep.dislerProva => 'disler_prova', JobStep.dayanakProva => 'dayanak_prova', JobStep.kronProva => 'kron_prova', JobStep.fotografOnay => 'fotograf_onay', JobStep.kaliteKontrol => 'kalite_kontrol', JobStep.teslimOncesiKontrol => 'teslim_oncesi_kontrol', JobStep.cilaBitim => 'cila_bitim', }; bool get requiresClinicApproval => switch (this) { JobStep.modelHazirlik || JobStep.kaliteKontrol || JobStep.teslimOncesiKontrol => false, _ => true, }; bool get isLabOptional => switch (this) { JobStep.modelHazirlik || JobStep.fotografOnay || JobStep.kaliteKontrol || JobStep.teslimOncesiKontrol => true, _ => false, }; } extension JobWorkflowTypeExt on JobWorkflowType { String get label => switch (this) { JobWorkflowType.arjinat => 'Arjinat', JobWorkflowType.geleneksel => 'Geleneksel', JobWorkflowType.dijital => 'Dijital', }; String get value => switch (this) { JobWorkflowType.arjinat => 'arjinat', JobWorkflowType.geleneksel => 'geleneksel', JobWorkflowType.dijital => 'dijital', }; } // ── Prosthetic type ─────────────────────────────────────────────────────────── extension ProstheticTypeExt on ProstheticType { String get label => switch (this) { ProstheticType.metalPorselen => 'Metal Porselen', ProstheticType.zirkonyum => 'Zirkonyum', ProstheticType.implantUstuZirkonyum => 'İmplant Üstü Zirkonyum', ProstheticType.gecici => 'Geçici', ProstheticType.eMax => 'E-Max', ProstheticType.tamProtez => 'Tam Protez', ProstheticType.parsiyel => 'Parsiyel Protez', ProstheticType.diger => 'Diğer', }; String get value => switch (this) { ProstheticType.metalPorselen => 'metal_porselen', ProstheticType.zirkonyum => 'zirkonyum', ProstheticType.implantUstuZirkonyum => 'implant_ustu_zirkonyum', ProstheticType.gecici => 'gecici', ProstheticType.eMax => 'e_max', ProstheticType.tamProtez => 'tam_protez', ProstheticType.parsiyel => 'parsiyel', ProstheticType.diger => 'diger', }; ProstheticFamily get family => switch (this) { ProstheticType.metalPorselen || ProstheticType.zirkonyum || ProstheticType.eMax => ProstheticFamily.sabit, ProstheticType.implantUstuZirkonyum => ProstheticFamily.implant, ProstheticType.tamProtez || ProstheticType.parsiyel => ProstheticFamily.hareketli, ProstheticType.gecici => ProstheticFamily.gecici, ProstheticType.diger => ProstheticFamily.ozel, }; } // ── Step template ───────────────────────────────────────────────────────────── class JobWorkflowPreset { const JobWorkflowPreset({ required this.title, required this.summary, required this.steps, }); final String title; final String summary; final List steps; } const optionalLabStepCatalog = [ JobStep.modelHazirlik, JobStep.fotografOnay, JobStep.kaliteKontrol, JobStep.teslimOncesiKontrol, ]; bool isOptionalStepApplicable( JobStep step, { required JobWorkflowType workflowType, required ProstheticFamily family, required bool provaRequired, }) { switch (step) { case JobStep.modelHazirlik: return workflowType != JobWorkflowType.dijital && family != ProstheticFamily.gecici; case JobStep.fotografOnay: return family != ProstheticFamily.hareketli || provaRequired; case JobStep.kaliteKontrol: case JobStep.teslimOncesiKontrol: return true; default: return false; } } List mergeOptionalLabSteps({ required List baseSteps, required List optionalSteps, required JobWorkflowType workflowType, required ProstheticFamily family, required bool provaRequired, }) { final merged = List.from(baseSteps); for (final step in optionalSteps) { if (!step.isLabOptional || merged.contains(step) || !isOptionalStepApplicable( step, workflowType: workflowType, family: family, provaRequired: provaRequired, )) { continue; } final finalIndex = merged.indexOf(JobStep.cilaBitim); switch (step) { case JobStep.modelHazirlik: final afterControl = merged.contains(JobStep.olcuKontrol) ? merged.indexOf(JobStep.olcuKontrol) + 1 : 0; merged.insert(afterControl, step); case JobStep.fotografOnay: case JobStep.kaliteKontrol: case JobStep.teslimOncesiKontrol: merged.insert(finalIndex.clamp(0, merged.length), step); default: break; } } return merged; } JobWorkflowPreset buildJobWorkflowPreset({ required ProstheticType prostheticType, JobWorkflowType? workflowType, required bool provaRequired, List optionalSteps = const [], }) { final normalizedWorkflow = workflowType ?? JobWorkflowType.geleneksel; final family = prostheticType.family; List steps; switch (normalizedWorkflow) { case JobWorkflowType.dijital: steps = switch (family) { ProstheticFamily.sabit => provaRequired ? const [ JobStep.dijitalTasarim, JobStep.ustYapiProva, JobStep.cilaBitim, ] : const [ JobStep.dijitalTasarim, JobStep.cilaBitim, ], ProstheticFamily.implant => provaRequired ? const [ JobStep.dijitalTasarim, JobStep.dayanakProva, JobStep.kronProva, JobStep.cilaBitim, ] : const [ JobStep.dijitalTasarim, JobStep.cilaBitim, ], ProstheticFamily.hareketli => provaRequired ? const [ JobStep.dijitalTasarim, JobStep.mumProva, JobStep.dislerProva, JobStep.cilaBitim, ] : const [ JobStep.dijitalTasarim, JobStep.cilaBitim, ], ProstheticFamily.gecici => const [ JobStep.dijitalTasarim, JobStep.cilaBitim, ], ProstheticFamily.ozel => provaRequired ? const [ JobStep.dijitalTasarim, JobStep.altYapiProva, JobStep.cilaBitim, ] : const [ JobStep.dijitalTasarim, JobStep.cilaBitim, ], }; case JobWorkflowType.arjinat: case JobWorkflowType.geleneksel: steps = switch (family) { ProstheticFamily.sabit => provaRequired ? const [ JobStep.olcuKontrol, JobStep.altYapiProva, JobStep.ustYapiProva, JobStep.cilaBitim, ] : const [ JobStep.olcuKontrol, JobStep.cilaBitim, ], ProstheticFamily.implant => provaRequired ? const [ JobStep.olcuKontrol, JobStep.dayanakProva, JobStep.kronProva, JobStep.cilaBitim, ] : const [ JobStep.olcuKontrol, JobStep.cilaBitim, ], ProstheticFamily.hareketli => provaRequired ? const [ JobStep.olcuKontrol, JobStep.mumProva, JobStep.dislerProva, JobStep.cilaBitim, ] : const [ JobStep.olcuKontrol, JobStep.cilaBitim, ], ProstheticFamily.gecici => const [ JobStep.olcuKontrol, JobStep.cilaBitim, ], ProstheticFamily.ozel => provaRequired ? const [ JobStep.olcuKontrol, JobStep.altYapiProva, JobStep.cilaBitim, ] : const [ JobStep.olcuKontrol, JobStep.cilaBitim, ], }; } final title = '${normalizedWorkflow.label} · ${provaRequired ? "Provalı" : "Provasız"}'; final summary = switch ((normalizedWorkflow, family, provaRequired)) { (JobWorkflowType.dijital, ProstheticFamily.sabit, false) => 'Dijital tasarım onayı sonrası üretim ve teslim odaklı akış.', (JobWorkflowType.dijital, _, false) => 'Fiziksel prova azaltılmış, dijital onay ve hızlı teslim akışı.', (JobWorkflowType.dijital, _, true) => 'Dijital hazırlık üzerine klinik prova kapıları eklenmiş hibrit akış.', (JobWorkflowType.arjinat, _, false) => 'Ölçü/model kontrolü sonrası doğrudan üretim ve teslim akışı.', (JobWorkflowType.arjinat, _, true) => 'Arjinat ölçüden gelen işlerde klinik prova kapılarıyla ilerleyen akış.', (JobWorkflowType.geleneksel, _, false) => 'Klasik ölçüden gelen, minimum temaslı ve hızlı teslim akışı.', (JobWorkflowType.geleneksel, _, true) => 'Klasik laboratuvar süreçlerine uygun, prova bazlı aşamalı akış.', }; return JobWorkflowPreset( title: title, summary: summary, steps: mergeOptionalLabSteps( baseSteps: steps, optionalSteps: optionalSteps, workflowType: normalizedWorkflow, family: family, provaRequired: provaRequired, ), ); } /// Returns the ordered clinic-facing approval steps for a job. List jobStepTemplate( ProstheticType type, bool provaRequired, { JobWorkflowType? workflowType, List optionalSteps = const [], }) { return buildJobWorkflowPreset( prostheticType: type, workflowType: workflowType, provaRequired: provaRequired, optionalSteps: optionalSteps, ).steps; } // ── Job ─────────────────────────────────────────────────────────────────────── class Job { const Job({ required this.id, required this.clinicTenantId, required this.labTenantId, required this.patientCode, required this.prostheticType, required this.memberCount, required this.status, required this.dateCreated, this.patientId, this.patientName, this.prostheticId, this.prostheticName, this.teeth = const [], this.color, this.description, this.price, this.currency, this.currentStep, this.location = JobLocation.atClinic, this.workflowType, this.dueDate, this.clinicName, this.labName, this.attachments = const [], this.provaRequired = true, this.workflowSteps = const [], }); final String id; final String clinicTenantId; final String labTenantId; final String? patientId; final String? patientName; final String patientCode; final String? prostheticId; final String? prostheticName; final ProstheticType prostheticType; final int memberCount; final List teeth; final String? color; final String? description; final double? price; final String? currency; final JobStatus status; final JobStep? currentStep; final JobLocation location; final JobWorkflowType? workflowType; final DateTime? dueDate; final DateTime dateCreated; final List attachments; final bool provaRequired; final List workflowSteps; // Denormalized from relation joins — list views only final String? clinicName; final String? labName; // ── copyWith ────────────────────────────────────────────────────────────── Job copyWith({ JobStatus? status, JobStep? currentStep, JobLocation? location, JobWorkflowType? workflowType, String? clinicName, String? labName, bool clearCurrentStep = false, }) => Job( id: id, clinicTenantId: clinicTenantId, labTenantId: labTenantId, patientId: patientId, patientName: patientName, patientCode: patientCode, prostheticId: prostheticId, prostheticName: prostheticName, prostheticType: prostheticType, memberCount: memberCount, teeth: teeth, color: color, description: description, price: price, currency: currency, status: status ?? this.status, currentStep: clearCurrentStep ? null : (currentStep ?? this.currentStep), location: location ?? this.location, workflowType: workflowType ?? this.workflowType, dueDate: dueDate, dateCreated: dateCreated, attachments: attachments, provaRequired: provaRequired, workflowSteps: workflowSteps, clinicName: clinicName ?? this.clinicName, labName: labName ?? this.labName, ); // ── Step helpers ────────────────────────────────────────────────────────── List get stepTemplate => workflowSteps.isNotEmpty ? workflowSteps : jobStepTemplate( prostheticType, provaRequired, workflowType: workflowType, optionalSteps: workflowSteps.where((step) => step.isLabOptional).toList(), ); JobWorkflowPreset get workflowPreset { final preset = buildJobWorkflowPreset( prostheticType: prostheticType, workflowType: workflowType, provaRequired: provaRequired, optionalSteps: workflowSteps.where((step) => step.isLabOptional).toList(), ); if (workflowSteps.isEmpty) return preset; return JobWorkflowPreset( title: preset.title, summary: preset.summary, steps: workflowSteps, ); } bool get isLastStep => currentStep != null && currentStep == stepTemplate.last; /// The next step after currentStep in this job's template, or null if done. JobStep? get nextStep { if (currentStep == null) return stepTemplate.firstOrNull; final idx = stepTemplate.indexOf(currentStep!); if (idx < 0 || idx >= stepTemplate.length - 1) return null; return stepTemplate[idx + 1]; } factory Job.fromJson(Map j) { final expand = j['expand'] as Map?; final clinicExp = expand?['clinic_tenant_id'] as Map?; final labExp = expand?['lab_tenant_id'] as Map?; final patientExp = expand?['patient_id'] as Map?; final prostheticExp = expand?['prosthetic_id'] as Map?; String? str(dynamic v) { final s = v as String?; return (s == null || s.isEmpty) ? null : s; } return Job( id: j['id'] as String, clinicTenantId: j['clinic_tenant_id'] as String, labTenantId: j['lab_tenant_id'] as String, patientId: str(j['patient_id']), patientName: _patientName(patientExp), patientCode: j['patient_code'] as String, prostheticId: str(j['prosthetic_id']), prostheticName: prostheticExp?['name'] as String?, prostheticType: _parseProstheticType(j['prosthetic_type'] as String), memberCount: (j['member_count'] as num).toInt(), teeth: j['teeth'] is List ? (j['teeth'] as List).map((e) => e.toString()).toList() : [], color: str(j['color']), description: str(j['description']), price: (j['price'] as num?)?.toDouble(), currency: str(j['currency']), status: _parseStatus(j['status'] as String), currentStep: str(j['current_step']) != null ? _parseStep(j['current_step'] as String) : null, location: j['location'] == 'at_lab' ? JobLocation.atLab : JobLocation.atClinic, workflowType: str(j['workflow_type']) != null ? _parseWorkflowType(j['workflow_type'] as String) : null, dueDate: str(j['due_date']) != null ? DateTime.parse(j['due_date'] as String) : null, dateCreated: DateTime.parse(j['created'] as String), clinicName: clinicExp?['company_name'] as String?, labName: labExp?['company_name'] as String?, attachments: j['attachments'] is List ? (j['attachments'] as List).map((e) => e.toString()).toList() : [], provaRequired: (j['prova_required'] as bool?) ?? true, workflowSteps: j['workflow_steps'] is List ? (j['workflow_steps'] as List) .map((e) => _parseStep(e.toString())) .toList() : const [], ); } static JobStatus _parseStatus(String s) => switch (s) { 'in_progress' => JobStatus.inProgress, 'sent' => JobStatus.sent, 'delivered' => JobStatus.delivered, 'cancelled' => JobStatus.cancelled, _ => JobStatus.pending, }; static JobStep _parseStep(String s) => switch (s) { 'olcu_kontrol' => JobStep.olcuKontrol, 'dijital_tasarim' => JobStep.dijitalTasarim, 'model_hazirlik' => JobStep.modelHazirlik, 'alt_yapi_prova' => JobStep.altYapiProva, 'ust_yapi_prova' => JobStep.ustYapiProva, 'mum_prova' => JobStep.mumProva, 'disler_prova' => JobStep.dislerProva, 'dayanak_prova' => JobStep.dayanakProva, 'kron_prova' => JobStep.kronProva, 'fotograf_onay' => JobStep.fotografOnay, 'kalite_kontrol' => JobStep.kaliteKontrol, 'teslim_oncesi_kontrol' => JobStep.teslimOncesiKontrol, 'cila_bitim' => JobStep.cilaBitim, _ => JobStep.olcu, }; static JobStep parseStepValue(String s) => _parseStep(s); static JobWorkflowType _parseWorkflowType(String s) => switch (s) { 'arjinat' => JobWorkflowType.arjinat, 'dijital' => JobWorkflowType.dijital, _ => JobWorkflowType.geleneksel, }; static String? _patientName(Map? patientExp) { if (patientExp == null) return null; final first = (patientExp['first_name'] as String?)?.trim(); final last = (patientExp['last_name'] as String?)?.trim(); final parts = [first, last] .where((part) => part != null && part.isNotEmpty) .cast() .toList(); if (parts.isNotEmpty) return parts.join(' '); final code = (patientExp['patient_code'] as String?)?.trim(); return (code == null || code.isEmpty) ? null : code; } static ProstheticType _parseProstheticType(String s) => switch (s) { 'zirkonyum' => ProstheticType.zirkonyum, 'implant_ustu_zirkonyum' => ProstheticType.implantUstuZirkonyum, 'gecici' => ProstheticType.gecici, 'e_max' => ProstheticType.eMax, 'tam_protez' => ProstheticType.tamProtez, 'parsiyel' => ProstheticType.parsiyel, 'diger' => ProstheticType.diger, _ => ProstheticType.metalPorselen, }; }