diff --git a/base_project/lib/BuilderField/shared/fields/barcode_field.dart b/base_project/lib/BuilderField/shared/fields/barcode_field.dart new file mode 100644 index 0000000..6090dc8 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/barcode_field.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:barcode_widget/barcode_widget.dart'; +import 'base_field.dart'; + +class BarcodeField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final Future Function()? scanner; // returns scanned text + + BarcodeField({ + required this.fieldKey, + required this.label, + this.hint = '', + this.isRequired = false, + this.scanner, + }); + + @override + String? Function(String?)? get validator => (v) { + if (isRequired && (v == null || v.trim().isEmpty)) return 'Required'; + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: colorScheme.outlineVariant.withOpacity(0.3)), + ), + child: Column( + children: [ + if (controller.text.trim().isNotEmpty) ...[ + BarcodeWidget( + barcode: Barcode.code128(), + data: controller.text.trim(), + width: 220, + height: 80, + ), + const SizedBox(height: 12), + ], + Row( + children: [ + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: + hint.isEmpty ? 'Enter or scan barcode' : hint, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + filled: true, + ), + validator: validator, + onChanged: (_) => onChanged?.call(), + ), + ), + const SizedBox(width: 8), + IconButton( + tooltip: 'Scan Barcode', + icon: Icon(Icons.document_scanner_rounded, + color: colorScheme.primary), + onPressed: scanner == null + ? null + : () async { + final val = await scanner!.call(); + if (val != null) { + controller.text = val; + onChanged?.call(); + } + }, + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/calculated_field.dart b/base_project/lib/BuilderField/shared/fields/calculated_field.dart index b107d6c..31bf0e5 100644 --- a/base_project/lib/BuilderField/shared/fields/calculated_field.dart +++ b/base_project/lib/BuilderField/shared/fields/calculated_field.dart @@ -124,13 +124,13 @@ class _CalculatedWidgetState extends State<_CalculatedWidget> { case 'Addition': acc += nums[i]; break; - case 'sub': + case 'Subtraction': acc -= nums[i]; break; - case 'mul': + case 'Multiplication': acc *= nums[i]; break; - case 'div': + case 'Division': if (nums[i] != 0) acc /= nums[i]; break; } diff --git a/base_project/lib/BuilderField/shared/fields/currency_field.dart b/base_project/lib/BuilderField/shared/fields/currency_field.dart new file mode 100644 index 0000000..4297ff0 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/currency_field.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'base_field.dart'; + +class CurrencyField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final String currencySymbol; + final int decimalDigits; + final List> + currencyOptions; // [{code: 'INR', symbol: '₹', name: 'India (INR)'}] + + CurrencyField({ + required this.fieldKey, + required this.label, + this.hint = '', + this.isRequired = false, + this.currencySymbol = '₹', + this.decimalDigits = 2, + List>? currencyOptions, + }) : currencyOptions = currencyOptions ?? + const [ + {'code': 'INR', 'symbol': '₹', 'name': 'India (INR)'}, + {'code': 'USD', 'symbol': '\$', 'name': 'USA (USD)'}, + {'code': 'EUR', 'symbol': '€', 'name': 'Euro (EUR)'}, + {'code': 'GBP', 'symbol': '£', 'name': 'UK (GBP)'}, + {'code': 'AUD', 'symbol': 'A\$', 'name': 'Australia (AUD)'}, + {'code': 'CAD', 'symbol': 'C\$', 'name': 'Canada (CAD)'}, + {'code': 'CHF', 'symbol': 'CHF', 'name': 'Switzerland (CHF)'}, + {'code': 'CNY', 'symbol': 'Â¥', 'name': 'China (CNY)'}, + {'code': 'HKD', 'symbol': 'HK\$', 'name': 'Hong Kong (HKD)'}, + {'code': 'NZD', 'symbol': 'NZ\$', 'name': 'New Zealand (NZD)'}, + {'code': 'SGD', 'symbol': 'S\$', 'name': 'Singapore (SGD)'}, + {'code': 'ZAR', 'symbol': 'R', 'name': 'South Africa (ZAR)'}, + {'code': 'SEK', 'symbol': 'kr', 'name': 'Sweden (SEK)'}, + {'code': 'NOK', 'symbol': 'kr', 'name': 'Norway (NOK)'}, + {'code': 'MXN', 'symbol': '\$', 'name': 'Mexico (MXN)'}, + {'code': 'BRL', 'symbol': 'R\$', 'name': 'Brazil (BRL)'}, + {'code': 'RUB', 'symbol': '₽', 'name': 'Russia (RUB)'}, + {'code': 'KRW', 'symbol': '₩', 'name': 'South Korea (KRW)'}, + {'code': 'TRY', 'symbol': '₺', 'name': 'Turkey (TRY)'}, + {'code': 'JPY', 'symbol': '¥', 'name': 'Japan (JPY)'}, + ]; + + @override + String? Function(String?)? get validator => (value) { + final v = value?.trim() ?? ''; + if (isRequired && v.isEmpty) return 'Required'; + if (v.isNotEmpty && double.tryParse(v.replaceAll(',', '')) == null) { + return 'Invalid amount'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return _CurrencyFieldWidget( + controller: controller, + colorScheme: colorScheme, + label: label, + hint: hint.isEmpty ? '0.${'0' * decimalDigits}' : hint, + isRequired: isRequired, + decimalDigits: decimalDigits, + defaultSymbol: currencySymbol, + options: currencyOptions, + onChanged: onChanged, + validator: validator, + ); + } +} + +class _CurrencyFieldWidget extends StatefulWidget { + final TextEditingController controller; + final ColorScheme colorScheme; + final String label; + final String hint; + final bool isRequired; + final int decimalDigits; + final String defaultSymbol; + final List> options; + final VoidCallback? onChanged; + final String? Function(String?)? validator; + + const _CurrencyFieldWidget({ + required this.controller, + required this.colorScheme, + required this.label, + required this.hint, + required this.isRequired, + required this.decimalDigits, + required this.defaultSymbol, + required this.options, + this.onChanged, + this.validator, + }); + + @override + State<_CurrencyFieldWidget> createState() => _CurrencyFieldWidgetState(); +} + +class _CurrencyFieldWidgetState extends State<_CurrencyFieldWidget> { + late String _selectedSymbol; + + @override + void initState() { + super.initState(); + _selectedSymbol = widget.defaultSymbol; + } + + @override + Widget build(BuildContext context) { + final cs = widget.colorScheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.label, + style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: cs.outlineVariant.withOpacity(0.4)), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedSymbol, + items: widget.options + .map((o) => DropdownMenuItem( + value: o['symbol'], + child: Row( + children: [ + Text(o['symbol'] ?? ''), + const SizedBox(width: 8), + Text( + o['code'] ?? '', + style: TextStyle( + color: cs.onSurface.withOpacity(0.7), + fontSize: 12), + ), + ], + ), + )) + .toList(), + onChanged: (val) { + if (val == null) return; + setState(() => _selectedSymbol = val); + widget.onChanged?.call(); + }, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextFormField( + controller: widget.controller, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]')), + ], + decoration: InputDecoration( + hintText: widget.hint, + prefixText: '$_selectedSymbol ', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12)), + filled: true, + ), + validator: widget.validator, + onChanged: (_) => widget.onChanged?.call(), + ), + ), + ], + ), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/field_group_field.dart b/base_project/lib/BuilderField/shared/fields/field_group_field.dart new file mode 100644 index 0000000..9dc8f7b --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/field_group_field.dart @@ -0,0 +1,319 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../ui/entity_form.dart'; + +import 'custom_text_field.dart'; +import 'number_field.dart' as shared_number; +import 'date_field.dart' as shared_date; +import 'currency_field.dart'; +import 'qr_code_field.dart'; +import 'barcode_field.dart'; + +/// FieldGroupField +/// - Visual group that renders multiple sub-inputs inside a styled container +/// - Uses EntityForm's composite submit capability via assignByJsonPaths + paths +/// - The group controller stores a JSON string of path->value; on submit, EntityForm +/// assigns each value into the request body by its json path. +class FieldGroupField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final List? subFields; + // Schema-driven: field name -> type (e.g., 'text', 'number', 'date', 'currency', 'qrcode', 'barcode') + final String? groupPrefix; // e.g., 'primary' + final Map? schema; + + FieldGroupField({ + required this.fieldKey, + required this.label, + this.hint = '', + this.isRequired = false, + this.subFields, + this.groupPrefix, + this.schema, + }); + + @override + String? Function(String?)? get validator => (v) { + if (!isRequired) return null; + final fields = _effectiveSubFields; + if (fields.any((sf) => sf.isRequired)) { + final scope = _scope; + if (scope == null) return null; + for (final sf in fields) { + if (!sf.isRequired) continue; + final c = scope.controllers[sf.path]; + if (c == null || c.text.trim().isEmpty) { + return 'Please complete required fields'; + } + } + } + return null; + }; + + static EntityFormScope? get _scope => _EntityFormScopeAccessor.scope; + + @override + Map? get customProperties => { + 'excludeFromSubmit': true, + 'assignByJsonPaths': true, + 'paths': _effectiveSubFields.map((e) => e.path).toList(), + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return _EntityFormScopeAccessor( + builder: (scope) { + final fields = _effectiveSubFields; + return Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: + Border.all(color: colorScheme.outlineVariant.withOpacity(0.4)), + boxShadow: [ + BoxShadow( + color: colorScheme.shadow.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 6), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + if (hint.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + hint, + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + const SizedBox(height: 12), + ...fields.map((sf) => _buildSubField( + scope, sf, colorScheme, controller, onChanged)), + ], + ), + ), + ); + }, + ); + } + + Widget _buildSubField( + EntityFormScope? scope, + GroupSubField sf, + ColorScheme colorScheme, + TextEditingController groupController, + VoidCallback? onChanged, + ) { + final existing = scope?.controllers[sf.path]; + final TextEditingController controller = + existing ?? TextEditingController(); + // Ensure controller is registered in scope for inner fields + if (scope != null && existing == null) { + scope.controllers[sf.path] = controller; + } + // Prefill subfield from group's encoded initial JSON (built by EntityForm) + if (controller.text.isEmpty && groupController.text.isNotEmpty) { + try { + final decoded = const JsonDecoder().convert(groupController.text); + if (decoded is Map) { + final dynamic v = decoded[sf.path]; + if (v != null && v.toString().isNotEmpty) { + controller.text = v.toString(); + } + } + } catch (_) { + // Ignore decode errors; groupController may not be JSON yet + } + } + controller.addListener(() { + // Encode all subfield values into the group controller for submit mapping + final Map map = {}; + for (final s in _effectiveSubFields) { + final c = scope?.controllers[s.path]; + if (c != null && c.text.isNotEmpty) { + map[s.path] = c.text.trim(); + } + } + groupController.text = _encode(map); + onChanged?.call(); + scope?.notifyParent(); + }); + + // Build appropriate shared field by type, but render it inline + final BaseField field = _toSharedField(sf); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (sf.label.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Text( + sf.label, + style: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + ), + ), + field.buildField( + controller: controller, + colorScheme: colorScheme, + onChanged: onChanged, + ), + ], + ), + ); + } + + String _encode(Map map) => const JsonEncoder().convert(map); + + List get _effectiveSubFields { + if (subFields != null && subFields!.isNotEmpty) return subFields!; + final List out = []; + final prefix = groupPrefix ?? ''; + final schemaMap = schema ?? const {}; + schemaMap.forEach((name, type) { + final path = prefix.isEmpty ? name : '$prefix.$name'; + out.add(GroupSubField( + path: path, + label: _capitalize(name.replaceAll('_', ' ')), + hint: '', + keyboardType: _keyboardFromType(type), + isRequired: false, + type: type, + )); + }); + return out; + } + + TextInputType _keyboardFromType(String type) { + switch (type.toLowerCase()) { + case 'number': + case 'currency': + return TextInputType.number; + default: + return TextInputType.text; + } + } + + String _capitalize(String s) { + if (s.isEmpty) return s; + return s[0].toUpperCase() + s.substring(1); + } + + BaseField _toSharedField(GroupSubField sf) { + final t = (sf.type ?? 'text').toLowerCase(); + switch (t) { + case 'number': + return shared_number.NumberField( + fieldKey: sf.path, + label: sf.label, + hint: sf.hint, + isRequired: sf.isRequired, + ); + case 'date': + return shared_date.DateField( + fieldKey: sf.path, + label: sf.label, + hint: sf.hint, + isRequired: sf.isRequired, + ); + case 'currency': + return CurrencyField( + fieldKey: sf.path, + label: sf.label, + hint: sf.hint, + isRequired: sf.isRequired, + ); + case 'qrcode': + case 'qr': + return QRCodeField( + fieldKey: sf.path, + label: sf.label, + hint: sf.hint, + isRequired: sf.isRequired, + ); + case 'barcode': + return BarcodeField( + fieldKey: sf.path, + label: sf.label, + hint: sf.hint, + isRequired: sf.isRequired, + ); + case 'text': + default: + return CustomTextField( + fieldKey: sf.path, + label: sf.label, + hint: sf.hint, + isRequired: sf.isRequired, + ); + } + } +} + +class GroupSubField { + final String path; // JSON path for EntityForm to assign during submit + final String label; + final String hint; + final TextInputType keyboardType; + final bool isRequired; + final String? type; // schema type identifier + + GroupSubField({ + required this.path, + required this.label, + this.hint = '', + this.keyboardType = TextInputType.text, + this.isRequired = false, + this.type, + }); +} + +/// Helper to access EntityFormScope in a builder without changing base components +class _EntityFormScopeAccessor extends StatefulWidget { + final Widget Function(EntityFormScope? scope) builder; + const _EntityFormScopeAccessor({required this.builder}); + + static EntityFormScope? get scope => _lastScope; + static EntityFormScope? _lastScope; + + @override + State<_EntityFormScopeAccessor> createState() => + _EntityFormScopeAccessorState(); +} + +class _EntityFormScopeAccessorState extends State<_EntityFormScopeAccessor> { + @override + Widget build(BuildContext context) { + final scope = EntityFormScope.of(context); + _EntityFormScopeAccessor._lastScope = scope; + return widget.builder(scope); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/qr_code_field.dart b/base_project/lib/BuilderField/shared/fields/qr_code_field.dart new file mode 100644 index 0000000..53d15a8 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/qr_code_field.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'base_field.dart'; + +class QRCodeField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final Future Function()? scanner; // returns scanned text + + QRCodeField({ + required this.fieldKey, + required this.label, + this.hint = '', + this.isRequired = false, + this.scanner, + }); + + @override + String? Function(String?)? get validator => (v) { + if (isRequired && (v == null || v.trim().isEmpty)) return 'Required'; + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: colorScheme.outlineVariant.withOpacity(0.3)), + ), + child: Column( + children: [ + if (controller.text.trim().isNotEmpty) ...[ + QrImageView( + data: controller.text.trim(), + version: QrVersions.auto, + size: 160, + gapless: false, + ), + const SizedBox(height: 12), + ], + Row( + children: [ + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: + hint.isEmpty ? 'Enter or scan QR data' : hint, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10)), + filled: true, + ), + validator: validator, + onChanged: (_) => onChanged?.call(), + ), + ), + const SizedBox(width: 8), + IconButton( + tooltip: 'Scan QR', + icon: Icon(Icons.qr_code_scanner_rounded, + color: colorScheme.primary), + onPressed: scanner == null + ? null + : () async { + final val = await scanner!.call(); + if (val != null) { + controller.text = val; + onChanged?.call(); + } + }, + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/value_list_picker_field.dart b/base_project/lib/BuilderField/shared/fields/value_list_picker_field.dart index 8981a73..02819c7 100644 --- a/base_project/lib/BuilderField/shared/fields/value_list_picker_field.dart +++ b/base_project/lib/BuilderField/shared/fields/value_list_picker_field.dart @@ -70,18 +70,26 @@ class _ValueListPickerWidget extends StatefulWidget { class _ValueListPickerWidgetState extends State<_ValueListPickerWidget> { bool _loading = false; + String? _loadError; Future _open() async { + if (!mounted) return; setState(() => _loading = true); - List> options = const []; - try { - options = await widget.optionsLoader(); - } finally { - setState(() => _loading = false); - } + + // Kick off the load but render the sheet immediately with a loader + final Future>> loadFuture = (() async { + try { + _loadError = null; + final list = await widget.optionsLoader(); + return list; + } catch (e) { + _loadError = e.toString(); + return const >[]; + } + })(); final cs = widget.colorScheme; - await showModalBottomSheet( + final selected = await showModalBottomSheet>( context: context, isScrollControlled: true, backgroundColor: cs.surface, @@ -90,48 +98,257 @@ class _ValueListPickerWidgetState extends State<_ValueListPickerWidget> { ), builder: (context) { return SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: options.isEmpty - ? Center( - child: Text('No data', - style: TextStyle(color: cs.onSurfaceVariant))) - : ListView.separated( - itemCount: options.length, - separatorBuilder: (_, __) => - Divider(height: 1, color: cs.outline.withOpacity(0.08)), - itemBuilder: (context, index) { - final o = options[index]; - return ListTile( - leading: CircleAvatar( - child: Text( - ((o['name'] ?? o['title'] ?? 'N') as String) - .substring(0, 1) - .toUpperCase())), - title: Text((o['name'] ?? o['title'] ?? '').toString()), - subtitle: Text( - (o['description'] ?? o['email'] ?? o['phone'] ?? '') - .toString()), - onTap: () { - final scope = EntityFormScope.of(context); - if (scope != null) { - widget.fillMappings - .forEach((sourceKey, targetFieldKey) { - final v = o[sourceKey]; - final c = scope.controllers[targetFieldKey]; - if (c != null) c.text = v?.toString() ?? ''; - }); - scope.notifyParent(); + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header with close button + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Expanded( + child: Text( + widget.label, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ), + IconButton( + tooltip: 'Close', + icon: + Icon(Icons.close_rounded, color: cs.onSurfaceVariant), + onPressed: () => Navigator.of(context).pop(), + ) + ], + ), + ), + const SizedBox(height: 4), + FutureBuilder>>( + future: loadFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Padding( + padding: const EdgeInsets.all(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, color: cs.primary), + ), + const SizedBox(width: 12), + Text('Loading...', + style: TextStyle(color: cs.onSurfaceVariant)), + ], + ), + ); + } + if (snapshot.hasError || _loadError != null) { + final String msg = snapshot.error?.toString() ?? + _loadError ?? + 'Failed to load'; + return Padding( + padding: const EdgeInsets.all(16), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: cs.errorContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.error.withOpacity(0.2)), + ), + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Icon(Icons.error_outline, color: cs.error), + const SizedBox(width: 8), + Expanded( + child: Text(msg, + style: TextStyle(color: cs.onSurfaceVariant)), + ), + ], + ), + ), + ); + } + final options = + snapshot.data ?? const >[]; + if (options.isEmpty) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Icon(Icons.inbox_rounded, + color: cs.onSurfaceVariant, size: 36), + const SizedBox(height: 8), + Text('No data found', + style: TextStyle(color: cs.onSurfaceVariant)), + const SizedBox(height: 12), + ElevatedButton.icon( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close_rounded), + label: const Text('Close'), + style: ElevatedButton.styleFrom( + backgroundColor: cs.primary, + foregroundColor: cs.onPrimary, + ), + ) + ], + ), + ); + } + return Flexible( + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 8), + itemCount: options.length, + separatorBuilder: (_, __) => Divider( + height: 1, color: cs.outline.withOpacity(0.08)), + itemBuilder: (context, index) { + final o = options[index]; + + dynamic _readByPath( + Map m, String path) { + if (m.isEmpty || path.isEmpty) return null; + if (path.contains('.')) { + dynamic current = m; + for (final seg in path.split('.')) { + if (current is Map && current.containsKey(seg)) { + current = current[seg]; + } else { + current = null; + break; + } + } + return current; } - Navigator.of(context).pop(); - }, - ); - }, - ), + return m[path]; + } + + final List mappingKeys = + widget.fillMappings.keys.toList(); + print('mappingKeys: $mappingKeys'); + final String titleKey = + mappingKeys.isNotEmpty ? mappingKeys[0] : 'name'; + final String subtitleKey = + mappingKeys.length > 1 ? mappingKeys[1] : ''; + + String title = (_readByPath(o, titleKey) ?? + o['name'] ?? + o['title'] ?? + '') + .toString(); + String subtitle = subtitleKey.isNotEmpty + ? (_readByPath(o, subtitleKey) ?? '').toString() + : (o['description'] ?? + o['email'] ?? + o['phone'] ?? + '') + .toString(); + + final String leadingText = + (title.isNotEmpty ? title[0] : 'N').toUpperCase(); + return ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + cs.primary, + cs.primary.withOpacity(0.7) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: cs.primary.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4)), + ], + ), + alignment: Alignment.center, + child: Text(leadingText, + style: TextStyle( + color: cs.onPrimary, + fontWeight: FontWeight.w700)), + ), + title: Text(title, + style: TextStyle( + color: cs.onSurface, + fontWeight: FontWeight.w600)), + subtitle: subtitle.isEmpty + ? null + : Text(subtitle, + style: TextStyle(color: cs.onSurfaceVariant)), + trailing: Icon(Icons.chevron_right_rounded, + color: cs.onSurfaceVariant), + onTap: () { + Navigator.of(context).pop>(o); + }, + ); + }, + ), + ); + }, + ), + const SizedBox(height: 16), + ], ), ); }, ); + + if (!mounted) return; + setState(() => _loading = false); + + // After sheet closes, if a selection was returned, fill controllers safely + if (selected != null) { + final scope = EntityFormScope.of(context); + if (scope != null) { + dynamic _readByPath(Map m, String path) { + if (m.isEmpty || path.isEmpty) return null; + if (path.contains('.')) { + dynamic current = m; + for (final seg in path.split('.')) { + if (current is Map && current.containsKey(seg)) { + current = current[seg]; + } else { + current = null; + break; + } + } + return current; + } + return m[path]; + } + + widget.fillMappings.forEach((sourceKey, targetFieldKey) { + dynamic v = _readByPath(selected, sourceKey); + if (v == null) { + v = _readByPath(selected, '${sourceKey}1') ?? + _readByPath(selected, '${sourceKey}2') ?? + _readByPath(selected, sourceKey.toLowerCase()) ?? + _readByPath(selected, sourceKey.toUpperCase()); + } + final c = scope.controllers[targetFieldKey]; + if (c != null) { + try { + c.text = v?.toString() ?? ''; + } catch (_) { + // Controller may be disposed; ignore safely + } + } + }); + scope.notifyParent(); + } + } } @override diff --git a/base_project/lib/core/providers/dynamic_theme_provider.dart b/base_project/lib/core/providers/dynamic_theme_provider.dart index 166c22f..a10de5b 100644 --- a/base_project/lib/core/providers/dynamic_theme_provider.dart +++ b/base_project/lib/core/providers/dynamic_theme_provider.dart @@ -1,8 +1,7 @@ -import 'dart:typed_data'; -import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import '../theme/dynamic_color_scheme.dart'; -import 'package:flutter/foundation.dart' show compute; class DynamicThemeProvider extends ChangeNotifier { ColorScheme? _dynamicColorScheme;