Files
2026-06-20 18:24:40 +03:00

675 lines
23 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<JobStep> steps;
}
const optionalLabStepCatalog = <JobStep>[
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<JobStep> mergeOptionalLabSteps({
required List<JobStep> baseSteps,
required List<JobStep> optionalSteps,
required JobWorkflowType workflowType,
required ProstheticFamily family,
required bool provaRequired,
}) {
final merged = List<JobStep>.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<JobStep> optionalSteps = const [],
}) {
final normalizedWorkflow = workflowType ?? JobWorkflowType.geleneksel;
final family = prostheticType.family;
List<JobStep> 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<JobStep> jobStepTemplate(
ProstheticType type,
bool provaRequired, {
JobWorkflowType? workflowType,
List<JobStep> 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<String> 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<String> attachments;
final bool provaRequired;
final List<JobStep> 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<JobStep> 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<String, dynamic> j) {
final expand = j['expand'] as Map<String, dynamic>?;
final clinicExp = expand?['clinic_tenant_id'] as Map<String, dynamic>?;
final labExp = expand?['lab_tenant_id'] as Map<String, dynamic>?;
final patientExp = expand?['patient_id'] as Map<String, dynamic>?;
final prostheticExp = expand?['prosthetic_id'] as Map<String, dynamic>?;
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<String, dynamic>? 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<String>()
.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,
};
}