entityfield
This commit is contained in:
		
							parent
							
								
									f3acdf65c0
								
							
						
					
					
						commit
						1e8b611228
					
				| @ -0,0 +1,651 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| 
 | ||||
| import '../../../core/constants/ui_constants.dart'; | ||||
| import '../../../core/providers/dynamic_theme_provider.dart'; | ||||
| import '../ui/entity_form.dart'; | ||||
| import 'autocomplete_dropdown_field.dart' as FAutoDropdown; | ||||
| import 'autocomplete_multiselect_field.dart' as FAutoMultiSelect; | ||||
| import 'base_field.dart'; | ||||
| import 'captcha_field.dart' as FCaptcha; | ||||
| import 'checkbox_field.dart' as FCheckbox; | ||||
| import 'custom_text_field.dart' as FText; | ||||
| import 'date_field.dart' as FDate; | ||||
| import 'datetime_field.dart' as FDateTime; | ||||
| import 'dropdown_field.dart' as FDropdown; | ||||
| import 'email_field.dart' as FEmail; | ||||
| import 'number_field.dart' as FNumber; | ||||
| import 'password_field.dart' as FPassword; | ||||
| import 'phone_field.dart' as FPhone; | ||||
| import 'switch_field.dart' as FSwitch; | ||||
| import 'url_field.dart' as FUrl; | ||||
| 
 | ||||
