import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/utils/utils.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; class DropValue { int id; String value; String subValue; bool? isEnabled; String? valueN; DropValue(this.id, this.value, this.subValue, {this.valueN, this.isEnabled = true}); bool operator ==(o) => o is DropValue && o.value == value && o.id == id; @override String toString() { return 'DropValue{id: $id, value: $value, subValue: $subValue, valueN: $valueN, isEnabled: $isEnabled}'; } } /// Resolves which label to display for a [DropValue], matching the localization /// rules: Arabic + useValueNForArabic -> valueN (falls back to value), Arabic + /// useSubValueForArabic -> subValue, otherwise -> value. String _dropdownDisplayLabel( DropValue value, { required bool isArabic, required bool useSubValueForArabic, required bool useValueNForArabic, }) { if (isArabic && useValueNForArabic) { return value.valueN ?? value.value; } if (isArabic && useSubValueForArabic) { return value.subValue; } return value.value; } class DropdownField extends StatefulWidget { final String? hint; final String errorValue; final List? list; final DropValue? dropdownValue; final Function(DropValue) onSelect; final Function()? onTap; final bool showAppointmentPickerVariant; final TextStyle? textStyle; final bool isSelectAble; final bool isArabic; final bool useSubValueForArabic; final bool useValueNForArabic; const DropdownField(this.onSelect, {super.key, this.hint, this.list, this.dropdownValue, this.onTap, this.errorValue = "", this.showAppointmentPickerVariant = false, this.textStyle, this.isSelectAble = true, this.isArabic = false, this.useSubValueForArabic = false, this.useValueNForArabic = false}); @override State createState() => _DropdownFieldState(); } class _DropdownFieldState extends State { DropValue? dropdownValue; List defaultV = [ DropValue(1, "One", ""), DropValue(2, "Two", ""), ]; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { dropdownValue = widget.dropdownValue; return Column( children: [ // Always show hint as label when provided if (widget.hint != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: (widget.hint ?? "").toText( color: borderColor, fontSize: 10, maxLines: 2, fontWeight: MyFonts.Medium, ), ) ], ), 4.height, ], // Show disabled text field when not selectable if (!widget.isSelectAble) Container( decoration: Utils.containerColorRadiusBorderWidth(MyColors.greyShadowColor, 0, MyColors.greyACColor, 2), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), width: double.infinity, child: (dropdownValue == null ? "" : _dropdownDisplayLabel( dropdownValue!, isArabic: widget.isArabic, useSubValueForArabic: widget.useSubValueForArabic, useValueNForArabic: widget.useValueNForArabic, )) .toText( color: MyColors.darkTextColor, fontSize: 15, fontWeight: MyFonts.Medium, ), ) else // Show normal dropdown when selectable Builder(builder: (context) { // Distinguish "no list passed" (use placeholder defaultV) from "list explicitly empty" // (real data fetched but nothing available) so we can show a meaningful empty-state. final List items = widget.list ?? defaultV; final bool hasNoData = widget.list != null && widget.list!.isEmpty; return InkWell( onTap: hasNoData ? null : () { widget.onTap?.call(); _openSearchablePicker(context, items); }, child: Container( decoration: widget.showAppointmentPickerVariant ? null : Utils.containerColorRadiusBorderWidth(MyColors.white, 0, MyColors.darkPrimaryColor, 2), margin: const EdgeInsets.all(0), padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2), width: widget.showAppointmentPickerVariant ? 170 : null, child: Row( mainAxisSize: widget.showAppointmentPickerVariant ? MainAxisSize.min : MainAxisSize.max, children: [ Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: (dropdownValue == null ? (hasNoData ? LocaleKeys.noDataAvailable.tr() : "") : _dropdownDisplayLabel( dropdownValue!, isArabic: widget.isArabic, useSubValueForArabic: widget.useSubValueForArabic, useValueNForArabic: widget.useValueNForArabic, )) .toText( fontSize: 15, color: hasNoData ? MyColors.greyACColor : MyColors.darkTextColor, fontWeight: MyFonts.Medium, maxLines: 1, ), ), ), Icon( Icons.keyboard_arrow_down_sharp, size: widget.showAppointmentPickerVariant ? 26 : 16, color: hasNoData ? MyColors.greyACColor : MyColors.darkTextColor, ), ], ), ), ); }), if (widget.errorValue != "") Row( mainAxisAlignment: MainAxisAlignment.end, children: [ widget.errorValue.toText(fontSize: 14, color: Colors.red), ], ).paddingOnly(right: 10), ], ); } void _openSearchablePicker(BuildContext context, List items) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) { return _SearchableDropdownSheet( items: items, selectedValue: dropdownValue, isArabic: widget.isArabic, useSubValueForArabic: widget.useSubValueForArabic, useValueNForArabic: widget.useValueNForArabic, onSelected: (value) { setState(() { dropdownValue = value; }); widget.onSelect(value); }, ); }, ); } } class _SearchableDropdownSheet extends StatefulWidget { final List items; final DropValue? selectedValue; final Function(DropValue) onSelected; final bool isArabic; final bool useSubValueForArabic; final bool useValueNForArabic; const _SearchableDropdownSheet({ required this.items, required this.selectedValue, required this.onSelected, this.isArabic = false, this.useSubValueForArabic = false, this.useValueNForArabic = false, }); @override State<_SearchableDropdownSheet> createState() => _SearchableDropdownSheetState(); } class _SearchableDropdownSheetState extends State<_SearchableDropdownSheet> { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; @override void dispose() { _searchController.dispose(); super.dispose(); } String _labelFor(DropValue value) => _dropdownDisplayLabel( value, isArabic: widget.isArabic, useSubValueForArabic: widget.useSubValueForArabic, useValueNForArabic: widget.useValueNForArabic, ); List get _filteredItems { if (_searchQuery.isEmpty) return widget.items; return widget.items.where((element) => _labelFor(element).toLowerCase().contains(_searchQuery.toLowerCase())).toList(); } Widget _buildSearchField() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Container( height: 55, decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[300]!, width: 1), ), child: TextField( controller: _searchController, autofocus: false, style: const TextStyle(fontSize: 16), onChanged: (value) { setState(() { _searchQuery = value; }); }, decoration: InputDecoration( hintStyle: TextStyle(fontSize: 16, color: Colors.grey[600]), prefixIcon: Icon(Icons.search, size: 26, color: Colors.grey[700]), suffixIcon: _searchQuery.isEmpty ? null : InkWell( onTap: () { setState(() { _searchController.clear(); _searchQuery = ''; }); }, child: Icon(Icons.close, size: 22, color: Colors.grey[700]), ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), ), ), ), ); } @override Widget build(BuildContext context) { final filtered = _filteredItems; return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Container( decoration: const BoxDecoration( color: MyColors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(18)), ), constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7), child: SafeArea( top: false, // Material ancestor so ListTile ink splashes render on top of the white background above, // instead of being hidden behind it. child: Material( type: MaterialType.transparency, child: Column( mainAxisSize: MainAxisSize.min, children: [ 12.height, Container( width: 40, height: 4, decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), ), 14.height, _buildSearchField(), 10.height, Flexible( child: filtered.isEmpty ? Padding( padding: const EdgeInsets.symmetric(vertical: 30), child: LocaleKeys.noDataAvailable.tr().toText(fontSize: 15, color: MyColors.greyACColor), ) : ListView.separated( shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 4), itemCount: filtered.length, separatorBuilder: (context, index) => Divider(height: 1, thickness: 1, color: Colors.grey[300]), itemBuilder: (context, index) { DropValue value = filtered[index]; bool isEnabled = value.isEnabled ?? true; bool isSelected = widget.selectedValue?.id == value.id; return ListTile( enabled: isEnabled, selected: isSelected, dense: false, selectedTileColor: MyColors.darkPrimaryColor.withValues(alpha: 0.08), title: _labelFor(value).toText( fontSize: 15, color: !isEnabled ? MyColors.greyACColor : (isSelected ? MyColors.darkPrimaryColor : MyColors.darkTextColor), fontWeight: isSelected ? MyFonts.Bold : MyFonts.Medium, ), trailing: isSelected ? const Icon(Icons.check, color: MyColors.darkPrimaryColor) : null, onTap: !isEnabled ? null : () { widget.onSelected(value); Navigator.pop(context); }, ); }, ), ), 8.height, ], ), ), ), ), ); } }