feat: mostrar dirección y distancia en cards de profesionales

- Agrega getters patientLat/patientLng al ProfessionalsProvider
- Calcula distancia Haversine entre la ubicación del paciente y el consultorio
  del profesional, mostrándola en azul (ej: "1.3 km" o "800 m") en la card
- Muestra la dirección del consultorio (icono de tienda) en ambos layouts:
  horizontal (compact) y vertical (grid)
- Distancia solo visible cuando el paciente tiene coordenadas registradas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-22 08:32:49 -05:00
co-authored by Claude Sonnet 4.6
parent 5fdc87dad7
commit 387b647abc
2 changed files with 92 additions and 0 deletions
@@ -31,6 +31,9 @@ class ProfessionalsProvider extends ChangeNotifier {
}
}
double? get patientLat => _locationLat;
double? get patientLng => _locationLng;
void setLocationContext({required String? city, required double lat, required double lng}) {
if (city != null && city.isNotEmpty) _locationCity = city;
_locationLat = lat;
+89
View File
@@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:prosapp_web_app/models/location_preferences.dart';
import 'package:prosapp_web_app/models/usuario_profesional.dart';
import 'package:prosapp_web_app/providers/professionals_provider.dart';
import 'package:prosapp_web_app/providers/theme_provider.dart';
@@ -127,6 +129,8 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
cardBg: cardBg,
border: border,
compact: cols == 1,
patientLat: provider.patientLat,
patientLng: provider.patientLng,
onTap: () async {
final result = await NavigationService
.navigateToFuture(
@@ -152,12 +156,31 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
// ── Card ─────────────────────────────────────────────────────────────────────
double? _haversineKm(double? lat1, double? lng1, double? lat2, double? lng2) {
if (lat1 == null || lng1 == null || lat2 == null || lng2 == null) return null;
if (lat2 == 0.0 && lng2 == 0.0) return null;
const r = 6371.0;
final dLat = (lat2 - lat1) * math.pi / 180;
final dLng = (lng2 - lng1) * math.pi / 180;
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
math.cos(lat1 * math.pi / 180) * math.cos(lat2 * math.pi / 180) *
math.sin(dLng / 2) * math.sin(dLng / 2);
return r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
}
String _formatDistance(double km) {
if (km < 1) return '${(km * 1000).round()} m';
return '${km.toStringAsFixed(1)} km';
}
class _ProfCard extends StatelessWidget {
final UsuarioProfesional data;
final bool isDark;
final Color cardBg;
final Color border;
final bool compact;
final double? patientLat;
final double? patientLng;
final VoidCallback onTap;
const _ProfCard({
@@ -167,6 +190,8 @@ class _ProfCard extends StatelessWidget {
required this.border,
required this.compact,
required this.onTap,
this.patientLat,
this.patientLng,
});
@override
@@ -179,6 +204,12 @@ class _ProfCard extends StatelessWidget {
final score = data.averageScore;
final pm = data.professionalInfo.paymentMethods;
final profLat = data.professionalInfo.latitude;
final profLng = data.professionalInfo.longitude;
final distKm = _haversineKm(patientLat, patientLng, profLat, profLng);
final hasOffice = data.professionalInfo.locationPreferences != LocationPreferences.delivery;
final address = data.professionalInfo.address;
if (compact) {
// Fila horizontal en pantallas pequeñas
return Material(
@@ -227,7 +258,27 @@ class _ProfCard extends StatelessWidget {
style: TextStyle(
fontSize: 11, color: textSecondary)),
]),
if (distKm != null) ...[
const SizedBox(width: 8),
Icon(Icons.near_me_outlined, size: 11, color: const Color(0xFF42A4EF)),
const SizedBox(width: 2),
Text(_formatDistance(distKm),
style: const TextStyle(fontSize: 11, color: Color(0xFF42A4EF), fontWeight: FontWeight.w600)),
],
]),
if (hasOffice && address.isNotEmpty) ...[
const SizedBox(height: 3),
Row(children: [
Icon(Icons.store_outlined, size: 11, color: textSecondary),
const SizedBox(width: 2),
Flexible(
child: Text(address,
style: TextStyle(fontSize: 11, color: textSecondary),
maxLines: 1,
overflow: TextOverflow.ellipsis),
),
]),
],
],
),
),
@@ -378,6 +429,44 @@ class _ProfCard extends StatelessWidget {
),
],
// Distancia al consultorio
if (distKm != null) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.near_me_outlined, size: 11, color: Color(0xFF42A4EF)),
const SizedBox(width: 3),
Text(_formatDistance(distKm),
style: const TextStyle(
fontSize: 11,
color: Color(0xFF42A4EF),
fontWeight: FontWeight.w600)),
],
),
],
// Dirección del consultorio
if (hasOffice && address.isNotEmpty) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.store_outlined, size: 11, color: textSecondary),
const SizedBox(width: 3),
Flexible(
child: Text(
address,
style: TextStyle(fontSize: 11, color: textSecondary),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
const Spacer(),
// Métodos de pago