| /// A generic field that shows an Insert button to create a related entity inline. | ||||
| /// | ||||
| /// Usage: | ||||
| /// - Provide `fieldKey` for where to store the selected/created value | ||||
| /// - Provide `label` for display | ||||
| /// - Provide `relationTitle` for the modal header | ||||
| /// - Either provide `fieldSchema` (preferred, like OneToOne/OneToMany) or `formFieldsBuilder` | ||||
| /// - Provide `onCreate` callback which receives form data and returns created entity map | ||||
| /// - Configure `valueKey` to pick what to write back (e.g., 'id') into the parent controller | ||||
| /// - Optionally set `displayKey` to show a summary of the selected value | ||||
| class RelatedEntityInsertField extends BaseField { | ||||
|   final String fieldKey; | ||||
|   final String label; | ||||
|   final String hint; | ||||
|   final bool isRequired; | ||||
|   final String relationTitle; | ||||
|   final Future<Map<String, dynamic>> Function(Map<String, dynamic> formData) | ||||
|       onCreate; | ||||
|   final List<Map<String, dynamic>>? fieldSchema; | ||||
|   final List<BaseField> Function()? formFieldsBuilder; | ||||
|   final String valueKey; | ||||
|   final String? displayKey; | ||||
| 
 | ||||
|   RelatedEntityInsertField({ | ||||
|     required this.fieldKey, | ||||
|     required this.label, | ||||
|     this.hint = '', | ||||
|     this.isRequired = false, | ||||
|     required this.relationTitle, | ||||
|     required this.onCreate, | ||||
|     this.fieldSchema, | ||||
|     this.formFieldsBuilder, | ||||
|     this.valueKey = 'id', | ||||
|     this.displayKey, | ||||
|   }) : assert(fieldSchema != null || formFieldsBuilder != null, | ||||
|             'Either fieldSchema or formFieldsBuilder must be provided'); | ||||
| 
 | ||||
|   @override | ||||
|   String? Function(String?)? get validator => (value) { | ||||
|         if (!isRequired) return null; | ||||
|         if (value == null || value.trim().isEmpty) return '$label is required'; | ||||
|         return null; | ||||
|       }; | ||||
| 
 | ||||
|   @override | ||||
|   Map<String, dynamic>? get customProperties => { | ||||
|         'isRelationInsert': true, | ||||
|         'relationTitle': relationTitle, | ||||
|         'valueKey': valueKey, | ||||
|         'displayKey': displayKey, | ||||
|       }; | ||||
| 
 | ||||
|   @override | ||||
|   Widget buildField({ | ||||
|     required TextEditingController controller, | ||||
|     required ColorScheme colorScheme, | ||||
|     VoidCallback? onChanged, | ||||
|   }) { | ||||
|     return _RelatedEntityInsertWidget( | ||||
|       label: label, | ||||
|       hint: hint, | ||||
|       controller: controller, | ||||
|       colorScheme: colorScheme, | ||||
|       relationTitle: relationTitle, | ||||
|       onCreate: onCreate, | ||||
|       fieldSchema: fieldSchema, | ||||
|       formFieldsBuilder: formFieldsBuilder, | ||||
|       valueKey: valueKey, | ||||
|       displayKey: displayKey, | ||||
|       validate: validator, | ||||
|       onChanged: onChanged, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Static helper to open the insert dialog anywhere (e.g., in list actions) | ||||
|   /// Returns the written value (e.g., created id as string) or null if cancelled | ||||
|   static Future<String?> showInsertDialog({ | ||||
|     required BuildContext context, | ||||
|     required String relationTitle, | ||||
|     required Future<Map<String, dynamic>> Function(Map<String, dynamic>) | ||||
|         onCreate, | ||||
|     List<Map<String, dynamic>>? fieldSchema, | ||||
|     List<BaseField> Function()? formFieldsBuilder, | ||||
|     String valueKey = 'id', | ||||
|     String? displayKey, | ||||
|   }) async { | ||||
|     final TextEditingController ctrl = TextEditingController(); | ||||
|     bool isSubmitting = false; | ||||
|     final List<BaseField> fields = (formFieldsBuilder != null) | ||||
|         ? formFieldsBuilder() | ||||
|         : _fieldsFromSchemaStatic(fieldSchema ?? const []); | ||||
| 
 | ||||
|     final Map<String, dynamic>? created = | ||||
|         await showDialog<Map<String, dynamic>>( | ||||
|       context: context, | ||||
|       barrierDismissible: false, | ||||
|       builder: (dialogContext) { | ||||
|         return Consumer<DynamicThemeProvider>( | ||||
|           builder: (context, theme, child) { | ||||
|             final cs = theme.getCurrentColorScheme( | ||||
|                 Theme.of(dialogContext).brightness == Brightness.dark); | ||||
|             return StatefulBuilder( | ||||
|               builder: (context, setLocal) { | ||||
|                 return AlertDialog( | ||||
|                   backgroundColor: cs.surface, | ||||
|                   shape: RoundedRectangleBorder( | ||||
|                       borderRadius: | ||||
|                           BorderRadius.circular(UIConstants.radius16)), | ||||
|                   title: Text( | ||||
|                     relationTitle, | ||||
|                     style: TextStyle( | ||||
|                         color: cs.onSurface, fontWeight: FontWeight.w700), | ||||
|                   ), | ||||
|                   content: SingleChildScrollView( | ||||
|                     child: EntityForm( | ||||
|                       fields: fields, | ||||
|                       onSubmit: (formData) async { | ||||
|                         if (isSubmitting) return; | ||||
|                         setLocal(() => isSubmitting = true); | ||||
|                         try { | ||||
|                           final res = await onCreate(formData); | ||||
|                           if (!Navigator.of(dialogContext).mounted) return; | ||||
|                           Navigator.of(dialogContext).pop(res); | ||||
|                         } catch (_) { | ||||
|                           setLocal(() => isSubmitting = false); | ||||
|                         } | ||||
|                       }, | ||||
|                       submitButtonText: 'Create', | ||||
|                       isLoading: isSubmitting, | ||||
|                     ), | ||||
|                   ), | ||||
|                   actions: [ | ||||
|                     TextButton( | ||||
|                       onPressed: isSubmitting | ||||
|                           ? null | ||||
|                           : () => Navigator.of(dialogContext).pop(), | ||||
|                       child: const Text('Cancel'), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ); | ||||
|               }, | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
| 
 | ||||
|     if (created != null) { | ||||
|       ctrl.text = (created[valueKey])?.toString() ?? ''; | ||||
|       return ctrl.text.isEmpty ? null : ctrl.text; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   // Static schema â fields mapper for the dialog helper | ||||
|   static List<BaseField> _fieldsFromSchemaStatic( | ||||
|       List<Map<String, dynamic>> schema) { | ||||
|     return schema.map<BaseField>((f) { | ||||
|       final String type = f['type']?.toString() ?? 'text'; | ||||
|       final String flabel = f['label']?.toString() ?? ''; | ||||
|       final String fhint = f['hint']?.toString() ?? ''; | ||||
|       final String path = f['path']?.toString() ?? ''; | ||||
|       final bool requiredField = f['required'] == true; | ||||
|       final List<Map<String, dynamic>>? options = | ||||
|           (f['options'] as List?)?.cast<Map<String, dynamic>>(); | ||||
|       final String valueKey = f['valueKey']?.toString() ?? 'id'; | ||||
|       final String displayKey = f['displayKey']?.toString() ?? 'name'; | ||||
|       final int? maxLength = | ||||
|           f['maxLength'] is int ? f['maxLength'] as int : null; | ||||
|       final int? decimalPlaces = | ||||
|           f['decimalPlaces'] is int ? f['decimalPlaces'] as int : null; | ||||
|       final double? min = | ||||
|           (f['min'] is num) ? (f['min'] as num).toDouble() : null; | ||||
|       final double? max = | ||||
|           (f['max'] is num) ? (f['max'] as num).toDouble() : null; | ||||
| 
 | ||||
|       switch (type) { | ||||
|         case 'number': | ||||
|           return FNumber.NumberField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             min: min, | ||||
|             max: max, | ||||
|             decimalPlaces: decimalPlaces, | ||||
|           ); | ||||
|         case 'email': | ||||
|           return FEmail.EmailField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'phone': | ||||
|           return FPhone.PhoneField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'password': | ||||
|           return FPassword.PasswordField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'dropdown': | ||||
|           return FDropdown.DropdownField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             options: options ?? const [], | ||||
|             valueKey: valueKey, | ||||
|             displayKey: displayKey, | ||||
|           ); | ||||
|         case 'date': | ||||
|           return FDate.DateField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'datetime': | ||||
|           return FDateTime.DateTimeField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'switch': | ||||
|           return FSwitch.SwitchField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'checkbox': | ||||
|           return FCheckbox.CheckboxField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'captcha': | ||||
|           return FCaptcha.CaptchaField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'url': | ||||
|           return FUrl.UrlField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'autocomplete_dropdown': | ||||
|           return FAutoDropdown.AutocompleteDropdownField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             optionsLoader: () async => options ?? [], | ||||
|             valueKey: valueKey, | ||||
|             displayKey: displayKey, | ||||
|           ); | ||||
|         case 'autocomplete_multiselect': | ||||
|           return FAutoMultiSelect.AutocompleteMultiSelectField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             optionsLoader: () async => options ?? [], | ||||
|             valueKey: valueKey, | ||||
|             displayKey: displayKey, | ||||
|           ); | ||||
|         case 'text': | ||||
|         default: | ||||
|           return FText.CustomTextField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             maxLength: maxLength, | ||||
|           ); | ||||
|       } | ||||
|     }).toList(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class _RelatedEntityInsertWidget extends StatefulWidget { | ||||
|   final String label; | ||||
|   final String hint; | ||||
|   final TextEditingController controller; | ||||
|   final ColorScheme colorScheme; | ||||
|   final String relationTitle; | ||||
|   final Future<Map<String, dynamic>> Function(Map<String, dynamic>) onCreate; | ||||
|   final List<Map<String, dynamic>>? fieldSchema; | ||||
|   final List<BaseField> Function()? formFieldsBuilder; | ||||
|   final String valueKey; | ||||
|   final String? displayKey; | ||||
|   final String? Function(String?)? validate; | ||||
|   final VoidCallback? onChanged; | ||||
| 
 | ||||
|   const _RelatedEntityInsertWidget({ | ||||
|     required this.label, | ||||
|     required this.hint, | ||||
|     required this.controller, | ||||
|     required this.colorScheme, | ||||
|     required this.relationTitle, | ||||
|     required this.onCreate, | ||||
|     this.fieldSchema, | ||||
|     this.formFieldsBuilder, | ||||
|     required this.valueKey, | ||||
|     required this.displayKey, | ||||
|     required this.validate, | ||||
|     required this.onChanged, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
|   State<_RelatedEntityInsertWidget> createState() => | ||||
|       _RelatedEntityInsertWidgetState(); | ||||
| } | ||||
| 
 | ||||
| class _RelatedEntityInsertWidgetState | ||||
|     extends State<_RelatedEntityInsertWidget> { | ||||
|   bool _creating = false; | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final cs = widget.colorScheme; | ||||
|     final String selectedDisplay = _computeDisplay(); | ||||
| 
 | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         if (widget.label.isNotEmpty) | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.only(bottom: 8), | ||||
|             child: Text( | ||||
|               widget.label, | ||||
|               style: TextStyle( | ||||
|                   fontWeight: FontWeight.w700, | ||||
|                   color: cs.onSurface, | ||||
|                   fontSize: 14), | ||||
|             ), | ||||
|           ), | ||||
|         Container( | ||||
|           decoration: BoxDecoration( | ||||
|             borderRadius: BorderRadius.circular(12), | ||||
|             border: Border.all(color: cs.primary.withOpacity(0.15)), | ||||
|             gradient: LinearGradient( | ||||
|               begin: Alignment.topLeft, | ||||
|               end: Alignment.bottomRight, | ||||
|               colors: [ | ||||
|                 cs.surface, | ||||
|                 cs.surface.withOpacity(0.97), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|           padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               Expanded( | ||||
|                 child: Text( | ||||
|                   selectedDisplay.isEmpty | ||||
|                       ? (widget.hint.isEmpty ? 'No selection' : widget.hint) | ||||
|                       : selectedDisplay, | ||||
|                   style: TextStyle( | ||||
|                       color: cs.onSurface.withOpacity(0.75), | ||||
|                       fontWeight: FontWeight.w500), | ||||
|                   overflow: TextOverflow.ellipsis, | ||||
|                 ), | ||||
|               ), | ||||
|               const SizedBox(width: 8), | ||||
|               ElevatedButton.icon( | ||||
|                 onPressed: _creating ? null : _openInsertDialog, | ||||
|                 icon: Icon(Icons.add_rounded, color: cs.onPrimary, size: 18), | ||||
|                 label: Text('Insert', | ||||
|                     style: TextStyle( | ||||
|                         color: cs.onPrimary, fontWeight: FontWeight.w600)), | ||||
|                 style: ElevatedButton.styleFrom( | ||||
|                   backgroundColor: cs.primary, | ||||
|                   padding: | ||||
|                       const EdgeInsets.symmetric(horizontal: 14, vertical: 10), | ||||
|                   elevation: 2, | ||||
|                   shape: RoundedRectangleBorder( | ||||
|                       borderRadius: BorderRadius.circular(10)), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         if (widget.validate != null) | ||||
|           Builder( | ||||
|             builder: (context) { | ||||
|               final error = widget.validate!(widget.controller.text); | ||||
|               return error != null | ||||
|                   ? Padding( | ||||
|                       padding: const EdgeInsets.only(top: 6), | ||||
|                       child: Text(error, | ||||
|                           style: TextStyle( | ||||
|                               color: cs.error, | ||||
|                               fontSize: 12, | ||||
|                               fontWeight: FontWeight.w600)), | ||||
|                     ) | ||||
|                   : const SizedBox.shrink(); | ||||
|             }, | ||||
|           ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   String _computeDisplay() { | ||||
|     if (widget.displayKey == null || widget.displayKey!.isEmpty) { | ||||
|       return widget.controller.text.trim(); | ||||
|     } | ||||
|     return widget.controller.text.trim(); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _openInsertDialog() async { | ||||
|     setState(() => _creating = true); | ||||
|     try { | ||||
|       final List<BaseField> fields = (widget.formFieldsBuilder != null) | ||||
|           ? widget.formFieldsBuilder!() | ||||
|           : _fieldsFromSchema(widget.fieldSchema ?? const []); | ||||
| 
 | ||||
|       final result = await showDialog<Map<String, dynamic>>( | ||||
|         context: context, | ||||
|         barrierDismissible: false, | ||||
|         builder: (context) { | ||||
|           bool isSubmitting = false; | ||||
|           return StatefulBuilder( | ||||
|             builder: (context, setLocal) { | ||||
|               return AlertDialog( | ||||
|                 backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|                 shape: RoundedRectangleBorder( | ||||
|                     borderRadius: BorderRadius.circular(16)), | ||||
|                 title: Text( | ||||
|                   widget.relationTitle, | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).colorScheme.onSurface, | ||||
|                     fontWeight: FontWeight.w700, | ||||
|                   ), | ||||
|                 ), | ||||
|                 content: SingleChildScrollView( | ||||
|                   child: EntityForm( | ||||
|                     fields: fields, | ||||
|                     onSubmit: (formData) async { | ||||
|                       if (isSubmitting) return; | ||||
|                       setLocal(() => isSubmitting = true); | ||||
|                       try { | ||||
|                         final created = await widget.onCreate(formData); | ||||
|                         if (!mounted) return; | ||||
|                         Navigator.of(context).pop(created); | ||||
|                       } catch (_) { | ||||
|                         setLocal(() => isSubmitting = false); | ||||
|                       } | ||||
|                     }, | ||||
|                     submitButtonText: 'Create', | ||||
|                     isLoading: isSubmitting, | ||||
|                   ), | ||||
|                 ), | ||||
|                 actions: [ | ||||
|                   TextButton( | ||||
|                     onPressed: | ||||
|                         isSubmitting ? null : () => Navigator.of(context).pop(), | ||||
|                     child: const Text('Cancel'), | ||||
|                   ), | ||||
|                 ], | ||||
|               ); | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|       ); | ||||
| 
 | ||||
|       if (result != null) { | ||||
|         final dynamic value = result[widget.valueKey]; | ||||
|         final String textValue = value?.toString() ?? ''; | ||||
|         widget.controller.text = textValue; | ||||
|         widget.onChanged?.call(); | ||||
|         EntityFormScope.of(context)?.notifyParent(); | ||||
|       } | ||||
|     } finally { | ||||
|       if (mounted) setState(() => _creating = false); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   List<BaseField> _fieldsFromSchema(List<Map<String, dynamic>> schema) { | ||||
|     return schema.map<BaseField>((f) { | ||||
|       final String type = f['type']?.toString() ?? 'text'; | ||||
|       final String flabel = f['label']?.toString() ?? ''; | ||||
|       final String fhint = f['hint']?.toString() ?? ''; | ||||
|       final String path = f['path']?.toString() ?? ''; | ||||
|       final bool requiredField = f['required'] == true; | ||||
|       final List<Map<String, dynamic>>? options = | ||||
|           (f['options'] as List?)?.cast<Map<String, dynamic>>(); | ||||
|       final String valueKey = f['valueKey']?.toString() ?? 'id'; | ||||
|       final String displayKey = f['displayKey']?.toString() ?? 'name'; | ||||
|       final int? maxLength = | ||||
|           f['maxLength'] is int ? f['maxLength'] as int : null; | ||||
|       final int? decimalPlaces = | ||||
|           f['decimalPlaces'] is int ? f['decimalPlaces'] as int : null; | ||||
|       final double? min = | ||||
|           (f['min'] is num) ? (f['min'] as num).toDouble() : null; | ||||
|       final double? max = | ||||
|           (f['max'] is num) ? (f['max'] as num).toDouble() : null; | ||||
| 
 | ||||
|       switch (type) { | ||||
|         case 'number': | ||||
|           return FNumber.NumberField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             min: min, | ||||
|             max: max, | ||||
|             decimalPlaces: decimalPlaces, | ||||
|           ); | ||||
|         case 'email': | ||||
|           return FEmail.EmailField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'phone': | ||||
|           return FPhone.PhoneField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'password': | ||||
|           return FPassword.PasswordField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'dropdown': | ||||
|           return FDropdown.DropdownField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             options: options ?? const [], | ||||
|             valueKey: valueKey, | ||||
|             displayKey: displayKey, | ||||
|           ); | ||||
|         case 'date': | ||||
|           return FDate.DateField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'datetime': | ||||
|           return FDateTime.DateTimeField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'switch': | ||||
|           return FSwitch.SwitchField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'checkbox': | ||||
|           return FCheckbox.CheckboxField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'captcha': | ||||
|           return FCaptcha.CaptchaField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'url': | ||||
|           return FUrl.UrlField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|           ); | ||||
|         case 'autocomplete_dropdown': | ||||
|           return FAutoDropdown.AutocompleteDropdownField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             optionsLoader: () async => options ?? [], | ||||
|             valueKey: valueKey, | ||||
|             displayKey: displayKey, | ||||
|           ); | ||||
|         case 'autocomplete_multiselect': | ||||
|           return FAutoMultiSelect.AutocompleteMultiSelectField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             optionsLoader: () async => options ?? [], | ||||
|             valueKey: valueKey, | ||||
|             displayKey: displayKey, | ||||
|           ); | ||||
|         case 'text': | ||||
|         default: | ||||
|           return FText.CustomTextField( | ||||
|             fieldKey: path, | ||||
|             label: flabel, | ||||
|             hint: fhint, | ||||
|             isRequired: requiredField, | ||||
|             maxLength: maxLength, | ||||
|           ); | ||||
|       } | ||||
|     }).toList(); | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| import 'package:base_project/core/providers/dynamic_theme_provider.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import '../../../core/providers/dynamic_theme_provider.dart'; | ||||
| import '../../../core/constants/ui_constants.dart'; | ||||
| import '../../../shared/widgets/app_bar/modern_app_bar.dart'; | ||||
| import 'entity_card.dart'; | ||||
| @ -22,6 +22,9 @@ class EntityList extends StatefulWidget { | ||||
|   final String title; | ||||
|   final Function()? onAddNew; | ||||
|   final List<Map<String, dynamic>> displayFields; | ||||
|   // Optional: header insert action (e.g., Childform/Insert Support) | ||||
|   final Function()? onInsertAction; | ||||
|   final String? insertActionLabel; | ||||
| 
 | ||||
|   const EntityList({ | ||||
|     super.key, | ||||
| @ -39,6 +42,8 @@ class EntityList extends StatefulWidget { | ||||
|     required this.title, | ||||
|     this.onAddNew, | ||||
|     this.displayFields = const [], | ||||
|     this.onInsertAction, | ||||
|     this.insertActionLabel, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
| @ -97,13 +102,21 @@ class _EntityListState extends State<EntityList> with TickerProviderStateMixin { | ||||
|             title: widget.title, | ||||
|             showBackButton: true, | ||||
|             actions: [ | ||||
|               if (widget.onInsertAction != null) | ||||
|                 TextButton( | ||||
|                   onPressed: widget.onInsertAction, | ||||
|                   child: Text( | ||||
|                     widget.insertActionLabel ?? 'Unknown', | ||||
|                     style: TextStyle( | ||||
|                       color: colorScheme.primary, | ||||
|                       fontWeight: FontWeight.w600, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               if (widget.onAddNew != null) | ||||
|                 IconButton( | ||||
|                   onPressed: widget.onAddNew, | ||||
|                   icon: Icon( | ||||
|                     Icons.add_rounded, | ||||
|                     color: colorScheme.primary, | ||||
|                   ), | ||||
|                   icon: Icon(Icons.add_rounded, color: colorScheme.primary), | ||||
|                   tooltip: 'Add New', | ||||
|                 ), | ||||
|             ], | ||||
| @ -114,9 +127,7 @@ class _EntityListState extends State<EntityList> with TickerProviderStateMixin { | ||||
|               _buildSearchBar(colorScheme), | ||||
| 
 | ||||
|               // Content | ||||
|               Expanded( | ||||
|                 child: _buildContent(colorScheme), | ||||
|               ), | ||||
|               Expanded(child: _buildContent(colorScheme)), | ||||
|             ], | ||||
|           ), | ||||
|         ); | ||||
| @ -136,10 +147,7 @@ class _EntityListState extends State<EntityList> with TickerProviderStateMixin { | ||||
|         gradient: LinearGradient( | ||||
|           begin: Alignment.topLeft, | ||||
|           end: Alignment.bottomRight, | ||||
|           colors: [ | ||||
|             colorScheme.surface, | ||||
|             colorScheme.surface.withOpacity(0.95), | ||||
|           ], | ||||
|           colors: [colorScheme.surface, colorScheme.surface.withOpacity(0.95)], | ||||
|         ), | ||||
|         borderRadius: BorderRadius.circular(UIConstants.radius16), | ||||
|         border: Border.all( | ||||
| @ -234,10 +242,7 @@ class _EntityListState extends State<EntityList> with TickerProviderStateMixin { | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           CircularProgressIndicator( | ||||
|             color: colorScheme.primary, | ||||
|             strokeWidth: 3, | ||||
|           ), | ||||
|           CircularProgressIndicator(color: colorScheme.primary, strokeWidth: 3), | ||||
|           const SizedBox(height: UIConstants.spacing16), | ||||
|           Text( | ||||
|             'Loading ${widget.title.toLowerCase()}...', | ||||
| @ -292,10 +297,7 @@ class _EntityListState extends State<EntityList> with TickerProviderStateMixin { | ||||
|           const SizedBox(height: UIConstants.spacing24), | ||||
|           ElevatedButton.icon( | ||||
|             onPressed: widget.onRefresh, | ||||
|             icon: Icon( | ||||
|               Icons.refresh_rounded, | ||||
|               color: colorScheme.onPrimary, | ||||
|             ), | ||||
|             icon: Icon(Icons.refresh_rounded, color: colorScheme.onPrimary), | ||||
|             label: Text( | ||||
|               'Retry', | ||||
|               style: TextStyle( | ||||
| @ -365,10 +367,7 @@ class _EntityListState extends State<EntityList> with TickerProviderStateMixin { | ||||
|             const SizedBox(height: UIConstants.spacing24), | ||||
|             ElevatedButton.icon( | ||||
|               onPressed: widget.onAddNew, | ||||
|               icon: Icon( | ||||
|                 Icons.add_rounded, | ||||
|                 color: colorScheme.onPrimary, | ||||
|               ), | ||||
|               icon: Icon(Icons.add_rounded, color: colorScheme.onPrimary), | ||||
|               label: Text( | ||||
|                 'Add ${widget.title}', | ||||
|                 style: TextStyle( | ||||
| @ -454,10 +453,7 @@ class _EntityListState extends State<EntityList> with TickerProviderStateMixin { | ||||
|         gradient: LinearGradient( | ||||
|           begin: Alignment.topLeft, | ||||
|           end: Alignment.bottomRight, | ||||
|           colors: [ | ||||
|             colorScheme.surface, | ||||
|             colorScheme.surface.withOpacity(0.95), | ||||
|           ], | ||||
|           colors: [colorScheme.surface, colorScheme.surface.withOpacity(0.95)], | ||||
|         ), | ||||
|         borderRadius: BorderRadius.circular(UIConstants.radius20), | ||||
|         border: Border.all( | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user