228 lines
6.6 KiB
Dart
228 lines
6.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../colors/app_colors.dart';
|
|
|
|
import '../strings/consultorios_strings.dart';
|
|
import '../widgets/tap_bounce.dart';
|
|
import 'clinic.dart';
|
|
import 'clinic_card.dart';
|
|
|
|
/// Conteúdo do separador Consultórios, embutido na navegação inferior do
|
|
/// [LoggedHomeScreen] (sem Scaffold/AppBar próprios). Não busca dados
|
|
/// sozinho — recebe tudo já carregado do ecrã pai (que mantém o cache vivo
|
|
/// entre trocas de separador), e só pede um novo carregamento através dos
|
|
/// callbacks fornecidos.
|
|
class ConsultoriosTab extends StatelessWidget {
|
|
const ConsultoriosTab({
|
|
super.key,
|
|
required this.address,
|
|
required this.loading,
|
|
required this.error,
|
|
required this.near,
|
|
required this.far,
|
|
required this.favoriteIds,
|
|
required this.onToggleFavorite,
|
|
required this.onRefresh,
|
|
required this.onAddAddress,
|
|
});
|
|
|
|
/// Morada guardada, ou vazia/nula se o utilizador ainda não registou uma.
|
|
final String? address;
|
|
final bool loading;
|
|
final String? error;
|
|
final List<Clinic> near;
|
|
final List<Clinic> far;
|
|
final Set<String> favoriteIds;
|
|
final ValueChanged<String> onToggleFavorite;
|
|
final Future<void> Function() onRefresh;
|
|
final VoidCallback onAddAddress;
|
|
|
|
bool get _hasAddress => (address ?? '').trim().isNotEmpty;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!_hasAddress) {
|
|
return _EmptyState(
|
|
icon: Icons.location_on_outlined,
|
|
title: ConsultoriosStrings.noAddressTitle,
|
|
message: ConsultoriosStrings.noAddressMessage,
|
|
actionLabel: ConsultoriosStrings.addAddress,
|
|
onAction: onAddAddress,
|
|
);
|
|
}
|
|
|
|
if (loading && near.isEmpty && far.isEmpty && error == null) {
|
|
return const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircularProgressIndicator(color: AppColors.teal),
|
|
SizedBox(height: 14),
|
|
Text(
|
|
ConsultoriosStrings.loading,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (error != null) {
|
|
return _EmptyState(
|
|
icon: Icons.wifi_off_rounded,
|
|
title: ConsultoriosStrings.fetchErrorTitle,
|
|
message: ConsultoriosStrings.fetchErrorMessage,
|
|
actionLabel: ConsultoriosStrings.retry,
|
|
onAction: onRefresh,
|
|
);
|
|
}
|
|
|
|
if (near.isEmpty && far.isEmpty) {
|
|
return _EmptyState(
|
|
icon: Icons.search_off_rounded,
|
|
title: ConsultoriosStrings.noResults,
|
|
onAction: onRefresh,
|
|
actionLabel: ConsultoriosStrings.retry,
|
|
);
|
|
}
|
|
|
|
return RefreshIndicator(
|
|
color: AppColors.teal,
|
|
onRefresh: onRefresh,
|
|
child: ListView(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
|
children: [
|
|
if (near.isNotEmpty) ...[
|
|
_SectionLabel(ConsultoriosStrings.sectionNear(near.length)),
|
|
const SizedBox(height: 10),
|
|
for (var i = 0; i < near.length; i++) ...[
|
|
if (i > 0) const SizedBox(height: 12),
|
|
ClinicCard(
|
|
clinic: near[i],
|
|
isFavorite: favoriteIds.contains(near[i].id),
|
|
onToggleFavorite: onToggleFavorite,
|
|
),
|
|
],
|
|
const SizedBox(height: 20),
|
|
],
|
|
if (far.isNotEmpty) ...[
|
|
_SectionLabel(ConsultoriosStrings.sectionFar(far.length)),
|
|
const SizedBox(height: 10),
|
|
for (var i = 0; i < far.length; i++) ...[
|
|
if (i > 0) const SizedBox(height: 12),
|
|
ClinicCard(
|
|
clinic: far[i],
|
|
isFavorite: favoriteIds.contains(far[i].id),
|
|
onToggleFavorite: onToggleFavorite,
|
|
),
|
|
],
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SectionLabel extends StatelessWidget {
|
|
const _SectionLabel(this.text);
|
|
|
|
final String text;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 4),
|
|
child: Text(
|
|
text,
|
|
style: const TextStyle(
|
|
color: AppColors.pink,
|
|
fontWeight: FontWeight.w900,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _EmptyState extends StatelessWidget {
|
|
const _EmptyState({
|
|
required this.icon,
|
|
required this.title,
|
|
this.message,
|
|
required this.actionLabel,
|
|
required this.onAction,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String title;
|
|
final String? message;
|
|
final String actionLabel;
|
|
final VoidCallback onAction;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 76,
|
|
height: 76,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.pinkBackground,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: AppColors.pink, size: 36),
|
|
),
|
|
const SizedBox(height: 18),
|
|
Text(
|
|
title,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
if (message != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
message!,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black.withValues(alpha: 0.6),
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 20),
|
|
TapBounce(
|
|
child: FilledButton(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: AppColors.teal,
|
|
foregroundColor: Colors.white,
|
|
shape: const StadiumBorder(),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 14,
|
|
),
|
|
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
|
),
|
|
onPressed: onAction,
|
|
child: Text(actionLabel),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|