267 lines
8.1 KiB
Dart
267 lines
8.1 KiB
Dart
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),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|