CTK 1.1.0

This commit is contained in:
Carlos Correia
2026-07-22 01:34:26 +01:00
parent f5884d8c36
commit 89b27e5e86
17 changed files with 1442 additions and 23 deletions

View 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),
),
),
],
),
),
);
}
}