From ce11aa813ee030caa031339e5068dd512fdeef65 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:09:42 -0500 Subject: [PATCH] fix: align route DTOs and field names with NestJS backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - event_model: snake_case fields, status translation map (es→en), public-calendar route - chat_model: both roles use GET /chat/my (JWT identifies user) - score: correct POST /comments DTO (destination_id, service_id, content, score, is_from_user) - horario: PATCH /professionals/me/schedules with array format instead of /users/me Co-Authored-By: Claude Sonnet 4.6 --- lib/src/models/chat_model.dart | 28 ++-- lib/src/models/event_model.dart | 165 ++++++++++++++-------- lib/src/presentation/screens/horario.dart | 28 +++- lib/src/presentation/screens/score.dart | 17 +-- 4 files changed, 150 insertions(+), 88 deletions(-) diff --git a/lib/src/models/chat_model.dart b/lib/src/models/chat_model.dart index 6712228..b121061 100644 --- a/lib/src/models/chat_model.dart +++ b/lib/src/models/chat_model.dart @@ -38,28 +38,22 @@ class ChatModel { ); } - static Future> getChatsByProId(String userId) async { - try { - final List data = - await ApiService.instance.get('/chat?professionalId=$userId'); - return data - .map((e) => ChatModel.fromJson(e as Map)) - .toList(); - } catch (e) { - print('error getChatsByProId $e'); - return []; - } - } + // Both roles use GET /chat/my — JWT identifies the current user + static Future> getChatsByProId(String userId) => + _getMyChats(); - static Future> getChatsByUserId(String userId) async { + static Future> getChatsByUserId(String userId) => + _getMyChats(); + + static Future> _getMyChats() async { try { - final List data = - await ApiService.instance.get('/chat?userId=$userId'); - return data + final data = await ApiService.instance.get('/chat/my'); + final List raw = data is List ? data : (data['data'] ?? []); + return raw .map((e) => ChatModel.fromJson(e as Map)) .toList(); } catch (e) { - print('error getChatsByUserId $e'); + print('error getMyChats $e'); return []; } } diff --git a/lib/src/models/event_model.dart b/lib/src/models/event_model.dart index e665ba9..ca0b1d2 100644 --- a/lib/src/models/event_model.dart +++ b/lib/src/models/event_model.dart @@ -4,6 +4,16 @@ import 'package:prosappco/src/services/api_service.dart'; final uid = AuthenticationRepository.instance.getCurrentUserUid(); +// Backend status strings +const _statusMap = { + 'pendiente': 'pending', + 'aprobado': 'accepted', + 'negado': 'denied', + 'activo': 'active', + 'cancelado': 'cancelled', + 'completado': 'completed', +}; + class EventoService { Future createEvent( String title, @@ -22,22 +32,32 @@ class EventoService { bool userScored, ) async { try { - final Map result = await ApiService.instance.post('/services', { - 'user_id': uid, - 'title': title, - 'description': description, - 'day': day, - 'range1Hour1': range1Hour1, - 'range1Hour2': range1Hour2, + String fmt(String t) { + // Normalize "8:30 AM" / "08:30" → "HH:MM" + try { + final parts = t.replaceAll(RegExp(r'[APM ]'), '').split(':'); + final h = int.parse(parts[0]).toString().padLeft(2, '0'); + final m = (parts.length > 1 ? int.parse(parts[1]) : 0) + .toString() + .padLeft(2, '0'); + return '$h:$m'; + } catch (_) { + return '00:00'; + } + } + + final Map result = + await ApiService.instance.post('/services', { 'professional_id': professionalId, - 'ubicacion': ubicacion, + 'day': day, + 'description': description, + 'rate': tarifa ?? 0, + 'range1_hour1': fmt(range1Hour1), + 'range1_hour2': fmt(range1Hour2), 'address': address, 'latitude': latitude, 'longitude': longitude, - 'status': status, - 'tarifa': tarifa ?? 0, - 'professional_scored': professionalScored, - 'user_scored': userScored, + 'location_preference': ubicacion == 'domicilio' ? 'delivery' : 'office', }); return result['id']?.toString(); } catch (e) { @@ -47,54 +67,53 @@ class EventoService { } } +// Calendar for logged-in professional — uses JWT, ignores day filter on backend Future> getByProId(String day) async { try { - final List data = await ApiService.instance - .get('/services?day=$day&status=aprobado'); - List eventos = []; - for (var element in data) { - final event = Event.fromJson(element as Map); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - eventos.add(event); - } - return eventos; + final data = await ApiService.instance.get('/services/professional/calendar'); + final List raw = data is List ? data : (data['data'] ?? []); + return raw + .map((e) => Event.fromJson(e as Map)) + .where((e) => e.day.startsWith(day.substring(0, 10))) + .toList(); } catch (e) { print('Error getByProId $e'); return []; } } +// Active/accepted services for logged-in professional Future> getByProIdAll(String state1, String state2) async { try { - final List data = await ApiService.instance - .get('/services?professionalId=$uid&status=$state1,$state2'); - List eventos = []; - for (var element in data) { - final event = Event.fromJson(element as Map); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - eventos.add(event); - } - return eventos; + final data = await ApiService.instance.get('/services/professional'); + final List raw = data is Map ? (data['data'] ?? []) : (data as List); + final allowed = { + _statusMap[state1] ?? state1, + _statusMap[state2] ?? state2, + }; + return raw + .map((e) => Event.fromJson(e as Map)) + .where((e) => allowed.contains(e.status)) + .toList(); } catch (e) { print('Error getByProIdAll $e'); return []; } } +// Active/accepted services for logged-in user Future> getByUserIdAll(String state1, String state2) async { try { - final List data = await ApiService.instance - .get('/services?userId=$uid&status=$state1,$state2'); - List eventos = []; - for (var element in data) { - final event = Event.fromJson(element as Map); - event.scoresModel = - await ScoresModel.scoreTo(event.userId, false, false); - eventos.add(event); - } - return eventos; + final data = await ApiService.instance.get('/services/me'); + final List raw = data is Map ? (data['data'] ?? []) : (data as List); + final allowed = { + _statusMap[state1] ?? state1, + _statusMap[state2] ?? state2, + }; + return raw + .map((e) => Event.fromJson(e as Map)) + .where((e) => allowed.contains(e.status)) + .toList(); } catch (e) { print('Error getByUserIdAll $e'); return []; @@ -134,7 +153,7 @@ class Event { this.address, this.longitud, this.latitud, - this.status = 'pendiente', + this.status = 'pending', this.timeStamp, this.tarifa, this.professionalScored = false, @@ -143,25 +162,44 @@ class Event { factory Event.fromJson(Map json) { DateTime? ts; - final raw = json['Timestamp'] ?? json['created_at']; + final raw = json['created_at'] ?? json['Timestamp']; if (raw is String) ts = DateTime.tryParse(raw); + // Backend sends ISO date for range times; extract HH:MM + String parseTime(dynamic v) { + if (v == null) return ''; + final s = v.toString(); + if (s.contains('T')) { + final dt = DateTime.tryParse(s); + if (dt != null) { + return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + } + } + return s; + } + + // Nested professional user_id takes priority as professionalId + final nestedPro = json['professionals']; + final professionalId = (nestedPro is Map) + ? (nestedPro['user_id']?.toString() ?? json['professional_id']?.toString() ?? '') + : json['professional_id']?.toString() ?? ''; + return Event( id: json['id']?.toString() ?? '', - title: json['title'] ?? '', + title: json['description'] ?? json['title'] ?? '', description: json['description'], day: json['day'] ?? '', - range1Hour1: json['range1Hour1'] ?? '', - range1Hour2: json['range1Hour2'], + range1Hour1: parseTime(json['range1_hour1'] ?? json['range1Hour1']), + range1Hour2: parseTime(json['range1_hour2'] ?? json['range1Hour2']), userId: json['user_id'] ?? '', - professionalId: json['professional_id'] ?? '', - ubicacion: json['ubicacion'] ?? '', + professionalId: professionalId, + ubicacion: json['location_preference'] ?? json['ubicacion'] ?? '', address: json['address'] ?? '', longitud: (json['longitude'] ?? 0).toDouble(), latitud: (json['latitude'] ?? 0).toDouble(), - status: json['status'] ?? 'pendiente', + status: json['status'] ?? 'pending', timeStamp: ts, - tarifa: json['tarifa'] ?? json['tarifas'] ?? 0, + tarifa: (json['rate'] as num?)?.toInt() ?? json['tarifa'] ?? 0, professionalScored: json['professional_scored'] ?? false, userScored: json['user_scored'] ?? false, ); @@ -179,11 +217,13 @@ class Event { } } + // All services for a given professional (by user_id) — uses public calendar static Future> getEventsAllById(String proId) async { try { - final List data = - await ApiService.instance.get('/services?professionalId=$proId'); - return data + final data = + await ApiService.instance.get('/services/public-calendar/$proId'); + final List raw = (data is Map ? data['services'] : data) ?? []; + return raw .map((e) => Event.fromJson(e as Map)) .toList(); } catch (e) { @@ -194,10 +234,12 @@ class Event { static Future> getEventsAllByIdAndStatus(String proId) async { try { - final List data = await ApiService.instance - .get('/services?professionalId=$proId&status=aprobado,pendiente'); - return data + final data = + await ApiService.instance.get('/services/public-calendar/$proId'); + final List raw = (data is Map ? data['services'] : data) ?? []; + return raw .map((e) => Event.fromJson(e as Map)) + .where((e) => e.status == 'accepted' || e.status == 'pending') .toList(); } catch (e) { print('Error getEventsAllByIdAndStatus $e'); @@ -208,10 +250,13 @@ class Event { static Future> getEventsAllByIdStatus( String proId, String state) async { try { - final List data = await ApiService.instance - .get('/services?professionalId=$proId&status=$state'); - return data + final backendStatus = _statusMap[state] ?? state; + final data = + await ApiService.instance.get('/services/professional/calendar'); + final List raw = data is List ? data : []; + return raw .map((e) => Event.fromJson(e as Map)) + .where((e) => e.status == backendStatus) .toList(); } catch (e) { print('Error getEventsAllByIdStatus $e'); diff --git a/lib/src/presentation/screens/horario.dart b/lib/src/presentation/screens/horario.dart index 5867c79..39d3582 100644 --- a/lib/src/presentation/screens/horario.dart +++ b/lib/src/presentation/screens/horario.dart @@ -44,12 +44,38 @@ class HorarioScreen extends StatelessWidget { }; }); - await ApiService.instance.patch('/users/me', {'horario': horariosMap}); + // Convert map keyed by day number to array format expected by backend + final List> schedulesList = []; + horariosMap.forEach((key, value) { + schedulesList.add({ + 'day_of_week': int.tryParse(key) ?? 1, + 'enabled': value['habilitado'] ?? false, + 'continuous_day': value['jornadaContinua'] ?? false, + 'range1_hour1': _fmtTime(value['range1Hour1']), + 'range1_hour2': _fmtTime(value['range1Hour2']), + 'range2_hour1': _fmtTime(value['range2Hour1']), + 'range2_hour2': _fmtTime(value['range2Hour2']), + }); + }); + await ApiService.instance + .patch('/professionals/me/schedules', {'schedules': schedulesList}); } catch (e) { print('Error al actualizar el horario: $e'); } } + // Convert "8:30 AM" / "HH:MM" strings to "HH:MM" for the backend + String? _fmtTime(String? t) { + if (t == null) return null; + try { + final clean = t.replaceAll(RegExp(r'\s?[APM]+', caseSensitive: false), '').trim(); + final parts = clean.split(':'); + return '${int.parse(parts[0]).toString().padLeft(2, '0')}:${(parts.length > 1 ? int.parse(parts[1]) : 0).toString().padLeft(2, '0')}'; + } catch (_) { + return null; + } + } + String? formatTimeOfDay(TimeOfDay? time) { if (time != null) { final now = DateTime.now(); diff --git a/lib/src/presentation/screens/score.dart b/lib/src/presentation/screens/score.dart index 39dd53a..4ecaf52 100644 --- a/lib/src/presentation/screens/score.dart +++ b/lib/src/presentation/screens/score.dart @@ -51,18 +51,15 @@ class ScoreScreenState extends State { Future _submit() async { try { final bool isPro = widget.pro; - // Update the service scored flag - await ApiService.instance.patch('/services/${widget.evento.id}', { - if (isPro) 'professional_scored': true, - if (!isPro) 'user_scored': true, - }); - // Post the score/comment + // Backend sets professional_scored/user_scored automatically on POST /comments await ApiService.instance.post('/comments', { - 'comment': commentController.text, - 'from_user': isPro ? widget.evento.professionalId : widget.evento.userId, - 'is_from_professional': !isPro, + 'service_id': widget.evento.id, + 'destination_id': isPro + ? widget.evento.userId + : widget.evento.professionalId, + 'content': commentController.text, 'score': _rating, - 'to_user': isPro ? widget.evento.userId : widget.evento.professionalId, + 'is_from_user': !isPro, }); if (mounted) Navigator.pop(context); } catch (e) {