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> _getHeaders() async { final Map 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> 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 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)).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 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); } catch (_) { return null; } } /// Update an existing client. `client.id` must be a valid numeric backend ID. Future 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); } catch (_) { return null; } } /// Delete a client by ID. Returns true if successful. Future 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> 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> 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 createPricing(Map 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 updatePricing(String id, Map 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 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 createSurvey(Map 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 updateSurvey(String id, Map 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 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 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 _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 _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, ); } }