first commit

This commit is contained in:
=
2026-09-21 16:33:51 -04:00
commit a899479dc9
204 changed files with 21132 additions and 0 deletions
@@ -0,0 +1,266 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_colors.dart';
import '../../data/models/dashboard_metrics_model.dart';
class DashboardMetricsCards extends StatelessWidget {
final DashboardMetricsModel metrics;
const DashboardMetricsCards({super.key, required this.metrics});
String _formatNumber(int n) {
return n.toString().replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
);
}
@override
Widget build(BuildContext context) {
final cards = [
// 1. Total de Nuevos Pacientes
_buildMetricCard(
title: 'Total Nuevos Pacientes',
value: _formatNumber(metrics.nuevosPacientes),
subtitle: 'Registrados en la plataforma',
icon: Icons.person_add_alt_1_rounded,
iconColor: AppColors.primary,
bgColor: AppColors.primaryContainer,
),
// 2. Total de Consultados Hoy
_buildMetricCard(
title: 'Total Consultados Hoy',
value: _formatNumber(metrics.consultadosHoy),
subtitle: 'Atenciones médicas realizadas',
icon: Icons.assignment_turned_in_rounded,
iconColor: AppColors.teal,
bgColor: AppColors.teal.withValues(alpha: 0.12),
),
// 3. Total de Citas para Hoy
_buildMetricCard(
title: 'Total Citas para Hoy',
value: _formatNumber(metrics.citasHoy),
subtitle: 'Pacientes agendados',
icon: Icons.event_available_rounded,
iconColor: AppColors.warning,
bgColor: AppColors.warning.withValues(alpha: 0.12),
),
// 4. Total de Pacientes por Sexo
_buildGenderCard(),
];
return LayoutBuilder(
builder: (context, constraints) {
const spacing = 16.0;
// Distribución equitativa en 4 columnas (pantallas amplias)
if (constraints.maxWidth >= 1000) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: cards[0]),
const SizedBox(width: spacing),
Expanded(child: cards[1]),
const SizedBox(width: spacing),
Expanded(child: cards[2]),
const SizedBox(width: spacing),
Expanded(child: cards[3]),
],
),
);
}
// Distribución equitativa en 2 columnas (pantallas medianas)
if (constraints.maxWidth >= 550) {
return Column(
children: [
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: cards[0]),
const SizedBox(width: spacing),
Expanded(child: cards[1]),
],
),
),
const SizedBox(height: spacing),
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: cards[2]),
const SizedBox(width: spacing),
Expanded(child: cards[3]),
],
),
),
],
);
}
// 1 columna ocupando 100% del ancho (pantallas móviles / compactas)
return Column(
children: [
cards[0],
const SizedBox(height: spacing),
cards[1],
const SizedBox(height: spacing),
cards[2],
const SizedBox(height: spacing),
cards[3],
],
);
},
);
}
Widget _buildMetricCard({
required String title,
String? value,
Widget? valueWidget,
required String subtitle,
required IconData icon,
required Color iconColor,
required Color bgColor,
}) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: iconColor, size: 24),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 4),
valueWidget ??
Text(
value ?? '',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 11,
color: AppColors.textMuted,
),
),
],
),
),
],
),
);
}
Widget _buildGenderCard() {
final total = metrics.pacientesMasculinos + metrics.pacientesFemeninos;
final mascPct = total > 0 ? ((metrics.pacientesMasculinos / total) * 100).toInt() : 81;
final femPct = total > 0 ? ((metrics.pacientesFemeninos / total) * 100).toInt() : 19;
return _buildMetricCard(
title: 'Pacientes por Sexo',
valueWidget: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
_formatNumber(metrics.pacientesMasculinos),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
const Text(
' M',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
const SizedBox(width: 8),
const Text(
'/',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.border,
),
),
const SizedBox(width: 8),
Text(
_formatNumber(metrics.pacientesFemeninos),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Color(0xFFEC4899),
),
),
const Text(
' F',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFFEC4899),
),
),
],
),
),
subtitle: '$mascPct% Masc · $femPct% Fem',
icon: Icons.people_alt_rounded,
iconColor: AppColors.secondary,
bgColor: AppColors.secondary.withValues(alpha: 0.12),
);
}
}