import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:geolocator/geolocator.dart'; import '../data/india_cities.dart'; import '../data/india_locations.dart'; import '../models/client.dart'; import '../services/location_service.dart'; import '../theme/app_theme.dart'; import '../utils/snackbar_utils.dart'; class SurveyScreen extends StatefulWidget { final List clients; final void Function(Client) onAddClient; final void Function(int) onNavigate; final bool backendReady; final bool isActive; const SurveyScreen({ super.key, required this.clients, required this.onAddClient, required this.onNavigate, required this.backendReady, this.isActive = false, }); @override State createState() => _SurveyScreenState(); } enum _SurveyState { idle, surveying, success } class _SurveyScreenState extends State with SingleTickerProviderStateMixin { _SurveyState _state = _SurveyState.idle; int _step = 1; bool _onConsentStep = false; bool _capturingLocation = false; DataConsent _chosenConsent = DataConsent.full; // Step 1 — basic identity + contact + routing final _nameCtrl = TextEditingController(); final _phoneCtrl = TextEditingController(); final _neighbourhoodCtrl = TextEditingController(); final _pincodeCtrl = TextEditingController(); final _customProviderCtrl = TextEditingController(); IndiaCity? _businessCity; // origin — auto-detected from GPS or picked List _destCities = []; // transiting destination cities (multi-select) SurveyLocation? _capturedLocation; // GPS captured during Step 1 (reused at submit) final bool _detectingLocation = false; String _frequency = ''; BusinessType _businessType = BusinessType.ecommerce; List _selectedSegments = ['First Mile']; // Step 2 — logistics metrics double _parcelVolume = 450; final _contractsCtrl = TextEditingController(text: '12'); // Step 3 — provider & notes String _provider = 'DTDC Express'; final _notesCtrl = TextEditingController(); late AnimationController _animCtrl; late Animation _fadeAnim; final ScrollController _scrollController = ScrollController(); static const _frequencyOptions = [ 'Daily', 'Weekly', 'Bi-weekly', 'Monthly', 'On-demand', ]; @override void initState() { super.initState(); _animCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 300)); _fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut); _animCtrl.forward(); } @override void didUpdateWidget(covariant SurveyScreen oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive && !oldWidget.isActive) { if (_scrollController.hasClients) { _scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); } } } @override void dispose() { _scrollController.dispose(); _nameCtrl.dispose(); _phoneCtrl.dispose(); _neighbourhoodCtrl.dispose(); _pincodeCtrl.dispose(); _customProviderCtrl.dispose(); _contractsCtrl.dispose(); _notesCtrl.dispose(); _animCtrl.dispose(); super.dispose(); } List get _lastSurveyed => widget.clients.where((c) => c.surveySubmitted).take(3).toList(); /// Average parcel volume across clients with the same business type. int get _avgVolumeForType { final peers = widget.clients .where((c) => c.businessType == _businessType && c.parcelVolume > 0) .toList(); if (peers.isEmpty) return 0; return (peers.fold(0.0, (s, c) => s + c.parcelVolume) / peers.length).round(); } // ── Location ────────────────────────────────────────────────────────────── Future _showProminentLocationDisclosure() async { final result = await showDialog( context: context, barrierDismissible: false, builder: (ctx) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 56, height: 56, decoration: const BoxDecoration( color: AppColors.blueLight, shape: BoxShape.circle, ), child: const Icon(Icons.location_on, size: 28, color: AppColors.primary), ), const SizedBox(height: 16), const Text( 'Location Disclosure', textAlign: TextAlign.center, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.darkBlue, ), ), const SizedBox(height: 8), const Text( 'DoorMile CRM collects location data to record the GPS coordinates of your client visit when you submit a survey, enabling verification of field visits. This data is collected only when you explicitly press the submit survey button, even when the app is in use.', textAlign: TextAlign.center, style: TextStyle( fontSize: 12.5, color: AppColors.textSecondary, height: 1.4, ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 44, child: ElevatedButton( onPressed: () => Navigator.pop(ctx, true), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 0, ), child: const Text( 'Agree & Continue', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, ), ), ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 44, child: TextButton( onPressed: () => Navigator.pop(ctx, false), style: TextButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text( 'Cancel', style: TextStyle( fontSize: 13, color: AppColors.textSecondary, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ), ); return result ?? false; } /// Checks service + permission (showing alerts as needed), then captures GPS. /// Returns [SurveyLocation.empty] if unavailable / declined. Future _acquireLocation() async { final status = await LocationService.checkStatus(); if (!mounted) return SurveyLocation.empty(); if (status == LocationCheckResult.serviceDisabled) { final proceed = await _showLocationAlert( icon: Icons.location_off_outlined, title: 'Location is Turned Off', message: 'Enable location on your device so the client visit can be ' 'logged with GPS coordinates.', primaryLabel: 'Turn On Location', onPrimary: LocationService.openLocationSettings, ); if (!proceed || !mounted) return SurveyLocation.empty(); } else if (status == LocationCheckResult.permissionDenied) { final agreed = await _showProminentLocationDisclosure(); if (!agreed || !mounted) return SurveyLocation.empty(); final perm = await Geolocator.requestPermission(); if (!mounted) return SurveyLocation.empty(); if (perm == LocationPermission.denied || perm == LocationPermission.deniedForever) { final proceed = await _showLocationAlert( icon: Icons.lock_outline, title: 'Location Permission Needed', message: 'Allow DoorMile to access your location so the business ' 'location can be detected automatically.', primaryLabel: 'Open App Settings', onPrimary: LocationService.openAppSettings, ); if (!proceed || !mounted) return SurveyLocation.empty(); } } else if (status == LocationCheckResult.permissionPermanentlyDenied) { final proceed = await _showLocationAlert( icon: Icons.lock_outline, title: 'Location Permission Blocked', message: 'Location access was blocked. Open App Settings and allow ' '"Location" for DoorMile to detect the business location.', primaryLabel: 'Open App Settings', onPrimary: LocationService.openAppSettings, ); if (!proceed || !mounted) return SurveyLocation.empty(); } setState(() => _capturingLocation = true); SurveyLocation loc = SurveyLocation.empty(); try { loc = await LocationService.capture(); } catch (_) {} if (!mounted) return loc; setState(() => _capturingLocation = false); return loc; } /// Step 1 action — detect the business location from the device GPS and /// auto-fill the business city (and neighbourhood, if empty). Future _detectLocation() async { final loc = await _acquireLocation(); if (!mounted) return; if (!loc.hasFix) { SnackbarUtils.showError(context, "Couldn't get GPS. Pick the business city manually below."); return; } setState(() { _capturedLocation = loc; final matched = findCityByName(loc.city); _businessCity = matched ?? (loc.city.isNotEmpty ? IndiaCity(loc.city, '—', loc.state) : _businessCity); if (loc.zone.isNotEmpty && _neighbourhoodCtrl.text.trim().isEmpty) { _neighbourhoodCtrl.text = loc.zone; } // Pre-fill the optional pincode from GPS, if the agent hasn't typed one. if (loc.pincode.isNotEmpty && _pincodeCtrl.text.trim().isEmpty) { _pincodeCtrl.text = loc.pincode; } }); } /// Submit entry point — reuses the GPS captured during Step 1 if available, /// otherwise acquires it now. Future _captureAndSubmit({required bool basicOnly}) async { if (!basicOnly && _nameCtrl.text.trim().isEmpty) return; SurveyLocation loc; if (_capturedLocation != null && _capturedLocation!.hasFix) { loc = _capturedLocation!; } else { loc = await _acquireLocation(); if (!mounted) return; } if (basicOnly) { _doSubmitBasicOnly(loc); } else { _doSubmitFull(loc); } } /// Shows a styled location alert. /// /// Returns `true` → user chose "Submit Without Location" (proceed). /// Returns `false` → user chose the settings action (don't submit now). Future _showLocationAlert({ required IconData icon, required String title, required String message, required String primaryLabel, required Future Function() onPrimary, }) async { final result = await showDialog( context: context, barrierDismissible: false, builder: (ctx) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.fromLTRB(24, 28, 24, 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: const Color(0xFFFFF8ED), shape: BoxShape.circle, ), child: Icon(icon, size: 26, color: const Color(0xFFE87C00)), ), const SizedBox(height: 16), Text(title, textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 8), Text(message, textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: AppColors.textSecondary)), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 44, child: ElevatedButton( onPressed: () { Navigator.pop(ctx, false); onPrimary(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), elevation: 0, ), child: Text(primaryLabel, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700)), ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 44, child: TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), child: Text('Submit Without Location', style: TextStyle( fontSize: 13, color: AppColors.textSecondary, fontWeight: FontWeight.w600)), ), ), ], ), ), ), ); return result ?? true; } void _doSubmitBasicOnly(SurveyLocation loc) { _chosenConsent = DataConsent.basicOnly; widget.onAddClient(Client( id: 'client_${DateTime.now().millisecondsSinceEpoch}', name: _nameCtrl.text.trim(), city: _businessCity?.name ?? '', businessType: _businessType, parcelVolume: 0.0, activeContracts: 0, provider: 'Not disclosed', efficiency: 'Not disclosed', status: ClientStatus.newClient, lastUpdated: 'Just now', surveySubmitted: true, notes: '', dataConsent: DataConsent.basicOnly, phone: _phoneCtrl.text.trim(), frequency: _frequency, businessState: _businessCity?.state ?? '', transitType: '', transitFrom: _businessCity?.name ?? '', transitTo: _destCities.map((c) => c.name).join(', '), neighbourhood: _neighbourhoodCtrl.text.trim(), logisticsSegment: _selectedSegments.join(', '), pincode: _pincodeCtrl.text.trim(), surveyLat: loc.lat, surveyLng: loc.lng, surveyAddress: loc.address, surveyZone: loc.zone, surveyPincode: loc.pincode, )); _transition(_SurveyState.success); } void _doSubmitFull(SurveyLocation loc) { _chosenConsent = DataConsent.full; String providerName = _provider == 'Others' ? _customProviderCtrl.text.trim() : _provider; if (providerName.isEmpty) providerName = 'Others'; String efficiency = 'High Efficiency'; if (providerName == 'Blue Dart') efficiency = 'Premium Service'; if (providerName == 'Gati-KWE') efficiency = 'Cost Leader'; if (providerName == 'Delhivery') efficiency = 'Local Last Mile'; widget.onAddClient(Client( id: 'client_${DateTime.now().millisecondsSinceEpoch}', name: _nameCtrl.text.trim(), city: _businessCity?.name ?? '', businessType: _businessType, parcelVolume: _parcelVolume, activeContracts: int.tryParse(_contractsCtrl.text) ?? 12, provider: providerName, efficiency: efficiency, status: ClientStatus.newClient, lastUpdated: 'Just now', surveySubmitted: true, notes: _notesCtrl.text.trim(), dataConsent: DataConsent.full, phone: _phoneCtrl.text.trim(), frequency: _frequency, businessState: _businessCity?.state ?? '', transitType: '', transitFrom: _businessCity?.name ?? '', transitTo: _destCities.map((c) => c.name).join(', '), neighbourhood: _neighbourhoodCtrl.text.trim(), logisticsSegment: _selectedSegments.join(', '), pincode: _pincodeCtrl.text.trim(), surveyLat: loc.lat, surveyLng: loc.lng, surveyAddress: loc.address, surveyZone: loc.zone, surveyPincode: loc.pincode, )); _transition(_SurveyState.success); } void _transition(_SurveyState next) { _animCtrl.reset(); setState(() => _state = next); _animCtrl.forward(); } void _reset() { _nameCtrl.clear(); _phoneCtrl.clear(); _neighbourhoodCtrl.clear(); _pincodeCtrl.clear(); _customProviderCtrl.clear(); _contractsCtrl.text = '12'; _notesCtrl.clear(); setState(() { _businessCity = null; _destCities = []; _capturedLocation = null; _businessType = BusinessType.ecommerce; _selectedSegments = ['First Mile']; _frequency = ''; _parcelVolume = 450; _provider = 'DTDC Express'; _step = 1; _onConsentStep = false; }); _transition(_SurveyState.idle); } @override Widget build(BuildContext context) { return Stack( children: [ FadeTransition( opacity: _fadeAnim, child: SingleChildScrollView( controller: _scrollController, padding: const EdgeInsets.fromLTRB(16, 20, 16, 100), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ WideTopBanner( backendReady: widget.backendReady, subtitle: 'SURVEY CONSOLE', ), const SizedBox(height: 20), _buildBody(), ], ), ), ), if (_capturingLocation) _buildLocationOverlay(), ], ); } Widget _buildLocationOverlay() { return Container( color: Colors.black.withValues(alpha: 0.55), child: Center( child: Container( margin: const EdgeInsets.symmetric(horizontal: 48), padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 28), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 24) ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: const Color(0xFFEFF4FF), shape: BoxShape.circle, ), child: Icon(Icons.location_searching, size: 28, color: AppColors.primary), ), const SizedBox(height: 16), const Text('Getting Location', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 6), Text( 'Capturing GPS for field visit log…', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, color: AppColors.textSecondary), ), const SizedBox(height: 18), SizedBox( width: 28, height: 28, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(AppColors.primary), ), ), ], ), ), ), ); } Widget _buildBody() { switch (_state) { case _SurveyState.idle: return _buildIdle(); case _SurveyState.surveying: return _buildForm(); case _SurveyState.success: return _buildSuccess(); } } // ── Idle ────────────────────────────────────────────────────────────────── Widget _buildIdle() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.border.withValues(alpha: 0.25)), ), child: Column( children: [ Container( width: 56, height: 56, decoration: const BoxDecoration( color: Color(0xFFFFDAD7), shape: BoxShape.circle), child: Icon(Icons.assignment_turned_in_outlined, size: 28, color: AppColors.primary), ), const SizedBox(height: 14), const Text('Welcome back', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w800, color: AppColors.darkBlue)), const SizedBox(height: 4), Text('Ready to start your next logistics survey?', style: TextStyle( fontSize: 13, color: AppColors.textSecondary), textAlign: TextAlign.center), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 52, child: ElevatedButton.icon( onPressed: () => _transition(_SurveyState.surveying), icon: const Icon(Icons.insert_drive_file_outlined, size: 18, color: Colors.white), label: const Text('Start New Survey', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 15)), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14)), elevation: 2, ), ), ), ], ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('LAST 3 SURVEYED CLIENTS', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 1.2, color: AppColors.textSecondary)), GestureDetector( onTap: () => widget.onNavigate(1), child: Text('View all', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.primary)), ), ], ), const SizedBox(height: 10), Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border.withValues(alpha: 0.15)), ), child: Column( children: _lastSurveyed.isEmpty ? [ Padding( padding: const EdgeInsets.all(20), child: Text('No surveys yet', style: TextStyle(color: AppColors.textSecondary)), ) ] : _lastSurveyed.asMap().entries.map((e) { final isLast = e.key == _lastSurveyed.length - 1; final client = e.value; return InkWell( onTap: () => widget.onNavigate(1), borderRadius: isLast ? const BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16)) : null, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14), decoration: BoxDecoration( border: isLast ? null : Border( bottom: BorderSide( color: AppColors.border .withValues(alpha: 0.1))), ), child: Row( children: [ Container( width: 40, height: 40, decoration: const BoxDecoration( color: Color(0xFFEFF4FF), shape: BoxShape.circle), child: Icon(Icons.business, size: 20, color: AppColors.primary), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Flexible( child: Text(client.name, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), ), if (client.dataConsent == DataConsent.basicOnly) ...[ const SizedBox(width: 6), const ConsentBadge(small: true), ], ]), Text( '${client.city.isNotEmpty ? client.city : "—"} · Submitted ${client.lastUpdated}', style: TextStyle( fontSize: 11, color: AppColors.textSecondary)), ], ), ), Icon(Icons.chevron_right, size: 20, color: AppColors.muted), ], ), ), ); }).toList(), ), ), const SizedBox(height: 20), _SurveyBanner(), ], ); } // ── Form wrapper ────────────────────────────────────────────────────────── Widget _buildForm() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.border.withValues(alpha: 0.3)), boxShadow: [ BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 10) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Material( color: Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () { if (_onConsentStep) { setState(() => _onConsentStep = false); } else if (_step > 1) { setState(() => _step--); } else { _transition(_SurveyState.idle); } }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 6), child: Row(children: [ Icon(Icons.arrow_back_ios, size: 14, color: AppColors.textSecondary), const SizedBox(width: 4), Text('Back', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textSecondary)), ]), ), ), ), Text( _onConsentStep ? 'DATA SHARING' : 'STEP $_step OF 3', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 1.5, color: _onConsentStep ? const Color(0xFFE87C00) : AppColors.primary), ), ], ), const SizedBox(height: 14), // Progress ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: _onConsentStep ? 1 / 3 : _step / 3, minHeight: 6, backgroundColor: const Color(0xFFF8F9FF), valueColor: AlwaysStoppedAnimation( _onConsentStep ? const Color(0xFFFFB800) : AppColors.primary, ), ), ), const SizedBox(height: 20), // Content AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: KeyedSubtree( key: ValueKey(_onConsentStep ? 'consent' : 'step$_step'), child: _onConsentStep ? _buildConsentStep() : _step == 1 ? _buildStep1() : _step == 2 ? _buildStep2() : _buildStep3(), ), ), // Nav buttons (hidden on consent step — cards act as buttons) if (!_onConsentStep) ...[ const SizedBox(height: 20), Divider(height: 1, color: AppColors.border.withValues(alpha: 0.15)), const SizedBox(height: 16), Row(children: [ if (_step > 1) Expanded( child: OutlinedButton.icon( onPressed: () => setState(() => _step--), icon: const Icon(Icons.arrow_back_ios, size: 14), label: const Text('Back'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.textSecondary, side: BorderSide(color: AppColors.border), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), minimumSize: const Size.fromHeight(44), ), ), ), if (_step > 1) const SizedBox(width: 10), Expanded( child: _step < 3 ? ElevatedButton.icon( onPressed: () { if (_step == 1) { if (_nameCtrl.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Please enter a Client Name first.'), backgroundColor: AppColors.primary, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ); return; } setState(() => _onConsentStep = true); } else { setState(() => _step++); } }, icon: const Icon(Icons.arrow_forward_ios, size: 14, color: AppColors.darkBlue), label: const Text('Continue', style: TextStyle( color: AppColors.darkBlue, fontWeight: FontWeight.w700)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFD3E4FE), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), minimumSize: const Size.fromHeight(44), elevation: 0, ), ) : ElevatedButton.icon( onPressed: () => _captureAndSubmit(basicOnly: false), icon: const Icon(Icons.send, size: 14, color: Colors.white), label: const Text('Submit Survey', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700)), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), minimumSize: const Size.fromHeight(44), elevation: 1, ), ), ), ]), ], ], ), ); } // ── Step 1: client identity + contact + routing ─────────────────────────── Widget _buildStep1() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Basic Details', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 4), Text("Client identity and transit preferences.", style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), const SizedBox(height: 18), // Company name _fieldLabel('Client Name'), const SizedBox(height: 6), TextField( controller: _nameCtrl, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( hintText: 'e.g. Murugan Transports', prefixIcon: Icon(Icons.business, size: 18), ), ), const SizedBox(height: 14), // Phone _fieldLabel('Phone Number'), const SizedBox(height: 6), TextField( controller: _phoneCtrl, keyboardType: TextInputType.phone, decoration: const InputDecoration( hintText: '+91 98400-12345', prefixIcon: Icon(Icons.phone_outlined, size: 18), ), ), const SizedBox(height: 14), // Business Location — auto-detected from device GPS _fieldLabel('Business Location'), const SizedBox(height: 6), _LocationDetectCard( detecting: _detectingLocation || _capturingLocation, city: _businessCity, detectedAddress: _capturedLocation?.address ?? '', onDetect: _detectLocation, onPickManually: () => _openCityPicker( title: 'Select Business City', onPicked: (c) => setState(() => _businessCity = c), ), ), const SizedBox(height: 14), // Neighbourhood _fieldLabel('Neighbourhood'), const SizedBox(height: 6), TextField( controller: _neighbourhoodCtrl, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( hintText: 'e.g. T. Nagar, Anna Nagar...', prefixIcon: Icon(Icons.map_outlined, size: 18), ), ), const SizedBox(height: 14), // Pincode — optional; auto-filled from GPS but editable. _fieldLabel('PIN Code (optional)'), const SizedBox(height: 6), TextField( controller: _pincodeCtrl, keyboardType: TextInputType.number, maxLength: 6, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration( counterText: '', hintText: 'e.g. 641001', prefixIcon: Icon(Icons.local_post_office_outlined, size: 18), ), ), const SizedBox(height: 14), // Shipping route — origin is the GPS-detected business location, // destination is picked flight-booking style. _fieldLabel('Primary Shipping Route'), const SizedBox(height: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: const Color(0xFFF8F9FF), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border.withValues(alpha: 0.4)), ), child: Row(children: [ Expanded( child: _RouteEndpoint( icon: Icons.flight_takeoff, label: 'FROM', code: _businessCity?.code ?? '—', name: _businessCity?.name ?? 'Business location', ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 6), child: Icon(Icons.arrow_forward, size: 18, color: AppColors.primary), ), Expanded( child: GestureDetector( onTap: () => _openMultiCityPicker( title: 'Select Destination Cities', initialSelected: _destCities, onPicked: (cities) => setState(() => _destCities = cities), ), child: _RouteEndpoint( icon: Icons.flight_land, label: 'TO', code: _destCities.isEmpty ? 'Tap' : _destCities.length == 1 ? _destCities.first.code : '${_destCities.length} cities', name: _destCities.isEmpty ? 'Choose city' : _destCities.map((c) => c.name).join(', '), highlight: true, ), ), ), ]), ), const SizedBox(height: 14), // Frequency _fieldLabel('Shipment Frequency'), const SizedBox(height: 8), Wrap( spacing: 6, runSpacing: 6, children: _frequencyOptions.map((f) { final selected = _frequency == f; return GestureDetector( onTap: () => setState(() => _frequency = f), child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 8), decoration: BoxDecoration( color: selected ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: selected ? AppColors.primary : AppColors.border.withValues(alpha: 0.6), ), ), child: Text(f, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: selected ? Colors.white : AppColors.textSecondary, )), ), ); }).toList(), ), const SizedBox(height: 14), // Business type _fieldLabel('Business Type'), const SizedBox(height: 8), GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 3.5, children: BusinessType.values.map((t) { final selected = _businessType == t; return GestureDetector( onTap: () => setState(() => _businessType = t), child: AnimatedContainer( duration: const Duration(milliseconds: 150), decoration: BoxDecoration( color: selected ? const Color(0xFFD3E4FE) : Colors.white, borderRadius: BorderRadius.circular(10), border: Border.all( color: selected ? const Color(0xFFA6C8FF) : AppColors.border.withValues(alpha: 0.5), ), ), child: Center( child: Text(t.label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: selected ? AppColors.darkBlue : AppColors.textSecondary, )), ), ), ); }).toList(), ), // Logistics Segment const SizedBox(height: 14), _fieldLabel('Logistics Segment'), const SizedBox(height: 8), Row( children: ['First Mile', 'Middle Mile', 'Last Mile'].map((segment) { final selected = _selectedSegments.contains(segment); IconData iconData; switch (segment) { case 'First Mile': iconData = Icons.warehouse_outlined; break; case 'Middle Mile': iconData = Icons.local_shipping_outlined; break; default: iconData = Icons.markunread_mailbox_outlined; break; } return Expanded( child: Padding( padding: EdgeInsets.only( left: segment == 'First Mile' ? 0 : 4, right: segment == 'Last Mile' ? 0 : 4, ), child: GestureDetector( onTap: () { setState(() { if (selected) { _selectedSegments.remove(segment); } else { _selectedSegments.add(segment); } }); }, child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: BoxDecoration( color: selected ? const Color(0xFFEFF4FF) : Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: selected ? AppColors.primary : AppColors.border.withValues(alpha: 0.5), width: selected ? 1.5 : 1.0, ), boxShadow: selected ? [ BoxShadow( color: AppColors.primary.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 4), ) ] : null, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( iconData, size: 20, color: selected ? AppColors.primary : AppColors.textSecondary, ), const SizedBox(height: 6), Text( segment, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: selected ? AppColors.darkBlue : AppColors.textSecondary, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), // Custom premium checkbox Container( width: 16, height: 16, decoration: BoxDecoration( color: selected ? AppColors.primary : Colors.transparent, borderRadius: BorderRadius.circular(4), border: Border.all( color: selected ? AppColors.primary : AppColors.border, width: 1.5, ), ), child: Center( child: AnimatedScale( duration: const Duration(milliseconds: 150), scale: selected ? 1.0 : 0.0, child: const Icon( Icons.check, size: 11, color: Colors.white, ), ), ), ), ], ), ), ), ), ); }).toList(), ), ], ); } // ── Step 2: logistics metrics (volume with peer avg hint) ──────────────── Widget _buildStep2() { final avg = _avgVolumeForType; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Volume & Metrics', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 4), Text('Estimate their weekly shipments.', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _fieldLabel('Weekly Parcel Volume'), Text('${_parcelVolume.round()} /wk', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, fontFamily: 'monospace', color: AppColors.primary)), ], ), const SizedBox(height: 8), SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: AppColors.primary, inactiveTrackColor: const Color(0xFFF0F4FF), thumbColor: AppColors.primary, overlayColor: AppColors.primary.withValues(alpha: 0.1), thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10), ), child: Slider( value: _parcelVolume, min: 50, max: 1200, divisions: 115, onChanged: (v) => setState(() => _parcelVolume = v), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('50 (Low)', style: TextStyle( fontSize: 10, color: AppColors.textSecondary)), Text('1,200 (Extreme)', style: TextStyle( fontSize: 10, color: AppColors.textSecondary)), ], ), // peer average hint (computed from local client list) if (avg > 0) ...[ const SizedBox(height: 10), GestureDetector( onTap: () => setState(() => _parcelVolume = avg.toDouble()), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8), decoration: BoxDecoration( color: const Color(0xFFEFF4FF), borderRadius: BorderRadius.circular(10), border: Border.all( color: AppColors.primary.withValues(alpha: 0.25)), ), child: Row(children: [ Icon(Icons.insights, size: 14, color: AppColors.primary), const SizedBox(width: 8), Expanded( child: Text( 'Avg for ${_businessType.label} clients in your database: $avg /wk — tap to apply', style: TextStyle( fontSize: 11, color: AppColors.primary), ), ), ]), ), ), ], const SizedBox(height: 20), _fieldLabel('Active Contracts'), const SizedBox(height: 6), TextField( controller: _contractsCtrl, keyboardType: TextInputType.number, decoration: const InputDecoration( prefixIcon: Icon(Icons.description_outlined, size: 18), ), ), ], ); } // ── Step 3: provider & notes ────────────────────────────────────────────── Widget _buildStep3() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Provider & Notes', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 4), Text('Current logistics setup and extra details.', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)), const SizedBox(height: 16), _fieldLabel('Current Provider'), const SizedBox(height: 6), InkWell( onTap: _showProviderSelectorSheet, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border.withValues(alpha: 0.5)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( _provider, style: const TextStyle( fontSize: 13, color: AppColors.darkBlue, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), Icon(Icons.arrow_drop_down, color: AppColors.textSecondary), ], ), ), ), if (_provider == 'Others') ...[ const SizedBox(height: 12), _fieldLabel('Specify Other Provider'), const SizedBox(height: 6), TextField( controller: _customProviderCtrl, textCapitalization: TextCapitalization.words, decoration: const InputDecoration( hintText: 'e.g. My Local Logistics Operator', prefixIcon: Icon(Icons.edit_note, size: 18), ), ), ], const SizedBox(height: 16), _fieldLabel('Additional Notes'), const SizedBox(height: 6), TextField( controller: _notesCtrl, maxLines: 4, decoration: const InputDecoration( hintText: 'e.g. Planning to expand to Tamil Nadu soon...', ), ), ], ); } // ── Consent step ────────────────────────────────────────────────────────── Widget _buildConsentStep() { final clientName = _nameCtrl.text.trim(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Data Sharing Preference', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 4), Text( 'Is ${clientName.isEmpty ? "the client" : clientName} comfortable sharing full logistics details?', style: TextStyle(fontSize: 12, color: AppColors.textSecondary), ), const SizedBox(height: 20), // Basic only GestureDetector( onTap: () => _captureAndSubmit(basicOnly: true), child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: const Color(0xFFFFF8ED), borderRadius: BorderRadius.circular(16), border: Border.all( color: const Color(0xFFFFB800).withValues(alpha: 0.5)), ), child: Row(children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: const Color(0xFFFFEDC2), borderRadius: BorderRadius.circular(12)), child: const Icon(Icons.lock_outline, size: 22, color: Color(0xFFE87C00)), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Basic Info Only', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 2), Text( 'Save identity & routing only. Commercial details can be added later.', style: TextStyle( fontSize: 11, color: AppColors.textSecondary), ), ], ), ), Icon(Icons.chevron_right, size: 18, color: AppColors.muted), ]), ), ), const SizedBox(height: 12), // Full details GestureDetector( onTap: () => setState(() { _onConsentStep = false; _step = 2; }), child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: const Color(0xFFEFF4FF), borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.primary.withValues(alpha: 0.35)), ), child: Row(children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: const Color(0xFFD3E4FE), borderRadius: BorderRadius.circular(12)), child: Icon(Icons.lock_open_outlined, size: 22, color: AppColors.primary), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Share Full Details', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), const SizedBox(height: 2), Text('Include volume, contracts, provider & notes.', style: TextStyle( fontSize: 11, color: AppColors.textSecondary)), ], ), ), Icon(Icons.chevron_right, size: 18, color: AppColors.primary), ]), ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: const Color(0xFFF8F9FF), borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.border.withValues(alpha: 0.2)), ), child: Row(children: [ Icon(Icons.info_outline, size: 14, color: AppColors.textSecondary), const SizedBox(width: 8), Expanded( child: Text( 'Basic-only clients can be upgraded to full profile at any time from the Client Database.', style: TextStyle( fontSize: 10, color: AppColors.textSecondary), ), ), ]), ), ], ); } // ── Success ─────────────────────────────────────────────────────────────── Widget _buildSuccess() { final isBasic = _chosenConsent == DataConsent.basicOnly; return Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.border.withValues(alpha: 0.2)), ), child: Column( children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: isBasic ? const Color(0xFFFFF8ED) : const Color(0xFFECFDF5), shape: BoxShape.circle, ), child: Icon( isBasic ? Icons.lock_outline : Icons.check_circle, size: 40, color: isBasic ? const Color(0xFFE87C00) : const Color(0xFF10B981), ), ), const SizedBox(height: 20), Text( isBasic ? 'Basic Info Saved!' : 'Survey Submitted!', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w800, color: AppColors.darkBlue), ), const SizedBox(height: 8), Text( isBasic ? 'Client recorded with identity & routing. Collect commercial details later from the Client Database.' : 'Full profile saved and metrics recalculated.', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: AppColors.textSecondary), ), const SizedBox(height: 20), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF8F9FF), borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.border.withValues(alpha: 0.1)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('ENTRY SUMMARY', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2), decoration: BoxDecoration( color: isBasic ? const Color(0xFFFFEDC2) : const Color(0xFFD8F9EE), borderRadius: BorderRadius.circular(20), ), child: Text( isBasic ? 'BASIC ONLY' : 'COMPLETE', style: TextStyle( fontSize: 9, fontWeight: FontWeight.w700, color: isBasic ? const Color(0xFFE87C00) : AppColors.green), ), ), ], ), Divider( height: 14, color: AppColors.border.withValues(alpha: 0.15)), _summaryRow('Client Name', _nameCtrl.text), if (_phoneCtrl.text.isNotEmpty) _summaryRow('Phone', _phoneCtrl.text), if (_pincodeCtrl.text.isNotEmpty) _summaryRow('PIN Code', _pincodeCtrl.text), _summaryRow( 'Location', [ if (_businessCity != null) _businessCity!.name, if (_businessCity != null) _businessCity!.state, if (_neighbourhoodCtrl.text.isNotEmpty) _neighbourhoodCtrl.text, ].join(', ').ifEmpty('—')), if (_businessCity != null || _destCities.isNotEmpty) _summaryRow('Route', '${_businessCity?.code ?? "—"} → ${_destCities.isEmpty ? "—" : _destCities.map((c) => c.code).join(", ")}'), if (_frequency.isNotEmpty) _summaryRow('Frequency', _frequency), if (_selectedSegments.isNotEmpty) _summaryRow('Segment', _selectedSegments.join(', ')), if (!isBasic) ...[ _summaryRow( 'Volume', '${_parcelVolume.round()} /wk'), _summaryRow('Provider', _provider), ], ], ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 44, child: ElevatedButton( onPressed: () { widget.onNavigate(0); _reset(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), child: const Text('Go to Dashboard', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700)), ), ), const SizedBox(height: 10), SizedBox( width: double.infinity, height: 44, child: OutlinedButton( onPressed: _reset, style: OutlinedButton.styleFrom( side: BorderSide(color: AppColors.border), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), child: Text('Log Another Survey', style: TextStyle( color: AppColors.textSecondary, fontWeight: FontWeight.w600)), ), ), ], ), ); } Widget _fieldLabel(String text) => Text(text, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.darkBlue)); /// Opens the searchable city picker bottom sheet. Future _openCityPicker({ required String title, required void Function(IndiaCity) onPicked, }) async { final picked = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _CityPickerSheet(title: title), ); if (picked != null) onPicked(picked); } // ── Searchable multi-city picker bottom sheet ──────────────────────────────── Future _openMultiCityPicker({ required String title, required List initialSelected, required void Function(List) onPicked, }) async { final picked = await showModalBottomSheet>( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _MultiCityPickerSheet( title: title, initialSelected: initialSelected, ), ); if (picked != null) { onPicked(picked); } } Future _showProviderSelectorSheet() async { final picked = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => ProviderPickerSheet( initialProvider: _provider, ), ); if (picked != null) { setState(() { _provider = picked; }); } } } // ── GPS business-location detect card ──────────────────────────────────────── class _LocationDetectCard extends StatelessWidget { final bool detecting; final IndiaCity? city; final String detectedAddress; final VoidCallback onDetect; final VoidCallback onPickManually; const _LocationDetectCard({ required this.detecting, required this.city, required this.detectedAddress, required this.onDetect, required this.onPickManually, }); @override Widget build(BuildContext context) { final hasCity = city != null; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: hasCity ? const Color(0xFFEFF4FF) : const Color(0xFFF8F9FF), borderRadius: BorderRadius.circular(14), border: Border.all( color: hasCity ? AppColors.primary.withValues(alpha: 0.4) : AppColors.border.withValues(alpha: 0.5), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: hasCity ? AppColors.primary : AppColors.primary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Icon( hasCity ? Icons.location_on : Icons.my_location, size: 20, color: hasCity ? Colors.white : AppColors.primary, ), ), const SizedBox(width: 12), Expanded( child: hasCity ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Flexible( child: Text(city!.name, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w800, color: AppColors.darkBlue), overflow: TextOverflow.ellipsis), ), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(4), ), child: Text(city!.code, style: const TextStyle( fontSize: 10, fontWeight: FontWeight.w800, color: Colors.white)), ), ]), if (city!.state.isNotEmpty) Text(city!.state, style: TextStyle( fontSize: 11, color: AppColors.textSecondary)), ], ) : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Detect from GPS', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), Text('Auto-fill the client\'s business city', style: TextStyle( fontSize: 11, color: AppColors.textSecondary)), ], ), ), if (detecting) const SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2.5), ) else IconButton( onPressed: onDetect, icon: Icon(Icons.gps_fixed, color: AppColors.primary), tooltip: 'Detect via GPS', ), ], ), if (hasCity && detectedAddress.isNotEmpty) ...[ const SizedBox(height: 8), Text(detectedAddress, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 10.5, color: AppColors.textSecondary)), ], const SizedBox(height: 10), Row(children: [ if (!detecting) Expanded( child: OutlinedButton.icon( onPressed: onDetect, icon: const Icon(Icons.my_location, size: 14), label: Text(hasCity ? 'Re-detect' : 'Use GPS Location', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700)), style: OutlinedButton.styleFrom( foregroundColor: AppColors.primary, side: BorderSide(color: AppColors.primary.withValues(alpha: 0.5)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), minimumSize: const Size.fromHeight(38), ), ), ), if (!detecting) const SizedBox(width: 8), Expanded( child: TextButton.icon( onPressed: onPickManually, icon: const Icon(Icons.search, size: 14), label: const Text('Choose City', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700)), style: TextButton.styleFrom( foregroundColor: AppColors.textSecondary, backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: AppColors.border.withValues(alpha: 0.5)), ), minimumSize: const Size.fromHeight(38), ), ), ), ]), ], ), ); } } // ── Flight-style route endpoint ─────────────────────────────────────────────── class _RouteEndpoint extends StatelessWidget { final IconData icon; final String label; final String code; final String name; final bool highlight; const _RouteEndpoint({ required this.icon, required this.label, required this.code, required this.name, this.highlight = false, }); @override Widget build(BuildContext context) { final color = highlight ? AppColors.primary : AppColors.darkBlue; return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Row(mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 11, color: AppColors.textSecondary), const SizedBox(width: 3), Text(label, style: TextStyle( fontSize: 9, fontWeight: FontWeight.w700, letterSpacing: 0.5, color: AppColors.textSecondary)), ]), const SizedBox(height: 3), Text(code, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: color), maxLines: 1, overflow: TextOverflow.ellipsis), Text(name, style: TextStyle( fontSize: 10, color: AppColors.textSecondary), maxLines: 1, overflow: TextOverflow.ellipsis), ], ); } } // ── Searchable city picker bottom sheet ─────────────────────────────────────── class _CityPickerSheet extends StatefulWidget { final String title; const _CityPickerSheet({required this.title}); @override State<_CityPickerSheet> createState() => _CityPickerSheetState(); } class _CityPickerSheetState extends State<_CityPickerSheet> { final _searchCtrl = TextEditingController(); String _query = ''; @override void dispose() { _searchCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final results = indiaCities.where((c) => c.matches(_query)).toList(); return DraggableScrollableSheet( initialChildSize: 0.85, minChildSize: 0.5, maxChildSize: 0.95, expand: false, builder: (_, scrollCtrl) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ const SizedBox(height: 10), Container( width: 40, height: 4, decoration: BoxDecoration( color: AppColors.muted.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 8), child: Row(children: [ Expanded( child: Text(widget.title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: AppColors.darkBlue)), ), GestureDetector( onTap: () => Navigator.pop(context), child: Container( width: 32, height: 32, decoration: const BoxDecoration( color: Color(0xFFF8F9FF), shape: BoxShape.circle), child: Icon(Icons.close, size: 16, color: AppColors.textSecondary), ), ), ]), ), // Search bar Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: TextField( controller: _searchCtrl, autofocus: true, onChanged: (v) => setState(() => _query = v), decoration: InputDecoration( hintText: 'Search city, code or state…', hintStyle: TextStyle(color: AppColors.muted, fontSize: 13), prefixIcon: Icon(Icons.search, color: AppColors.muted, size: 20), suffixIcon: _query.isEmpty ? null : IconButton( icon: Icon(Icons.clear, size: 18, color: AppColors.muted), onPressed: () { _searchCtrl.clear(); setState(() => _query = ''); }, ), ), ), ), const SizedBox(height: 8), Expanded( child: results.isEmpty ? Center( child: Text('No cities match "$_query"', style: TextStyle(color: AppColors.textSecondary)), ) : ListView.builder( controller: scrollCtrl, padding: const EdgeInsets.symmetric(horizontal: 12), itemCount: results.length, itemBuilder: (_, i) { final c = results[i]; return ListTile( onTap: () => Navigator.pop(context, c), leading: Container( width: 46, height: 34, decoration: BoxDecoration( color: const Color(0xFFEFF4FF), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Text(c.code, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w800, color: AppColors.primary)), ), title: Text(c.name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), subtitle: Text(c.state, style: TextStyle( fontSize: 11, color: AppColors.textSecondary)), trailing: Icon(Icons.chevron_right, size: 18, color: AppColors.muted), ); }, ), ), ], ), ), ); } } // ── Survey banner (enhanced) ────────────────────────────────────────────────── class _SurveyBanner extends StatelessWidget { @override Widget build(BuildContext context) { return Container( height: 150, decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF0B1C30), // Dark Blue Color(0xFF450A0A), // Dark Red/Burgundy ], ), boxShadow: [ BoxShadow( color: const Color(0xFF0B1C30).withValues(alpha: 0.15), blurRadius: 16, offset: const Offset(0, 8), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: Stack( children: [ // Glow orb 1 (Red Accent) Positioned( right: -30, top: -30, child: Container( width: 140, height: 140, decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.primary.withValues(alpha: 0.25), ), ), ), // Glow orb 2 (Blue Accent) Positioned( left: 80, bottom: -40, child: Container( width: 100, height: 100, decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.blueAccent.withValues(alpha: 0.15), ), ), ), // Dot Grid overlay Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(24), child: CustomPaint(painter: _DotGridPainter()), ), ), // Main content Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ // Tag/Badge Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), border: Border.all( color: Colors.white.withValues(alpha: 0.15), ), ), child: const Text( 'SMART CRM CORE', style: TextStyle( fontSize: 8, fontWeight: FontWeight.w800, color: Color(0xFFFFF0F1), letterSpacing: 1.0, ), ), ), const SizedBox(height: 10), const Text( 'Streamlined Surveys', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w800, color: Colors.white, letterSpacing: 0.4, ), ), const SizedBox(height: 4), Text( 'Capturing field data has never been easier. Syncs automatically.', style: TextStyle( fontSize: 11, color: Colors.white.withValues(alpha: 0.85), height: 1.3, ), ), ], ), ), const SizedBox(width: 16), // Glassmorphic circular icon container Container( width: 52, height: 52, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.1), shape: BoxShape.circle, border: Border.all( color: Colors.white.withValues(alpha: 0.2), width: 1.5, ), ), child: const Center( child: Icon( Icons.rocket_launch_rounded, size: 24, color: Colors.white, ), ), ), ], ), ), ], ), ), ); } } // ── Consent badge (reused by clients_screen) ────────────────────────────────── class ConsentBadge extends StatelessWidget { final bool small; const ConsentBadge({super.key, this.small = false}); @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.symmetric( horizontal: small ? 5 : 7, vertical: small ? 2 : 3), decoration: BoxDecoration( color: const Color(0xFFFFEDC2), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.lock_outline, size: small ? 8 : 10, color: const Color(0xFFE87C00)), SizedBox(width: small ? 2 : 3), Text('BASIC', style: TextStyle( fontSize: small ? 8 : 9, fontWeight: FontWeight.w800, color: const Color(0xFFE87C00))), ], ), ); } } // ── Helpers ─────────────────────────────────────────────────────────────────── Widget _summaryRow(String label, String value) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: TextStyle( fontSize: 11, color: AppColors.textSecondary)), Flexible( child: Text(value, textAlign: TextAlign.end, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.darkBlue)), ), ], ), ); } extension _StringX on String { String ifEmpty(String fallback) => isEmpty ? fallback : this; } // ── Dot grid painter ────────────────────────────────────────────────────────── class _DotGridPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withValues(alpha: 0.06) ..strokeWidth = 1; const spacing = 28.0; for (double x = 0; x < size.width; x += spacing) { for (double y = 0; y < size.height; y += spacing) { canvas.drawCircle(Offset(x, y), 1.5, paint); } } } @override bool shouldRepaint(_DotGridPainter oldDelegate) => false; } class _MultiCityPickerSheet extends StatefulWidget { final String title; final List initialSelected; const _MultiCityPickerSheet({ required this.title, required this.initialSelected, }); @override State<_MultiCityPickerSheet> createState() => _MultiCityPickerSheetState(); } class _MultiCityPickerSheetState extends State<_MultiCityPickerSheet> { final _searchCtrl = TextEditingController(); String _query = ''; late List _selectedCities; @override void initState() { super.initState(); _selectedCities = List.from(widget.initialSelected); } @override void dispose() { _searchCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final results = indiaCities.where((c) => c.matches(_query)).toList(); return DraggableScrollableSheet( initialChildSize: 0.85, minChildSize: 0.5, maxChildSize: 0.95, expand: false, builder: (_, scrollCtrl) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ const SizedBox(height: 10), Container( width: 40, height: 4, decoration: BoxDecoration( color: AppColors.muted.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 8), child: Row( children: [ Expanded( child: Text(widget.title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: AppColors.darkBlue)), ), if (_selectedCities.isNotEmpty) TextButton( onPressed: () => setState(() => _selectedCities.clear()), style: TextButton.styleFrom( foregroundColor: AppColors.primary, padding: const EdgeInsets.symmetric(horizontal: 8), ), child: const Text('Clear All', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold)), ), const SizedBox(width: 8), GestureDetector( onTap: () => Navigator.pop(context), child: Container( width: 32, height: 32, decoration: const BoxDecoration( color: Color(0xFFF8F9FF), shape: BoxShape.circle), child: Icon(Icons.close, size: 16, color: AppColors.textSecondary), ), ), ], ), ), // Search bar Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: TextField( controller: _searchCtrl, autofocus: true, onChanged: (v) => setState(() => _query = v), decoration: InputDecoration( hintText: 'Search city, code or state…', hintStyle: TextStyle(color: AppColors.muted, fontSize: 13), prefixIcon: Icon(Icons.search, color: AppColors.muted, size: 20), suffixIcon: _query.isEmpty ? null : IconButton( icon: Icon(Icons.clear, size: 18, color: AppColors.muted), onPressed: () { _searchCtrl.clear(); setState(() => _query = ''); }, ), ), ), ), const SizedBox(height: 8), Expanded( child: results.isEmpty ? Center( child: Text('No cities match "$_query"', style: TextStyle(color: AppColors.textSecondary)), ) : ListView.builder( controller: scrollCtrl, padding: const EdgeInsets.symmetric(horizontal: 12), itemCount: results.length, itemBuilder: (_, i) { final c = results[i]; final isSelected = _selectedCities.any((x) => x.name == c.name); return CheckboxListTile( value: isSelected, onChanged: (val) { setState(() { if (val == true) { if (!_selectedCities.any((x) => x.name == c.name)) { _selectedCities.add(c); } } else { _selectedCities.removeWhere((x) => x.name == c.name); } }); }, activeColor: AppColors.primary, checkboxShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), ), title: Text(c.name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.darkBlue)), subtitle: Text(c.state, style: TextStyle( fontSize: 11, color: AppColors.textSecondary)), secondary: Container( width: 46, height: 34, decoration: BoxDecoration( color: const Color(0xFFEFF4FF), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Text(c.code, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w800, color: AppColors.primary)), ), ); }, ), ), SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: () => Navigator.pop(context, _selectedCities), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), elevation: 1, ), child: Text( _selectedCities.isEmpty ? 'Apply' : 'Apply (${_selectedCities.length} Selected)', style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w700), ), ), ), ), ), ], ), ), ); } } class ProviderPickerSheet extends StatefulWidget { final String initialProvider; const ProviderPickerSheet({ super.key, required this.initialProvider, }); @override State createState() => _ProviderPickerSheetState(); } class _ProviderPickerSheetState extends State { final _searchCtrl = TextEditingController(); String _query = ''; @override void dispose() { _searchCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final lowercaseQuery = _query.toLowerCase(); // Build grouped results based on the search query final Map> filteredGroups = {}; categorisedProviders.forEach((category, providers) { final matches = providers.where((p) => p.toLowerCase().contains(lowercaseQuery)).toList(); if (matches.isNotEmpty) { filteredGroups[category] = matches; } }); final showOthers = _query.isEmpty || 'others'.contains(lowercaseQuery); return DraggableScrollableSheet( initialChildSize: 0.85, minChildSize: 0.5, maxChildSize: 0.95, expand: false, builder: (_, scrollCtrl) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ const SizedBox(height: 10), Container( width: 40, height: 4, decoration: BoxDecoration( color: AppColors.muted.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 8), child: Row( children: [ const Expanded( child: Text('Select Service Provider', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: AppColors.darkBlue)), ), GestureDetector( onTap: () => Navigator.pop(context), child: Container( width: 32, height: 32, decoration: const BoxDecoration( color: Color(0xFFF8F9FF), shape: BoxShape.circle), child: Icon(Icons.close, size: 16, color: AppColors.textSecondary), ), ), ], ), ), // Search bar Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: TextField( controller: _searchCtrl, autofocus: true, onChanged: (v) => setState(() => _query = v), decoration: InputDecoration( hintText: 'Search service provider...', hintStyle: TextStyle(color: AppColors.muted, fontSize: 13), prefixIcon: Icon(Icons.search, color: AppColors.muted, size: 20), suffixIcon: _query.isEmpty ? null : IconButton( icon: Icon(Icons.clear, size: 18, color: AppColors.muted), onPressed: () { _searchCtrl.clear(); setState(() => _query = ''); }, ), ), ), ), const SizedBox(height: 8), Expanded( child: (filteredGroups.isEmpty && !showOthers) ? Center( child: Text('No providers match "$_query"', style: TextStyle(color: AppColors.textSecondary)), ) : ListView.builder( controller: scrollCtrl, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: (showOthers ? 1 : 0) + filteredGroups.length * 2, // category title + items itemBuilder: (context, index) { if (showOthers && index == 0) { // Render "Others" option return Padding( padding: const EdgeInsets.only(top: 8.0), child: ListTile( onTap: () => Navigator.pop(context, 'Others'), contentPadding: const EdgeInsets.symmetric(horizontal: 12), leading: Container( width: 36, height: 36, decoration: const BoxDecoration( color: Color(0xFFFFF8ED), shape: BoxShape.circle, ), child: const Icon(Icons.add, color: Color(0xFFE87C00), size: 18), ), title: const Text( 'Others', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.darkBlue, ), ), subtitle: const Text( 'Manually enter custom provider', style: TextStyle( fontSize: 11, color: Color(0xFFE87C00), fontWeight: FontWeight.w500, ), ), trailing: widget.initialProvider == 'Others' ? const Icon(Icons.check_circle, color: AppColors.primary, size: 20) : null, ), ); } // Calculate the actual index in the groups int groupIndex = showOthers ? index - 1 : index; int mapIndex = groupIndex ~/ 2; bool isHeader = groupIndex % 2 == 0; final keys = filteredGroups.keys.toList(); final category = keys[mapIndex]; final providers = filteredGroups[category]!; if (isHeader) { return Padding( padding: const EdgeInsets.only(top: 20.0, bottom: 8.0, left: 12.0), child: Text( category.toUpperCase(), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 1.0, color: AppColors.primary, ), ), ); } else { return Column( children: providers.map((provider) { final isSelected = widget.initialProvider == provider; return ListTile( onTap: () => Navigator.pop(context, provider), contentPadding: const EdgeInsets.symmetric(horizontal: 12), title: Text( provider, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.darkBlue, ), ), trailing: isSelected ? const Icon(Icons.check_circle, color: AppColors.primary, size: 20) : null, ); }).toList(), ); } }, ), ), ], ), ), ); } }