Add pricing entry flow and platform admin foundations

This commit is contained in:
egecankomur
2026-06-20 18:24:40 +03:00
parent 1d36ccdf30
commit ac42681f7e
44 changed files with 6567 additions and 1419 deletions
+90 -29
View File
@@ -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',
};
}