381 lines
13 KiB
Dart
381 lines
13 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|