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:
co-authored by
Claude Sonnet 4.6
parent
af91e0ba04
commit
7fb0615169
+3
-2
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user