Apply clinic lab workflow feedback
This commit is contained in:
@@ -27,14 +27,47 @@ class PricingService {
|
|||||||
int billableUnitsForType(ProstheticType type, int memberCount) {
|
int billableUnitsForType(ProstheticType type, int memberCount) {
|
||||||
final safeCount = memberCount <= 0 ? 1 : memberCount;
|
final safeCount = memberCount <= 0 ? 1 : memberCount;
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
ProstheticType.tamProtez || ProstheticType.parsiyel => 1,
|
ProstheticType.hareketli ||
|
||||||
|
ProstheticType.tamProtez ||
|
||||||
|
ProstheticType.parsiyel =>
|
||||||
|
1,
|
||||||
_ => safeCount,
|
_ => safeCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int billableUnitsForTeeth(ProstheticType type, Iterable<int> teeth) {
|
||||||
|
final selected = teeth.toSet();
|
||||||
|
if (selected.isEmpty) return billableUnitsForType(type, 0);
|
||||||
|
if (type.family == ProstheticFamily.hareketli) return 1;
|
||||||
|
|
||||||
|
final upper = {
|
||||||
|
for (int i = 11; i <= 18; i++) i,
|
||||||
|
for (int i = 21; i <= 28; i++) i,
|
||||||
|
};
|
||||||
|
final lower = {
|
||||||
|
for (int i = 31; i <= 38; i++) i,
|
||||||
|
for (int i = 41; i <= 48; i++) i,
|
||||||
|
};
|
||||||
|
|
||||||
|
var units = 0;
|
||||||
|
final remaining = Set<int>.from(selected);
|
||||||
|
if (upper.every(selected.contains)) {
|
||||||
|
units += 1;
|
||||||
|
remaining.removeAll(upper);
|
||||||
|
}
|
||||||
|
if (lower.every(selected.contains)) {
|
||||||
|
units += 1;
|
||||||
|
remaining.removeAll(lower);
|
||||||
|
}
|
||||||
|
return units + remaining.length;
|
||||||
|
}
|
||||||
|
|
||||||
String unitLabelForType(ProstheticType type) {
|
String unitLabelForType(ProstheticType type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
ProstheticType.tamProtez || ProstheticType.parsiyel => 'vaka',
|
ProstheticType.hareketli ||
|
||||||
|
ProstheticType.tamProtez ||
|
||||||
|
ProstheticType.parsiyel =>
|
||||||
|
'vaka',
|
||||||
_ => 'diş',
|
_ => 'diş',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -45,14 +78,18 @@ class PricingService {
|
|||||||
required int memberCount,
|
required int memberCount,
|
||||||
required String clinicTenantId,
|
required String clinicTenantId,
|
||||||
required List<ClinicDiscount> discounts,
|
required List<ClinicDiscount> discounts,
|
||||||
|
Iterable<int>? teeth,
|
||||||
}) {
|
}) {
|
||||||
final billableUnits = billableUnitsForType(prostheticType, memberCount);
|
final billableUnits = teeth == null
|
||||||
|
? billableUnitsForType(prostheticType, memberCount)
|
||||||
|
: billableUnitsForTeeth(prostheticType, teeth);
|
||||||
final unitPrice = product.unitPrice ?? 0;
|
final unitPrice = product.unitPrice ?? 0;
|
||||||
final baseAmount = unitPrice * billableUnits;
|
final baseAmount = unitPrice * billableUnits;
|
||||||
|
|
||||||
final applicable = discounts.where((discount) {
|
final applicable = discounts.where((discount) {
|
||||||
if (!discount.isActive) return false;
|
if (!discount.isActive) return false;
|
||||||
if (!(discount.appliesToAll || discount.clinicTenantId == clinicTenantId)) {
|
if (!(discount.appliesToAll ||
|
||||||
|
discount.clinicTenantId == clinicTenantId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!(discount.appliesToAllTypes ||
|
if (!(discount.appliesToAllTypes ||
|
||||||
@@ -77,9 +114,8 @@ class PricingService {
|
|||||||
billableUnits: billableUnits,
|
billableUnits: billableUnits,
|
||||||
unitPrice: unitPrice,
|
unitPrice: unitPrice,
|
||||||
baseAmount: baseAmount,
|
baseAmount: baseAmount,
|
||||||
discountAmount: (baseAmount - finalAmount)
|
discountAmount:
|
||||||
.clamp(0, double.infinity)
|
(baseAmount - finalAmount).clamp(0, double.infinity).toDouble(),
|
||||||
.toDouble(),
|
|
||||||
finalAmount: finalAmount,
|
finalAmount: finalAmount,
|
||||||
appliedDiscounts: applicable,
|
appliedDiscounts: applicable,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ClinicJobsRepository {
|
|||||||
perPage: limit,
|
perPage: limit,
|
||||||
filter: filterParts.join(' && '),
|
filter: filterParts.join(' && '),
|
||||||
expand: _listExpand,
|
expand: _listExpand,
|
||||||
|
sort: '-created',
|
||||||
);
|
);
|
||||||
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
||||||
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
|
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
|
||||||
@@ -136,14 +137,14 @@ class ClinicJobsRepository {
|
|||||||
'location': 'at_lab',
|
'location': 'at_lab',
|
||||||
});
|
});
|
||||||
final updated = Job.fromJson(record.toJson());
|
final updated = Job.fromJson(record.toJson());
|
||||||
unawaited(JobHistoryService.instance.append(
|
await JobHistoryService.instance.append(
|
||||||
jobId: jobId,
|
jobId: jobId,
|
||||||
clinicTenantId: job.clinicTenantId,
|
clinicTenantId: job.clinicTenantId,
|
||||||
labTenantId: job.labTenantId,
|
labTenantId: job.labTenantId,
|
||||||
action: JobHistoryAction.revisionRequested,
|
action: JobHistoryAction.revisionRequested,
|
||||||
step: job.currentStep,
|
step: job.currentStep,
|
||||||
note: note,
|
note: note,
|
||||||
));
|
);
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +196,7 @@ class ClinicJobsRepository {
|
|||||||
filter: 'patient_id = "$patientId"',
|
filter: 'patient_id = "$patientId"',
|
||||||
perPage: limit,
|
perPage: limit,
|
||||||
expand: _listExpand,
|
expand: _listExpand,
|
||||||
|
sort: '-created',
|
||||||
);
|
);
|
||||||
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
||||||
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
|
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
|
||||||
|
|||||||
@@ -149,8 +149,14 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
|
|||||||
labId,
|
labId,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
);
|
);
|
||||||
final matchingProducts =
|
final matchingProducts = products
|
||||||
products.where((p) => p.prostheticType == ptValue).toList();
|
.where(
|
||||||
|
(p) =>
|
||||||
|
p.prostheticType == ptValue ||
|
||||||
|
parseProstheticTypeValue(p.prostheticType) ==
|
||||||
|
_selectedProstheticType,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
ProstheticProduct? product;
|
ProstheticProduct? product;
|
||||||
if (_selectedProduct != null) {
|
if (_selectedProduct != null) {
|
||||||
@@ -198,6 +204,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
|
|||||||
memberCount: _selectedTeeth.length,
|
memberCount: _selectedTeeth.length,
|
||||||
clinicTenantId: clinicTenantId,
|
clinicTenantId: clinicTenantId,
|
||||||
discounts: discounts,
|
discounts: discounts,
|
||||||
|
teeth: _selectedTeeth,
|
||||||
);
|
);
|
||||||
setState(() {
|
setState(() {
|
||||||
_availableProducts = matchingProducts;
|
_availableProducts = matchingProducts;
|
||||||
@@ -688,7 +695,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Protez türü seçin',
|
hintText: 'Protez türü seçin',
|
||||||
),
|
),
|
||||||
items: ProstheticType.values
|
items: visibleProstheticTypes
|
||||||
.map(
|
.map(
|
||||||
(pt) => DropdownMenuItem(
|
(pt) => DropdownMenuItem(
|
||||||
value: pt,
|
value: pt,
|
||||||
@@ -753,6 +760,10 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
|
|||||||
: _InfoBannerTone.info,
|
: _InfoBannerTone.info,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
if (_selectedProduct?.description?.trim().isNotEmpty == true) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_ProductDescriptionCard(product: _selectedProduct!),
|
||||||
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
const _SectionLabel(label: 'İş Tipi'),
|
const _SectionLabel(label: 'İş Tipi'),
|
||||||
@@ -1404,6 +1415,56 @@ class _PricePreviewChip extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ProductDescriptionCard extends StatelessWidget {
|
||||||
|
const _ProductDescriptionCard({required this.product});
|
||||||
|
|
||||||
|
final ProstheticProduct product;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surfaceVariant,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: AppColors.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.notes_rounded, size: 16, color: AppColors.textMuted),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Ürün açıklaması',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
product.description!.trim(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
height: 1.35,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SectionLabel extends StatelessWidget {
|
class _SectionLabel extends StatelessWidget {
|
||||||
const _SectionLabel({required this.label});
|
const _SectionLabel({required this.label});
|
||||||
final String label;
|
final String label;
|
||||||
|
|||||||
@@ -85,4 +85,28 @@ class LabFinanceRepository {
|
|||||||
}
|
}
|
||||||
await FinanceService.instance.confirmJobPayment(jobId);
|
await FinanceService.instance.confirmJobPayment(jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> confirmPayments(List<FinanceEntry> entries) async {
|
||||||
|
final jobIds = <String>{};
|
||||||
|
final standaloneIds = <String>[];
|
||||||
|
for (final entry in entries) {
|
||||||
|
if (!entry.status.isOpen) continue;
|
||||||
|
if (entry.jobId.isNotEmpty) {
|
||||||
|
jobIds.add(entry.jobId);
|
||||||
|
} else {
|
||||||
|
standaloneIds.add(entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final jobId in jobIds) {
|
||||||
|
await FinanceService.instance.confirmJobPayment(jobId);
|
||||||
|
}
|
||||||
|
final paidAt = DateTime.now().toIso8601String();
|
||||||
|
for (final id in standaloneIds) {
|
||||||
|
await _pb.collection('finance_entries').update(id, body: {
|
||||||
|
'status': 'paid',
|
||||||
|
'paid_at': paidAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,68 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmCounterpartyPayments(
|
||||||
|
CounterpartyFinanceSummary item,
|
||||||
|
List<FinanceEntry> pending,
|
||||||
|
String Function(double) formatAmount,
|
||||||
|
) async {
|
||||||
|
final entries = pending
|
||||||
|
.where((entry) =>
|
||||||
|
entry.status.isOpen &&
|
||||||
|
(entry.counterpartyTenantId == item.counterpartyTenantId ||
|
||||||
|
entry.counterpartyName == item.counterpartyName))
|
||||||
|
.toList();
|
||||||
|
if (entries.isEmpty) return;
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('Toplu Ödeme Onayla'),
|
||||||
|
content: Text(
|
||||||
|
'${item.counterpartyName} için ${entries.length} açık kayıt '
|
||||||
|
'${formatAmount(item.pendingAmount)} toplam tutarla onaylansın mı?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text('İptal'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text('Toplu Onayla'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await LabFinanceRepository.instance.confirmPayments(entries);
|
||||||
|
_load();
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Toplu ödeme onaylandı.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Hata: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendPaymentReminder(CounterpartyFinanceSummary item) async {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'${item.counterpartyName} için ödeme hatırlatması hazırlandı.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isSortActive = _sort != _FinanceSort.newestFirst;
|
final isSortActive = _sort != _FinanceSort.newestFirst;
|
||||||
@@ -252,6 +314,9 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
|
|||||||
title: 'Klinik Bazlı Alacak',
|
title: 'Klinik Bazlı Alacak',
|
||||||
items: data.counterparties,
|
items: data.counterparties,
|
||||||
formatAmount: formatAmount,
|
formatAmount: formatAmount,
|
||||||
|
onConfirmAll: (item) => _confirmCounterpartyPayments(
|
||||||
|
item, pending, formatAmount),
|
||||||
|
onSendReminder: _sendPaymentReminder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PillTabs(
|
PillTabs(
|
||||||
@@ -568,11 +633,15 @@ class _CounterpartySummaryList extends StatelessWidget {
|
|||||||
required this.title,
|
required this.title,
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.formatAmount,
|
required this.formatAmount,
|
||||||
|
required this.onConfirmAll,
|
||||||
|
required this.onSendReminder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final List<CounterpartyFinanceSummary> items;
|
final List<CounterpartyFinanceSummary> items;
|
||||||
final String Function(double) formatAmount;
|
final String Function(double) formatAmount;
|
||||||
|
final Future<void> Function(CounterpartyFinanceSummary item) onConfirmAll;
|
||||||
|
final Future<void> Function(CounterpartyFinanceSummary item) onSendReminder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -616,6 +685,26 @@ class _CounterpartySummaryList extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (item.pendingAmount > 0) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => onSendReminder(item),
|
||||||
|
tooltip: 'Ödeme hatırlat',
|
||||||
|
icon: const Icon(Icons.notifications_active_outlined),
|
||||||
|
color: AppColors.pending,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => onConfirmAll(item),
|
||||||
|
icon: const Icon(Icons.done_all_rounded, size: 16),
|
||||||
|
label: const Text('Toplu Onay'),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: AppColors.success,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -418,10 +418,32 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
|||||||
icon: Icons.notes,
|
icon: Icons.notes,
|
||||||
label: 'Açıklama',
|
label: 'Açıklama',
|
||||||
value: job.description!),
|
value: job.description!),
|
||||||
|
if (job.workflowType == JobWorkflowType.arjinat ||
|
||||||
|
job.workflowType == JobWorkflowType.geleneksel) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_WorkflowAttentionBanner(type: job.workflowType!),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
FutureBuilder<List<JobHistoryEntry>>(
|
||||||
|
future: _historyFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final entries = snapshot.data ?? const <JobHistoryEntry>[];
|
||||||
|
final revisions = entries
|
||||||
|
.where((entry) =>
|
||||||
|
entry.action == JobHistoryAction.revisionRequested &&
|
||||||
|
entry.note?.trim().isNotEmpty == true)
|
||||||
|
.toList();
|
||||||
|
if (revisions.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: _LatestRevisionBanner(entry: revisions.last),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Stepper
|
// Stepper
|
||||||
@@ -549,6 +571,106 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _WorkflowAttentionBanner extends StatelessWidget {
|
||||||
|
const _WorkflowAttentionBanner({required this.type});
|
||||||
|
|
||||||
|
final JobWorkflowType type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isArjinat = type == JobWorkflowType.arjinat;
|
||||||
|
final color = isArjinat ? AppColors.pending : AppColors.accent;
|
||||||
|
final bg = isArjinat ? AppColors.pendingBg : AppColors.inProgressBg;
|
||||||
|
final text = isArjinat
|
||||||
|
? 'Arjinat ölçü: ölçü/model kontrolünü daha dikkatli doğrulayın.'
|
||||||
|
: 'Geleneksel ölçü: fiziksel ölçü ve kapanış kaydı kontrolü beklenir.';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.35)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isArjinat
|
||||||
|
? Icons.priority_high_rounded
|
||||||
|
: Icons.assignment_turned_in_outlined,
|
||||||
|
color: color,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LatestRevisionBanner extends StatelessWidget {
|
||||||
|
const _LatestRevisionBanner({required this.entry});
|
||||||
|
|
||||||
|
final JobHistoryEntry entry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.cancelledBg,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: AppColors.cancelled.withValues(alpha: 0.25)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.loop_rounded, color: AppColors.cancelled, size: 20),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
entry.step == null
|
||||||
|
? 'Revizyon Notu'
|
||||||
|
: 'Revizyon Notu · ${entry.step!.label}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: AppColors.cancelled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
Text(
|
||||||
|
entry.note!.trim(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1.35,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Hand to Clinic Sheet ─────────────────────────────────────────────────────
|
// ── Hand to Clinic Sheet ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _HandToClinicSheet extends StatefulWidget {
|
class _HandToClinicSheet extends StatefulWidget {
|
||||||
@@ -578,11 +700,14 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
|||||||
final isLast = widget.job.isLastStep;
|
final isLast = widget.job.isLastStep;
|
||||||
final stepLabel = currentStep?.label ?? '';
|
final stepLabel = currentStep?.label ?? '';
|
||||||
final requiresClinicApproval = currentStep?.requiresClinicApproval ?? true;
|
final requiresClinicApproval = currentStep?.requiresClinicApproval ?? true;
|
||||||
|
final continuesInLab = !widget.job.provaRequired &&
|
||||||
|
currentStep == JobStep.olcuKontrol &&
|
||||||
|
widget.job.nextStep == JobStep.cilaBitim;
|
||||||
final buttonLabel = isLast
|
final buttonLabel = isLast
|
||||||
? (widget.job.provaRequired
|
? (widget.job.provaRequired
|
||||||
? 'Son Prova · Teslime Gönder'
|
? 'Son Prova · Teslime Gönder'
|
||||||
: 'Teslime Gönder')
|
: 'Teslime Gönder')
|
||||||
: requiresClinicApproval
|
: requiresClinicApproval && !continuesInLab
|
||||||
? '$stepLabel için Kliniğe Gönder'
|
? '$stepLabel için Kliniğe Gönder'
|
||||||
: '$stepLabel tamamlandı, sonraki adıma geç';
|
: '$stepLabel tamamlandı, sonraki adıma geç';
|
||||||
final buttonColor = isLast ? AppColors.success : AppColors.inProgress;
|
final buttonColor = isLast ? AppColors.success : AppColors.inProgress;
|
||||||
@@ -613,7 +738,7 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
|||||||
Text(
|
Text(
|
||||||
isLast
|
isLast
|
||||||
? 'İş teslim edilecek olarak işaretlenecek.'
|
? 'İş teslim edilecek olarak işaretlenecek.'
|
||||||
: requiresClinicApproval
|
: requiresClinicApproval && !continuesInLab
|
||||||
? 'İş klinikteki prova veya onay için gönderilecek.'
|
? 'İş klinikteki prova veya onay için gönderilecek.'
|
||||||
: 'Bu iç adım tamamlanacak ve iş laboratuvarda ilerleyecek.',
|
: 'Bu iç adım tamamlanacak ve iş laboratuvarda ilerleyecek.',
|
||||||
style: const TextStyle(color: AppColors.textSecondary),
|
style: const TextStyle(color: AppColors.textSecondary),
|
||||||
@@ -649,7 +774,7 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
|
|||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(isLast
|
content: Text(isLast
|
||||||
? 'İş teslim için gönderildi'
|
? 'İş teslim için gönderildi'
|
||||||
: requiresClinicApproval
|
: requiresClinicApproval && !continuesInLab
|
||||||
? 'Onay için kliniğe gönderildi'
|
? 'Onay için kliniğe gönderildi'
|
||||||
: 'İş bir sonraki iç adıma geçirildi')),
|
: 'İş bir sonraki iç adıma geçirildi')),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class LabJobsRepository {
|
|||||||
perPage: limit,
|
perPage: limit,
|
||||||
filter: filterParts.join(' && '),
|
filter: filterParts.join(' && '),
|
||||||
expand: _listExpand,
|
expand: _listExpand,
|
||||||
|
sort: '-created',
|
||||||
);
|
);
|
||||||
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
||||||
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
|
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
|
||||||
@@ -44,6 +45,7 @@ class LabJobsRepository {
|
|||||||
perPage: limit,
|
perPage: limit,
|
||||||
filter: filterParts.join(' && '),
|
filter: filterParts.join(' && '),
|
||||||
expand: _listExpand,
|
expand: _listExpand,
|
||||||
|
sort: location == null ? '-created' : null,
|
||||||
);
|
);
|
||||||
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
@@ -89,9 +91,12 @@ class LabJobsRepository {
|
|||||||
|
|
||||||
final isFinal = currentStep == JobStep.cilaBitim;
|
final isFinal = currentStep == JobStep.cilaBitim;
|
||||||
final nextStep = job.nextStep;
|
final nextStep = job.nextStep;
|
||||||
|
final shouldStayAtLabAfterMeasure = !job.provaRequired &&
|
||||||
|
currentStep == JobStep.olcuKontrol &&
|
||||||
|
nextStep == JobStep.cilaBitim;
|
||||||
final patch = isFinal
|
final patch = isFinal
|
||||||
? {'status': 'sent', 'location': 'at_clinic'}
|
? {'status': 'sent', 'location': 'at_clinic'}
|
||||||
: currentStep.requiresClinicApproval
|
: currentStep.requiresClinicApproval && !shouldStayAtLabAfterMeasure
|
||||||
? {'location': 'at_clinic'}
|
? {'location': 'at_clinic'}
|
||||||
: {
|
: {
|
||||||
'current_step': nextStep?.value,
|
'current_step': nextStep?.value,
|
||||||
@@ -104,7 +109,9 @@ class LabJobsRepository {
|
|||||||
jobId: jobId,
|
jobId: jobId,
|
||||||
clinicTenantId: job.clinicTenantId,
|
clinicTenantId: job.clinicTenantId,
|
||||||
labTenantId: job.labTenantId,
|
labTenantId: job.labTenantId,
|
||||||
action: currentStep.requiresClinicApproval || isFinal
|
action: (currentStep.requiresClinicApproval &&
|
||||||
|
!shouldStayAtLabAfterMeasure) ||
|
||||||
|
isFinal
|
||||||
? JobHistoryAction.handedToClinic
|
? JobHistoryAction.handedToClinic
|
||||||
: JobHistoryAction.stepCompleted,
|
: JobHistoryAction.stepCompleted,
|
||||||
step: currentStep,
|
step: currentStep,
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import '../../../models/prosthetic_product.dart';
|
|||||||
import 'lab_products_repository.dart';
|
import 'lab_products_repository.dart';
|
||||||
|
|
||||||
const _prostheticTypes = [
|
const _prostheticTypes = [
|
||||||
('metal_porselen', 'Metal Porselen'),
|
('sabit', 'Sabit'),
|
||||||
('zirkonyum', 'Zirkonyum'),
|
('hareketli', 'Hareketli'),
|
||||||
('implant_ustu_zirkonyum', 'İmplant Üstü Zirkonyum'),
|
|
||||||
('gecici', 'Geçici'),
|
('gecici', 'Geçici'),
|
||||||
('e_max', 'E-Max'),
|
|
||||||
('diger', 'Diğer'),
|
('diger', 'Diğer'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -19,19 +17,42 @@ String _typeLabel(String value) {
|
|||||||
for (final t in _prostheticTypes) {
|
for (final t in _prostheticTypes) {
|
||||||
if (t.$1 == value) return t.$2;
|
if (t.$1 == value) return t.$2;
|
||||||
}
|
}
|
||||||
return value;
|
return switch (value) {
|
||||||
|
'metal_porselen' ||
|
||||||
|
'zirkonyum' ||
|
||||||
|
'implant_ustu_zirkonyum' ||
|
||||||
|
'e_max' =>
|
||||||
|
'Sabit',
|
||||||
|
'tam_protez' || 'parsiyel' => 'Hareketli',
|
||||||
|
_ => value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _typeValueForForm(String? value) {
|
||||||
|
return switch (value) {
|
||||||
|
'sabit' ||
|
||||||
|
'metal_porselen' ||
|
||||||
|
'zirkonyum' ||
|
||||||
|
'implant_ustu_zirkonyum' ||
|
||||||
|
'e_max' =>
|
||||||
|
'sabit',
|
||||||
|
'hareketli' || 'tam_protez' || 'parsiyel' => 'hareketli',
|
||||||
|
'gecici' => 'gecici',
|
||||||
|
'diger' => 'diger',
|
||||||
|
_ => _prostheticTypes.first.$1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Adaptive sheet helper ────────────────────────────────────────────────────
|
// ── Adaptive sheet helper ────────────────────────────────────────────────────
|
||||||
|
|
||||||
void _showAdaptive(BuildContext context, Widget content) {
|
void _showAdaptive(BuildContext context, Widget content) {
|
||||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
final isDesktop =
|
||||||
|
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => Dialog(
|
builder: (_) => Dialog(
|
||||||
shape:
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: content,
|
child: content,
|
||||||
@@ -98,8 +119,8 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text('Ürünü Sil'),
|
title: const Text('Ürünü Sil'),
|
||||||
content: Text(
|
content:
|
||||||
'"${product.name}" ürününü silmek istediğinize emin misiniz?'),
|
Text('"${product.name}" ürününü silmek istediğinize emin misiniz?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(false),
|
onPressed: () => Navigator.of(ctx).pop(false),
|
||||||
@@ -107,8 +128,7 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(true),
|
onPressed: () => Navigator.of(ctx).pop(true),
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(backgroundColor: AppColors.cancelled),
|
||||||
backgroundColor: AppColors.cancelled),
|
|
||||||
child: const Text('Sil'),
|
child: const Text('Sil'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -190,8 +210,7 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('Hata: ${snap.error}',
|
Text('Hata: ${snap.error}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(color: AppColors.textSecondary)),
|
||||||
color: AppColors.textSecondary)),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _load,
|
onPressed: _load,
|
||||||
@@ -206,9 +225,11 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
final q = _searchQuery.toLowerCase().trim();
|
final q = _searchQuery.toLowerCase().trim();
|
||||||
final products = q.isEmpty
|
final products = q.isEmpty
|
||||||
? allProducts
|
? allProducts
|
||||||
: allProducts.where((p) =>
|
: allProducts
|
||||||
p.name.toLowerCase().contains(q) ||
|
.where((p) =>
|
||||||
_typeLabel(p.prostheticType).toLowerCase().contains(q)).toList();
|
p.name.toLowerCase().contains(q) ||
|
||||||
|
_typeLabel(p.prostheticType).toLowerCase().contains(q))
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (allProducts.isEmpty) {
|
if (allProducts.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@@ -226,18 +247,21 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
q.isNotEmpty ? 'Sonuç bulunamadı' : 'Henüz ürün eklenmedi',
|
q.isNotEmpty
|
||||||
|
? 'Sonuç bulunamadı'
|
||||||
|
: 'Henüz ürün eklenmedi',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.textPrimary),
|
color: AppColors.textPrimary),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (q.isEmpty) FilledButton.icon(
|
if (q.isEmpty)
|
||||||
onPressed: () => _showProductSheet(),
|
FilledButton.icon(
|
||||||
icon: const Icon(Icons.add),
|
onPressed: () => _showProductSheet(),
|
||||||
label: const Text('İlk Ürünü Ekle'),
|
icon: const Icon(Icons.add),
|
||||||
),
|
label: const Text('İlk Ürünü Ekle'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -260,7 +284,10 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
const Text(
|
||||||
'Sonuç bulunamadı',
|
'Sonuç bulunamadı',
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.textPrimary),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -272,10 +299,12 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
itemCount: products.length,
|
itemCount: products.length,
|
||||||
itemBuilder: (ctx, i) {
|
itemBuilder: (ctx, i) {
|
||||||
final product = products[i];
|
final product = products[i];
|
||||||
final statusColor =
|
final statusColor = product.isActive
|
||||||
product.isActive ? AppColors.inProgress : AppColors.textMuted;
|
? AppColors.inProgress
|
||||||
final statusBg =
|
: AppColors.textMuted;
|
||||||
product.isActive ? AppColors.inProgressBg : AppColors.surfaceVariant;
|
final statusBg = product.isActive
|
||||||
|
? AppColors.inProgressBg
|
||||||
|
: AppColors.surfaceVariant;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -293,8 +322,7 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
|
|||||||
border: Border.all(color: AppColors.border),
|
border: Border.all(color: AppColors.border),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color:
|
color: Colors.black.withValues(alpha: 0.04),
|
||||||
Colors.black.withValues(alpha: 0.04),
|
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2))
|
offset: const Offset(0, 2))
|
||||||
]),
|
]),
|
||||||
@@ -411,7 +439,7 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
_priceCtrl = TextEditingController(
|
_priceCtrl = TextEditingController(
|
||||||
text: p?.unitPrice != null ? p!.unitPrice!.toString() : '');
|
text: p?.unitPrice != null ? p!.unitPrice!.toString() : '');
|
||||||
_descCtrl = TextEditingController(text: p?.description ?? '');
|
_descCtrl = TextEditingController(text: p?.description ?? '');
|
||||||
_selectedType = p?.prostheticType ?? _prostheticTypes.first.$1;
|
_selectedType = _typeValueForForm(p?.prostheticType);
|
||||||
_currency = p?.currency ?? 'TRY';
|
_currency = p?.currency ?? 'TRY';
|
||||||
_isActive = p?.isActive ?? true;
|
_isActive = p?.isActive ?? true;
|
||||||
}
|
}
|
||||||
@@ -437,8 +465,7 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
unitPrice: price,
|
unitPrice: price,
|
||||||
currency: _currency,
|
currency: _currency,
|
||||||
isActive: _isActive,
|
isActive: _isActive,
|
||||||
description:
|
description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
|
||||||
_descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -466,7 +493,8 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
final isDesktop =
|
||||||
|
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
|
||||||
final isEdit = widget.existing != null;
|
final isEdit = widget.existing != null;
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -479,9 +507,7 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
top: 24,
|
top: 24,
|
||||||
bottom: isDesktop
|
bottom: isDesktop ? 24 : MediaQuery.of(context).viewInsets.bottom + 24,
|
||||||
? 24
|
|
||||||
: MediaQuery.of(context).viewInsets.bottom + 24,
|
|
||||||
),
|
),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -495,17 +521,14 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
isEdit ? 'Ürünü Düzenle' : 'Yeni Ürün',
|
isEdit ? 'Ürünü Düzenle' : 'Yeni Ürün',
|
||||||
style: Theme.of(context)
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
.textTheme
|
fontWeight: FontWeight.bold,
|
||||||
.titleMedium
|
color: AppColors.textPrimary),
|
||||||
?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColors.textPrimary),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close,
|
icon:
|
||||||
color: AppColors.textSecondary),
|
const Icon(Icons.close, color: AppColors.textSecondary),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -515,8 +538,7 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
// Name
|
// Name
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameCtrl,
|
controller: _nameCtrl,
|
||||||
decoration:
|
decoration: const InputDecoration(labelText: 'Ürün Adı *'),
|
||||||
const InputDecoration(labelText: 'Ürün Adı *'),
|
|
||||||
validator: (v) =>
|
validator: (v) =>
|
||||||
v == null || v.trim().isEmpty ? 'Ürün adı gerekli' : null,
|
v == null || v.trim().isEmpty ? 'Ürün adı gerekli' : null,
|
||||||
),
|
),
|
||||||
@@ -525,8 +547,7 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
// Prosthetic type dropdown
|
// Prosthetic type dropdown
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: _selectedType,
|
initialValue: _selectedType,
|
||||||
decoration:
|
decoration: const InputDecoration(labelText: 'Protez Tipi *'),
|
||||||
const InputDecoration(labelText: 'Protez Tipi *'),
|
|
||||||
items: _prostheticTypes
|
items: _prostheticTypes
|
||||||
.map((t) => DropdownMenuItem(
|
.map((t) => DropdownMenuItem(
|
||||||
value: t.$1,
|
value: t.$1,
|
||||||
@@ -546,8 +567,8 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
controller: _priceCtrl,
|
controller: _priceCtrl,
|
||||||
decoration:
|
decoration:
|
||||||
const InputDecoration(labelText: 'Birim Fiyat'),
|
const InputDecoration(labelText: 'Birim Fiyat'),
|
||||||
keyboardType: const TextInputType.numberWithOptions(
|
keyboardType:
|
||||||
decimal: true),
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v != null && v.isNotEmpty) {
|
if (v != null && v.isNotEmpty) {
|
||||||
if (double.tryParse(v) == null) {
|
if (double.tryParse(v) == null) {
|
||||||
@@ -581,8 +602,8 @@ class _ProductFormState extends State<_ProductForm> {
|
|||||||
// Description
|
// Description
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _descCtrl,
|
controller: _descCtrl,
|
||||||
decoration: const InputDecoration(
|
decoration:
|
||||||
labelText: 'Açıklama (isteğe bağlı)'),
|
const InputDecoration(labelText: 'Açıklama (isteğe bağlı)'),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
+43
-49
@@ -21,19 +21,28 @@ enum JobLocation { atClinic, atLab }
|
|||||||
|
|
||||||
enum JobWorkflowType { arjinat, geleneksel, dijital }
|
enum JobWorkflowType { arjinat, geleneksel, dijital }
|
||||||
|
|
||||||
enum ProstheticFamily { sabit, implant, hareketli, gecici, ozel }
|
enum ProstheticFamily { sabit, hareketli, gecici, ozel }
|
||||||
|
|
||||||
enum ProstheticType {
|
enum ProstheticType {
|
||||||
|
sabit,
|
||||||
|
hareketli,
|
||||||
|
gecici,
|
||||||
|
diger,
|
||||||
metalPorselen,
|
metalPorselen,
|
||||||
zirkonyum,
|
zirkonyum,
|
||||||
implantUstuZirkonyum,
|
implantUstuZirkonyum,
|
||||||
gecici,
|
|
||||||
eMax,
|
eMax,
|
||||||
tamProtez,
|
tamProtez,
|
||||||
parsiyel,
|
parsiyel,
|
||||||
diger,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visibleProstheticTypes = <ProstheticType>[
|
||||||
|
ProstheticType.sabit,
|
||||||
|
ProstheticType.hareketli,
|
||||||
|
ProstheticType.gecici,
|
||||||
|
ProstheticType.diger,
|
||||||
|
];
|
||||||
|
|
||||||
// ── Status ────────────────────────────────────────────────────────────────────
|
// ── Status ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
extension JobStatusExt on JobStatus {
|
extension JobStatusExt on JobStatus {
|
||||||
@@ -119,6 +128,7 @@ extension JobStepExt on JobStep {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool get isLabOptional => switch (this) {
|
bool get isLabOptional => switch (this) {
|
||||||
|
JobStep.dijitalTasarim ||
|
||||||
JobStep.modelHazirlik ||
|
JobStep.modelHazirlik ||
|
||||||
JobStep.fotografOnay ||
|
JobStep.fotografOnay ||
|
||||||
JobStep.kaliteKontrol ||
|
JobStep.kaliteKontrol ||
|
||||||
@@ -146,32 +156,38 @@ extension JobWorkflowTypeExt on JobWorkflowType {
|
|||||||
|
|
||||||
extension ProstheticTypeExt on ProstheticType {
|
extension ProstheticTypeExt on ProstheticType {
|
||||||
String get label => switch (this) {
|
String get label => switch (this) {
|
||||||
|
ProstheticType.sabit => 'Sabit',
|
||||||
|
ProstheticType.hareketli => 'Hareketli',
|
||||||
|
ProstheticType.gecici => 'Geçici',
|
||||||
|
ProstheticType.diger => 'Diğer',
|
||||||
ProstheticType.metalPorselen => 'Metal Porselen',
|
ProstheticType.metalPorselen => 'Metal Porselen',
|
||||||
ProstheticType.zirkonyum => 'Zirkonyum',
|
ProstheticType.zirkonyum => 'Zirkonyum',
|
||||||
ProstheticType.implantUstuZirkonyum => 'İmplant Üstü Zirkonyum',
|
ProstheticType.implantUstuZirkonyum => 'İmplant Üstü Zirkonyum',
|
||||||
ProstheticType.gecici => 'Geçici',
|
|
||||||
ProstheticType.eMax => 'E-Max',
|
ProstheticType.eMax => 'E-Max',
|
||||||
ProstheticType.tamProtez => 'Tam Protez',
|
ProstheticType.tamProtez => 'Tam Protez',
|
||||||
ProstheticType.parsiyel => 'Parsiyel Protez',
|
ProstheticType.parsiyel => 'Parsiyel Protez',
|
||||||
ProstheticType.diger => 'Diğer',
|
|
||||||
};
|
};
|
||||||
String get value => switch (this) {
|
String get value => switch (this) {
|
||||||
|
ProstheticType.sabit => 'sabit',
|
||||||
|
ProstheticType.hareketli => 'hareketli',
|
||||||
|
ProstheticType.gecici => 'gecici',
|
||||||
|
ProstheticType.diger => 'diger',
|
||||||
ProstheticType.metalPorselen => 'metal_porselen',
|
ProstheticType.metalPorselen => 'metal_porselen',
|
||||||
ProstheticType.zirkonyum => 'zirkonyum',
|
ProstheticType.zirkonyum => 'zirkonyum',
|
||||||
ProstheticType.implantUstuZirkonyum => 'implant_ustu_zirkonyum',
|
ProstheticType.implantUstuZirkonyum => 'implant_ustu_zirkonyum',
|
||||||
ProstheticType.gecici => 'gecici',
|
|
||||||
ProstheticType.eMax => 'e_max',
|
ProstheticType.eMax => 'e_max',
|
||||||
ProstheticType.tamProtez => 'tam_protez',
|
ProstheticType.tamProtez => 'tam_protez',
|
||||||
ProstheticType.parsiyel => 'parsiyel',
|
ProstheticType.parsiyel => 'parsiyel',
|
||||||
ProstheticType.diger => 'diger',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ProstheticFamily get family => switch (this) {
|
ProstheticFamily get family => switch (this) {
|
||||||
|
ProstheticType.sabit ||
|
||||||
ProstheticType.metalPorselen ||
|
ProstheticType.metalPorselen ||
|
||||||
ProstheticType.zirkonyum ||
|
ProstheticType.zirkonyum ||
|
||||||
|
ProstheticType.implantUstuZirkonyum ||
|
||||||
ProstheticType.eMax =>
|
ProstheticType.eMax =>
|
||||||
ProstheticFamily.sabit,
|
ProstheticFamily.sabit,
|
||||||
ProstheticType.implantUstuZirkonyum => ProstheticFamily.implant,
|
ProstheticType.hareketli ||
|
||||||
ProstheticType.tamProtez ||
|
ProstheticType.tamProtez ||
|
||||||
ProstheticType.parsiyel =>
|
ProstheticType.parsiyel =>
|
||||||
ProstheticFamily.hareketli,
|
ProstheticFamily.hareketli,
|
||||||
@@ -180,6 +196,19 @@ extension ProstheticTypeExt on ProstheticType {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProstheticType parseProstheticTypeValue(String value) => switch (value) {
|
||||||
|
'sabit' ||
|
||||||
|
'metal_porselen' ||
|
||||||
|
'zirkonyum' ||
|
||||||
|
'implant_ustu_zirkonyum' ||
|
||||||
|
'e_max' =>
|
||||||
|
ProstheticType.sabit,
|
||||||
|
'hareketli' || 'tam_protez' || 'parsiyel' => ProstheticType.hareketli,
|
||||||
|
'gecici' => ProstheticType.gecici,
|
||||||
|
'diger' => ProstheticType.diger,
|
||||||
|
_ => ProstheticType.diger,
|
||||||
|
};
|
||||||
|
|
||||||
// ── Step template ─────────────────────────────────────────────────────────────
|
// ── Step template ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class JobWorkflowPreset {
|
class JobWorkflowPreset {
|
||||||
@@ -195,6 +224,7 @@ class JobWorkflowPreset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const optionalLabStepCatalog = <JobStep>[
|
const optionalLabStepCatalog = <JobStep>[
|
||||||
|
JobStep.dijitalTasarim,
|
||||||
JobStep.modelHazirlik,
|
JobStep.modelHazirlik,
|
||||||
JobStep.fotografOnay,
|
JobStep.fotografOnay,
|
||||||
JobStep.kaliteKontrol,
|
JobStep.kaliteKontrol,
|
||||||
@@ -208,6 +238,8 @@ bool isOptionalStepApplicable(
|
|||||||
required bool provaRequired,
|
required bool provaRequired,
|
||||||
}) {
|
}) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
|
case JobStep.dijitalTasarim:
|
||||||
|
return workflowType == JobWorkflowType.dijital;
|
||||||
case JobStep.modelHazirlik:
|
case JobStep.modelHazirlik:
|
||||||
return workflowType != JobWorkflowType.dijital &&
|
return workflowType != JobWorkflowType.dijital &&
|
||||||
family != ProstheticFamily.gecici;
|
family != ProstheticFamily.gecici;
|
||||||
@@ -243,6 +275,8 @@ List<JobStep> mergeOptionalLabSteps({
|
|||||||
|
|
||||||
final finalIndex = merged.indexOf(JobStep.cilaBitim);
|
final finalIndex = merged.indexOf(JobStep.cilaBitim);
|
||||||
switch (step) {
|
switch (step) {
|
||||||
|
case JobStep.dijitalTasarim:
|
||||||
|
merged.insert(0, step);
|
||||||
case JobStep.modelHazirlik:
|
case JobStep.modelHazirlik:
|
||||||
final afterControl = merged.contains(JobStep.olcuKontrol)
|
final afterControl = merged.contains(JobStep.olcuKontrol)
|
||||||
? merged.indexOf(JobStep.olcuKontrol) + 1
|
? merged.indexOf(JobStep.olcuKontrol) + 1
|
||||||
@@ -274,48 +308,30 @@ JobWorkflowPreset buildJobWorkflowPreset({
|
|||||||
steps = switch (family) {
|
steps = switch (family) {
|
||||||
ProstheticFamily.sabit => provaRequired
|
ProstheticFamily.sabit => provaRequired
|
||||||
? const [
|
? const [
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.ustYapiProva,
|
JobStep.ustYapiProva,
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
]
|
]
|
||||||
: const [
|
: const [
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.cilaBitim,
|
|
||||||
],
|
|
||||||
ProstheticFamily.implant => provaRequired
|
|
||||||
? const [
|
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.dayanakProva,
|
|
||||||
JobStep.kronProva,
|
|
||||||
JobStep.cilaBitim,
|
|
||||||
]
|
|
||||||
: const [
|
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
],
|
],
|
||||||
ProstheticFamily.hareketli => provaRequired
|
ProstheticFamily.hareketli => provaRequired
|
||||||
? const [
|
? const [
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.mumProva,
|
JobStep.mumProva,
|
||||||
JobStep.dislerProva,
|
JobStep.dislerProva,
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
]
|
]
|
||||||
: const [
|
: const [
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
],
|
],
|
||||||
ProstheticFamily.gecici => const [
|
ProstheticFamily.gecici => const [
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
],
|
],
|
||||||
ProstheticFamily.ozel => provaRequired
|
ProstheticFamily.ozel => provaRequired
|
||||||
? const [
|
? const [
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.altYapiProva,
|
JobStep.altYapiProva,
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
]
|
]
|
||||||
: const [
|
: const [
|
||||||
JobStep.dijitalTasarim,
|
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -333,17 +349,6 @@ JobWorkflowPreset buildJobWorkflowPreset({
|
|||||||
JobStep.olcuKontrol,
|
JobStep.olcuKontrol,
|
||||||
JobStep.cilaBitim,
|
JobStep.cilaBitim,
|
||||||
],
|
],
|
||||||
ProstheticFamily.implant => provaRequired
|
|
||||||
? const [
|
|
||||||
JobStep.olcuKontrol,
|
|
||||||
JobStep.dayanakProva,
|
|
||||||
JobStep.kronProva,
|
|
||||||
JobStep.cilaBitim,
|
|
||||||
]
|
|
||||||
: const [
|
|
||||||
JobStep.olcuKontrol,
|
|
||||||
JobStep.cilaBitim,
|
|
||||||
],
|
|
||||||
ProstheticFamily.hareketli => provaRequired
|
ProstheticFamily.hareketli => provaRequired
|
||||||
? const [
|
? const [
|
||||||
JobStep.olcuKontrol,
|
JobStep.olcuKontrol,
|
||||||
@@ -579,7 +584,7 @@ class Job {
|
|||||||
patientCode: j['patient_code'] as String,
|
patientCode: j['patient_code'] as String,
|
||||||
prostheticId: str(j['prosthetic_id']),
|
prostheticId: str(j['prosthetic_id']),
|
||||||
prostheticName: prostheticExp?['name'] as String?,
|
prostheticName: prostheticExp?['name'] as String?,
|
||||||
prostheticType: _parseProstheticType(j['prosthetic_type'] as String),
|
prostheticType: parseProstheticTypeValue(j['prosthetic_type'] as String),
|
||||||
memberCount: (j['member_count'] as num).toInt(),
|
memberCount: (j['member_count'] as num).toInt(),
|
||||||
teeth: j['teeth'] is List
|
teeth: j['teeth'] is List
|
||||||
? (j['teeth'] as List).map((e) => e.toString()).toList()
|
? (j['teeth'] as List).map((e) => e.toString()).toList()
|
||||||
@@ -660,15 +665,4 @@ class Job {
|
|||||||
final code = (patientExp['patient_code'] as String?)?.trim();
|
final code = (patientExp['patient_code'] as String?)?.trim();
|
||||||
return (code == null || code.isEmpty) ? null : code;
|
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user