fix: improve job step notes and product pricing guardrails
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user