Files
lab-app/lib/models/job.dart
T
2026-06-12 00:04:53 +03:00

365 lines
14 KiB
Dart
Raw 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
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 JobWorkflowType { arjinat, geleneksel, dijital }
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',
};
}
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',
};
}
// ── Step template ─────────────────────────────────────────────────────────────
/// Returns the ordered step list for a given prosthetic type + prova flag.
List<JobStep> 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.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,
});
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;
// 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,
clinicName: clinicName ?? this.clinicName,
labName: labName ?? this.labName,
);
// ── Step helpers ──────────────────────────────────────────────────────────
List<JobStep> 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<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,
);
}
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 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,
};
}