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:
co-authored by
Claude Sonnet 4.6
parent
5fdc87dad7
commit
387b647abc
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user