Files
crm_mobile/lib/screens/survey_screen.dart

2844 lines
105 KiB
Dart

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<Client> 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<SurveyScreen> createState() => _SurveyScreenState();
}
enum _SurveyState { idle, surveying, success }
class _SurveyScreenState extends State<SurveyScreen>
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<IndiaCity> _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<String> _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<double> _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<Client> 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<bool> _showProminentLocationDisclosure() async {
final result = await showDialog<bool>(
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<SurveyLocation> _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<void> _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<void> _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<bool> _showLocationAlert({
required IconData icon,
required String title,
required String message,
required String primaryLabel,
required Future<void> Function() onPrimary,
}) async {
final result = await showDialog<bool>(
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<Color>(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<Color>(
_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<void> _openCityPicker({
required String title,
required void Function(IndiaCity) onPicked,
}) async {
final picked = await showModalBottomSheet<IndiaCity>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _CityPickerSheet(title: title),
);
if (picked != null) onPicked(picked);
}
// ── Searchable multi-city picker bottom sheet ────────────────────────────────
Future<void> _openMultiCityPicker({
required String title,
required List<IndiaCity> initialSelected,
required void Function(List<IndiaCity>) onPicked,
}) async {
final picked = await showModalBottomSheet<List<IndiaCity>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _MultiCityPickerSheet(
title: title,
initialSelected: initialSelected,
),
);
if (picked != null) {
onPicked(picked);
}
}
Future<void> _showProviderSelectorSheet() async {
final picked = await showModalBottomSheet<String>(
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<IndiaCity> 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<IndiaCity> _selectedCities;
@override
void initState() {
super.initState();
_selectedCities = List<IndiaCity>.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<ProviderPickerSheet> createState() => _ProviderPickerSheetState();
}
class _ProviderPickerSheetState extends State<ProviderPickerSheet> {
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<String, List<String>> 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(),
);
}
},
),
),
],
),
),
);
}
}