bug fix
This commit is contained in:
parent
1594274fa2
commit
ce0979d782
@ -142,6 +142,8 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
|
|
||||||
Map<String, dynamic> _buildCreatePayload() {
|
Map<String, dynamic> _buildCreatePayload() {
|
||||||
final poId = int.tryParse(_selectedPoId ?? '');
|
final poId = int.tryParse(_selectedPoId ?? '');
|
||||||
|
final receivableLines =
|
||||||
|
_lines.where((line) => line.currentQty > 0).toList();
|
||||||
final payload = <String, dynamic>{
|
final payload = <String, dynamic>{
|
||||||
'grn_date': DateFormatter.toApiDate(_grnDate ?? DateTime.now()),
|
'grn_date': DateFormatter.toApiDate(_grnDate ?? DateTime.now()),
|
||||||
'po_id': poId,
|
'po_id': poId,
|
||||||
@ -162,7 +164,8 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
_putOptionalUserId(payload, 'received_by', _receivedById);
|
_putOptionalUserId(payload, 'received_by', _receivedById);
|
||||||
_putOptionalUserId(payload, 'quality_checked_by', _qualityCheckedById);
|
_putOptionalUserId(payload, 'quality_checked_by', _qualityCheckedById);
|
||||||
payload['remarks'] = _remarksController.text.trim();
|
payload['remarks'] = _remarksController.text.trim();
|
||||||
payload['items'] = _lines.map((line) => line.toPayload()).toList();
|
payload['items'] =
|
||||||
|
receivableLines.map((line) => line.toPayload()).toList();
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,14 +191,21 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? _lineItemsError() {
|
String? _lineItemsError() {
|
||||||
if (_lines.isEmpty) return 'Add at least one line item with quantity';
|
final receivableLines =
|
||||||
for (final line in _lines) {
|
_lines.where((line) => line.currentQty > 0).toList();
|
||||||
if (line.currentQty <= 0) {
|
if (receivableLines.isEmpty) {
|
||||||
return 'Enter quantity for line ${line.lineNo}';
|
return 'Enter quantity for at least one line item';
|
||||||
}
|
}
|
||||||
|
for (final line in receivableLines) {
|
||||||
if (line.acceptedQty < 0) {
|
if (line.acceptedQty < 0) {
|
||||||
return 'Accepted quantity must be zero or more for line ${line.lineNo}';
|
return 'Accepted quantity must be zero or more for line ${line.lineNo}';
|
||||||
}
|
}
|
||||||
|
if (line.acceptedQty > line.orderedQty) {
|
||||||
|
return 'Line ${line.lineNo}: accepted qty cannot exceed ordered qty (${_formatOrdered(line.orderedQty)})';
|
||||||
|
}
|
||||||
|
if (line.currentQty > line.remainingQty) {
|
||||||
|
return 'Line ${line.lineNo}: quantity cannot exceed remaining (${_formatOrdered(line.remainingQty)})';
|
||||||
|
}
|
||||||
if (line.rejectedQty > 0 &&
|
if (line.rejectedQty > 0 &&
|
||||||
line.rejectionReasonController.text.trim().isEmpty) {
|
line.rejectionReasonController.text.trim().isEmpty) {
|
||||||
return 'Rejection reason is required for line ${line.lineNo}';
|
return 'Rejection reason is required for line ${line.lineNo}';
|
||||||
@ -204,6 +214,11 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatOrdered(double value) {
|
||||||
|
if (value % 1 == 0) return value.toInt().toString();
|
||||||
|
return value.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
final formState = _formKey.currentState;
|
final formState = _formKey.currentState;
|
||||||
if (formState == null) return;
|
if (formState == null) return;
|
||||||
|
|||||||
@ -332,6 +332,26 @@ class _GrnLineItemCardState extends State<_GrnLineItemCard> {
|
|||||||
const TextInputType.numberWithOptions(decimal: true),
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
inputFormatters: _qtyFormatters,
|
inputFormatters: _qtyFormatters,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
onChanged: (_) => widget.onChanged(),
|
||||||
|
validator: (value) {
|
||||||
|
final text = value?.trim() ?? '';
|
||||||
|
if (text.isEmpty) return null;
|
||||||
|
final accepted = double.tryParse(text);
|
||||||
|
if (accepted == null) return 'Enter a valid quantity';
|
||||||
|
if (accepted < 0) return 'Cannot be negative';
|
||||||
|
if (accepted > item.orderedQty) {
|
||||||
|
return 'Cannot exceed ordered qty (${_formatQty(item.orderedQty)})';
|
||||||
|
}
|
||||||
|
if (accepted > item.remainingQty) {
|
||||||
|
return 'Cannot exceed remaining qty (${_formatQty(item.remainingQty)})';
|
||||||
|
}
|
||||||
|
final current = accepted + item.rejectedQty;
|
||||||
|
if (current > item.remainingQty) {
|
||||||
|
return 'Accepted + rejected exceeds remaining (${_formatQty(item.remainingQty)})';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
AppTextField(
|
AppTextField(
|
||||||
key: ValueKey('$lineKey-rejected'),
|
key: ValueKey('$lineKey-rejected'),
|
||||||
@ -342,6 +362,20 @@ class _GrnLineItemCardState extends State<_GrnLineItemCard> {
|
|||||||
const TextInputType.numberWithOptions(decimal: true),
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
inputFormatters: _qtyFormatters,
|
inputFormatters: _qtyFormatters,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
onChanged: (_) => widget.onChanged(),
|
||||||
|
validator: (value) {
|
||||||
|
final text = value?.trim() ?? '';
|
||||||
|
if (text.isEmpty) return null;
|
||||||
|
final rejected = double.tryParse(text);
|
||||||
|
if (rejected == null) return 'Enter a valid quantity';
|
||||||
|
if (rejected < 0) return 'Cannot be negative';
|
||||||
|
final current = item.acceptedQty + rejected;
|
||||||
|
if (current > item.remainingQty) {
|
||||||
|
return 'Accepted + rejected exceeds remaining (${_formatQty(item.remainingQty)})';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_GrnLineReadOnlyField(
|
_GrnLineReadOnlyField(
|
||||||
key: ValueKey('$lineKey-current'),
|
key: ValueKey('$lineKey-current'),
|
||||||
|
|||||||
@ -514,7 +514,7 @@ String masterCellValue(Map<String, dynamic> row, MasterFieldDef field) {
|
|||||||
}
|
}
|
||||||
final nested = row[baseKey];
|
final nested = row[baseKey];
|
||||||
if (nested is Map) {
|
if (nested is Map) {
|
||||||
for (final nestedKey in ['code', 'name', 'description']) {
|
for (final nestedKey in ['name', 'code', 'description']) {
|
||||||
final nestedValue = nested[nestedKey];
|
final nestedValue = nested[nestedKey];
|
||||||
if (nestedValue != null &&
|
if (nestedValue != null &&
|
||||||
nestedValue.toString().trim().isNotEmpty) {
|
nestedValue.toString().trim().isNotEmpty) {
|
||||||
|
|||||||
@ -84,6 +84,22 @@ class _MasterFormPanelState extends ConsumerState<MasterFormPanel> {
|
|||||||
String _fieldLabel(MasterFieldDef field) =>
|
String _fieldLabel(MasterFieldDef field) =>
|
||||||
field.required ? '${field.label} *' : field.label;
|
field.required ? '${field.label} *' : field.label;
|
||||||
|
|
||||||
|
/// Matches [AppTextField] / [AppSearchableDropdown] label clearance.
|
||||||
|
Widget _wrapField(Widget field) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: field,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDecoration _inputDecoration(MasterFieldDef field) {
|
||||||
|
return InputDecoration(
|
||||||
|
labelText: _fieldLabel(field),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildField(
|
Widget _buildField(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required MasterFieldDef field,
|
required MasterFieldDef field,
|
||||||
@ -213,15 +229,17 @@ class _MasterFormPanelState extends ConsumerState<MasterFormPanel> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case MasterFieldType.number:
|
case MasterFieldType.number:
|
||||||
return TextFormField(
|
return _wrapField(
|
||||||
key: ValueKey('$_formSessionId-${field.key}'),
|
TextFormField(
|
||||||
initialValue: value?.toString() ?? '',
|
key: ValueKey('$_formSessionId-${field.key}'),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
initialValue: value?.toString() ?? '',
|
||||||
decoration: InputDecoration(labelText: _fieldLabel(field)),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
validator: field.required
|
decoration: _inputDecoration(field),
|
||||||
? (v) => Validators.required(v, fieldName: field.label)
|
validator: field.required
|
||||||
: null,
|
? (v) => Validators.required(v, fieldName: field.label)
|
||||||
onChanged: (text) => notifier.updateValue(field.key, text),
|
: null,
|
||||||
|
onChanged: (text) => notifier.updateValue(field.key, text),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
case MasterFieldType.text:
|
case MasterFieldType.text:
|
||||||
@ -230,50 +248,52 @@ class _MasterFormPanelState extends ConsumerState<MasterFormPanel> {
|
|||||||
final formatters = isHsnCodeField
|
final formatters = isHsnCodeField
|
||||||
? Validators.hsnCodeInput
|
? Validators.hsnCodeInput
|
||||||
: Validators.inputFormattersForFieldKey(field.key);
|
: Validators.inputFormattersForFieldKey(field.key);
|
||||||
return TextFormField(
|
return _wrapField(
|
||||||
key: ValueKey('$_formSessionId-${field.key}'),
|
TextFormField(
|
||||||
initialValue: value?.toString() ?? '',
|
key: ValueKey('$_formSessionId-${field.key}'),
|
||||||
maxLines: field.multiline ? 3 : 1,
|
initialValue: value?.toString() ?? '',
|
||||||
keyboardType: isHsnCodeField
|
maxLines: field.multiline ? 3 : 1,
|
||||||
? TextInputType.number
|
keyboardType: isHsnCodeField
|
||||||
: _keyboardTypeForFieldKey(field.key),
|
? TextInputType.number
|
||||||
inputFormatters: formatters.isEmpty ? null : formatters,
|
: _keyboardTypeForFieldKey(field.key),
|
||||||
decoration: InputDecoration(labelText: _fieldLabel(field)),
|
inputFormatters: formatters.isEmpty ? null : formatters,
|
||||||
validator: (v) {
|
decoration: _inputDecoration(field),
|
||||||
if (isHsnCodeField) {
|
validator: (v) {
|
||||||
return Validators.uniqueHsnCode(
|
if (isHsnCodeField) {
|
||||||
|
return Validators.uniqueHsnCode(
|
||||||
|
v,
|
||||||
|
existingRecords: formState.existingRecords,
|
||||||
|
currentRecordId: widget.recordId,
|
||||||
|
fieldName: field.label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Validators.isMasterNameFieldKey(field.key)) {
|
||||||
|
return Validators.uniqueMasterName(
|
||||||
|
v,
|
||||||
|
nameKey: field.key,
|
||||||
|
existingRecords: formState.existingRecords,
|
||||||
|
currentRecordId: widget.recordId,
|
||||||
|
fieldName: field.label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Validators.isMasterCodeFieldKey(field.key)) {
|
||||||
|
return Validators.uniqueMasterCode(
|
||||||
|
v,
|
||||||
|
codeKey: field.key,
|
||||||
|
existingRecords: formState.existingRecords,
|
||||||
|
currentRecordId: widget.recordId,
|
||||||
|
fieldName: field.label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Validators.forFieldKey(
|
||||||
|
field.key,
|
||||||
v,
|
v,
|
||||||
existingRecords: formState.existingRecords,
|
required: field.required,
|
||||||
currentRecordId: widget.recordId,
|
|
||||||
fieldName: field.label,
|
fieldName: field.label,
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
if (Validators.isMasterNameFieldKey(field.key)) {
|
onChanged: (text) => notifier.updateValue(field.key, text),
|
||||||
return Validators.uniqueMasterName(
|
),
|
||||||
v,
|
|
||||||
nameKey: field.key,
|
|
||||||
existingRecords: formState.existingRecords,
|
|
||||||
currentRecordId: widget.recordId,
|
|
||||||
fieldName: field.label,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (Validators.isMasterCodeFieldKey(field.key)) {
|
|
||||||
return Validators.uniqueMasterCode(
|
|
||||||
v,
|
|
||||||
codeKey: field.key,
|
|
||||||
existingRecords: formState.existingRecords,
|
|
||||||
currentRecordId: widget.recordId,
|
|
||||||
fieldName: field.label,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Validators.forFieldKey(
|
|
||||||
field.key,
|
|
||||||
v,
|
|
||||||
required: field.required,
|
|
||||||
fieldName: field.label,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onChanged: (text) => notifier.updateValue(field.key, text),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,7 +348,10 @@ class _MasterFormPanelState extends ConsumerState<MasterFormPanel> {
|
|||||||
widgets.add(
|
widgets.add(
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: _buildField(context, field: left, formState: formState),
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: _buildField(context, field: left, formState: formState),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,9 +73,16 @@ class MasterRemoteDataSource {
|
|||||||
uomByItemId[id] = _asInt(item['uom_id']);
|
uomByItemId[id] = _asInt(item['uom_id']);
|
||||||
gstRateByItemId[id] = _asInt(item['gst_rate_id']);
|
gstRateByItemId[id] = _asInt(item['gst_rate_id']);
|
||||||
|
|
||||||
final name = _optionLabel(item);
|
final itemName = (item['item_name'] ?? item['name'])?.toString().trim() ?? '';
|
||||||
if (name.isEmpty) continue;
|
if (itemName.isEmpty) continue;
|
||||||
options.add(FilterOptionModel(id: id, name: name));
|
final itemCode = item['item_code']?.toString().trim();
|
||||||
|
options.add(
|
||||||
|
FilterOptionModel(
|
||||||
|
id: id,
|
||||||
|
name: itemName,
|
||||||
|
slug: (itemCode == null || itemCode.isEmpty) ? null : itemCode,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -246,13 +253,8 @@ class MasterRemoteDataSource {
|
|||||||
? ratePct.toDouble()
|
? ratePct.toDouble()
|
||||||
: double.tryParse(ratePct.toString());
|
: double.tryParse(ratePct.toString());
|
||||||
if (rate != null) {
|
if (rate != null) {
|
||||||
final rateLabel =
|
// Dropdowns show the rate only (e.g. "5%"), not the description.
|
||||||
rate % 1 == 0 ? '${rate.toInt()}%' : '$rate%';
|
return rate % 1 == 0 ? '${rate.toInt()}%' : '$rate%';
|
||||||
final desc = item['description'];
|
|
||||||
if (desc is String && desc.trim().isNotEmpty) {
|
|
||||||
return '$rateLabel — ${desc.trim()}';
|
|
||||||
}
|
|
||||||
return rateLabel;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -156,9 +156,16 @@ Future<List<FilterOptionModel>> _fetchActiveVendors(
|
|||||||
|
|
||||||
final data = result.data!;
|
final data = result.data!;
|
||||||
vendors.addAll(
|
vendors.addAll(
|
||||||
data.items.map(
|
data.items
|
||||||
(vendor) => FilterOptionModel(id: vendor.id, name: vendor.vendorName),
|
.where((vendor) {
|
||||||
),
|
if (!vendor.isActive) return false;
|
||||||
|
final status = (vendor.status ?? '').trim().toLowerCase();
|
||||||
|
return status != 'blacklisted';
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
(vendor) =>
|
||||||
|
FilterOptionModel(id: vendor.id, name: vendor.vendorName),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
totalPages = data.totalPages;
|
totalPages = data.totalPages;
|
||||||
page++;
|
page++;
|
||||||
|
|||||||
@ -811,10 +811,23 @@ class _AmountSummaryCard extends StatelessWidget {
|
|||||||
final TextEditingController discountController;
|
final TextEditingController discountController;
|
||||||
final bool isEditing;
|
final bool isEditing;
|
||||||
|
|
||||||
|
String? _validateDiscount(String? value) {
|
||||||
|
final text = value?.trim() ?? '';
|
||||||
|
if (text.isEmpty) return null;
|
||||||
|
final discount = double.tryParse(text);
|
||||||
|
if (discount == null) return 'Enter a valid amount';
|
||||||
|
if (discount < 0) return 'Cannot be negative';
|
||||||
|
if (discount > totals.maxDiscountAmount) {
|
||||||
|
return 'Cannot exceed ${CurrencyFormatter.format(totals.maxDiscountAmount)}';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final discountValue = double.tryParse(discountController.text.trim()) ?? 0;
|
final discountValue = double.tryParse(discountController.text.trim()) ?? 0;
|
||||||
|
final discountInvalid = _validateDiscount(discountController.text) != null;
|
||||||
final isDark = theme.brightness == Brightness.dark;
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
final primaryTint = theme.colorScheme.primary.withValues(
|
final primaryTint = theme.colorScheme.primary.withValues(
|
||||||
alpha: isDark ? 0.22 : 0.1,
|
alpha: isDark ? 0.22 : 0.1,
|
||||||
@ -846,7 +859,11 @@ class _AmountSummaryCard extends StatelessWidget {
|
|||||||
_SummaryInputRow(
|
_SummaryInputRow(
|
||||||
label: 'Discount amount',
|
label: 'Discount amount',
|
||||||
controller: discountController,
|
controller: discountController,
|
||||||
valueColor: discountValue > 0 ? AppColors.error : null,
|
valueColor: discountInvalid || discountValue > 0
|
||||||
|
? AppColors.error
|
||||||
|
: null,
|
||||||
|
validator: _validateDiscount,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Container(
|
Container(
|
||||||
@ -928,11 +945,15 @@ class _SummaryInputRow extends StatelessWidget {
|
|||||||
required this.label,
|
required this.label,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.valueColor,
|
this.valueColor,
|
||||||
|
this.validator,
|
||||||
|
this.autovalidateMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final Color? valueColor;
|
final Color? valueColor;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
final AutovalidateMode? autovalidateMode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -940,21 +961,28 @@ class _SummaryInputRow extends StatelessWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Padding(
|
||||||
label,
|
padding: const EdgeInsets.only(top: 10),
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
child: Text(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
label,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 120,
|
width: 140,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType:
|
||||||
|
const TextInputType.numberWithOptions(decimal: true),
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
|
autovalidateMode: autovalidateMode,
|
||||||
|
validator: validator,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: valueColor,
|
color: valueColor,
|
||||||
@ -968,6 +996,11 @@ class _SummaryInputRow extends StatelessWidget {
|
|||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
|
errorMaxLines: 2,
|
||||||
|
errorStyle: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -60,16 +60,21 @@ class PoOrderTotals {
|
|||||||
required this.taxableAmount,
|
required this.taxableAmount,
|
||||||
required this.taxAmount,
|
required this.taxAmount,
|
||||||
required this.grandTotal,
|
required this.grandTotal,
|
||||||
|
required this.maxDiscountAmount,
|
||||||
});
|
});
|
||||||
|
|
||||||
final double taxableAmount;
|
final double taxableAmount;
|
||||||
final double taxAmount;
|
final double taxAmount;
|
||||||
final double grandTotal;
|
final double grandTotal;
|
||||||
|
|
||||||
|
/// Taxable + Tax + Freight + Other (discount cannot exceed this).
|
||||||
|
final double maxDiscountAmount;
|
||||||
|
|
||||||
static const zero = PoOrderTotals(
|
static const zero = PoOrderTotals(
|
||||||
taxableAmount: 0,
|
taxableAmount: 0,
|
||||||
taxAmount: 0,
|
taxAmount: 0,
|
||||||
grandTotal: 0,
|
grandTotal: 0,
|
||||||
|
maxDiscountAmount: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 3.5 Taxable = sum of Line Amounts
|
/// 3.5 Taxable = sum of Line Amounts
|
||||||
@ -87,11 +92,15 @@ class PoOrderTotals {
|
|||||||
taxable += line.lineAmount;
|
taxable += line.lineAmount;
|
||||||
tax += line.gstAmount;
|
tax += line.gstAmount;
|
||||||
}
|
}
|
||||||
final grandTotal = taxable + tax + freight + otherCharges - discountAmount;
|
final maxDiscount = taxable + tax + freight + otherCharges;
|
||||||
|
final clampedDiscount =
|
||||||
|
discountAmount < 0 ? 0.0 : discountAmount;
|
||||||
|
final grandTotalRaw = maxDiscount - clampedDiscount;
|
||||||
return PoOrderTotals(
|
return PoOrderTotals(
|
||||||
taxableAmount: taxable,
|
taxableAmount: taxable,
|
||||||
taxAmount: tax,
|
taxAmount: tax,
|
||||||
grandTotal: grandTotal,
|
grandTotal: grandTotalRaw < 0 ? 0 : grandTotalRaw,
|
||||||
|
maxDiscountAmount: maxDiscount < 0 ? 0 : maxDiscount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -410,7 +419,12 @@ class _LineItemCardState extends ConsumerState<_LineItemCard> {
|
|||||||
.map((e) {
|
.map((e) {
|
||||||
final id = _parseId(e.id);
|
final id = _parseId(e.id);
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
return AppDropdownOption(value: id, label: e.name);
|
final code = e.slug?.trim();
|
||||||
|
return AppDropdownOption(
|
||||||
|
value: id,
|
||||||
|
label: e.name,
|
||||||
|
subtitle: (code == null || code.isEmpty) ? null : code,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.whereType<AppDropdownOption<int>>()
|
.whereType<AppDropdownOption<int>>()
|
||||||
.toList();
|
.toList();
|
||||||
@ -427,107 +441,158 @@ class _LineItemCardState extends ConsumerState<_LineItemCard> {
|
|||||||
...widget.gstRates.map((e) {
|
...widget.gstRates.map((e) {
|
||||||
final id = _parseId(e.id);
|
final id = _parseId(e.id);
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
return AppDropdownOption<int?>(value: id, label: e.name);
|
final pct = widget.gstRatePctById[e.id];
|
||||||
|
final label = pct != null
|
||||||
|
? (pct % 1 == 0 ? '${pct.toInt()}%' : '$pct%')
|
||||||
|
: e.name.split(' — ').first.trim();
|
||||||
|
return AppDropdownOption<int?>(value: id, label: label);
|
||||||
}),
|
}),
|
||||||
].whereType<AppDropdownOption<int?>>().toList();
|
].whereType<AppDropdownOption<int?>>().toList();
|
||||||
|
|
||||||
|
const spacing = 8.0;
|
||||||
|
const minRowWidth = 1040.0;
|
||||||
|
|
||||||
|
Widget flex({required int flex, required Widget child}) {
|
||||||
|
return Expanded(flex: flex, child: child);
|
||||||
|
}
|
||||||
|
|
||||||
|
final fields = <Widget>[
|
||||||
|
flex(
|
||||||
|
flex: 3,
|
||||||
|
child: MasterQuickAddDropdown<int>(
|
||||||
|
key: ValueKey('$lineKey-item'),
|
||||||
|
masterId: 'items',
|
||||||
|
label: 'Item *',
|
||||||
|
value: line.itemId,
|
||||||
|
hint: 'Select item',
|
||||||
|
searchHint: 'Search item name or code...',
|
||||||
|
options: itemOptions,
|
||||||
|
refreshLookups: () =>
|
||||||
|
ref.invalidate(purchaseOrderLookupsProvider),
|
||||||
|
parseCreatedId: int.tryParse,
|
||||||
|
onChanged: _onItemChanged,
|
||||||
|
validator: (v) => v == null ? 'Item is required' : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flex(
|
||||||
|
flex: 1,
|
||||||
|
child: AppTextField(
|
||||||
|
key: ValueKey('$lineKey-qty'),
|
||||||
|
controller: line.qtyController,
|
||||||
|
label: 'Qty *',
|
||||||
|
hint: '0',
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
validator: (v) {
|
||||||
|
if (v == null || v.trim().isEmpty) return 'Required';
|
||||||
|
final qty = double.tryParse(v);
|
||||||
|
if (qty == null || qty <= 0) return 'Invalid';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flex(
|
||||||
|
flex: 2,
|
||||||
|
child: MasterQuickAddDropdown<int>(
|
||||||
|
key: ValueKey('$lineKey-uom'),
|
||||||
|
masterId: 'uom',
|
||||||
|
label: 'UOM *',
|
||||||
|
value: line.uomId,
|
||||||
|
hint: 'Select UOM',
|
||||||
|
searchHint: 'Search UOM...',
|
||||||
|
options: uomOptions,
|
||||||
|
refreshLookups: () =>
|
||||||
|
ref.invalidate(purchaseOrderLookupsProvider),
|
||||||
|
parseCreatedId: int.tryParse,
|
||||||
|
onChanged: (v) => _updateLine(() => line.uomId = v),
|
||||||
|
validator: (v) => v == null ? 'Required' : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flex(
|
||||||
|
flex: 1,
|
||||||
|
child: AppTextField(
|
||||||
|
key: ValueKey('$lineKey-rate'),
|
||||||
|
controller: line.rateController,
|
||||||
|
label: 'Rate *',
|
||||||
|
hint: '0.00',
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
validator: (v) {
|
||||||
|
if (v == null || v.trim().isEmpty) return 'Required';
|
||||||
|
final rate = double.tryParse(v);
|
||||||
|
if (rate == null || rate < 0) return 'Invalid';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flex(
|
||||||
|
flex: 1,
|
||||||
|
child: AppTextField(
|
||||||
|
key: ValueKey('$lineKey-discount'),
|
||||||
|
controller: line.discountController,
|
||||||
|
label: 'Disc %',
|
||||||
|
hint: '0',
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flex(
|
||||||
|
flex: 2,
|
||||||
|
child: MasterQuickAddDropdown<int?>(
|
||||||
|
key: ValueKey('$lineKey-gst'),
|
||||||
|
masterId: 'gst_rates',
|
||||||
|
label: 'GST %',
|
||||||
|
value: line.gstRateId,
|
||||||
|
hint: 'Select',
|
||||||
|
searchHint: 'Search GST %...',
|
||||||
|
options: gstOptions,
|
||||||
|
refreshLookups: () =>
|
||||||
|
ref.invalidate(purchaseOrderLookupsProvider),
|
||||||
|
parseCreatedId: int.tryParse,
|
||||||
|
onChanged: (v) => _updateLine(() => line.gstRateId = v),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
flex(
|
||||||
|
flex: 2,
|
||||||
|
child: _AmountWithRemove(
|
||||||
|
amount: CurrencyFormatter.format(calc.lineAmount),
|
||||||
|
backgroundColor: amountBg,
|
||||||
|
onRemove: widget.onRemove,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: borderColor),
|
border: Border.all(color: borderColor),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: FormRow(
|
child: QuickAddInlineHost(
|
||||||
columnCount: 12,
|
child: LayoutBuilder(
|
||||||
spans: const [3, 1, 2, 1, 1, 2, 2],
|
builder: (context, constraints) {
|
||||||
spacing: 8,
|
final rowWidth = constraints.maxWidth < minRowWidth
|
||||||
stackBelowWidth: 1100,
|
? minRowWidth
|
||||||
children: [
|
: constraints.maxWidth;
|
||||||
MasterQuickAddDropdown<int>(
|
final row = SizedBox(
|
||||||
key: ValueKey('$lineKey-item'),
|
width: rowWidth,
|
||||||
masterId: 'items',
|
child: Row(
|
||||||
label: 'Item *',
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
value: line.itemId,
|
children: [
|
||||||
hint: 'Select item',
|
for (var i = 0; i < fields.length; i++) ...[
|
||||||
searchHint: 'Search item...',
|
if (i > 0) const SizedBox(width: spacing),
|
||||||
options: itemOptions,
|
fields[i],
|
||||||
refreshLookups: () =>
|
],
|
||||||
ref.invalidate(purchaseOrderLookupsProvider),
|
],
|
||||||
parseCreatedId: int.tryParse,
|
),
|
||||||
onChanged: _onItemChanged,
|
);
|
||||||
validator: (v) => v == null ? 'Item is required' : null,
|
|
||||||
),
|
if (constraints.maxWidth < minRowWidth) {
|
||||||
AppTextField(
|
return SingleChildScrollView(
|
||||||
key: ValueKey('$lineKey-qty'),
|
scrollDirection: Axis.horizontal,
|
||||||
controller: line.qtyController,
|
child: row,
|
||||||
label: 'Qty *',
|
);
|
||||||
hint: '0',
|
}
|
||||||
keyboardType:
|
return row;
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
},
|
||||||
validator: (v) {
|
),
|
||||||
if (v == null || v.trim().isEmpty) {
|
|
||||||
return 'Required';
|
|
||||||
}
|
|
||||||
final qty = double.tryParse(v);
|
|
||||||
if (qty == null || qty <= 0) return 'Invalid';
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MasterQuickAddDropdown<int>(
|
|
||||||
key: ValueKey('$lineKey-uom'),
|
|
||||||
masterId: 'uom',
|
|
||||||
label: 'UOM *',
|
|
||||||
value: line.uomId,
|
|
||||||
hint: 'Select UOM',
|
|
||||||
searchHint: 'Search UOM...',
|
|
||||||
options: uomOptions,
|
|
||||||
refreshLookups: () =>
|
|
||||||
ref.invalidate(purchaseOrderLookupsProvider),
|
|
||||||
parseCreatedId: int.tryParse,
|
|
||||||
onChanged: (v) => _updateLine(() => line.uomId = v),
|
|
||||||
validator: (v) => v == null ? 'Required' : null,
|
|
||||||
),
|
|
||||||
AppTextField(
|
|
||||||
key: ValueKey('$lineKey-rate'),
|
|
||||||
controller: line.rateController,
|
|
||||||
label: 'Rate *',
|
|
||||||
hint: '0.00',
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
validator: (v) {
|
|
||||||
if (v == null || v.trim().isEmpty) return 'Required';
|
|
||||||
final rate = double.tryParse(v);
|
|
||||||
if (rate == null || rate < 0) return 'Invalid';
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
AppTextField(
|
|
||||||
key: ValueKey('$lineKey-discount'),
|
|
||||||
controller: line.discountController,
|
|
||||||
label: 'Disc %',
|
|
||||||
hint: '0',
|
|
||||||
keyboardType:
|
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
),
|
|
||||||
MasterQuickAddDropdown<int?>(
|
|
||||||
key: ValueKey('$lineKey-gst'),
|
|
||||||
masterId: 'gst_rates',
|
|
||||||
label: 'GST rate',
|
|
||||||
value: line.gstRateId,
|
|
||||||
hint: 'Select GST rate',
|
|
||||||
searchHint: 'Search GST rate...',
|
|
||||||
options: gstOptions,
|
|
||||||
refreshLookups: () =>
|
|
||||||
ref.invalidate(purchaseOrderLookupsProvider),
|
|
||||||
parseCreatedId: int.tryParse,
|
|
||||||
onChanged: (v) => _updateLine(() => line.gstRateId = v),
|
|
||||||
),
|
|
||||||
_AmountWithRemove(
|
|
||||||
amount: CurrencyFormatter.format(calc.lineAmount),
|
|
||||||
backgroundColor: amountBg,
|
|
||||||
onRemove: widget.onRemove,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,31 @@ import 'package:flutter/material.dart';
|
|||||||
import 'app_searchable_dropdown.dart';
|
import 'app_searchable_dropdown.dart';
|
||||||
|
|
||||||
class AppDropdownOption<T> {
|
class AppDropdownOption<T> {
|
||||||
const AppDropdownOption({required this.value, required this.label});
|
const AppDropdownOption({
|
||||||
|
required this.value,
|
||||||
|
required this.label,
|
||||||
|
this.subtitle,
|
||||||
|
this.searchText,
|
||||||
|
});
|
||||||
|
|
||||||
final T value;
|
final T value;
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
/// Secondary line under [label] in the picker (e.g. item code).
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
/// Extra searchable text (defaults to [label] + [subtitle]).
|
||||||
|
final String? searchText;
|
||||||
|
|
||||||
|
String get searchable =>
|
||||||
|
(searchText ?? [label, if (subtitle != null) subtitle!].join(' '))
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
bool matchesQuery(String query) {
|
||||||
|
final q = query.trim().toLowerCase();
|
||||||
|
if (q.isEmpty) return true;
|
||||||
|
return searchable.contains(q);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Searchable dropdown — all app dropdowns use [AppSearchableDropdown] under the hood.
|
/// Searchable dropdown — all app dropdowns use [AppSearchableDropdown] under the hood.
|
||||||
|
|||||||
@ -237,15 +237,22 @@ class _AppSearchableDropdownState<T> extends State<AppSearchableDropdown<T>> {
|
|||||||
_overlayEntry != null
|
_overlayEntry != null
|
||||||
? Icons.arrow_drop_up
|
? Icons.arrow_drop_up
|
||||||
: Icons.arrow_drop_down,
|
: Icons.arrow_drop_down,
|
||||||
|
size: 24,
|
||||||
color: canOpen
|
color: canOpen
|
||||||
? colors.onSurfaceVariant
|
? colors.onSurfaceVariant
|
||||||
: theme.disabledColor,
|
: theme.disabledColor,
|
||||||
),
|
),
|
||||||
|
// Match AppTextField height (default icon box is 48px).
|
||||||
|
suffixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: 40,
|
||||||
|
minHeight: 40,
|
||||||
|
),
|
||||||
enabled: canOpen,
|
enabled: canOpen,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
displayLabel ?? '\u00A0',
|
displayLabel ?? '\u00A0',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
color: displayLabel == null
|
color: displayLabel == null
|
||||||
@ -301,9 +308,7 @@ class _SearchableDropdownPanelState<T>
|
|||||||
List<AppDropdownOption<T>> get _filtered {
|
List<AppDropdownOption<T>> get _filtered {
|
||||||
final q = _query.trim().toLowerCase();
|
final q = _query.trim().toLowerCase();
|
||||||
if (q.isEmpty) return widget.options;
|
if (q.isEmpty) return widget.options;
|
||||||
return widget.options
|
return widget.options.where((option) => option.matchesQuery(q)).toList();
|
||||||
.where((option) => option.label.toLowerCase().contains(q))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -364,6 +369,7 @@ class _SearchableDropdownPanelState<T>
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final option = filtered[index];
|
final option = filtered[index];
|
||||||
final isSelected = option.value == widget.selected;
|
final isSelected = option.value == widget.selected;
|
||||||
|
final subtitle = option.subtitle?.trim();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
@ -371,6 +377,15 @@ class _SearchableDropdownPanelState<T>
|
|||||||
option.label,
|
option.label,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
subtitle: subtitle == null || subtitle.isEmpty
|
||||||
|
? null
|
||||||
|
: Text(
|
||||||
|
subtitle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
trailing: isSelected
|
trailing: isSelected
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
@ -603,15 +618,21 @@ class _AppSearchableLookupFieldState<T>
|
|||||||
_overlayEntry != null
|
_overlayEntry != null
|
||||||
? Icons.arrow_drop_up
|
? Icons.arrow_drop_up
|
||||||
: Icons.arrow_drop_down,
|
: Icons.arrow_drop_down,
|
||||||
|
size: 24,
|
||||||
color: canOpen
|
color: canOpen
|
||||||
? colors.onSurfaceVariant
|
? colors.onSurfaceVariant
|
||||||
: theme.disabledColor,
|
: theme.disabledColor,
|
||||||
),
|
),
|
||||||
|
suffixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: 40,
|
||||||
|
minHeight: 40,
|
||||||
|
),
|
||||||
enabled: canOpen,
|
enabled: canOpen,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
displayLabel ?? '\u00A0',
|
displayLabel ?? '\u00A0',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
color: displayLabel == null
|
color: displayLabel == null
|
||||||
|
|||||||
@ -197,10 +197,15 @@ class _AppSearchableMultiSelectDropdownState<T>
|
|||||||
_overlayEntry != null
|
_overlayEntry != null
|
||||||
? Icons.arrow_drop_up
|
? Icons.arrow_drop_up
|
||||||
: Icons.arrow_drop_down,
|
: Icons.arrow_drop_down,
|
||||||
|
size: 24,
|
||||||
color: canOpen
|
color: canOpen
|
||||||
? colors.onSurfaceVariant
|
? colors.onSurfaceVariant
|
||||||
: theme.disabledColor,
|
: theme.disabledColor,
|
||||||
),
|
),
|
||||||
|
suffixIconConstraints: const BoxConstraints(
|
||||||
|
minWidth: 40,
|
||||||
|
minHeight: 40,
|
||||||
|
),
|
||||||
enabled: canOpen,
|
enabled: canOpen,
|
||||||
),
|
),
|
||||||
child: displayText.isEmpty
|
child: displayText.isEmpty
|
||||||
@ -262,9 +267,7 @@ class _SearchableMultiSelectPanelState<T>
|
|||||||
List<AppDropdownOption<T>> get _filtered {
|
List<AppDropdownOption<T>> get _filtered {
|
||||||
final q = _query.trim().toLowerCase();
|
final q = _query.trim().toLowerCase();
|
||||||
if (q.isEmpty) return widget.options;
|
if (q.isEmpty) return widget.options;
|
||||||
return widget.options
|
return widget.options.where((option) => option.matchesQuery(q)).toList();
|
||||||
.where((option) => option.label.toLowerCase().contains(q))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -328,6 +331,17 @@ class _SearchableMultiSelectPanelState<T>
|
|||||||
option.label,
|
option.label,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
subtitle: () {
|
||||||
|
final subtitle = option.subtitle?.trim();
|
||||||
|
if (subtitle == null || subtitle.isEmpty) return null;
|
||||||
|
return Text(
|
||||||
|
subtitle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}(),
|
||||||
value: isSelected,
|
value: isSelected,
|
||||||
onChanged: (checked) =>
|
onChanged: (checked) =>
|
||||||
widget.onToggle(option.value, checked ?? false),
|
widget.onToggle(option.value, checked ?? false),
|
||||||
|
|||||||
@ -251,9 +251,9 @@ class SidePanelSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Divider(height: 1, color: dividerColor),
|
Divider(height: 1, color: dividerColor),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
...children,
|
...children,
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class AppTextField extends StatelessWidget {
|
|||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.autofillHints,
|
this.autofillHints,
|
||||||
this.isDense = false,
|
this.isDense = false,
|
||||||
|
this.autovalidateMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@ -36,6 +37,7 @@ class AppTextField extends StatelessWidget {
|
|||||||
final bool enabled;
|
final bool enabled;
|
||||||
final Iterable<String>? autofillHints;
|
final Iterable<String>? autofillHints;
|
||||||
final bool isDense;
|
final bool isDense;
|
||||||
|
final AutovalidateMode? autovalidateMode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -46,6 +48,7 @@ class AppTextField extends StatelessWidget {
|
|||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
|
autovalidateMode: autovalidateMode,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
maxLength: maxLength,
|
maxLength: maxLength,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user