Files

471 lines
19 KiB
Dart
Raw Permalink Normal View History

2026-09-21 16:33:51 -04:00
import 'package:flutter/material.dart';
import '../../../../core/theme/app_colors.dart';
import '../../data/models/patient_summary_model.dart';
class TodayAppointmentsCard extends StatelessWidget {
final List<PatientSummaryModel> appointments;
final VoidCallback? onSchedulePressed;
final ValueChanged<PatientSummaryModel>? onPatientTap;
final bool isSecretary;
final Future<void> Function(PatientSummaryModel patient, String newStatus)? onStatusChange;
const TodayAppointmentsCard({
super.key,
required this.appointments,
this.onSchedulePressed,
this.onPatientTap,
this.isSecretary = false,
this.onStatusChange,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Encabezado
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.warning.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.event_available_rounded,
color: AppColors.warning,
size: 20,
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Citas para Hoy',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
Text(
'Agenda y turnos de consulta médica',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
if (onSchedulePressed != null)
TextButton(
onPressed: onSchedulePressed,
child: const Text(
'Ver todos',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
),
],
),
const SizedBox(height: 18),
const Divider(color: AppColors.divider, height: 1),
const SizedBox(height: 14),
// Contenido de Citas con Scroll Interno delimitado
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 330),
child: Scrollbar(
child: SingleChildScrollView(
child: appointments.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.event_note_rounded,
size: 40,
color: AppColors.textMuted.withValues(alpha: 0.6),
),
const SizedBox(height: 8),
const Text(
'No hay citas registradas para el día de hoy',
style: TextStyle(color: AppColors.textSecondary, fontSize: 13),
),
const SizedBox(height: 4),
const Text(
'Los nuevos turnos agendados aparecerán aquí',
style: TextStyle(color: AppColors.textMuted, fontSize: 11),
),
],
),
),
)
: Padding(
padding: const EdgeInsets.only(right: 6),
child: Column(
children: [
for (var i = 0; i < appointments.length; i++) ...[
if (i > 0) const Divider(color: AppColors.divider, height: 16),
Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(10),
child: InkWell(
onTap: onPatientTap != null && appointments[i].id > 0
? () => onPatientTap!(appointments[i])
: null,
borderRadius: BorderRadius.circular(10),
hoverColor: AppColors.primary.withValues(alpha: 0.05),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
child: Row(
children: [
// Icono de Cita
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: AppColors.teal.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.access_time_rounded,
color: AppColors.teal,
size: 20,
),
),
const SizedBox(width: 14),
// Paciente y Motivo de Consulta
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appointments[i].fullName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Wrap(
spacing: 8,
runSpacing: 2,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (appointments[i].telefono.isNotEmpty)
Text(
appointments[i].telefono,
style: const TextStyle(
fontSize: 11,
color: AppColors.textSecondary,
),
),
Text(
appointments[i].motCons.isNotEmpty
? appointments[i].motCons.replaceAll('\n', ' ').trim()
: 'Consulta de rutina',
style: const TextStyle(
fontSize: 11,
color: AppColors.textMuted,
),
),
],
),
],
),
),
const SizedBox(width: 10),
// Estado de Sala de Espera y Acciones Rápidas
_buildStatusAndAction(context, appointments[i]),
],
),
),
),
),
],
],
),
),
),
),
),
],
),
);
}
Widget _buildStatusAndAction(BuildContext context, PatientSummaryModel patient) {
const double uniformHeight = 28.0;
const double uniformRadius = 6.0;
if (patient.isEnEspera) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// Badge En Espera
Container(
height: uniformHeight,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: const Color(0xFFDCFCE7),
borderRadius: BorderRadius.circular(uniformRadius),
border: Border.all(color: const Color(0xFF86EFAC)),
),
alignment: Alignment.center,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.hourglass_top_rounded, size: 12, color: Color(0xFF15803D)),
SizedBox(width: 4),
Text(
'En Espera',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFF15803D),
),
),
],
),
),
// Botón Doctor: Llamar a Consulta
if (!isSecretary) ...[
const SizedBox(width: 6),
Tooltip(
message: 'Llamar a consulta médica ahora',
child: SizedBox(
height: uniformHeight,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(uniformRadius),
),
visualDensity: VisualDensity.compact,
),
onPressed: () async {
if (onStatusChange != null) {
await onStatusChange!(patient, 'en_consulta');
}
if (onPatientTap != null) {
onPatientTap!(patient);
}
},
icon: const Icon(Icons.arrow_forward_rounded, size: 12),
label: const Text(
'Atender',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700),
),
),
),
),
],
// Botón Secretaria: Revertir si fue error
if (isSecretary && onStatusChange != null) ...[
const SizedBox(width: 4),
Tooltip(
message: 'Quitar de la sala de espera',
child: InkWell(
onTap: () => onStatusChange!(patient, 'no_presente'),
borderRadius: BorderRadius.circular(uniformRadius),
child: Container(
height: uniformHeight,
width: uniformHeight,
alignment: Alignment.center,
child: const Icon(Icons.close_rounded, size: 15, color: AppColors.textMuted),
),
),
),
],
],
);
}
if (patient.isEnConsulta) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: onPatientTap != null ? () => onPatientTap!(patient) : null,
borderRadius: BorderRadius.circular(uniformRadius),
child: Container(
height: uniformHeight,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: const Color(0xFFE0F2FE),
borderRadius: BorderRadius.circular(uniformRadius),
border: Border.all(color: const Color(0xFF7DD3FC)),
),
alignment: Alignment.center,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.medical_services_rounded, size: 12, color: Color(0xFF0369A1)),
SizedBox(width: 4),
Text(
'En Consulta',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFF0369A1),
),
),
],
),
),
),
if (!isSecretary && onStatusChange != null) ...[
const SizedBox(width: 6),
Tooltip(
message: 'Finalizar consulta médica y marcar como Atendido',
child: SizedBox(
height: uniformHeight,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0284C7),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(uniformRadius),
),
visualDensity: VisualDensity.compact,
),
onPressed: () => onStatusChange!(patient, 'atendido'),
icon: const Icon(Icons.check_rounded, size: 13),
label: const Text(
'Finalizar',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700),
),
),
),
),
],
],
);
}
if (patient.isAtendido) {
return Container(
height: uniformHeight,
padding: const EdgeInsets.symmetric(horizontal: 9),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(uniformRadius),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
alignment: Alignment.center,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle_rounded, size: 12, color: Color(0xFF64748B)),
SizedBox(width: 4),
Text(
'Atendido',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFF64748B),
),
),
],
),
);
}
// Estado no_presente (No ha llegado aún)
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isSecretary && onStatusChange != null)
SizedBox(
height: uniformHeight,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(uniformRadius),
),
visualDensity: VisualDensity.compact,
),
onPressed: () => onStatusChange!(patient, 'en_espera'),
icon: const Icon(Icons.how_to_reg_rounded, size: 13),
label: const Text(
'Llegó',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700),
),
),
)
else
Container(
height: uniformHeight,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: AppColors.primaryContainer,
borderRadius: BorderRadius.circular(uniformRadius),
),
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.calendar_month_rounded,
size: 11,
color: AppColors.primary,
),
const SizedBox(width: 4),
Text(
patient.fecProxCita.isNotEmpty
? patient.fecProxCita.split(' ').first
: 'Hoy',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
],
),
),
],
);
}
}