The findById endpoint only selected {name, picture} from professionals.users,
so when a patient viewed a service detail the professional's id and phone
were missing. This caused:
- The call button to be hidden (phone was null)
- The chat route to be built with an empty professional ID ('/chat/')
making the Firestore stream unable to find the chat → spinner forever
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend create() now looks up the professional's fcm_token and sends a
push notification after the service is persisted. This replaces the
unreliable client-side notification in the Flutter app (which couldn't
use the server FCM key).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- findAllActive now accepts city, lat, lng params
- City filter: case-insensitive contains on users.city
- Proximity sort: Haversine within city before services/score ranking
- Controller exposes ?city=&lat=&lng= query params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /professionals now accepts ?search= instead of ?city=/page/limit
- Filters each word across profession, user name, and user city (AND of OR)
- Counts completed services per professional via Prisma _count filter
- Sorts: most completed services first, then highest average_score
- Always returns top 7 results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
findByProfessional had no status filter, so blocked slots (self_booked)
and historical cancelled/denied services appeared in the active view.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add gender: true to users select in findById() (both lookup paths)
and render it in the personal data card in the admin frontend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- GET /services/admin/professional/:id (JWT-guarded) returns paginated
services for any professional with embedded user (client) info
Admin professional detail page:
- Replace broken all-services-then-filter with new endpoint
- Add month calendar with colored dots per day (click to filter table)
- Services table shows client name/phone, time, colored status badges
- Fix DAY_NAMES array (was 0=Domingo, now 0=Lunes per DB convention)
- Schedule row shows both ranges + continuous_day flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BlockSlotDto: validates day (ISO date) and range1_hour1 (HH:MM)
- ServicesService.blockSlot(): finds professional by userId, checks for
conflicts (any non-cancelled/denied service at that slot), then creates
a service with status=self_booked using the professional's own user_id
- ServicesController: POST /services/block (JWT-guarded) wired to blockSlot
Existing PATCH /services/:id/status → cancelled handles unblocking since
VALID_TRANSITIONS already allows self_booked → cancelled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Before creating a service, now:
1. Checks if the requested slot (day + range1_hour1) is already occupied by
a non-cancelled/denied service for that professional — rejects with 400
2. Validates the requested day has an enabled schedule for the professional;
rejects if the professional does not work that weekday
3. Validates the requested hour falls within the configured hour ranges
(continuous day: range1_hour1→range2_hour2; split day: morning + afternoon
blocks) — rejects if outside configured hours
day_of_week conversion: JS Date.getDay() (0=Sun) → DB convention (0=Mon)
via (getDay()+6)%7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DTO: add payment_methods field so whitelist pipe no longer strips it
- Service upsert(): extract payment_methods and write as Prisma nested upsert instead of passing flat object
- Admin detail page: fix image regex to handle Firebase URLs with query params; fix payment_methods display for 1:1 Prisma relation (object, not array)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both flags are read by prosappweb from /settings to enable/disable
delivery service and rate display. Saves via the existing PATCH /settings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Checks is_active on email login, phone OTP login, and /auth/me so
existing tokens also stop working immediately after a user is blocked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 0004: adds is_active column to users table
- Backend: DELETE /users/:id endpoint + is_active field in UpdateUserDto
- Admin UI: block/unblock toggle and delete with confirmation on user detail
- Users list: shows "Bloqueado" badge for inactive users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Baselining 0001 caused rethus_code/rethus_validated to never be added
automatically. The SQL already uses IF NOT EXISTS so it is safe to deploy
on every fresh container even if the columns already exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- entrypoint.sh: resolve --applied 0000 and 0001 before deploying,
so prisma skips them and only runs 0002 (message_logs) and 0003 (suggestions)
- migration 0003: CREATE TABLE IF NOT EXISTS suggestions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrite 0000_init with IF NOT EXISTS on all tables, indexes and enums;
wrap FK constraints in DO/EXCEPTION blocks so re-runs never fail
- Add missing suggestions table to init migration
- Add migration 0002 to create message_logs table (IF NOT EXISTS)
- Add entrypoint.sh: runs prisma migrate deploy before starting the server
- Update Dockerfile to copy entrypoint.sh and use it as CMD
- Add prisma:migrate script to package.json for manual runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same events that trigger email (approve, deny, deactivate, setPending, upsert)
now also call tryPush() to send a silent FCM push to the professional's device.
NotificationsModule added to ProfessionalsModule imports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /settings/test-email: send test email to diagnose SMTP config
- Admin settings: test email form + last 20 email logs with status/error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mail/mail.service.ts: MailService with HTML templates (welcome, submitted,
approved, rejected, pending, deactivated). Silent if SMTP not configured.
- mail/mail.module.ts: global module so all services can inject MailService
- auth.service.ts: send welcome email on register()
- professionals.service.ts: notify admin on new submission; notify professional
on approve/deny/deactivate/setPending
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These fields were missing from the DTO so NestJS/class-validator was stripping
them from PATCH /professionals/me requests before they reached Prisma.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous code deleted the professional record; if pro_state=3 but no record
exists, the 404 blocked the retry. Now we unconditionally reset pro_state=0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- resetRejected: only set pro_state=0, no DELETE (avoids FK constraint errors
from services/reputations with onDelete: NoAction)
- upsert: detect re-submission after rejection (pro_state=0) and set pro_state=1
so the request appears in admin pending list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: DELETE /professionals/me resets rejected state so user can retry
- Backend: findRejected/findPending now filter by pro_state
- Admin settings: rejection_wait_days field (default 7)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deny() now marks is_active:false instead of deleting the record
- New GET /professionals/rejected endpoint (pro_state:3)
- findPendingApprovals now filters only pro_state:1
- Admin list: third tab 'No aprobados' with approve/review actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files uploaded by users were lost on every container rebuild.
Named volume uploads_data mounts to /app/uploads and is managed by Coolify.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, files were saved with http://localhost:3000/uploads/... URLs
which are unreachable from the browser. Now uses the public backend domain.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: deactivate and set-pending endpoints for status control
- Admin detail: Aprobar/Rechazar/Desactivar/En revisión buttons based on current state
- Admin detail: Documentos section showing ID photo and certificate
- Admin detail: deny now confirms before deleting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: include email and phone in users select for professionals
- Admin detail page: RETHUS section shows cedula+code with copy buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Detail page: use Number() before toFixed() since Prisma returns Decimal objects
- Service upsert: create professionals with is_active:false so they appear as pending
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Flutter reads 'professional_state' but Prisma returns 'pro_state'.
Add professional_state alias in me() response so proState is read correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useParams() can return null during SSR — destructuring null throws TypeError.
Use optional chaining params?.id to prevent the crash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Active professionals now have a link to the detail page where
they can be edited, RETHUS validated, etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PATCH /professionals/me now works for first-time submissions.
Creates the record with pro_state=1 if user has no professional entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- schema.prisma: modelo message_logs (channel, recipient, body, status, error)
- email-otp.service.ts: lee config SMTP desde DB (settings.smtp_config), registra log en message_logs
- sms.service.ts: registra log en message_logs tras cada envío (éxito y error)
- auth.module.ts: agrega PrismaModule, JWT expira en 90d
- settings.controller.ts: GET/PATCH /settings/smtp + GET /settings/message-logs
- settings.service.ts: método getMessageLogs con paginación y filtro por canal
- settings.module.ts: importa AuthModule + EmailOtpService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- schema.prisma: url = env("DATABASE_URL") era requerido por prisma migrate deploy
- Dockerfile: cambiar && por ; para que el servidor arranque aunque migrate falle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Prisma schema: rethus_code y rethus_validated en tabla professionals
- Migración 0001: ALTER TABLE agrega ambas columnas
- DTO: rethus_code y rethus_validated en CreateProfessionalDto y UpdateProfessionalDto
- Service: nuevo método validateRethus, rethus_code incluido en requestProfessional
- Controller: endpoint POST /professionals/:id/validate-rethus (JWT protegido)
- Admin listado pendientes: columna RETHUS con ícono verde/amarillo y enlace al detalle
- Admin detalle profesional: card de validación RETHUS con botón consultar Minsalud y marcar como validado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /settings retorna {} en lugar de undefined cuando no hay configuracion
(evita error jsonDecode en Flutter que dejaba soporte en gris)
- Autocomplete: agrega components=country:co, radius 20km, remove types=address
para mostrar mas resultados cercanos al usuario
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- support_title, support_description, support_days, support_hours
(los que lee la app Flutter para la pantalla de soporte)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Proxy a Google Places Autocomplete API:
- Lee la API key de /settings/maps-key del backend
- Acepta params: input, location
- Devuelve { results: [{ formatted_address, place_id }] }
- Headers CORS para permitir acceso desde app.prosapp.co
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: agrega PATCH /users/:id y PATCH /professionals/:id (protegidos).
Admin: pagina de usuario con edicion de nombre/telefono/ciudad/genero;
pagina de profesional con dos secciones editables independientes
(datos personales y perfil profesional).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: GET /settings/maps-key (public) + PATCH /settings/maps (protected)
- Admin: sección para guardar/ver estado de la Maps API key
- Flutter web carga la key dinámicamente desde el backend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- CRUD completo para countries, regions y cities
- GET /settings/policy/:key — página HTML pública para políticas
- GET/PATCH /settings/policies/:key — admin endpoints protegidos
Admin:
- /locations: árbol interactivo País → Región → Ciudad con add/edit/delete
- /settings: editor de Política de Privacidad y Términos con enlace público copiable
- Sidebar: Ciudades → Ubicaciones
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Flutter client calls jsonDecode on response body — empty void response
throws FormatException even when SMS was sent successfully.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Login page: split layout with gradient panel (desktop) + form,
ProsApp logo mark, feature list, consistent blue brand colors
- Sidebar: replace plain text with logo mark + brand name
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Add SmsService + SmsModule: send OTP via u-site.app provider, 5-min TTL
- Auth endpoints: POST /auth/send-otp, POST /auth/phone (login by phone+code),
POST /auth/verify-phone (link), PATCH /auth/change-password
- is_phone_verified included in JWT token response
- GET /comments (admin, JWT-protected) with author/destination names
Admin:
- Users list: link to detail page per row
- User detail: inline edit form (name, city, phone) with PATCH /users/:id
- Services list: link to detail page per row
- Service detail: status change dropdown (PATCH /services/:id/status)
- New Comments page: summary stats + full table with star ratings
- New SMS settings page: configure API key + send test SMS
- Sidebar: added Comments and SMS entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- services.module: import NotificationsModule
- services.service: inject NotificationsService, notify the other party
(user or professional) when service status changes to accepted/denied/
cancelled/active/completed
- ARQUITECTURA.md: updated to reflect current migration state (all migrated)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tailwind CSS 4 requires explicit content paths configuration. Without this,
the @tailwindcss/postcss plugin cannot find files to process, preventing
styles from being generated for the admin interface.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>