feat: migrate prosappco from Firebase to NestJS REST API (Fase 2)
- Replace all Firebase* repositories with Api* repositories using HTTP + SharedPreferences JWT - Remove Firebase.initializeApp() and firebase_messaging background handler from main.dart - Update DI (app_di.dart) to inject Api* repositories instead of Firebase* ones - Replace all Timestamp/cloud_firestore usage with ISO 8601 String dates - Stub PhoneVerificationService (Firebase phone OTP → backend OTP when implemented) - Add ApiService singleton with JWT management in lib/services/ - Legacy firebase_*_repository.dart files preserved for Fase 4 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
726cf12fd2
commit
733384091c
@@ -3,3 +3,4 @@ library service_repository;
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/models/models.dart';
|
||||
export 'src/repositories/firebase_service_repository.dart';
|
||||
export 'src/repositories/api_service_repository.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:service_repository/service_repository.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -14,7 +13,7 @@ class ServiceEntity extends Equatable {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final String day;
|
||||
final Timestamp createdAt;
|
||||
final String createdAt;
|
||||
final String description;
|
||||
final TimeOfDay range1Hour1;
|
||||
final TimeOfDay range1Hour2;
|
||||
@@ -54,7 +53,7 @@ class ServiceEntity extends Equatable {
|
||||
latitude: doc['latitude'] as double,
|
||||
longitude: doc['longitude'] as double,
|
||||
day: doc['day'] as String,
|
||||
createdAt: doc['created_at'] as Timestamp,
|
||||
createdAt: doc['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
description: doc['description'] as String,
|
||||
range1Hour1: parseTimeOfDay(doc['range1_hour1'] as String),
|
||||
range1Hour2: parseTimeOfDay(doc['range1_hour2'] as String),
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:service_repository/service_repository.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
/// API-backed replacement for FirebaseServiceRepository.
|
||||
/// Mirrors the same public API so existing blocs work without changes.
|
||||
class ApiServiceRepository {
|
||||
String? _token;
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _token = prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
Future<dynamic> _get(String path, {Map<String, String>? query}) async {
|
||||
final uri = Uri.parse('$_base$path').replace(queryParameters: query);
|
||||
final res = await http.get(uri, headers: await _headers());
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _post(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.post(
|
||||
Uri.parse('$_base$path'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _patch(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.patch(
|
||||
Uri.parse('$_base$path'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
ServiceEntity _fromApi(Map<String, dynamic> json) {
|
||||
String range1Hour1 = json['range1_hour1']?.toString() ?? '0:0';
|
||||
String range1Hour2 = json['range1_hour2']?.toString() ?? '0:0';
|
||||
|
||||
TimeOfDay parseTime(String s) {
|
||||
final parts = s.split(':');
|
||||
return TimeOfDay(
|
||||
hour: int.tryParse(parts[0]) ?? 0,
|
||||
minute: int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
return ServiceEntity(
|
||||
id: json['id']?.toString(),
|
||||
professionalId: json['professional_id']?.toString() ?? '',
|
||||
professionalScored: json['professional_scored'] as bool? ?? false,
|
||||
userId: json['user_id']?.toString() ?? '',
|
||||
userScored: json['user_scored'] as bool? ?? false,
|
||||
address: json['address']?.toString() ?? '',
|
||||
aditionalAddress: json['aditional_address']?.toString() ?? '',
|
||||
latitude: double.tryParse(json['latitude']?.toString() ?? '0') ?? 0.0,
|
||||
longitude: double.tryParse(json['longitude']?.toString() ?? '0') ?? 0.0,
|
||||
day: json['day']?.toString() ?? '',
|
||||
createdAt: json['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
description: json['description']?.toString() ?? '',
|
||||
range1Hour1: parseTime(range1Hour1),
|
||||
range1Hour2: parseTime(range1Hour2),
|
||||
rate: json['rate']?.toString() ?? '',
|
||||
status: intToEnumService((json['status'] as num?)?.toInt() ?? 0),
|
||||
location: intToEnum((json['location'] as num?)?.toInt() ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> createService(ServiceEntity entity) async {
|
||||
final data = await _post('/services', entity.toDocument());
|
||||
return data['id']?.toString() ?? '';
|
||||
}
|
||||
|
||||
Future<void> updateServiceStatus(String serviceId, ServiceStatus newStatus) async {
|
||||
await _patch('/services/$serviceId', {'status': enumToIntService(newStatus)});
|
||||
}
|
||||
|
||||
Stream<ServiceEntity> getService(String serviceId) {
|
||||
final controller = StreamController<ServiceEntity>();
|
||||
_get('/services', query: {'id': serviceId}).then((data) {
|
||||
if (data is List && data.isNotEmpty) {
|
||||
controller.add(_fromApi(data.first as Map<String, dynamic>));
|
||||
} else if (data is Map) {
|
||||
controller.add(_fromApi(data as Map<String, dynamic>));
|
||||
}
|
||||
controller.close();
|
||||
}).catchError((e) {
|
||||
controller.addError(e);
|
||||
controller.close();
|
||||
});
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesForUser(String userId) {
|
||||
return _streamList('/services', query: {'userId': userId}, statusFilter: [0, 1, 3]);
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesForProfessional(String professionalId) {
|
||||
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [1, 3]);
|
||||
}
|
||||
|
||||
Future<List<ServiceEntity>> getServicesForProfessionalforCalendar(String professionalId) async {
|
||||
try {
|
||||
final data = await _get('/services', query: {'professionalId': professionalId});
|
||||
if (data is! List) return [];
|
||||
return data
|
||||
.map((e) => _fromApi(e as Map<String, dynamic>))
|
||||
.where((s) => [0, 1, 2, 3, 6].contains(s.status.index))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesHistoryForUser(String userId) {
|
||||
return _streamList('/services', query: {'userId': userId}, statusFilter: [2, 4, 5]);
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesHistoryForProfessional(String professionalId) {
|
||||
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [2, 4, 5]);
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getPendingServicesForProfessional(String professionalId) {
|
||||
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [0]);
|
||||
}
|
||||
|
||||
Future<int> countPendingServicesForProfessional(String professionalId) async {
|
||||
try {
|
||||
final data = await _get('/services', query: {'professionalId': professionalId});
|
||||
if (data is! List) return 0;
|
||||
return data
|
||||
.map((e) => _fromApi(e as Map<String, dynamic>))
|
||||
.where((s) => s.status == ServiceStatus.pending)
|
||||
.length;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setProfessionalScored(String serviceId) async {
|
||||
await _patch('/services/$serviceId', {'professional_scored': true});
|
||||
}
|
||||
|
||||
Future<void> setUserScored(String serviceId) async {
|
||||
await _patch('/services/$serviceId', {'user_scored': true});
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> _streamList(
|
||||
String path, {
|
||||
Map<String, String>? query,
|
||||
List<int> statusFilter = const [],
|
||||
}) {
|
||||
final controller = StreamController<List<ServiceEntity>>();
|
||||
_get(path, query: query).then((data) {
|
||||
if (data is! List) {
|
||||
controller.add([]);
|
||||
} else {
|
||||
var list = data.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
|
||||
if (statusFilter.isNotEmpty) {
|
||||
list = list.where((s) => statusFilter.contains(s.status.index)).toList();
|
||||
}
|
||||
controller.add(list);
|
||||
}
|
||||
controller.close();
|
||||
}).catchError((e) {
|
||||
controller.add([]);
|
||||
controller.close();
|
||||
});
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user