fix: improve job step notes and product pricing guardrails

This commit is contained in:
egecankomur
2026-06-12 00:15:18 +03:00
parent b42f68214e
commit d504e505d3
3 changed files with 244 additions and 4 deletions
+122 -4
View File
@@ -85,6 +85,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
PricingBreakdown? _pricingBreakdown;
double? _effectivePrice;
bool _priceLoading = false;
String? _productAvailabilityMessage;
@override
void initState() {
@@ -134,6 +135,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
_labProduct = null;
_pricingBreakdown = null;
_effectivePrice = null;
_productAvailabilityMessage = null;
});
return;
}
@@ -163,13 +165,29 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
}
product ??= matchingProducts.isNotEmpty ? matchingProducts.first : null;
if (product == null || product.unitPrice == null) {
if (product == null) {
setState(() {
_availableProducts = matchingProducts;
_selectedProduct = product;
_selectedProduct = null;
_labProduct = null;
_pricingBreakdown = null;
_effectivePrice = null;
_productAvailabilityMessage =
'Bu laboratuvarda seçtiğiniz protez türü için ürün tanımlı değil.';
_priceLoading = false;
});
return;
}
if (product.unitPrice == null) {
setState(() {
_availableProducts = matchingProducts;
_selectedProduct = product;
_labProduct = product;
_pricingBreakdown = null;
_effectivePrice = null;
_productAvailabilityMessage =
'Seçilen ürün laboratuvarda mevcut ancak fiyatı tanımlı değil. Laboratuvarın fiyat tanımlaması gerekiyor.';
_priceLoading = false;
});
return;
@@ -188,6 +206,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
_labProduct = product;
_pricingBreakdown = breakdown;
_effectivePrice = breakdown.finalAmount;
_productAvailabilityMessage = null;
_priceLoading = false;
});
} catch (_) {
@@ -197,11 +216,29 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
_labProduct = null;
_pricingBreakdown = null;
_effectivePrice = null;
_productAvailabilityMessage =
'Ürün ve fiyat bilgisi alınırken bir hata oluştu.';
_priceLoading = false;
});
}
}
bool get _hasMissingProductForType =>
_selectedLab != null &&
_selectedProstheticType != null &&
!_priceLoading &&
_availableProducts.isEmpty;
bool get _hasSelectedProductWithoutPrice =>
_selectedProduct != null &&
_selectedProduct!.unitPrice == null;
bool get _canSubmitJob =>
!_isSubmitting &&
!_priceLoading &&
!_hasMissingProductForType &&
!_hasSelectedProductWithoutPrice;
Future<void> _searchPatients(String query) async {
final normalizedQuery = query.trim();
if (normalizedQuery.length < 2) {
@@ -307,6 +344,26 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
);
return;
}
if (_hasMissingProductForType) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Bu laboratuvarda seçtiğiniz protez türü için ürün tanımlı değil.',
),
),
);
return;
}
if (_hasSelectedProductWithoutPrice) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Seçilen ürünün fiyatı laboratuvarda tanımlı değil. İş açmadan önce fiyat tanımlanmalı.',
),
),
);
return;
}
if (_selectedTeeth.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('En az bir diş seçmelisiniz.')),
@@ -651,7 +708,11 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
.map(
(product) => DropdownMenuItem(
value: product,
child: Text(product.name),
child: Text(
product.unitPrice == null
? '${product.name} · Fiyat tanımsız'
: product.name,
),
),
)
.toList(),
@@ -668,6 +729,15 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
return null;
},
),
if (_productAvailabilityMessage != null) ...[
const SizedBox(height: 8),
_InlineInfoBanner(
message: _productAvailabilityMessage!,
tone: _hasMissingProductForType || _hasSelectedProductWithoutPrice
? _InfoBannerTone.warning
: _InfoBannerTone.info,
),
],
const SizedBox(height: 16),
_SectionLabel(label: 'İş Tipi'),
@@ -846,7 +916,7 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
const Center(child: CircularProgressIndicator())
else
FilledButton.icon(
onPressed: _submit,
onPressed: _canSubmitJob ? _submit : null,
icon: const Icon(Icons.check),
label: const Text('İş Oluştur'),
style: FilledButton.styleFrom(
@@ -861,6 +931,54 @@ class _NewJobScreenState extends ConsumerState<NewJobScreen> {
}
}
enum _InfoBannerTone { info, warning }
class _InlineInfoBanner extends StatelessWidget {
const _InlineInfoBanner({
required this.message,
required this.tone,
});
final String message;
final _InfoBannerTone tone;
@override
Widget build(BuildContext context) {
final isWarning = tone == _InfoBannerTone.warning;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: isWarning ? AppColors.pendingBg : AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isWarning ? AppColors.pending : AppColors.border,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
isWarning ? Icons.warning_amber_rounded : Icons.info_outline_rounded,
size: 18,
color: isWarning ? AppColors.pending : AppColors.textSecondary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
message,
style: TextStyle(
fontSize: 12,
color: isWarning ? AppColors.pending : AppColors.textSecondary,
fontWeight: isWarning ? FontWeight.w600 : FontWeight.w500,
),
),
),
],
),
);
}
}
class _TeethBulkBar extends StatelessWidget {
const _TeethBulkBar({
required this.selectedTeeth,