CTK 1.2.0
This commit is contained in:
@@ -12,8 +12,7 @@ class Clinic {
|
||||
required this.lat,
|
||||
required this.lon,
|
||||
required this.address,
|
||||
required this.phone,
|
||||
required this.hasPhone,
|
||||
required this.phoneNumbers,
|
||||
required this.openingHours,
|
||||
required this.hasOpeningHours,
|
||||
this.distanceKm,
|
||||
@@ -24,11 +23,17 @@ class Clinic {
|
||||
final double lat;
|
||||
final double lon;
|
||||
final String address;
|
||||
final String phone;
|
||||
final bool hasPhone;
|
||||
|
||||
/// Um consultório na OSM pode ter mais do que um contacto na mesma tag
|
||||
/// `phone` (separados por `;`/`,`) — já vêm separados aqui, para quem usa
|
||||
/// isto poder oferecer escolha em vez de tentar ligar para o texto todo
|
||||
/// de uma vez. Lista vazia quando não há telefone.
|
||||
final List<String> phoneNumbers;
|
||||
final String openingHours;
|
||||
final bool hasOpeningHours;
|
||||
|
||||
bool get hasPhone => phoneNumbers.isNotEmpty;
|
||||
|
||||
/// Distância (km) até à morada guardada — só preenchida depois de
|
||||
/// [OverpassService.fetchNearbyClinics] a calcular; `null` até lá.
|
||||
final double? distanceKm;
|
||||
@@ -40,8 +45,7 @@ class Clinic {
|
||||
lat: lat,
|
||||
lon: lon,
|
||||
address: address,
|
||||
phone: phone,
|
||||
hasPhone: hasPhone,
|
||||
phoneNumbers: phoneNumbers,
|
||||
openingHours: openingHours,
|
||||
hasOpeningHours: hasOpeningHours,
|
||||
distanceKm: distanceKm,
|
||||
@@ -76,10 +80,16 @@ class Clinic {
|
||||
(tags['addr:city'] ?? tags['addr:suburb']),
|
||||
].whereType<String>().where((s) => s.trim().isNotEmpty).toList();
|
||||
|
||||
final phone = (tags['phone'] as String?)?.trim() ??
|
||||
final phoneRaw = (tags['phone'] as String?)?.trim() ??
|
||||
(tags['contact:phone'] as String?)?.trim();
|
||||
final phoneNumbers = (phoneRaw == null || phoneRaw.isEmpty)
|
||||
? const <String>[]
|
||||
: phoneRaw
|
||||
.split(RegExp(r'[;,]'))
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
final openingHours = (tags['opening_hours'] as String?)?.trim();
|
||||
final hasPhone = phone != null && phone.isNotEmpty;
|
||||
final hasOpeningHours = openingHours != null && openingHours.isNotEmpty;
|
||||
|
||||
return Clinic(
|
||||
@@ -92,8 +102,7 @@ class Clinic {
|
||||
address: addressParts.isNotEmpty
|
||||
? addressParts.join(' • ')
|
||||
: ConsultoriosStrings.noAddress,
|
||||
phone: hasPhone ? phone : ConsultoriosStrings.noPhone,
|
||||
hasPhone: hasPhone,
|
||||
phoneNumbers: phoneNumbers,
|
||||
openingHours: hasOpeningHours
|
||||
? openingHours
|
||||
: ConsultoriosStrings.noOpeningHours,
|
||||
|
||||
@@ -23,8 +23,8 @@ class ClinicCard extends StatelessWidget {
|
||||
final bool isFavorite;
|
||||
final ValueChanged<String> onToggleFavorite;
|
||||
|
||||
Future<void> _call(BuildContext context) async {
|
||||
final uri = Uri(scheme: 'tel', path: clinic.phone);
|
||||
Future<void> _call(BuildContext context, String number) async {
|
||||
final uri = Uri(scheme: 'tel', path: number);
|
||||
try {
|
||||
final launched = await launchUrl(uri);
|
||||
if (!launched && context.mounted) {
|
||||
@@ -35,6 +35,82 @@ class ClinicCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Um consultório na OSM pode listar vários contactos na mesma tag — em
|
||||
/// vez de tentar ligar para o texto todo (o que nem é um número válido),
|
||||
/// mostra-se este seletor para escolher qual dos números ligar.
|
||||
Future<void> _showNumberPicker(BuildContext context) async {
|
||||
final chosen = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
backgroundColor: AppColors.pinkBackground,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 4, 18, 18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
ConsultoriosStrings.chooseNumberTitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.pink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
for (final number in clinic.phoneNumbers) ...[
|
||||
TapBounce(
|
||||
scale: 0.98,
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () => Navigator.of(ctx).pop(number),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.call_rounded,
|
||||
color: AppColors.teal,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
number,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (chosen != null && context.mounted) await _call(context, chosen);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -56,15 +132,6 @@ class ClinicCard extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.pinkBackground,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -132,45 +199,63 @@ class ClinicCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TapBounce(
|
||||
child: Material(
|
||||
color: clinic.hasPhone
|
||||
? AppColors.teal
|
||||
: Colors.black.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
onTap: clinic.hasPhone ? () => _call(context) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.call_rounded,
|
||||
size: 16,
|
||||
color: clinic.hasPhone
|
||||
? Colors.white
|
||||
: Colors.black.withValues(alpha: 0.35),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
clinic.phone,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13,
|
||||
color: clinic.hasPhone
|
||||
? Colors.white
|
||||
: Colors.black.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
],
|
||||
_buildPhoneButton(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneButton(BuildContext context) {
|
||||
final numbers = clinic.phoneNumbers;
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
if (numbers.isEmpty) {
|
||||
label = ConsultoriosStrings.noPhone;
|
||||
onTap = null;
|
||||
} else if (numbers.length == 1) {
|
||||
label = numbers.first;
|
||||
onTap = () => _call(context, numbers.first);
|
||||
} else {
|
||||
label = ConsultoriosStrings.chooseNumber(numbers.length);
|
||||
onTap = () => _showNumberPicker(context);
|
||||
}
|
||||
final enabled = onTap != null;
|
||||
final color = enabled ? Colors.white : Colors.black.withValues(alpha: 0.35);
|
||||
|
||||
return TapBounce(
|
||||
child: Material(
|
||||
color: enabled ? AppColors.teal : Colors.black.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
enabled ? Icons.call_rounded : Icons.call_outlined,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 13,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user