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
@@ -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),