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}) {
|
void setLocationContext({required String? city, required double lat, required double lng}) {
|
||||||
if (city != null && city.isNotEmpty) _locationCity = city;
|
if (city != null && city.isNotEmpty) _locationCity = city;
|
||||||
_locationLat = lat;
|
_locationLat = lat;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/material.dart';
|
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/models/usuario_profesional.dart';
|
||||||
import 'package:prosapp_web_app/providers/professionals_provider.dart';
|
import 'package:prosapp_web_app/providers/professionals_provider.dart';
|
||||||
import 'package:prosapp_web_app/providers/theme_provider.dart';
|
import 'package:prosapp_web_app/providers/theme_provider.dart';
|
||||||
@@ -127,6 +129,8 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
|
|||||||
cardBg: cardBg,
|
cardBg: cardBg,
|
||||||
border: border,
|
border: border,
|
||||||
compact: cols == 1,
|
compact: cols == 1,
|
||||||
|
patientLat: provider.patientLat,
|
||||||
|
patientLng: provider.patientLng,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await NavigationService
|
final result = await NavigationService
|
||||||
.navigateToFuture(
|
.navigateToFuture(
|
||||||
@@ -152,12 +156,31 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
|
|||||||
|
|
||||||
// ── Card ─────────────────────────────────────────────────────────────────────
|
// ── 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 {
|
class _ProfCard extends StatelessWidget {
|
||||||
final UsuarioProfesional data;
|
final UsuarioProfesional data;
|
||||||
final bool isDark;
|
final bool isDark;
|
||||||
final Color cardBg;
|
final Color cardBg;
|
||||||
final Color border;
|
final Color border;
|
||||||
final bool compact;
|
final bool compact;
|
||||||
|
final double? patientLat;
|
||||||
|
final double? patientLng;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const _ProfCard({
|
const _ProfCard({
|
||||||
@@ -167,6 +190,8 @@ class _ProfCard extends StatelessWidget {
|
|||||||
required this.border,
|
required this.border,
|
||||||
required this.compact,
|
required this.compact,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.patientLat,
|
||||||
|
this.patientLng,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -179,6 +204,12 @@ class _ProfCard extends StatelessWidget {
|
|||||||
final score = data.averageScore;
|
final score = data.averageScore;
|
||||||
final pm = data.professionalInfo.paymentMethods;
|
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) {
|
if (compact) {
|
||||||
// Fila horizontal en pantallas pequeñas
|
// Fila horizontal en pantallas pequeñas
|
||||||
return Material(
|
return Material(
|
||||||
@@ -227,7 +258,27 @@ class _ProfCard extends StatelessWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11, color: textSecondary)),
|
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(),
|
const Spacer(),
|
||||||
|
|
||||||
// Métodos de pago
|
// Métodos de pago
|
||||||
|
|||||||
Reference in New Issue
Block a user