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 0–1 over max 1 200) // [1] activeContracts (normalised 0–1 over max 40) // [2–5] businessType one-hot (ecommerce, manufacturing, wholesale, retail) // [6–9] 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 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 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 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 upsertClients(List 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> 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; final points = (body['result']?['points'] as List?) ?? []; return points.map((p) => _fromPoint(p as Map)).toList(); } /// Drop and recreate the collection — used for staging resets. Future deleteCollection() async { await _http.delete( Uri.parse('$_baseUrl/collections/$collectionName'), headers: _headers, ); } /// Delete a client by its string ID. Future 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 _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 _toVector(Client client) { final v = List.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 point) { final p = point['payload'] as Map; 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?) ?? '', ); } }