fix: align Flutter API repositories with backend routes (Fase 3 prep)

- api_service_repository: use correct endpoints (/services/me, /services/professional,
  /professional/requests, /professional/history, /professional/calendar)
  instead of broken /services?userId= query params
- api_service_repository: fix status string↔enum, createService DTO,
  paginated {data,meta} response, stub setProfessionalScored/setUserScored,
  extract professionalId from nested professionals.user_id
- api_score_repository: fix /comments/user/:id and /comments/professional/:id
- api_professional_repository + api_user_repository: handle paginated response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-17 18:29:47 -05:00
co-authored by Claude Sonnet 4.6
parent af91e0ba04
commit 7fb0615169
4 changed files with 117 additions and 115 deletions
@@ -198,8 +198,9 @@ class ApiProfessionalRepository {
Future<List<ProfessionalEntity>> getProfessionalInfo() async {
try {
final data = await _get('/professionals') as List;
return data.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
final body = await _get('/professionals');
final List raw = body is Map ? (body['data'] as List? ?? []) : (body as List? ?? []);
return raw.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
} catch (_) {
return [];
}
@@ -7,25 +7,18 @@ import 'package:score_repository/src/entities/comment_entity.dart';
const _base = 'https://backend.prosapp.co/api/v1';
/// API-backed replacement for FirebaseScoreRepository.
/// Mirrors the same public API so existing blocs work without changes.
class ApiScoreRepository {
ReputationEntity? _reputation;
final StreamController<ReputationEntity> _reputationController =
StreamController<ReputationEntity>.broadcast();
final _reputationController = StreamController<ReputationEntity>.broadcast();
String? _token;
ApiScoreRepository() {
_reputationController.add(_emptyReputation());
_reputationController.add(_empty());
}
ReputationEntity _emptyReputation() => const ReputationEntity(
total: 0,
average: 0,
totalPro: 0,
averagePro: 0,
);
ReputationEntity _empty() =>
const ReputationEntity(total: 0, average: 0, totalPro: 0, averagePro: 0);
Future<String?> _getToken() async {
if (_token != null) return _token;
@@ -43,7 +36,7 @@ class ApiScoreRepository {
Stream<ReputationEntity> streamReputation() => _reputationController.stream;
ReputationEntity getReputation() => _reputation ?? _emptyReputation();
ReputationEntity getReputation() => _reputation ?? _empty();
Future<ReputationEntity> getReputationByUserId(String userId) async {
try {
@@ -57,51 +50,33 @@ class ApiScoreRepository {
_reputationController.add(rep);
return rep;
} catch (_) {
return _emptyReputation();
return _empty();
}
}
Stream<List<CommentEntity>> getScoresForUser(String userId) {
Stream<List<CommentEntity>> getScoresForUser(String userId) =>
_streamComments('/comments/user/$userId');
Stream<List<CommentEntity>> getScoresForProfessional(String userId) =>
_streamComments('/comments/professional/$userId');
Stream<List<CommentEntity>> _streamComments(String path) {
final controller = StreamController<List<CommentEntity>>();
_fetchComments(userId: userId, isFromUser: false).then((list) {
_fetchComments(path).then((list) {
controller.add(list);
controller.close();
}).catchError((e) {
}).catchError((_) {
controller.add([]);
controller.close();
});
return controller.stream;
}
Stream<List<CommentEntity>> getScoresForProfessional(String userId) {
final controller = StreamController<List<CommentEntity>>();
_fetchComments(userId: userId, isFromUser: true).then((list) {
controller.add(list);
controller.close();
}).catchError((e) {
controller.add([]);
controller.close();
});
return controller.stream;
}
Future<List<CommentEntity>> _fetchComments({
required String userId,
required bool isFromUser,
}) async {
try {
final res = await http.get(
Uri.parse('$_base/comments/reputation/$userId'),
headers: await _headers(),
);
final data = jsonDecode(res.body);
if (data is! Map) return [];
// The endpoint returns reputation summary, not individual comments
// Return empty list since we only have aggregated data
return [];
} catch (_) {
return [];
}
Future<List<CommentEntity>> _fetchComments(String path) async {
final res = await http.get(Uri.parse('$_base$path'), headers: await _headers());
final body = jsonDecode(res.body);
final List raw = body is Map ? (body['data'] as List? ?? []) : (body as List? ?? []);
return raw.map((e) => CommentEntity.fromDocument(e as Map<String, dynamic>)).toList();
}
Future<void> addComment(CommentEntity comment) async {
@@ -110,7 +85,6 @@ class ApiScoreRepository {
Uri.parse('$_base/comments'),
headers: await _headers(),
body: jsonEncode({
'author_id': comment.authorId,
'destination_id': comment.destinationId,
'service_id': comment.serviceId,
'content': comment.content,
@@ -118,10 +92,7 @@ class ApiScoreRepository {
'is_from_user': comment.isFromUser,
}),
);
// Refresh reputation after adding a comment
await getReputationByUserId(comment.destinationId);
} catch (_) {
// Stub — do not crash if comment fails
}
} catch (_) {}
}
}
@@ -7,8 +7,31 @@ 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.
// Backend uses string status; Flutter uses int-indexed enum
const _statusToString = {
0: 'pending',
1: 'accepted',
2: 'denied',
3: 'active',
4: 'cancelled',
5: 'completed',
6: 'self_booked',
};
const _stringToStatusIndex = {
'pending': 0,
'accepted': 1,
'acepted': 1,
'denied': 2,
'active': 3,
'cancelled': 4,
'canceled': 4,
'completed': 5,
'self_booked': 6,
};
const _locationToString = ['office', 'delivery'];
class ApiServiceRepository {
String? _token;
@@ -26,9 +49,8 @@ class ApiServiceRepository {
};
}
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());
Future<dynamic> _get(String path) async {
final res = await http.get(Uri.parse('$_base$path'), headers: await _headers());
return jsonDecode(res.body);
}
@@ -51,10 +73,11 @@ class ApiServiceRepository {
}
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) {
TimeOfDay parseTime(String? s) {
if (s == null) return const TimeOfDay(hour: 0, minute: 0);
// Backend may return full ISO date or just "HH:MM"
final time = s.contains('T') ? DateTime.parse(s) : null;
if (time != null) return TimeOfDay(hour: time.hour, minute: time.minute);
final parts = s.split(':');
return TimeOfDay(
hour: int.tryParse(parts[0]) ?? 0,
@@ -62,9 +85,22 @@ class ApiServiceRepository {
);
}
final statusRaw = json['status']?.toString() ?? 'pending';
final statusIndex = _stringToStatusIndex[statusRaw] ?? 0;
final locationRaw = json['location_preference']?.toString() ?? 'office';
final locationIndex = locationRaw == 'delivery' ? 1 : 0;
// Use the professional's user_id if the response includes nested professional data.
// This keeps IDs consistent with the user-centric model used by the Flutter app.
final nestedPro = json['professionals'];
final professionalId = (nestedPro is Map)
? (nestedPro['user_id']?.toString() ?? json['professional_id']?.toString() ?? '')
: json['professional_id']?.toString() ?? '';
return ServiceEntity(
id: json['id']?.toString(),
professionalId: json['professional_id']?.toString() ?? '',
professionalId: professionalId,
professionalScored: json['professional_scored'] as bool? ?? false,
userId: json['user_id']?.toString() ?? '',
userScored: json['user_scored'] as bool? ?? false,
@@ -75,31 +111,48 @@ class ApiServiceRepository {
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),
range1Hour1: parseTime(json['range1_hour1']?.toString()),
range1Hour2: parseTime(json['range1_hour2']?.toString()),
rate: json['rate']?.toString() ?? '0',
status: intToEnumService(statusIndex),
location: intToEnum(locationIndex),
);
}
List<ServiceEntity> _parseList(dynamic body) {
final List raw = body is Map ? (body['data'] as List? ?? []) : (body as List? ?? []);
return raw.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
}
Future<String> createService(ServiceEntity entity) async {
final data = await _post('/services', entity.toDocument());
String fmt(TimeOfDay t) =>
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
final data = await _post('/services', {
'professional_id': entity.professionalId,
'day': entity.day,
'description': entity.description,
'rate': double.tryParse(entity.rate) ?? 0,
'range1_hour1': fmt(entity.range1Hour1),
'range1_hour2': fmt(entity.range1Hour2),
'address': entity.address,
'latitude': entity.latitude,
'longitude': entity.longitude,
'location_preference': _locationToString[entity.location.index],
});
return data['id']?.toString() ?? '';
}
Future<void> updateServiceStatus(String serviceId, ServiceStatus newStatus) async {
await _patch('/services/$serviceId', {'status': enumToIntService(newStatus)});
await _patch('/services/$serviceId/status', {
'status': _statusToString[newStatus.index] ?? 'pending',
});
}
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>));
}
_get('/services/$serviceId').then((data) {
if (data is Map) controller.add(_fromApi(data as Map<String, dynamic>));
controller.close();
}).catchError((e) {
controller.addError(e);
@@ -109,75 +162,51 @@ class ApiServiceRepository {
}
Stream<List<ServiceEntity>> getServicesForUser(String userId) {
return _streamList('/services', query: {'userId': userId}, statusFilter: [0, 1, 3]);
return _stream('/services/me');
}
Stream<List<ServiceEntity>> getServicesForProfessional(String professionalId) {
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [1, 3]);
return _stream('/services/professional');
}
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();
return _parseList(await _get('/services/professional/calendar'));
} catch (_) {
return [];
}
}
Stream<List<ServiceEntity>> getServicesHistoryForUser(String userId) {
return _streamList('/services', query: {'userId': userId}, statusFilter: [2, 4, 5]);
return _stream('/services/me/history');
}
Stream<List<ServiceEntity>> getServicesHistoryForProfessional(String professionalId) {
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [2, 4, 5]);
return _stream('/services/professional/history');
}
Stream<List<ServiceEntity>> getPendingServicesForProfessional(String professionalId) {
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [0]);
return _stream('/services/professional/requests');
}
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;
final body = await _get('/services/professional/requests');
if (body is Map) return (body['meta']?['total'] as num?)?.toInt() ?? 0;
return (body as List).length;
} catch (_) {
return 0;
}
}
Future<void> setProfessionalScored(String serviceId) async {
await _patch('/services/$serviceId', {'professional_scored': true});
}
// Backend sets these automatically when POST /comments is called
Future<void> setProfessionalScored(String serviceId) async {}
Future<void> setUserScored(String serviceId) async {}
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 [],
}) {
Stream<List<ServiceEntity>> _stream(String path) {
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);
}
_get(path).then((data) {
controller.add(_parseList(data));
controller.close();
}).catchError((e) {
controller.add([]);
@@ -201,7 +201,8 @@ class ApiUserRepository implements UserRepository {
@override
Future<List<MyUser>> getUsersProfessionalActive() async {
try {
final data = await _get('/professionals') as List;
final body = await _get('/professionals');
final List data = body is Map ? (body['data'] as List? ?? []) : (body as List? ?? []);
final List<MyUser> result = [];
for (final p in data) {
final userId = p['user_id']?.toString();