Add pricing entry flow and platform admin foundations
This commit is contained in:
@@ -5,11 +5,23 @@ extension FinanceTypeX on FinanceType {
|
||||
String get label => this == FinanceType.receivable ? 'Alacak' : 'Borç';
|
||||
}
|
||||
|
||||
enum FinanceStatus { pending, paid }
|
||||
enum FinanceStatus { pending, reported, paid }
|
||||
|
||||
extension FinanceStatusX on FinanceStatus {
|
||||
String get value => name;
|
||||
String get label => this == FinanceStatus.pending ? 'Bekliyor' : 'Ödendi';
|
||||
String get label {
|
||||
switch (this) {
|
||||
case FinanceStatus.pending:
|
||||
return 'Bekliyor';
|
||||
case FinanceStatus.reported:
|
||||
return 'Onay Bekliyor';
|
||||
case FinanceStatus.paid:
|
||||
return 'Onaylandı';
|
||||
}
|
||||
}
|
||||
|
||||
bool get isOpen =>
|
||||
this == FinanceStatus.pending || this == FinanceStatus.reported;
|
||||
}
|
||||
|
||||
class FinanceEntry {
|
||||
@@ -44,7 +56,11 @@ class FinanceEntry {
|
||||
factory FinanceEntry.fromJson(Map<String, dynamic> j) {
|
||||
final expand = j['expand'] as Map<String, dynamic>?;
|
||||
final jobExp = expand?['job_id'] as Map<String, dynamic>?;
|
||||
String? _str(dynamic v) { final s = v as String?; return (s == null || s.isEmpty) ? null : s; }
|
||||
String? parseOptionalString(dynamic v) {
|
||||
final s = v as String?;
|
||||
return (s == null || s.isEmpty) ? null : s;
|
||||
}
|
||||
|
||||
return FinanceEntry(
|
||||
id: j['id'] as String,
|
||||
tenantId: j['tenant_id'] as String,
|
||||
@@ -55,9 +71,9 @@ class FinanceEntry {
|
||||
currency: j['currency'] as String? ?? 'TRY',
|
||||
status: FinanceStatus.values.firstWhere((e) => e.value == j['status'],
|
||||
orElse: () => FinanceStatus.pending),
|
||||
counterpartyTenantId: _str(j['counterparty_tenant_id']),
|
||||
paidAt: _str(j['paid_at']),
|
||||
counterpartyName: _str(j['counterparty_name']),
|
||||
counterpartyTenantId: parseOptionalString(j['counterparty_tenant_id']),
|
||||
paidAt: parseOptionalString(j['paid_at']),
|
||||
counterpartyName: parseOptionalString(j['counterparty_name']),
|
||||
patientCode: jobExp?['patient_code'] as String?,
|
||||
dateCreated: j['created'] as String?,
|
||||
);
|
||||
|
||||
+397
-87
@@ -1,20 +1,28 @@
|
||||
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)
|
||||
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,
|
||||
@@ -30,18 +38,18 @@ enum ProstheticType {
|
||||
|
||||
extension JobStatusExt on JobStatus {
|
||||
String get label => switch (this) {
|
||||
JobStatus.pending => 'Bekliyor',
|
||||
JobStatus.pending => 'Bekliyor',
|
||||
JobStatus.inProgress => 'İşlemde',
|
||||
JobStatus.sent => 'Gönderildi',
|
||||
JobStatus.delivered => 'Teslim Alındı',
|
||||
JobStatus.cancelled => 'İptal',
|
||||
JobStatus.sent => 'Gönderildi',
|
||||
JobStatus.delivered => 'Teslim Alındı',
|
||||
JobStatus.cancelled => 'İptal',
|
||||
};
|
||||
String get value => switch (this) {
|
||||
JobStatus.pending => 'pending',
|
||||
JobStatus.pending => 'pending',
|
||||
JobStatus.inProgress => 'in_progress',
|
||||
JobStatus.sent => 'sent',
|
||||
JobStatus.delivered => 'delivered',
|
||||
JobStatus.cancelled => 'cancelled',
|
||||
JobStatus.sent => 'sent',
|
||||
JobStatus.delivered => 'delivered',
|
||||
JobStatus.cancelled => 'cancelled',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,37 +57,74 @@ extension JobStatusExt on JobStatus {
|
||||
|
||||
extension JobStepExt on JobStep {
|
||||
String get label => switch (this) {
|
||||
JobStep.olcu => 'Ölçü',
|
||||
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.mumProva => 'Mum Prova',
|
||||
JobStep.dislerProva => 'Dişler Prova',
|
||||
JobStep.dayanakProva => 'Dayanak Prova',
|
||||
JobStep.kronProva => 'Kron Prova',
|
||||
JobStep.cilaBitim => 'Cila / Bitim',
|
||||
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.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.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ığı',
|
||||
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.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.mumProva => 'mum_prova',
|
||||
JobStep.dislerProva => 'disler_prova',
|
||||
JobStep.dayanakProva => 'dayanak_prova',
|
||||
JobStep.kronProva => 'kron_prova',
|
||||
JobStep.cilaBitim => 'cila_bitim',
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,52 +146,277 @@ extension JobWorkflowTypeExt on JobWorkflowType {
|
||||
|
||||
extension ProstheticTypeExt on ProstheticType {
|
||||
String get label => switch (this) {
|
||||
ProstheticType.metalPorselen => 'Metal Porselen',
|
||||
ProstheticType.zirkonyum => 'Zirkonyum',
|
||||
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',
|
||||
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.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',
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// 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],
|
||||
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 ───────────────────────────────────────────────────────────────────────
|
||||
@@ -178,6 +448,7 @@ class Job {
|
||||
this.labName,
|
||||
this.attachments = const [],
|
||||
this.provaRequired = true,
|
||||
this.workflowSteps = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
@@ -203,6 +474,7 @@ class Job {
|
||||
final DateTime dateCreated;
|
||||
final List<String> attachments;
|
||||
final bool provaRequired;
|
||||
final List<JobStep> workflowSteps;
|
||||
|
||||
// Denormalized from relation joins — list views only
|
||||
final String? clinicName;
|
||||
@@ -236,20 +508,45 @@ class Job {
|
||||
price: price,
|
||||
currency: currency,
|
||||
status: status ?? this.status,
|
||||
currentStep: clearCurrentStep ? null : (currentStep ?? this.currentStep),
|
||||
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 => jobStepTemplate(prostheticType, provaRequired);
|
||||
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;
|
||||
@@ -310,28 +607,41 @@ class Job {
|
||||
? (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,
|
||||
'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,
|
||||
'cila_bitim' => JobStep.cilaBitim,
|
||||
_ => JobStep.olcu,
|
||||
'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,
|
||||
@@ -352,13 +662,13 @@ class Job {
|
||||
}
|
||||
|
||||
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,
|
||||
'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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import 'tenant.dart';
|
||||
|
||||
enum PlatformRole {
|
||||
superAdmin,
|
||||
support,
|
||||
financeOps,
|
||||
operations,
|
||||
readOnly,
|
||||
;
|
||||
|
||||
String get value => switch (this) {
|
||||
PlatformRole.superAdmin => 'super_admin',
|
||||
PlatformRole.support => 'support',
|
||||
PlatformRole.financeOps => 'finance_ops',
|
||||
PlatformRole.operations => 'operations',
|
||||
PlatformRole.readOnly => 'read_only',
|
||||
};
|
||||
|
||||
String get label => switch (this) {
|
||||
PlatformRole.superAdmin => 'Super Admin',
|
||||
PlatformRole.support => 'Destek',
|
||||
PlatformRole.financeOps => 'Finans Operasyon',
|
||||
PlatformRole.operations => 'Operasyon',
|
||||
PlatformRole.readOnly => 'Sadece Görüntüleme',
|
||||
};
|
||||
|
||||
static PlatformRole parse(String raw) => switch (raw) {
|
||||
'super_admin' => PlatformRole.superAdmin,
|
||||
'support' => PlatformRole.support,
|
||||
'finance_ops' => PlatformRole.financeOps,
|
||||
'operations' => PlatformRole.operations,
|
||||
'read_only' => PlatformRole.readOnly,
|
||||
_ => PlatformRole.readOnly,
|
||||
};
|
||||
}
|
||||
|
||||
class PlatformMembership {
|
||||
const PlatformMembership({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.role,
|
||||
this.status = 'active',
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final PlatformRole role;
|
||||
final String status;
|
||||
|
||||
bool get isActive => status == 'active';
|
||||
bool get isSuperAdmin => role == PlatformRole.superAdmin && isActive;
|
||||
bool get canManageBilling =>
|
||||
isActive &&
|
||||
(role == PlatformRole.superAdmin || role == PlatformRole.financeOps);
|
||||
bool get canManageTenants =>
|
||||
isActive &&
|
||||
(role == PlatformRole.superAdmin ||
|
||||
role == PlatformRole.operations ||
|
||||
role == PlatformRole.support);
|
||||
|
||||
factory PlatformMembership.fromJson(Map<String, dynamic> json) {
|
||||
return PlatformMembership(
|
||||
id: json['id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
role: PlatformRole.parse(json['role'] as String? ?? ''),
|
||||
status: (json['status'] as String?) ?? 'active',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum SubscriptionStatus { trialing, active, pastDue, cancelled, paused }
|
||||
|
||||
extension SubscriptionStatusX on SubscriptionStatus {
|
||||
String get value => switch (this) {
|
||||
SubscriptionStatus.trialing => 'trialing',
|
||||
SubscriptionStatus.active => 'active',
|
||||
SubscriptionStatus.pastDue => 'past_due',
|
||||
SubscriptionStatus.cancelled => 'cancelled',
|
||||
SubscriptionStatus.paused => 'paused',
|
||||
};
|
||||
|
||||
static SubscriptionStatus parse(String raw) => switch (raw) {
|
||||
'trialing' => SubscriptionStatus.trialing,
|
||||
'active' => SubscriptionStatus.active,
|
||||
'past_due' => SubscriptionStatus.pastDue,
|
||||
'cancelled' => SubscriptionStatus.cancelled,
|
||||
'paused' => SubscriptionStatus.paused,
|
||||
_ => SubscriptionStatus.trialing,
|
||||
};
|
||||
}
|
||||
|
||||
class TenantSubscription {
|
||||
const TenantSubscription({
|
||||
required this.id,
|
||||
required this.tenantId,
|
||||
required this.plan,
|
||||
required this.status,
|
||||
this.billingProvider,
|
||||
this.providerCustomerId,
|
||||
this.providerSubscriptionId,
|
||||
this.periodStart,
|
||||
this.periodEnd,
|
||||
this.aiMonthlyCredits = 0,
|
||||
this.aiBonusCredits = 0,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String tenantId;
|
||||
final TenantPlan plan;
|
||||
final SubscriptionStatus status;
|
||||
final String? billingProvider;
|
||||
final String? providerCustomerId;
|
||||
final String? providerSubscriptionId;
|
||||
final DateTime? periodStart;
|
||||
final DateTime? periodEnd;
|
||||
final int aiMonthlyCredits;
|
||||
final int aiBonusCredits;
|
||||
|
||||
int get totalAiCredits => aiMonthlyCredits + aiBonusCredits;
|
||||
|
||||
factory TenantSubscription.fromJson(Map<String, dynamic> json) {
|
||||
return TenantSubscription(
|
||||
id: json['id'] as String,
|
||||
tenantId: json['tenant_id'] as String,
|
||||
plan: Tenant.parsePlanValue(json['plan'] as String?),
|
||||
status: SubscriptionStatusX.parse(json['status'] as String? ?? ''),
|
||||
billingProvider: json['billing_provider'] as String?,
|
||||
providerCustomerId: json['provider_customer_id'] as String?,
|
||||
providerSubscriptionId: json['provider_subscription_id'] as String?,
|
||||
periodStart: _parseDate(json['period_start']),
|
||||
periodEnd: _parseDate(json['period_end']),
|
||||
aiMonthlyCredits: (json['ai_monthly_credits'] as num?)?.toInt() ?? 0,
|
||||
aiBonusCredits: (json['ai_bonus_credits'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum AiCreditEntryType {
|
||||
monthlyAllocation,
|
||||
bonusAllocation,
|
||||
usageDebit,
|
||||
manualAdjustment,
|
||||
refund,
|
||||
expire,
|
||||
;
|
||||
|
||||
String get value => switch (this) {
|
||||
AiCreditEntryType.monthlyAllocation => 'monthly_allocation',
|
||||
AiCreditEntryType.bonusAllocation => 'bonus_allocation',
|
||||
AiCreditEntryType.usageDebit => 'usage_debit',
|
||||
AiCreditEntryType.manualAdjustment => 'manual_adjustment',
|
||||
AiCreditEntryType.refund => 'refund',
|
||||
AiCreditEntryType.expire => 'expire',
|
||||
};
|
||||
|
||||
static AiCreditEntryType parse(String raw) => switch (raw) {
|
||||
'monthly_allocation' => AiCreditEntryType.monthlyAllocation,
|
||||
'bonus_allocation' => AiCreditEntryType.bonusAllocation,
|
||||
'usage_debit' => AiCreditEntryType.usageDebit,
|
||||
'manual_adjustment' => AiCreditEntryType.manualAdjustment,
|
||||
'refund' => AiCreditEntryType.refund,
|
||||
'expire' => AiCreditEntryType.expire,
|
||||
_ => AiCreditEntryType.manualAdjustment,
|
||||
};
|
||||
}
|
||||
|
||||
class AiCreditLedgerEntry {
|
||||
const AiCreditLedgerEntry({
|
||||
required this.id,
|
||||
required this.tenantId,
|
||||
required this.entryType,
|
||||
required this.delta,
|
||||
required this.balanceAfter,
|
||||
this.referenceType,
|
||||
this.referenceId,
|
||||
this.note,
|
||||
this.createdByUserId,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String tenantId;
|
||||
final AiCreditEntryType entryType;
|
||||
final int delta;
|
||||
final int balanceAfter;
|
||||
final String? referenceType;
|
||||
final String? referenceId;
|
||||
final String? note;
|
||||
final String? createdByUserId;
|
||||
final DateTime? createdAt;
|
||||
|
||||
factory AiCreditLedgerEntry.fromJson(Map<String, dynamic> json) {
|
||||
return AiCreditLedgerEntry(
|
||||
id: json['id'] as String,
|
||||
tenantId: json['tenant_id'] as String,
|
||||
entryType: AiCreditEntryType.parse(json['entry_type'] as String? ?? ''),
|
||||
delta: (json['delta'] as num?)?.toInt() ?? 0,
|
||||
balanceAfter: (json['balance_after'] as num?)?.toInt() ?? 0,
|
||||
referenceType: json['reference_type'] as String?,
|
||||
referenceId: json['reference_id'] as String?,
|
||||
note: json['note'] as String?,
|
||||
createdByUserId: json['created_by_user_id'] as String?,
|
||||
createdAt: _parseDate(json['created']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AiUsageLog {
|
||||
const AiUsageLog({
|
||||
required this.id,
|
||||
required this.tenantId,
|
||||
required this.userId,
|
||||
required this.action,
|
||||
required this.creditCost,
|
||||
this.model,
|
||||
this.jobId,
|
||||
this.tokenInput,
|
||||
this.tokenOutput,
|
||||
this.latencyMs,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String tenantId;
|
||||
final String userId;
|
||||
final String action;
|
||||
final int creditCost;
|
||||
final String? model;
|
||||
final String? jobId;
|
||||
final int? tokenInput;
|
||||
final int? tokenOutput;
|
||||
final int? latencyMs;
|
||||
final DateTime? createdAt;
|
||||
|
||||
factory AiUsageLog.fromJson(Map<String, dynamic> json) {
|
||||
return AiUsageLog(
|
||||
id: json['id'] as String,
|
||||
tenantId: json['tenant_id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
action: json['action'] as String? ?? '',
|
||||
creditCost: (json['credit_cost'] as num?)?.toInt() ?? 0,
|
||||
model: json['model'] as String?,
|
||||
jobId: json['job_id'] as String?,
|
||||
tokenInput: (json['token_input'] as num?)?.toInt(),
|
||||
tokenOutput: (json['token_output'] as num?)?.toInt(),
|
||||
latencyMs: (json['latency_ms'] as num?)?.toInt(),
|
||||
createdAt: _parseDate(json['created']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AdminAuditLog {
|
||||
const AdminAuditLog({
|
||||
required this.id,
|
||||
required this.actorUserId,
|
||||
required this.actorRole,
|
||||
required this.actionType,
|
||||
this.targetCollection,
|
||||
this.targetRecordId,
|
||||
this.targetTenantId,
|
||||
this.summary,
|
||||
this.metadata,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String actorUserId;
|
||||
final PlatformRole actorRole;
|
||||
final String actionType;
|
||||
final String? targetCollection;
|
||||
final String? targetRecordId;
|
||||
final String? targetTenantId;
|
||||
final String? summary;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final DateTime? createdAt;
|
||||
|
||||
factory AdminAuditLog.fromJson(Map<String, dynamic> json) {
|
||||
final metadata = json['metadata'];
|
||||
return AdminAuditLog(
|
||||
id: json['id'] as String,
|
||||
actorUserId: json['actor_user_id'] as String,
|
||||
actorRole: PlatformRole.parse(json['actor_role'] as String? ?? ''),
|
||||
actionType: json['action_type'] as String? ?? '',
|
||||
targetCollection: json['target_collection'] as String?,
|
||||
targetRecordId: json['target_record_id'] as String?,
|
||||
targetTenantId: json['target_tenant_id'] as String?,
|
||||
summary: json['summary'] as String?,
|
||||
metadata: metadata is Map<String, dynamic> ? metadata : null,
|
||||
createdAt: _parseDate(json['created']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? _parseDate(dynamic raw) {
|
||||
final value = raw as String?;
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return DateTime.tryParse(value);
|
||||
}
|
||||
+90
-29
@@ -1,25 +1,27 @@
|
||||
import 'job.dart';
|
||||
|
||||
enum TenantKind { lab, clinic }
|
||||
|
||||
enum TenantRole {
|
||||
owner,
|
||||
admin,
|
||||
technician, // lab: işler + ürünler
|
||||
delivery, // lab: işler
|
||||
finance, // lab+clinic: finans
|
||||
doctor, // clinic: işler + hastalar
|
||||
member, // legacy — full access
|
||||
delivery, // lab: işler
|
||||
finance, // lab+clinic: finans
|
||||
doctor, // clinic: işler + hastalar
|
||||
member, // legacy — full access
|
||||
;
|
||||
|
||||
String get value => name;
|
||||
|
||||
String get label => switch (this) {
|
||||
TenantRole.owner => 'Sahibi',
|
||||
TenantRole.admin => 'Yönetici',
|
||||
TenantRole.owner => 'Sahibi',
|
||||
TenantRole.admin => 'Yönetici',
|
||||
TenantRole.technician => 'Teknisyen',
|
||||
TenantRole.delivery => 'Teslimat Elemanı',
|
||||
TenantRole.finance => 'Finans Elemanı',
|
||||
TenantRole.doctor => 'Hekim',
|
||||
TenantRole.member => 'Üye',
|
||||
TenantRole.delivery => 'Teslimat Elemanı',
|
||||
TenantRole.finance => 'Finans Elemanı',
|
||||
TenantRole.doctor => 'Hekim',
|
||||
TenantRole.member => 'Üye',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,36 +33,71 @@ class Tenant {
|
||||
required this.kind,
|
||||
required this.memberNumber,
|
||||
required this.companyName,
|
||||
this.companyAddress,
|
||||
this.city,
|
||||
this.district,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.logo,
|
||||
this.defaultCurrency = 'TRY',
|
||||
this.status = 'active',
|
||||
this.plan,
|
||||
this.maxMembers,
|
||||
this.workflowOverrideStepKeys = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
final TenantKind kind;
|
||||
final String memberNumber;
|
||||
final String companyName;
|
||||
final String? companyAddress;
|
||||
final String? city;
|
||||
final String? district;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? logo;
|
||||
final String defaultCurrency;
|
||||
final String status;
|
||||
final TenantPlan? plan;
|
||||
final int? maxMembers;
|
||||
final List<String> workflowOverrideStepKeys;
|
||||
|
||||
bool get isLab => kind == TenantKind.lab;
|
||||
bool get isClinic => kind == TenantKind.clinic;
|
||||
bool get hasLocation => latitude != null && longitude != null;
|
||||
List<JobStep> get workflowOverrideSteps => workflowOverrideStepKeys
|
||||
.map(Job.parseStepValue)
|
||||
.where((step) => step.isLabOptional)
|
||||
.toList();
|
||||
String get locationLabel {
|
||||
final parts = [
|
||||
if ((district ?? '').trim().isNotEmpty) district!.trim(),
|
||||
if ((city ?? '').trim().isNotEmpty) city!.trim(),
|
||||
];
|
||||
if (parts.isNotEmpty) return parts.join(' / ');
|
||||
return (companyAddress ?? '').trim();
|
||||
}
|
||||
|
||||
factory Tenant.fromJson(Map<String, dynamic> j) => Tenant(
|
||||
id: j['id'] as String,
|
||||
kind: j['kind'] == 'lab' ? TenantKind.lab : TenantKind.clinic,
|
||||
memberNumber: (j['member_number'] as String?) ?? '',
|
||||
companyName: j['company_name'] as String,
|
||||
companyAddress: j['company_address'] as String?,
|
||||
city: j['city'] as String?,
|
||||
district: j['district'] as String?,
|
||||
latitude: (j['latitude'] as num?)?.toDouble(),
|
||||
longitude: (j['longitude'] as num?)?.toDouble(),
|
||||
logo: j['logo'] as String?,
|
||||
defaultCurrency: (j['default_currency'] as String?) ?? 'TRY',
|
||||
status: (j['status'] as String?) ?? 'active',
|
||||
plan: _parsePlan(j['plan'] as String?),
|
||||
maxMembers: (j['max_members'] as num?)?.toInt(),
|
||||
workflowOverrideStepKeys: j['workflow_overrides'] is List
|
||||
? (j['workflow_overrides'] as List)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: const [],
|
||||
);
|
||||
|
||||
static TenantPlan? _parsePlan(String? p) => switch (p) {
|
||||
@@ -69,6 +106,9 @@ class Tenant {
|
||||
'enterprise' => TenantPlan.enterprise,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
static TenantPlan parsePlanValue(String? value) =>
|
||||
_parsePlan(value) ?? TenantPlan.starter;
|
||||
}
|
||||
|
||||
class TenantMembership {
|
||||
@@ -85,22 +125,43 @@ class TenantMembership {
|
||||
// ── Access helpers ────────────────────────────────────────────────────────
|
||||
bool get isOwner => role == TenantRole.owner;
|
||||
bool get isAdmin => role == TenantRole.admin || role == TenantRole.owner;
|
||||
bool get canManageUsers => role == TenantRole.owner || role == TenantRole.admin;
|
||||
bool get canManageUsers =>
|
||||
role == TenantRole.owner || role == TenantRole.admin;
|
||||
bool get canManageJobs => role != TenantRole.finance;
|
||||
bool get canManageFinance => role == TenantRole.owner || role == TenantRole.admin || role == TenantRole.finance || role == TenantRole.member;
|
||||
bool get canManageProducts => role == TenantRole.owner || role == TenantRole.admin || role == TenantRole.technician || role == TenantRole.member;
|
||||
bool get canViewPatients => role == TenantRole.owner || role == TenantRole.admin || role == TenantRole.doctor || role == TenantRole.member;
|
||||
bool get canManageConnections => role == TenantRole.owner || role == TenantRole.admin || role == TenantRole.member;
|
||||
bool get canManageFinance =>
|
||||
role == TenantRole.owner ||
|
||||
role == TenantRole.admin ||
|
||||
role == TenantRole.finance ||
|
||||
role == TenantRole.member;
|
||||
bool get canManageProducts =>
|
||||
role == TenantRole.owner ||
|
||||
role == TenantRole.admin ||
|
||||
role == TenantRole.technician ||
|
||||
role == TenantRole.member;
|
||||
bool get canViewPatients =>
|
||||
role == TenantRole.owner ||
|
||||
role == TenantRole.admin ||
|
||||
role == TenantRole.doctor ||
|
||||
role == TenantRole.member;
|
||||
bool get canManageConnections =>
|
||||
role == TenantRole.owner ||
|
||||
role == TenantRole.admin ||
|
||||
role == TenantRole.member;
|
||||
|
||||
// ── Fine-grained job actions ──────────────────────────────────────────────
|
||||
/// Can create new jobs (clinic side: owner/admin/doctor/member; not delivery/finance)
|
||||
bool get canCreateJobs => role != TenantRole.delivery && role != TenantRole.finance;
|
||||
bool get canCreateJobs =>
|
||||
role != TenantRole.delivery && role != TenantRole.finance;
|
||||
|
||||
/// Can confirm physical delivery (delivery role + supervisors)
|
||||
bool get canDeliverJobs => role != TenantRole.finance;
|
||||
|
||||
/// Can cancel or fully manage job lifecycle (not delivery-only or finance)
|
||||
bool get canCancelJobs => role == TenantRole.owner || role == TenantRole.admin || role == TenantRole.member || role == TenantRole.doctor;
|
||||
bool get canCancelJobs =>
|
||||
role == TenantRole.owner ||
|
||||
role == TenantRole.admin ||
|
||||
role == TenantRole.member ||
|
||||
role == TenantRole.doctor;
|
||||
|
||||
/// Primary focus is delivery — restrict to delivery-relevant UI
|
||||
bool get isDeliveryOnly => role == TenantRole.delivery;
|
||||
@@ -124,22 +185,22 @@ class TenantMembership {
|
||||
}
|
||||
|
||||
static TenantRole parseRole(String r) => switch (r) {
|
||||
'owner' => TenantRole.owner,
|
||||
'admin' => TenantRole.admin,
|
||||
'owner' => TenantRole.owner,
|
||||
'admin' => TenantRole.admin,
|
||||
'technician' => TenantRole.technician,
|
||||
'delivery' => TenantRole.delivery,
|
||||
'finance' => TenantRole.finance,
|
||||
'doctor' => TenantRole.doctor,
|
||||
_ => TenantRole.member,
|
||||
'delivery' => TenantRole.delivery,
|
||||
'finance' => TenantRole.finance,
|
||||
'doctor' => TenantRole.doctor,
|
||||
_ => TenantRole.member,
|
||||
};
|
||||
|
||||
String get roleLabel => switch (role) {
|
||||
TenantRole.owner => 'Sahibi',
|
||||
TenantRole.admin => 'Yönetici',
|
||||
TenantRole.owner => 'Sahibi',
|
||||
TenantRole.admin => 'Yönetici',
|
||||
TenantRole.technician => 'Teknisyen',
|
||||
TenantRole.delivery => 'Teslimat Elemanı',
|
||||
TenantRole.finance => 'Finans Elemanı',
|
||||
TenantRole.doctor => 'Hekim',
|
||||
TenantRole.member => 'Üye',
|
||||
TenantRole.delivery => 'Teslimat Elemanı',
|
||||
TenantRole.finance => 'Finans Elemanı',
|
||||
TenantRole.doctor => 'Hekim',
|
||||
TenantRole.member => 'Üye',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user