CTK 1.1.0
This commit is contained in:
227
lib/consultorios/consultorios_screen.dart
Normal file
227
lib/consultorios/consultorios_screen.dart
Normal file
@@ -0,0 +1,227 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user