inou/app/lib/features/dashboard/dashboard_page.dart

278 lines
8.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:inou_app/design/inou_theme.dart';
import 'package:inou_app/design/inou_text.dart';
import 'package:inou_app/design/widgets/widgets.dart';
import 'models.dart';
import 'mock_data.dart';
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
return InouAuthPage(
userName: 'Johan', // TODO: get from auth state
onProfileTap: () => context.go('/profile'),
onLogoutTap: () => context.go('/login'),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Text('Dossiers', style: InouText.pageTitle),
const SizedBox(height: 8),
Text(
'Manage your health dossiers and those shared with you.',
style: InouText.intro,
),
const SizedBox(height: 32),
// Dossier grid
LayoutBuilder(
builder: (context, constraints) {
final crossAxisCount = constraints.maxWidth > 900
? 3
: constraints.maxWidth > 600
? 2
: 1;
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.6,
),
itemCount: mockDossiers.length + 1, // +1 for add card
itemBuilder: (context, index) {
if (index == mockDossiers.length) {
return _AddDossierCard();
}
return _DossierCard(dossier: mockDossiers[index]);
},
);
},
),
const SizedBox(height: 48),
const InouFooter(),
],
),
),
),
),
);
}
}
class _DossierCard extends StatelessWidget {
final DossierSummary dossier;
const _DossierCard({required this.dossier});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => context.go('/dossier/${dossier.id}'),
borderRadius: InouTheme.borderRadiusLg,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: InouTheme.bgCard,
border: Border.all(color: InouTheme.border),
borderRadius: InouTheme.borderRadiusLg,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header row with name and edit
Row(
children: [
Expanded(
child: Text(
dossier.name,
style: InouText.h3.copyWith(fontSize: 20),
overflow: TextOverflow.ellipsis,
),
),
if (dossier.canEdit)
IconButton(
icon: const Icon(Icons.edit_outlined, size: 18),
color: InouTheme.textMuted,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
// TODO: navigate to edit
},
),
],
),
// Relation/role
const SizedBox(height: 4),
Row(
children: [
Text(
dossier.isSelf
? 'you'
: 'my role: ${dossier.relation ?? "Unknown"}',
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
),
if (dossier.isCareReceiver) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: InouTheme.successLight,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'care',
style: InouText.bodySmall.copyWith(
color: InouTheme.success,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
// DOB & Sex
const SizedBox(height: 8),
Text(
[
if (dossier.dateOfBirth != null) dossier.dateOfBirth,
if (dossier.sex != null) dossier.sex,
].join(' · '),
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
),
const Spacer(),
// Stats row
if (dossier.stats.hasAnyData) ...[
Wrap(
spacing: 12,
runSpacing: 4,
children: [
if (dossier.stats.imaging > 0)
_StatChip(
'📷 ${dossier.stats.imaging} ${dossier.stats.imaging == 1 ? 'study' : 'studies'}',
),
if (dossier.stats.labs > 0)
_StatChip(
'🧪 ${dossier.stats.labs} ${dossier.stats.labs == 1 ? 'lab' : 'labs'}',
),
if (dossier.stats.genome) const _StatChip('🧬 genome'),
if (dossier.stats.documents > 0)
_StatChip(
'📄 ${dossier.stats.documents} ${dossier.stats.documents == 1 ? 'doc' : 'docs'}',
),
if (dossier.stats.medications > 0)
_StatChip('💊 ${dossier.stats.medications} meds'),
if (dossier.stats.supplements > 0)
_StatChip('🌿 ${dossier.stats.supplements} supps'),
],
),
] else ...[
Text(
'No data yet',
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
),
],
const SizedBox(height: 12),
// View button
InouButton(
text: 'View',
size: ButtonSize.small,
onPressed: () => context.go('/dossier/${dossier.id}'),
),
],
),
),
);
}
}
class _StatChip extends StatelessWidget {
final String text;
const _StatChip(this.text);
@override
Widget build(BuildContext context) {
return Text(
text,
style: InouText.bodySmall.copyWith(
color: InouTheme.textMuted,
fontSize: 12,
),
);
}
}
class _AddDossierCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
// TODO: navigate to add dossier
},
borderRadius: InouTheme.borderRadiusLg,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: InouTheme.border,
width: 2,
style: BorderStyle.solid,
),
borderRadius: InouTheme.borderRadiusLg,
),
child: CustomPaint(
painter: _DashedBorderPainter(),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'+',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w300,
color: InouTheme.accent,
),
),
const SizedBox(height: 4),
Text(
'Add dossier',
style: InouText.body.copyWith(color: InouTheme.textMuted),
),
],
),
),
),
),
);
}
}
class _DashedBorderPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Empty - we use the container border for now
// Could implement dashed border if needed
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}