first commit
This commit is contained in:
@@ -0,0 +1,960 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/widgets/medical_modal.dart';
|
||||
import '../../../patient/presentation/views/patient_modal_view.dart';
|
||||
import '../../data/models/patient_summary_model.dart';
|
||||
import '../../data/services/dashboard_service.dart';
|
||||
|
||||
class AppointmentsModalView extends StatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
final DashboardService? service;
|
||||
final bool isSecretary;
|
||||
final void Function(int patientId, String newStatus)? onStatusChanged;
|
||||
|
||||
const AppointmentsModalView({
|
||||
super.key,
|
||||
this.initialDate,
|
||||
this.service,
|
||||
this.isSecretary = false,
|
||||
this.onStatusChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AppointmentsModalView> createState() => _AppointmentsModalViewState();
|
||||
}
|
||||
|
||||
class _AppointmentsModalViewState extends State<AppointmentsModalView> {
|
||||
late final DashboardService _service;
|
||||
late DateTime _selectedDate;
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
List<PatientSummaryModel> _appointments = [];
|
||||
String _waitingFilter = 'todos'; // 'todos', 'en_espera', 'en_consulta', 'atendido', 'no_presente'
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_service = widget.service ?? DashboardService();
|
||||
_selectedDate = widget.initialDate ?? DateTime.now();
|
||||
_fetchAppointments(_selectedDate);
|
||||
}
|
||||
|
||||
Future<void> _handleStatusChange(PatientSummaryModel item, String newStatus) async {
|
||||
final success = await _service.updatePatientWaitingStatus(item.id, newStatus);
|
||||
if (success && mounted) {
|
||||
setState(() {
|
||||
final index = _appointments.indexWhere((p) => p.id == item.id);
|
||||
if (index != -1) {
|
||||
_appointments[index] = _appointments[index].copyWith(estadoEspera: newStatus);
|
||||
}
|
||||
});
|
||||
if (widget.onStatusChanged != null) {
|
||||
widget.onStatusChanged!(item.id, newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<PatientSummaryModel> get _filteredAppointments {
|
||||
if (_waitingFilter == 'todos') return _appointments;
|
||||
if (_waitingFilter == 'en_espera') return _appointments.where((a) => a.isEnEspera).toList();
|
||||
if (_waitingFilter == 'en_consulta') return _appointments.where((a) => a.isEnConsulta).toList();
|
||||
if (_waitingFilter == 'atendido') return _appointments.where((a) => a.isAtendido).toList();
|
||||
if (_waitingFilter == 'no_presente') return _appointments.where((a) => a.isNoPresente).toList();
|
||||
return _appointments;
|
||||
}
|
||||
|
||||
bool _isToday(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
return date.year == now.year && date.month == now.month && date.day == now.day;
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
final day = date.day.toString().padLeft(2, '0');
|
||||
final month = date.month.toString().padLeft(2, '0');
|
||||
final year = date.year.toString();
|
||||
return '$day/$month/$year';
|
||||
}
|
||||
|
||||
String _formatDateHuman(DateTime date) {
|
||||
const weekdays = [
|
||||
'Lunes',
|
||||
'Martes',
|
||||
'Miércoles',
|
||||
'Jueves',
|
||||
'Viernes',
|
||||
'Sábado',
|
||||
'Domingo'
|
||||
];
|
||||
const months = [
|
||||
'Enero',
|
||||
'Febrero',
|
||||
'Marzo',
|
||||
'Abril',
|
||||
'Mayo',
|
||||
'Junio',
|
||||
'Julio',
|
||||
'Agosto',
|
||||
'Septiembre',
|
||||
'Octubre',
|
||||
'Noviembre',
|
||||
'Diciembre'
|
||||
];
|
||||
|
||||
final weekdayName = weekdays[date.weekday - 1];
|
||||
final monthName = months[date.month - 1];
|
||||
return '$weekdayName, ${date.day} de $monthName de ${date.year}';
|
||||
}
|
||||
|
||||
Future<void> _fetchAppointments(DateTime date) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final results = await _service.getAppointmentsByDate(date);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_appointments = results;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = 'Error al cargar las citas para la fecha seleccionada: $e';
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onDateSelected(DateTime newDate) {
|
||||
if (newDate.year == _selectedDate.year &&
|
||||
newDate.month == _selectedDate.month &&
|
||||
newDate.day == _selectedDate.day) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedDate = newDate;
|
||||
});
|
||||
_fetchAppointments(newDate);
|
||||
}
|
||||
|
||||
Future<void> _pickDate() async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedDate,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2050),
|
||||
helpText: 'SELECCIONAR FECHA DE CITAS',
|
||||
confirmText: 'FILTRAR',
|
||||
cancelText: 'CANCELAR',
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: AppColors.primary,
|
||||
onPrimary: Colors.white,
|
||||
onSurface: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
_onDateSelected(picked);
|
||||
}
|
||||
}
|
||||
|
||||
void _openPatient(int patientId) {
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (_) => PatientModalView(
|
||||
patientId: patientId,
|
||||
isSecretary: widget.isSecretary,
|
||||
onSaved: (_) {
|
||||
_fetchAppointments(_selectedDate);
|
||||
if (widget.onStatusChanged != null) {
|
||||
widget.onStatusChanged!(patientId, 'atendido');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFilteringToday = _isToday(_selectedDate);
|
||||
|
||||
return MedicalModalDialog(
|
||||
titleIcon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.event_available_rounded,
|
||||
color: AppColors.warning,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
title: 'Agenda de Citas Médicas',
|
||||
subtitle: const Text(
|
||||
'Consulte y gestione los pacientes citados por fecha de consulta',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.textMuted),
|
||||
),
|
||||
maxWidth: 820,
|
||||
maxHeight: 700,
|
||||
bottomBar: _buildBottomBar(),
|
||||
child: Column(
|
||||
children: [
|
||||
// Barra de Filtro de Fecha
|
||||
_buildDateFilterBar(isFilteringToday),
|
||||
|
||||
// Divisor
|
||||
const Divider(height: 1, color: Color(0xFFE2E8F0)),
|
||||
|
||||
// Contenido Principal (Lista de Citas)
|
||||
Expanded(
|
||||
child: _buildBody(isFilteringToday),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateFilterBar(bool isFilteringToday) {
|
||||
final filterText = isFilteringToday
|
||||
? 'Hoy (${_formatDate(_selectedDate)})'
|
||||
: _formatDate(_selectedDate);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF8FAFC),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
// Indicador dinámico de filtro activo (Hoy vs Fecha Específica)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isFilteringToday
|
||||
? AppColors.primaryContainer
|
||||
: const Color(0xFFEFF6FF),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isFilteringToday
|
||||
? AppColors.primary.withValues(alpha: 0.3)
|
||||
: const Color(0xFF93C5FD),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
isFilteringToday ? Icons.today_rounded : Icons.date_range_rounded,
|
||||
size: 16,
|
||||
color: isFilteringToday ? AppColors.primary : const Color(0xFF2563EB),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: 'Filtro: ',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: filterText,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isFilteringToday
|
||||
? AppColors.primary
|
||||
: const Color(0xFF1D4ED8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Botones de Navegación Rápida
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Botón Día Anterior
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_left_rounded, size: 20),
|
||||
tooltip: 'Día anterior',
|
||||
splashRadius: 18,
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => _onDateSelected(_selectedDate.subtract(const Duration(days: 1))),
|
||||
),
|
||||
|
||||
// Botón acceso directo a "Hoy"
|
||||
InkWell(
|
||||
onTap: _isLoading || isFilteringToday
|
||||
? null
|
||||
: () => _onDateSelected(DateTime.now()),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isFilteringToday
|
||||
? AppColors.primary
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: isFilteringToday
|
||||
? AppColors.primary
|
||||
: const Color(0xFFCBD5E1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.refresh_rounded,
|
||||
size: 14,
|
||||
color: isFilteringToday ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Hoy',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isFilteringToday ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Botón Día Siguiente
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chevron_right_rounded, size: 20),
|
||||
tooltip: 'Día siguiente',
|
||||
splashRadius: 18,
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => _onDateSelected(_selectedDate.add(const Duration(days: 1))),
|
||||
),
|
||||
|
||||
const SizedBox(width: 6),
|
||||
|
||||
// Botón Selector de Fecha
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isLoading ? null : _pickDate,
|
||||
icon: const Icon(Icons.calendar_month_outlined, size: 16),
|
||||
label: const Text(
|
||||
'Cambiar Fecha',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
side: const BorderSide(color: Color(0xFFCBD5E1)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 12,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
_formatDateHuman(_selectedDate),
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: AppColors.textMuted,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
// Píldoras de Filtro de Sala de Espera
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_buildFilterChip('todos', 'Todos (${_appointments.length})', null),
|
||||
_buildFilterChip(
|
||||
'en_espera',
|
||||
'En Espera (${_appointments.where((a) => a.isEnEspera).length})',
|
||||
const Color(0xFF15803D),
|
||||
),
|
||||
_buildFilterChip(
|
||||
'en_consulta',
|
||||
'En Consulta (${_appointments.where((a) => a.isEnConsulta).length})',
|
||||
const Color(0xFF0369A1),
|
||||
),
|
||||
_buildFilterChip(
|
||||
'atendido',
|
||||
'Atendidos (${_appointments.where((a) => a.isAtendido).length})',
|
||||
const Color(0xFF64748B),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(String value, String label, Color? highlightColor) {
|
||||
final isSelected = _waitingFilter == value;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_waitingFilter = value;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (highlightColor ?? AppColors.primary).withValues(alpha: 0.15)
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? (highlightColor ?? AppColors.primary)
|
||||
: const Color(0xFFE2E8F0),
|
||||
width: isSelected ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: isSelected
|
||||
? (highlightColor ?? AppColors.primary)
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(bool isFilteringToday) {
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 14),
|
||||
Text(
|
||||
'Consultando agenda médica...',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_errorMessage != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline_rounded, color: AppColors.error, size: 40),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 13, color: AppColors.error),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _fetchAppointments(_selectedDate),
|
||||
icon: const Icon(Icons.refresh_rounded, size: 16),
|
||||
label: const Text('Reintentar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_appointments.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(36),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.event_busy_rounded,
|
||||
size: 42,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
isFilteringToday
|
||||
? 'No hay citas programadas para el día de hoy'
|
||||
: 'No hay citas registradas para el ${_formatDate(_selectedDate)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
isFilteringToday
|
||||
? 'Puede consultar otra fecha utilizando el selector de calendario superior.'
|
||||
: 'Intente seleccionar otro día o presione "Hoy" para volver a la agenda actual.',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final list = _filteredAppointments;
|
||||
|
||||
if (list.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(36),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.filter_list_off_rounded, size: 38, color: AppColors.textMuted),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'No hay pacientes con el filtro seleccionado',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => setState(() => _waitingFilter = 'todos'),
|
||||
child: const Text('Mostrar todos'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
itemCount: list.length,
|
||||
separatorBuilder: (_, index) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final item = list[index];
|
||||
return _buildAppointmentTile(item);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppointmentTile(PatientSummaryModel item) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: item.isEnEspera ? const Color(0xFF86EFAC) : const Color(0xFFE2E8F0),
|
||||
width: item.isEnEspera ? 1.5 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InkWell(
|
||||
onTap: item.id > 0 ? () => _openPatient(item.id) : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icono de Cita
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: item.isEnEspera
|
||||
? const Color(0xFFDCFCE7)
|
||||
: AppColors.teal.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
item.isEnEspera ? Icons.chair_rounded : Icons.calendar_today_rounded,
|
||||
color: item.isEnEspera ? const Color(0xFF15803D) : AppColors.teal,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
|
||||
// Datos del Paciente y Motivo
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.fullName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (item.cedula.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
item.cedula,
|
||||
style: const TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
if (item.telefono.isNotEmpty) ...[
|
||||
const Icon(Icons.phone_outlined, size: 13, color: AppColors.textMuted),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
item.telefono,
|
||||
style: const TextStyle(fontSize: 11.5, color: AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
const Icon(Icons.medical_information_outlined, size: 13, color: AppColors.textMuted),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.motCons.isNotEmpty
|
||||
? item.motCons.replaceAll('\n', ' ').trim()
|
||||
: 'Consulta médica programada',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 11.5, color: AppColors.textMuted),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Acciones de Sala de Espera / Estado
|
||||
_buildAppointmentActions(item),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Botón Ver Ficha / Abrir Historial
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppointmentActions(PatientSummaryModel item) {
|
||||
const double uniformHeight = 28.0;
|
||||
const double uniformRadius = 6.0;
|
||||
|
||||
if (item.isEnEspera) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!widget.isSecretary) ...[
|
||||
const SizedBox(width: 6),
|
||||
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 {
|
||||
await _handleStatusChange(item, 'en_consulta');
|
||||
_openPatient(item.id);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_forward_rounded, size: 12),
|
||||
label: const Text('Atender', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(width: 4),
|
||||
Tooltip(
|
||||
message: 'Quitar de la sala de espera',
|
||||
child: InkWell(
|
||||
onTap: () => _handleStatusChange(item, '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 (item.isEnConsulta) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => _openPatient(item.id),
|
||||
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 (!widget.isSecretary) ...[
|
||||
const SizedBox(width: 6),
|
||||
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: () => _handleStatusChange(item, 'atendido'),
|
||||
icon: const Icon(Icons.check_rounded, size: 13),
|
||||
label: const Text('Finalizar', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (item.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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// no_presente
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.isSecretary)
|
||||
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: () => _handleStatusChange(item, 'en_espera'),
|
||||
icon: const Icon(Icons.how_to_reg_rounded, size: 13),
|
||||
label: const Text('Llegó', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF8FAFC),
|
||||
border: Border(top: BorderSide(color: Color(0xFFE2E8F0))),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.keyboard_outlined, size: 12, color: AppColors.textMuted),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'ESC para salir',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textMuted, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text(
|
||||
'Cerrar',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user