2844 lines
105 KiB
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(),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|