Apply clinic lab workflow feedback

This commit is contained in:
egecankomur
2026-07-15 22:13:01 +03:00
parent ac42681f7e
commit f44e07f378
9 changed files with 478 additions and 119 deletions
@@ -39,6 +39,7 @@ class ClinicJobsRepository {
perPage: limit,
filter: filterParts.join(' && '),
expand: _listExpand,
sort: '-created',
);
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
@@ -136,14 +137,14 @@ class ClinicJobsRepository {
'location': 'at_lab',
});
final updated = Job.fromJson(record.toJson());
unawaited(JobHistoryService.instance.append(
await JobHistoryService.instance.append(
jobId: jobId,
clinicTenantId: job.clinicTenantId,
labTenantId: job.labTenantId,
action: JobHistoryAction.revisionRequested,
step: job.currentStep,
note: note,
));
);
return updated;
}
@@ -195,6 +196,7 @@ class ClinicJobsRepository {
filter: 'patient_id = "$patientId"',
perPage: limit,
expand: _listExpand,
sort: '-created',
);
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
+64 -3
View File
@@ -149,8 +149,14 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
labId,
isActive: true,
);
final matchingProducts =
products.where((p) => p.prostheticType == ptValue).toList();
final matchingProducts = products
.where(
(p) =>
p.prostheticType == ptValue ||
parseProstheticTypeValue(p.prostheticType) ==
_selectedProstheticType,
)
.toList();
ProstheticProduct? product;
if (_selectedProduct != null) {
@@ -198,6 +204,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
memberCount: _selectedTeeth.length,
clinicTenantId: clinicTenantId,
discounts: discounts,
teeth: _selectedTeeth,
);
setState(() {
_availableProducts = matchingProducts;
@@ -688,7 +695,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
decoration: const InputDecoration(
hintText: 'Protez türü seçin',
),
items: ProstheticType.values
items: visibleProstheticTypes
.map(
(pt) => DropdownMenuItem(
value: pt,
@@ -753,6 +760,10 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
: _InfoBannerTone.info,
),
],
if (_selectedProduct?.description?.trim().isNotEmpty == true) ...[
const SizedBox(height: 8),
_ProductDescriptionCard(product: _selectedProduct!),
],
const SizedBox(height: 16),
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 {
const _SectionLabel({required this.label});
final String label;
@@ -85,4 +85,28 @@ class LabFinanceRepository {
}
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
Widget build(BuildContext context) {
final isSortActive = _sort != _FinanceSort.newestFirst;
@@ -252,6 +314,9 @@ class _LabFinanceScreenState extends ConsumerState<LabFinanceScreen>
title: 'Klinik Bazlı Alacak',
items: data.counterparties,
formatAmount: formatAmount,
onConfirmAll: (item) => _confirmCounterpartyPayments(
item, pending, formatAmount),
onSendReminder: _sendPaymentReminder,
),
),
PillTabs(
@@ -568,11 +633,15 @@ class _CounterpartySummaryList extends StatelessWidget {
required this.title,
required this.items,
required this.formatAmount,
required this.onConfirmAll,
required this.onSendReminder,
});
final String title;
final List<CounterpartyFinanceSummary> items;
final String Function(double) formatAmount;
final Future<void> Function(CounterpartyFinanceSummary item) onConfirmAll;
final Future<void> Function(CounterpartyFinanceSummary item) onSendReminder;
@override
Widget build(BuildContext context) {
@@ -616,6 +685,26 @@ class _CounterpartySummaryList extends StatelessWidget {
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),
@@ -418,10 +418,32 @@ class _LabJobDetailScreenState extends ConsumerState<LabJobDetailScreen> {
icon: Icons.notes,
label: 'Açıklama',
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),
// 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 ─────────────────────────────────────────────────────
class _HandToClinicSheet extends StatefulWidget {
@@ -578,11 +700,14 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
final isLast = widget.job.isLastStep;
final stepLabel = currentStep?.label ?? '';
final requiresClinicApproval = currentStep?.requiresClinicApproval ?? true;
final continuesInLab = !widget.job.provaRequired &&
currentStep == JobStep.olcuKontrol &&
widget.job.nextStep == JobStep.cilaBitim;
final buttonLabel = isLast
? (widget.job.provaRequired
? 'Son Prova · Teslime Gönder'
: 'Teslime Gönder')
: requiresClinicApproval
: requiresClinicApproval && !continuesInLab
? '$stepLabel için Kliniğe Gönder'
: '$stepLabel tamamlandı, sonraki adıma geç';
final buttonColor = isLast ? AppColors.success : AppColors.inProgress;
@@ -613,7 +738,7 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
Text(
isLast
? 'İş teslim edilecek olarak işaretlenecek.'
: requiresClinicApproval
: requiresClinicApproval && !continuesInLab
? 'İş klinikteki prova veya onay için gönderilecek.'
: 'Bu iç adım tamamlanacak ve iş laboratuvarda ilerleyecek.',
style: const TextStyle(color: AppColors.textSecondary),
@@ -649,7 +774,7 @@ class _HandToClinicSheetState extends State<_HandToClinicSheet> {
SnackBar(
content: Text(isLast
? 'İş teslim için gönderildi'
: requiresClinicApproval
: requiresClinicApproval && !continuesInLab
? 'Onay için kliniğe gönderildi'
: 'İş bir sonraki iç adıma geçirildi')),
);
@@ -28,6 +28,7 @@ class LabJobsRepository {
perPage: limit,
filter: filterParts.join(' && '),
expand: _listExpand,
sort: '-created',
);
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
..sort((a, b) => b.dateCreated.compareTo(a.dateCreated)));
@@ -44,6 +45,7 @@ class LabJobsRepository {
perPage: limit,
filter: filterParts.join(' && '),
expand: _listExpand,
sort: location == null ? '-created' : null,
);
return (result.items.map((r) => Job.fromJson(r.toJson())).toList()
..sort((a, b) {
@@ -89,9 +91,12 @@ class LabJobsRepository {
final isFinal = currentStep == JobStep.cilaBitim;
final nextStep = job.nextStep;
final shouldStayAtLabAfterMeasure = !job.provaRequired &&
currentStep == JobStep.olcuKontrol &&
nextStep == JobStep.cilaBitim;
final patch = isFinal
? {'status': 'sent', 'location': 'at_clinic'}
: currentStep.requiresClinicApproval
: currentStep.requiresClinicApproval && !shouldStayAtLabAfterMeasure
? {'location': 'at_clinic'}
: {
'current_step': nextStep?.value,
@@ -104,7 +109,9 @@ class LabJobsRepository {
jobId: jobId,
clinicTenantId: job.clinicTenantId,
labTenantId: job.labTenantId,
action: currentStep.requiresClinicApproval || isFinal
action: (currentStep.requiresClinicApproval &&
!shouldStayAtLabAfterMeasure) ||
isFinal
? JobHistoryAction.handedToClinic
: JobHistoryAction.stepCompleted,
step: currentStep,
@@ -7,11 +7,9 @@ import '../../../models/prosthetic_product.dart';
import 'lab_products_repository.dart';
const _prostheticTypes = [
('metal_porselen', 'Metal Porselen'),
('zirkonyum', 'Zirkonyum'),
('implant_ustu_zirkonyum', 'İmplant Üstü Zirkonyum'),
('sabit', 'Sabit'),
('hareketli', 'Hareketli'),
('gecici', 'Geçici'),
('e_max', 'E-Max'),
('diger', 'Diğer'),
];
@@ -19,19 +17,42 @@ String _typeLabel(String value) {
for (final t in _prostheticTypes) {
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 ────────────────────────────────────────────────────
void _showAdaptive(BuildContext context, Widget content) {
final isDesktop = MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
final isDesktop =
MediaQuery.sizeOf(context).width > AppLayout.sidebarBreakpoint;
if (isDesktop) {
showDialog(
context: context,
builder: (_) => Dialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: content,
@@ -98,8 +119,8 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Ürünü Sil'),
content: Text(
'"${product.name}" ürününü silmek istediğinize emin misiniz?'),
content:
Text('"${product.name}" ürününü silmek istediğinize emin misiniz?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
@@ -107,8 +128,7 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: FilledButton.styleFrom(
backgroundColor: AppColors.cancelled),
style: FilledButton.styleFrom(backgroundColor: AppColors.cancelled),
child: const Text('Sil'),
),
],
@@ -190,8 +210,7 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
),
const SizedBox(height: 16),
Text('Hata: ${snap.error}',
style: const TextStyle(
color: AppColors.textSecondary)),
style: const TextStyle(color: AppColors.textSecondary)),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: _load,
@@ -206,9 +225,11 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
final q = _searchQuery.toLowerCase().trim();
final products = q.isEmpty
? allProducts
: allProducts.where((p) =>
p.name.toLowerCase().contains(q) ||
_typeLabel(p.prostheticType).toLowerCase().contains(q)).toList();
: allProducts
.where((p) =>
p.name.toLowerCase().contains(q) ||
_typeLabel(p.prostheticType).toLowerCase().contains(q))
.toList();
if (allProducts.isEmpty) {
return Center(
@@ -226,18 +247,21 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
),
const SizedBox(height: 16),
Text(
q.isNotEmpty ? 'Sonuç bulunamadı' : 'Henüz ürün eklenmedi',
q.isNotEmpty
? 'Sonuç bulunamadı'
: 'Henüz ürün eklenmedi',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary),
),
const SizedBox(height: 12),
if (q.isEmpty) FilledButton.icon(
onPressed: () => _showProductSheet(),
icon: const Icon(Icons.add),
label: const Text('İlk Ürünü Ekle'),
),
if (q.isEmpty)
FilledButton.icon(
onPressed: () => _showProductSheet(),
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 Text(
'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,
itemBuilder: (ctx, i) {
final product = products[i];
final statusColor =
product.isActive ? AppColors.inProgress : AppColors.textMuted;
final statusBg =
product.isActive ? AppColors.inProgressBg : AppColors.surfaceVariant;
final statusColor = product.isActive
? AppColors.inProgress
: AppColors.textMuted;
final statusBg = product.isActive
? AppColors.inProgressBg
: AppColors.surfaceVariant;
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: GestureDetector(
@@ -293,8 +322,7 @@ class _LabProductsScreenState extends ConsumerState<LabProductsScreen> {
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color:
Colors.black.withValues(alpha: 0.04),
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2))
]),
@@ -411,7 +439,7 @@ class _ProductFormState extends State<_ProductForm> {
_priceCtrl = TextEditingController(
text: p?.unitPrice != null ? p!.unitPrice!.toString() : '');
_descCtrl = TextEditingController(text: p?.description ?? '');
_selectedType = p?.prostheticType ?? _prostheticTypes.first.$1;
_selectedType = _typeValueForForm(p?.prostheticType);
_currency = p?.currency ?? 'TRY';
_isActive = p?.isActive ?? true;
}
@@ -437,8 +465,7 @@ class _ProductFormState extends State<_ProductForm> {
unitPrice: price,
currency: _currency,
isActive: _isActive,
description:
_descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
);
try {
@@ -466,7 +493,8 @@ class _ProductFormState extends State<_ProductForm> {
@override
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;
return Container(
decoration: BoxDecoration(
@@ -479,9 +507,7 @@ class _ProductFormState extends State<_ProductForm> {
left: 20,
right: 20,
top: 24,
bottom: isDesktop
? 24
: MediaQuery.of(context).viewInsets.bottom + 24,
bottom: isDesktop ? 24 : MediaQuery.of(context).viewInsets.bottom + 24,
),
child: Form(
key: _formKey,
@@ -495,17 +521,14 @@ class _ProductFormState extends State<_ProductForm> {
Expanded(
child: Text(
isEdit ? 'Ürünü Düzenle' : 'Yeni Ürün',
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: AppColors.textPrimary),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppColors.textPrimary),
),
),
IconButton(
icon: const Icon(Icons.close,
color: AppColors.textSecondary),
icon:
const Icon(Icons.close, color: AppColors.textSecondary),
onPressed: () => Navigator.of(context).pop(),
),
],
@@ -515,8 +538,7 @@ class _ProductFormState extends State<_ProductForm> {
// Name
TextFormField(
controller: _nameCtrl,
decoration:
const InputDecoration(labelText: 'Ürün Adı *'),
decoration: const InputDecoration(labelText: 'Ürün Adı *'),
validator: (v) =>
v == null || v.trim().isEmpty ? 'Ürün adı gerekli' : null,
),
@@ -525,8 +547,7 @@ class _ProductFormState extends State<_ProductForm> {
// Prosthetic type dropdown
DropdownButtonFormField<String>(
initialValue: _selectedType,
decoration:
const InputDecoration(labelText: 'Protez Tipi *'),
decoration: const InputDecoration(labelText: 'Protez Tipi *'),
items: _prostheticTypes
.map((t) => DropdownMenuItem(
value: t.$1,
@@ -546,8 +567,8 @@ class _ProductFormState extends State<_ProductForm> {
controller: _priceCtrl,
decoration:
const InputDecoration(labelText: 'Birim Fiyat'),
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
validator: (v) {
if (v != null && v.isNotEmpty) {
if (double.tryParse(v) == null) {
@@ -581,8 +602,8 @@ class _ProductFormState extends State<_ProductForm> {
// Description
TextFormField(
controller: _descCtrl,
decoration: const InputDecoration(
labelText: 'Açıklama (isteğe bağlı)'),
decoration:
const InputDecoration(labelText: 'Açıklama (isteğe bağlı)'),
maxLines: 2,
),
const SizedBox(height: 12),