Initial commit: DLS - Dental Lab System

- Flutter + PocketBase dental lab management system
- Clinic & lab dashboards, job tracking, patient management
- Product catalog, finance tracking, multi-language support
- AI assistant integration, realtime notifications
- Windows installer (Inno Setup) included
- Developed by kovakyazilim.com
This commit is contained in:
Emre Emir
2026-06-11 15:57:31 +03:00
commit 8bbc9dbff2
226 changed files with 31308 additions and 0 deletions
+312
View File
@@ -0,0 +1,312 @@
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<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.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<String> 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<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,
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<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>?;
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,
};
}