Initial commit of Doormile CRM Mobile with UI modernizations

This commit is contained in:
2026-06-26 13:43:37 +05:30
commit 3965046984
183 changed files with 18135 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
/// A single authenticated user.
class AuthUser {
final String email;
final String name;
final String role;
final String token;
const AuthUser({
required this.email,
required this.name,
required this.role,
required this.token,
});
Map<String, dynamic> toJson() => {
'email': email,
'name': name,
'role': role,
'token': token,
};
factory AuthUser.fromJson(Map<String, dynamic> json) => AuthUser(
email: json['email'] as String? ?? '',
name: json['name'] as String? ?? '',
role: json['role'] as String? ?? '',
token: json['token'] as String? ?? '',
);
}
class AuthService {
static const String _base = 'https://api.doormile.com/api/v1';
/// Login with real backend API
Future<AuthUser?> login(String email, String password) async {
try {
final res = await http.post(
Uri.parse('$_base/admin/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'email': email, 'password': password}),
).timeout(const Duration(seconds: 15));
if (res.statusCode == 200) {
final data = jsonDecode(res.body);
final token = data['token'] ?? '';
final userObj = data['user'] ?? {};
// Sometimes APIs return user inside 'data' or directly. Adjusting based on typical JWT responses.
final emailStr = userObj['email'] ?? email;
final nameStr = userObj['name'] ?? userObj['first_name'] ?? 'Admin';
final roleStr = userObj['role'] ?? 'admin';
final user = AuthUser(
email: emailStr,
name: nameStr,
role: roleStr,
token: token,
);
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_user_data', jsonEncode(user.toJson()));
return user;
}
return null;
} catch (_) {
return null;
}
}
/// Restore user from local storage
Future<AuthUser?> getSavedUser() async {
try {
final prefs = await SharedPreferences.getInstance();
final userData = prefs.getString('auth_user_data');
if (userData != null && userData.isNotEmpty) {
return AuthUser.fromJson(jsonDecode(userData));
}
} catch (_) {}
return null;
}
}

View File

@@ -0,0 +1,380 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../models/client.dart';
class CrmApiService {
static const String _base = 'https://api.doormile.com/api/v1';
final http.Client _http;
String? _authToken;
CrmApiService({http.Client? client}) : _http = client ?? http.Client();
void setToken(String token) {
_authToken = token;
}
Future<Map<String, String>> _getHeaders() async {
final Map<String, String> headers = {
'Content-Type': 'application/json',
};
if (_authToken != null && _authToken!.isNotEmpty) {
headers['Authorization'] = 'Bearer $_authToken';
} else {
// Fallback to shared preferences just in case
try {
final prefs = await SharedPreferences.getInstance();
final userData = prefs.getString('auth_user_data');
if (userData != null && userData.isNotEmpty) {
final token = jsonDecode(userData)['token']?.toString();
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
}
} catch (_) {}
}
return headers;
}
// ── Public API ──────────────────────────────────────────────────────────────
/// Fetch all clients from the backend.
/// Throws on network failure so the caller can distinguish "backend down"
/// from "backend returned an empty list".
Future<List<Client>> loadClients() async {
try {
final res = await _http
.get(Uri.parse('$_base/crm/clients?limit=1000'), headers: await _getHeaders())
.timeout(const Duration(seconds: 10));
if (res.statusCode != 200) {
debugPrint('loadClients failed: ${res.statusCode} ${res.body}');
return [];
}
final decoded = jsonDecode(res.body);
List<dynamic> body = [];
if (decoded is List) {
body = decoded;
} else if (decoded is Map && decoded['data'] is List) {
body = decoded['data'];
}
return body.map((j) => _fromJson(j as Map<String, dynamic>)).toList();
} catch (e) {
debugPrint('loadClients exception: $e');
return [];
}
}
/// Create a new client on the backend. Returns the server-assigned record,
/// or null if the call fails (caller keeps the locally-created record).
Future<Client?> createClient(Client client) async {
try {
final res = await _http
.post(
Uri.parse('$_base/crm/clients'),
headers: await _getHeaders(),
body: jsonEncode(_toCreatePayload(client)),
)
.timeout(const Duration(seconds: 10));
if (res.statusCode != 201) return null;
return _fromJson(jsonDecode(res.body) as Map<String, dynamic>);
} catch (_) {
return null;
}
}
/// Update an existing client. `client.id` must be a valid numeric backend ID.
Future<Client?> updateClient(Client client) async {
final numId = int.tryParse(client.id);
if (numId == null) return null; // temp local ID — not yet persisted
try {
final res = await _http
.put(
Uri.parse('$_base/crm/clients/$numId'),
headers: await _getHeaders(),
body: jsonEncode(_toUpdatePayload(client)),
)
.timeout(const Duration(seconds: 10));
if (res.statusCode != 200) return null;
return _fromJson(jsonDecode(res.body) as Map<String, dynamic>);
} catch (_) {
return null;
}
}
/// Delete a client by ID. Returns true if successful.
Future<bool> deleteClient(String clientId) async {
final numId = int.tryParse(clientId);
if (numId == null) return false;
try {
final res = await _http
.delete(
Uri.parse('$_base/crm/clients/$numId'),
headers: await _getHeaders(),
)
.timeout(const Duration(seconds: 10));
return res.statusCode == 200;
} catch (_) {
return false;
}
}
/// Fetch all competitor branch surveys
Future<List<dynamic>> fetchSurveys() async {
try {
final res = await _http
.get(Uri.parse('$_base/admin/competitor-branches?limit=1000'), headers: await _getHeaders())
.timeout(const Duration(seconds: 10));
if (res.statusCode != 200) return [];
final data = jsonDecode(res.body);
if (data is List) return data;
if (data is Map && data['data'] is List) return data['data'];
return [];
} catch (_) {
return [];
}
}
/// Fetch carrier pricing
Future<List<dynamic>> fetchPricing() async {
try {
final res = await _http
.get(Uri.parse('$_base/admin/carrier-pricing?limit=1000'), headers: await _getHeaders())
.timeout(const Duration(seconds: 10));
if (res.statusCode != 200) return [];
final data = jsonDecode(res.body);
if (data is List) return data;
if (data is Map && data['data'] is List) return data['data'];
return [];
} catch (_) {
return [];
}
}
/// Create carrier pricing
Future<bool> createPricing(Map<String, dynamic> data) async {
try {
final res = await _http
.post(
Uri.parse('$_base/admin/carrier-pricing'),
headers: await _getHeaders(),
body: jsonEncode({
'company': data['company'],
'weight_slab': data['weight_slab'],
'zone': data['zone'] ?? '',
'service_type': data['service_type'] ?? '',
'rate': data['rate'].toString(),
}),
)
.timeout(const Duration(seconds: 10));
return res.statusCode == 200 || res.statusCode == 201 || res.statusCode == 204;
} catch (_) {
return false;
}
}
/// Update carrier pricing
Future<bool> updatePricing(String id, Map<String, dynamic> data) async {
try {
final res = await _http
.put(
Uri.parse('$_base/admin/carrier-pricing/$id'),
headers: await _getHeaders(),
body: jsonEncode({
'company': data['company'],
'weight_slab': data['weight_slab'],
'zone': data['zone'] ?? '',
'service_type': data['service_type'] ?? '',
'rate': data['rate'].toString(),
}),
)
.timeout(const Duration(seconds: 10));
return res.statusCode == 200 || res.statusCode == 204;
} catch (_) {
return false;
}
}
/// Delete carrier pricing
Future<bool> deletePricing(String id) async {
try {
final res = await _http
.delete(
Uri.parse('$_base/admin/carrier-pricing/$id'),
headers: await _getHeaders(),
)
.timeout(const Duration(seconds: 10));
return res.statusCode == 200 || res.statusCode == 204;
} catch (_) {
return false;
}
}
/// Create competitor branch (survey)
Future<bool> createSurvey(Map<String, dynamic> data) async {
try {
final res = await _http
.post(
Uri.parse('$_base/admin/competitor-branches'),
headers: await _getHeaders(),
body: jsonEncode(data),
)
.timeout(const Duration(seconds: 10));
return res.statusCode == 200 || res.statusCode == 201 || res.statusCode == 204;
} catch (_) {
return false;
}
}
/// Update competitor branch (survey)
Future<bool> updateSurvey(String id, Map<String, dynamic> data) async {
try {
final res = await _http
.put(
Uri.parse('$_base/admin/competitor-branches/$id'),
headers: await _getHeaders(),
body: jsonEncode(data),
)
.timeout(const Duration(seconds: 10));
return res.statusCode == 200 || res.statusCode == 204;
} catch (_) {
return false;
}
}
/// Delete competitor branch (survey)
Future<bool> deleteSurvey(String id) async {
try {
final res = await _http
.delete(
Uri.parse('$_base/admin/competitor-branches/$id'),
headers: await _getHeaders(),
)
.timeout(const Duration(seconds: 10));
return res.statusCode == 200 || res.statusCode == 204;
} catch (_) {
return false;
}
}
// ── Serialisation ───────────────────────────────────────────────────────────
Client _fromJson(Map<String, dynamic> j) => Client(
id: j['id'].toString(),
name: _joinName(j['first_name'], j['last_name']),
city: j['city'] as String? ?? '',
businessType: _parseBusinessType(j['businessType'] as String?),
parcelVolume: (j['parcelVolume'] as num?)?.toDouble() ?? 0.0,
activeContracts: j['activeContracts'] as int? ?? 0,
provider: j['provider'] as String? ?? '',
efficiency: j['efficiency'] as String? ?? '',
status: _parseStatus(j['status'] as String?),
lastUpdated: j['lastUpdated'] as String? ?? '',
surveySubmitted: true, // in DB = was submitted
notes: j['notes'] as String? ?? '',
dataConsent: _parseConsent(j['dataConsent'] as String?),
phone: j['phone'] as String? ?? '',
frequency: j['frequency'] as String? ?? '',
businessState: j['businessState'] as String? ?? '',
transitFrom: j['transitFrom'] as String? ?? '',
transitTo: j['transitTo'] as String? ?? '',
neighbourhood: j['neighbourhood'] as String? ?? '',
logisticsSegment: j['logisticsSegment'] as String? ?? '',
pincode: j['pincode'] as String? ?? '',
surveyLat: (j['survey_lat'] as num?)?.toDouble() ?? 0.0,
surveyLng: (j['survey_long'] as num?)?.toDouble() ?? 0.0,
surveyAddress: j['surveyAddress'] as String? ?? '',
surveyZone: j['surveyZone'] as String? ?? '',
surveyPincode: j['surveyPincode'] as String? ?? '',
recordedByName: '',
recordedByEmail: j['email'] as String? ?? '',
recordedByRole: j['role'] as String? ?? '',
);
Map<String, dynamic> _toCreatePayload(Client c) => {
'first_name': c.name,
'phone': c.phone,
'city': c.city,
'businessState': c.businessState,
'businessType': c.businessType.name,
'status': c.status.name,
'frequency': c.frequency,
'parcelVolume': c.parcelVolume,
'activeContracts': c.activeContracts,
'provider': c.provider,
'efficiency': c.efficiency,
'logisticsSegment': c.logisticsSegment,
'transitFrom': c.transitFrom,
'transitTo': c.transitTo,
'neighbourhood': c.neighbourhood,
'pincode': c.pincode,
'surveyAddress': c.surveyAddress,
'surveyZone': c.surveyZone,
'surveyPincode': c.surveyPincode,
'survey_lat': c.surveyLat,
'survey_long': c.surveyLng,
'notes': c.notes,
'dataConsent': c.dataConsent.name,
'registration_source': 'mobile',
};
Map<String, dynamic> _toUpdatePayload(Client c) => {
'first_name': c.name,
'phone': c.phone,
'city': c.city,
'businessState': c.businessState,
'businessType': c.businessType.name,
'status': c.status.name,
'frequency': c.frequency,
'parcelVolume': c.parcelVolume,
'activeContracts': c.activeContracts,
'provider': c.provider,
'efficiency': c.efficiency,
'logisticsSegment': c.logisticsSegment,
'transitFrom': c.transitFrom,
'transitTo': c.transitTo,
'neighbourhood': c.neighbourhood,
'pincode': c.pincode,
'surveyAddress': c.surveyAddress,
'surveyZone': c.surveyZone,
'surveyPincode': c.surveyPincode,
'survey_lat': c.surveyLat,
'survey_long': c.surveyLng,
'notes': c.notes,
'dataConsent': c.dataConsent.name,
};
// ── Helpers ─────────────────────────────────────────────────────────────────
String _joinName(dynamic first, dynamic last) {
final f = (first as String?) ?? '';
final l = (last as String?) ?? '';
final joined = [f, l].where((s) => s.isNotEmpty).join(' ');
return joined.isEmpty ? '' : joined;
}
BusinessType _parseBusinessType(String? s) {
if (s == null) return BusinessType.retail;
return BusinessType.values.firstWhere(
(b) => b.name == s,
orElse: () => BusinessType.retail,
);
}
ClientStatus _parseStatus(String? s) {
if (s == null) return ClientStatus.newClient;
return ClientStatus.values.firstWhere(
(st) => st.name == s,
orElse: () => ClientStatus.newClient,
);
}
DataConsent _parseConsent(String? s) {
if (s == null) return DataConsent.full;
return DataConsent.values.firstWhere(
(d) => d.name == s,
orElse: () => DataConsent.full,
);
}
}

View File

@@ -0,0 +1,146 @@
import 'dart:convert';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
/// Why location cannot be captured (used to show the right alert).
enum LocationCheckResult {
ready,
serviceDisabled, // device GPS/location is switched off
permissionDenied, // user denied — can ask again
permissionPermanentlyDenied, // "Don't ask again" was checked
}
/// GPS coordinates + reverse-geocoded details captured at survey submission.
class SurveyLocation {
final double lat;
final double lng;
final String address;
final String zone; // suburb / neighbourhood
final String city; // detected city / town
final String state; // detected state
final String pincode; // postal code from reverse-geocode
const SurveyLocation({
required this.lat,
required this.lng,
required this.address,
required this.zone,
this.city = '',
this.state = '',
this.pincode = '',
});
factory SurveyLocation.empty() =>
const SurveyLocation(lat: 0.0, lng: 0.0, address: '', zone: '');
bool get hasFix => lat != 0.0 || lng != 0.0;
}
class LocationService {
/// Checks service + permission WITHOUT triggering a system prompt.
/// Use this to decide whether to show an in-app alert before capturing.
static Future<LocationCheckResult> checkStatus() async {
if (!await Geolocator.isLocationServiceEnabled()) {
return LocationCheckResult.serviceDisabled;
}
final perm = await Geolocator.checkPermission();
if (perm == LocationPermission.deniedForever) {
return LocationCheckResult.permissionPermanentlyDenied;
}
if (perm == LocationPermission.denied) {
return LocationCheckResult.permissionDenied;
}
return LocationCheckResult.ready;
}
/// Requests permission (one-shot OS prompt) then captures GPS + address.
/// Never throws — returns [SurveyLocation.empty] on any failure.
static Future<SurveyLocation> capture() async {
try {
if (!await Geolocator.isLocationServiceEnabled()) {
return SurveyLocation.empty();
}
var perm = await Geolocator.checkPermission();
if (perm == LocationPermission.denied) {
perm = await Geolocator.requestPermission();
}
if (perm == LocationPermission.denied ||
perm == LocationPermission.deniedForever) {
return SurveyLocation.empty();
}
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
),
).timeout(const Duration(seconds: 10));
final geo = await _reverseGeocode(position.latitude, position.longitude);
return SurveyLocation(
lat: position.latitude,
lng: position.longitude,
address: geo['display_name'] ?? '',
zone: geo['zone'] ?? '',
city: geo['city'] ?? '',
state: geo['state'] ?? '',
pincode: geo['pincode'] ?? '',
);
} catch (_) {
return SurveyLocation.empty();
}
}
/// Opens the device's Location / GPS settings page.
static Future<void> openLocationSettings() =>
Geolocator.openLocationSettings();
/// Opens this app's permission settings page (for permanently-denied case).
static Future<void> openAppSettings() => Geolocator.openAppSettings();
static Future<Map<String, String>> _reverseGeocode(
double lat, double lng) async {
try {
final uri = Uri.parse(
'https://nominatim.openstreetmap.org/reverse'
'?lat=$lat&lon=$lng&format=json&addressdetails=1',
);
final res = await http
.get(uri, headers: {'User-Agent': 'DoorMile-CRM/1.0'})
.timeout(const Duration(seconds: 6));
if (res.statusCode != 200) return {};
final body = jsonDecode(res.body) as Map<String, dynamic>;
final addr = (body['address'] as Map<String, dynamic>?) ?? {};
final zone = (addr['suburb'] ??
addr['neighbourhood'] ??
addr['quarter'] ??
addr['village'] ??
'') as String;
final city = (addr['city'] ??
addr['town'] ??
addr['municipality'] ??
addr['county'] ??
addr['state_district'] ??
'') as String;
final state = (addr['state'] ?? '') as String;
final pincode = (addr['postcode'] ?? '') as String;
return {
'display_name': (body['display_name'] as String?) ?? '',
'zone': zone,
'city': city,
'state': state,
'pincode': pincode,
};
} catch (_) {
return {};
}
}
}

