enum JobStatus { pending, inProgress, sent, delivered, cancelled } enum JobStep { olcu, // legacy fallback 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 cilaBitim, // son cila / bitim (her şablonda son adım) } enum JobLocation { atClinic, atLab } 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.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.cilaBitim => 'Cila / Bitim', }; /// One-liner shown under the step on the stepper String get description => switch (this) { JobStep.olcu => 'İlk ölçü alındı', 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.cilaBitim => 'Son cila ve teslim hazırlığı', }; String get value => switch (this) { JobStep.olcu => 'olcu', 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.cilaBitim => 'cila_bitim', }; } // ── 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', }; } // ── Step template ───────────────────────────────────────────────────────────── /// Returns the ordered step list for a given prosthetic type + prova flag. List jobStepTemplate(ProstheticType type, bool provaRequired) { if (!provaRequired) return const [JobStep.cilaBitim]; return switch (type) { // Sabit seramik: alt yapı coping + bisküvi prova + cila ProstheticType.metalPorselen || ProstheticType.zirkonyum || ProstheticType.eMax => const [JobStep.altYapiProva, JobStep.ustYapiProva, JobStep.cilaBitim], // İmplant: dayanak + kron prova + cila ProstheticType.implantUstuZirkonyum => const [JobStep.dayanakProva, JobStep.kronProva, JobStep.cilaBitim], // Hareketli protez: mum + dişler prova + cila ProstheticType.tamProtez || ProstheticType.parsiyel => const [JobStep.mumProva, JobStep.dislerProva, JobStep.cilaBitim], // Geçici: sadece cila (prova gereksiz) ProstheticType.gecici => const [JobStep.cilaBitim], // Diğer: tek ara prova + cila _ => const [JobStep.altYapiProva, JobStep.cilaBitim], }; } // ── 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.prostheticId, this.teeth = const [], this.color, this.description, this.price, this.currency, this.currentStep, this.location = JobLocation.atClinic, this.dueDate, this.clinicName, this.labName, this.attachments = const [], this.provaRequired = true, }); final String id; final String clinicTenantId; final String labTenantId; final String? patientId; final String patientCode; final String? prostheticId; 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 DateTime? dueDate; final DateTime dateCreated; final List attachments; final bool provaRequired; // Denormalized from relation joins — list views only final String? clinicName; final String? labName; // ── copyWith ────────────────────────────────────────────────────────────── Job copyWith({ JobStatus? status, JobStep? currentStep, JobLocation? location, String? clinicName, String? labName, bool clearCurrentStep = false, }) => Job( id: id, clinicTenantId: clinicTenantId, labTenantId: labTenantId, patientId: patientId, patientCode: patientCode, prostheticId: prostheticId, 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, dueDate: dueDate, dateCreated: dateCreated, attachments: attachments, provaRequired: provaRequired, clinicName: clinicName ?? this.clinicName, labName: labName ?? this.labName, ); // ── Step helpers ────────────────────────────────────────────────────────── List get stepTemplate => jobStepTemplate(prostheticType, provaRequired); 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?; 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']), patientCode: j['patient_code'] as String, prostheticId: str(j['prosthetic_id']), 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, 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, ); } 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) { '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, 'cila_bitim' => JobStep.cilaBitim, _ => JobStep.olcu, }; 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, }; }