import 'package:flutter/material.dart'; import '../models/office.dart'; import '../theme/m3_motion.dart'; import 'app_breakpoints.dart'; class _PickResult { const _PickResult(this.officeId); final String? officeId; } /// A tappable form field that opens a searchable office picker. /// /// Replaces [DropdownButtonFormField] when the office list is large. class OfficeSelectorField extends StatelessWidget { const OfficeSelectorField({ super.key, required this.offices, required this.selectedOfficeId, required this.onChanged, this.allowUnassigned = false, this.labelText = 'Office', }); final List offices; final String? selectedOfficeId; /// Null disables the field (shows dimmed, no tap). final ValueChanged? onChanged; /// When true, an "Unassigned" tile appears at the top of the picker. final bool allowUnassigned; final String labelText; String _displayText() { if (selectedOfficeId == null) { return allowUnassigned ? 'Unassigned' : 'Select office…'; } try { return offices.firstWhere((o) => o.id == selectedOfficeId).name; } catch (_) { return 'Select office…'; } } Future _openPicker(BuildContext context) async { final sorted = List.from(offices) ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); final result = await _showOfficePicker( context, offices: sorted, initialOfficeId: selectedOfficeId, allowUnassigned: allowUnassigned, ); if (result != null) onChanged?.call(result.officeId); } @override Widget build(BuildContext context) { final isEnabled = onChanged != null; final cs = Theme.of(context).colorScheme; return InkWell( onTap: isEnabled ? () => _openPicker(context) : null, borderRadius: BorderRadius.circular(16), child: InputDecorator( decoration: InputDecoration( labelText: labelText, enabled: isEnabled, suffixIcon: Icon( Icons.search_rounded, color: isEnabled ? cs.onSurfaceVariant : cs.onSurface.withValues(alpha: 0.38), ), ), child: Text( _displayText(), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isEnabled ? cs.onSurface : cs.onSurface.withValues(alpha: 0.38), ), ), ), ); } } Future<_PickResult?> _showOfficePicker( BuildContext context, { required List offices, String? initialOfficeId, bool allowUnassigned = false, }) { final isMobile = MediaQuery.sizeOf(context).width < AppBreakpoints.tablet; List buildItems( String? selected, String search, void Function(String?) onPick, ColorScheme cs, ) { final filtered = search.isEmpty ? offices : offices .where((o) => o.name.toLowerCase().contains(search.toLowerCase())) .toList(); return [ if (allowUnassigned) ListTile( dense: true, leading: Icon(Icons.block_rounded, color: cs.onSurfaceVariant), title: const Text('Unassigned'), selected: selected == null, trailing: selected == null ? Icon(Icons.check_rounded, size: 18, color: cs.primary) : null, onTap: () => onPick(null), ), for (final office in filtered) ListTile( dense: true, leading: Icon(Icons.business_rounded, color: cs.onSurfaceVariant), title: Text(office.name), selected: selected == office.id, trailing: selected == office.id ? Icon(Icons.check_rounded, size: 18, color: cs.primary) : null, onTap: () => onPick(office.id), ), if (filtered.isEmpty) Padding( padding: const EdgeInsets.all(16), child: Text( 'No offices match your search.', textAlign: TextAlign.center, style: TextStyle(color: cs.onSurfaceVariant), ), ), ]; } if (isMobile) { return m3ShowBottomSheet<_PickResult>( context: context, isScrollControlled: true, builder: (sheetCtx) { String search = ''; String? selected = initialOfficeId; return StatefulBuilder( builder: (ctx, setState) { final cs = Theme.of(ctx).colorScheme; return SafeArea( child: Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 8, bottom: MediaQuery.viewInsetsOf(ctx).bottom + 16, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ Expanded( child: Text( 'Select Office', style: Theme.of(ctx).textTheme.titleLarge, ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(ctx).pop(), ), ], ), const SizedBox(height: 8), TextField( decoration: const InputDecoration( hintText: 'Search office…', prefixIcon: Icon(Icons.search_rounded), isDense: true, ), onChanged: (v) => setState(() => search = v), ), const SizedBox(height: 4), SizedBox( height: 320, child: ListView( children: buildItems( selected, search, (id) => Navigator.of(ctx).pop(_PickResult(id)), cs, ), ), ), ], ), ), ); }, ); }, ); } return m3ShowDialog<_PickResult>( context: context, builder: (dialogCtx) { String search = ''; String? selected = initialOfficeId; return StatefulBuilder( builder: (ctx, setState) { final cs = Theme.of(ctx).colorScheme; return AlertDialog( title: const Text('Select Office'), contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 0), content: SizedBox( width: 400, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), child: TextField( decoration: const InputDecoration( hintText: 'Search office…', prefixIcon: Icon(Icons.search_rounded), isDense: true, ), onChanged: (v) => setState(() => search = v), ), ), SizedBox( height: 280, width: double.maxFinite, child: ListView( children: buildItems( selected, search, (id) => Navigator.of(ctx).pop(_PickResult(id)), cs, ), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancel'), ), ], ); }, ); }, ); }