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
@@ -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([]);