View File

@@ -0,0 +1,235 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/client.dart';
class QdrantService {
static const String _baseUrl = 'http://66.116.207.225:6333';
static const String _apiKey = 'Package@321#';
static const String collectionName = 'doormile_clients';
// 16-dim feature vector (city-agnostic — city stored in payload only)
//
// [0] parcelVolume (normalised 01 over max 1 200)
// [1] activeContracts (normalised 01 over max 40)
// [25] businessType one-hot (ecommerce, manufacturing, wholesale, retail)
// [69] status one-hot (newClient, pending, completed, updated)
// [10] surveySubmitted (0 / 1)
// [11] dataConsent basicOnly=0, full=1
// [12] hasPhone (0 / 1)
// [13] hasTransitRoute both from+to non-empty (0 / 1)
// [14] frequencyScore none=0, monthly=0.25, biweekly=0.5, weekly=0.75, daily=1
// [15] hasNeighbourhood (0 / 1)
static const int _vectorSize = 16;
final http.Client _http;
QdrantService({http.Client? client}) : _http = client ?? http.Client();
Map<String, String> get _headers => {
'Content-Type': 'application/json',
'api-key': _apiKey,
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
};
/// Creates the collection if it does not already exist.
Future<void> ensureCollection() async {
final res = await _http.get(
Uri.parse('$_baseUrl/collections/$collectionName'),
headers: _headers,
);
if (res.statusCode == 200) return;
await _http.put(
Uri.parse('$_baseUrl/collections/$collectionName'),
headers: _headers,
body: jsonEncode({
'vectors': {'size': _vectorSize, 'distance': 'Cosine'},
}),
);
}
/// Upsert (insert or update) a single client.
Future<void> upsertClient(Client client) async {
await _http.put(
Uri.parse('$_baseUrl/collections/$collectionName/points'),
headers: _headers,
body: jsonEncode({
'points': [_toPoint(client)],
}),
);
}
/// Batch upsert a list of clients.
Future<void> upsertClients(List<Client> clients) async {
if (clients.isEmpty) return;
await _http.put(
Uri.parse('$_baseUrl/collections/$collectionName/points'),
headers: _headers,
body: jsonEncode({
'points': clients.map(_toPoint).toList(),
}),
);
}
/// Fetch all stored clients from Qdrant (up to 500).
Future<List<Client>> loadClients() async {
final res = await _http.post(
Uri.parse('$_baseUrl/collections/$collectionName/points/scroll'),
headers: _headers,
body: jsonEncode({
'limit': 500,
'with_payload': true,
'with_vector': false,
}),
);
if (res.statusCode != 200) return [];
final body = jsonDecode(res.body) as Map<String, dynamic>;
final points = (body['result']?['points'] as List?) ?? [];
return points.map((p) => _fromPoint(p as Map<String, dynamic>)).toList();
}
/// Drop and recreate the collection — used for staging resets.
Future<void> deleteCollection() async {
await _http.delete(
Uri.parse('$_baseUrl/collections/$collectionName'),
headers: _headers,
);
}
/// Delete a client by its string ID.
Future<void> deleteClient(String clientId) async {
await _http.post(
Uri.parse('$_baseUrl/collections/$collectionName/points/delete'),
headers: _headers,
body: jsonEncode({
'points': [_stableId(clientId)],
}),
);
}
// ── Helpers ───────────────────────────────────────────────────────────────
int _stableId(String id) {
int h = 0;
for (final c in id.codeUnits) {
h = (h * 31 + c) & 0x7FFFFFFF;
}
return h == 0 ? 1 : h;
}
Map<String, dynamic> _toPoint(Client client) => {
'id': _stableId(client.id),
'vector': _toVector(client),
'payload': {
'clientId': client.id,
'name': client.name,
'city': client.city,
'businessType': client.businessType.name,
'parcelVolume': client.parcelVolume,
'activeContracts': client.activeContracts,
'provider': client.provider,
'efficiency': client.efficiency,
'status': client.status.name,
'lastUpdated': client.lastUpdated,
'surveySubmitted': client.surveySubmitted,
'notes': client.notes,
'dataConsent': client.dataConsent.name,
'phone': client.phone,
'frequency': client.frequency,
'businessState': client.businessState,
'transitType': client.transitType,
'transitFrom': client.transitFrom,
'transitTo': client.transitTo,
'neighbourhood': client.neighbourhood,
'logisticsSegment': client.logisticsSegment,
'pincode': client.pincode,
// GPS captured at submission — not shown in app, stored for field audit
'surveyLat': client.surveyLat,
'surveyLng': client.surveyLng,
'surveyAddress': client.surveyAddress,
'surveyZone': client.surveyZone,
'surveyPincode': client.surveyPincode,
// Field-agent attribution — who collected this survey (login session)
'recordedByName': client.recordedByName,
'recordedByEmail': client.recordedByEmail,
'recordedByRole': client.recordedByRole,
// Qdrant geo payload — enables geo-radius queries later
if (client.surveyLat != 0.0 || client.surveyLng != 0.0)
'surveyGeo': {'lat': client.surveyLat, 'lon': client.surveyLng},
},
};
List<double> _toVector(Client client) {
final v = List<double>.filled(_vectorSize, 0.0);
v[0] = (client.parcelVolume / 1200.0).clamp(0.0, 1.0);
v[1] = (client.activeContracts / 40.0).clamp(0.0, 1.0);
v[2 + client.businessType.index] = 1.0;
v[6 + client.status.index] = 1.0;
v[10] = client.surveySubmitted ? 1.0 : 0.0;
v[11] = client.dataConsent == DataConsent.full ? 1.0 : 0.0;
v[12] = client.phone.isNotEmpty ? 1.0 : 0.0;
v[13] = (client.transitFrom.isNotEmpty && client.transitTo.isNotEmpty)
? 1.0
: 0.0;
v[14] = _frequencyScore(client.frequency);
v[15] = client.neighbourhood.isNotEmpty ? 1.0 : 0.0;
return v;
}
double _frequencyScore(String freq) {
switch (freq.toLowerCase()) {
case 'daily':
return 1.0;
case 'weekly':
return 0.75;
case 'bi-weekly':
return 0.5;
case 'monthly':
return 0.25;
default:
return 0.0;
}
}
Client _fromPoint(Map<String, dynamic> point) {
final p = point['payload'] as Map<String, dynamic>;
return Client(
id: p['clientId'] as String,
name: p['name'] as String,
city: p['city'] as String,
businessType:
BusinessType.values.firstWhere((b) => b.name == p['businessType']),
parcelVolume: (p['parcelVolume'] as num?)?.toDouble() ?? 0.0,
activeContracts: p['activeContracts'] as int,
provider: p['provider'] as String,
efficiency: p['efficiency'] as String,
status: ClientStatus.values.firstWhere((s) => s.name == p['status']),
lastUpdated: p['lastUpdated'] as String,
surveySubmitted: p['surveySubmitted'] as bool,
notes: (p['notes'] as String?) ?? '',
dataConsent: DataConsent.values.firstWhere(
(d) => d.name == (p['dataConsent'] as String? ?? 'full'),
orElse: () => DataConsent.full,
),
phone: (p['phone'] as String?) ?? '',
frequency: (p['frequency'] as String?) ?? '',
businessState: (p['businessState'] as String?) ?? '',
transitType: (p['transitType'] as String?) ?? '',
transitFrom: (p['transitFrom'] as String?) ?? '',
transitTo: (p['transitTo'] as String?) ?? '',
neighbourhood: (p['neighbourhood'] as String?) ?? '',
logisticsSegment: (p['logisticsSegment'] as String?) ?? '',
pincode: (p['pincode'] as String?) ?? '',
surveyLat: (p['surveyLat'] as num?)?.toDouble() ?? 0.0,
surveyLng: (p['surveyLng'] as num?)?.toDouble() ?? 0.0,
surveyAddress: (p['surveyAddress'] as String?) ?? '',
surveyZone: (p['surveyZone'] as String?) ?? '',
surveyPincode: (p['surveyPincode'] as String?) ?? '',
recordedByName: (p['recordedByName'] as String?) ?? '',
recordedByEmail: (p['recordedByEmail'] as String?) ?? '',
recordedByRole: (p['recordedByRole'] as String?) ?? '',
);
}
}