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 DossierPage extends StatelessWidget { final String dossierId; const DossierPage({super.key, required this.dossierId}); @override Widget build(BuildContext context) { final data = getDossierById(dossierId); if (data == null) { return InouAuthPage( userName: 'Johan', onProfileTap: () => context.go('/profile'), onLogoutTap: () => context.go('/login'), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Dossier not found', style: InouText.sectionTitle), const SizedBox(height: 16), InouButton( text: '← Back to dossiers', variant: ButtonVariant.secondary, onPressed: () => context.go('/dashboard'), ), ], ), ), ); } return InouAuthPage( userName: 'Johan', onProfileTap: () => context.go('/profile'), onLogoutTap: () => context.go('/login'), child: SingleChildScrollView( padding: EdgeInsets.symmetric( horizontal: InouTheme.spaceXl, vertical: InouTheme.spaceXxxl, ), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: InouTheme.maxWidthNarrow), // 800px per styleguide child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header _DossierHeader(data: data), const SizedBox(height: 32), // Data sections _ImagingSection(studies: data.studies, dossierId: dossierId), _LabsSection(labs: data.labs), if (data.documents.isNotEmpty) _DocumentsSection(documents: data.documents), if (data.procedures.isNotEmpty) _ProceduresSection(procedures: data.procedures), if (data.assessments.isNotEmpty) _AssessmentsSection(assessments: data.assessments), if (data.hasGenome) _GeneticsSection(categories: data.geneticCategories), _UploadsSection(count: data.uploadCount, size: data.uploadSize, canEdit: data.canEdit), if (data.medications.isNotEmpty) _MedicationsSection(medications: data.medications), if (data.symptoms.isNotEmpty) _SymptomsSection(symptoms: data.symptoms), if (data.hospitalizations.isNotEmpty) _HospitalizationsSection(hospitalizations: data.hospitalizations), if (data.therapies.isNotEmpty) _TherapiesSection(therapies: data.therapies), _VitalsSection(), // Coming soon _PrivacySection(accessList: data.accessList, dossierId: dossierId, canManageAccess: data.canManageAccess), const SizedBox(height: 48), const InouFooter(), ], ), ), ), ), ); } } class _DossierHeader extends StatelessWidget { final DossierData data; const _DossierHeader({required this.data}); @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(data.dossier.name, style: InouText.pageTitle), const SizedBox(height: 8), Text( [ if (data.dossier.dateOfBirth != null) 'Born: ${data.dossier.dateOfBirth}', if (data.dossier.sex != null) data.dossier.sex, ].join(' · '), style: InouText.intro, ), ], ), ), InouButton( text: '← Back to dossiers', variant: ButtonVariant.secondary, size: ButtonSize.small, onPressed: () => context.go('/dashboard'), ), ], ); } } // ============================================ // DATA SECTION CARDS // ============================================ class _ImagingSection extends StatelessWidget { final List studies; final String dossierId; const _ImagingSection({required this.studies, required this.dossierId}); @override Widget build(BuildContext context) { final totalSlices = studies.fold(0, (sum, s) => sum + s.series.fold(0, (ss, ser) => ss + ser.sliceCount)); return _DataCard( title: 'IMAGING', indicatorColor: InouTheme.indicatorImaging, summary: studies.isEmpty ? 'No imaging data' : '${studies.length} studies, $totalSlices slices', trailing: studies.isNotEmpty ? InouButton( text: 'Open viewer', size: ButtonSize.small, onPressed: () { // TODO: open viewer }, ) : null, child: studies.isEmpty ? null : Column( children: [ for (var i = 0; i < studies.length && i < 5; i++) _ImagingStudyRow(study: studies[i], dossierId: dossierId), if (studies.length > 5) _ShowMoreRow( text: 'Show all ${studies.length} studies', onTap: () { // TODO: expand }, ), ], ), ); } } class _ImagingStudyRow extends StatefulWidget { final ImagingStudy study; final String dossierId; const _ImagingStudyRow({required this.study, required this.dossierId}); @override State<_ImagingStudyRow> createState() => _ImagingStudyRowState(); } class _ImagingStudyRowState extends State<_ImagingStudyRow> { bool _expanded = false; @override Widget build(BuildContext context) { final hasSeries = widget.study.seriesCount > 1; return Column( children: [ InkWell( onTap: hasSeries ? () => setState(() => _expanded = !_expanded) : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ if (hasSeries) SizedBox( width: 20, child: Text( _expanded ? '−' : '+', style: InouText.mono.copyWith(color: InouTheme.textMuted), ), ) else const SizedBox(width: 20), const SizedBox(width: 12), Expanded( child: Text( widget.study.description, style: InouText.body.copyWith(fontWeight: FontWeight.w500), ), ), if (hasSeries) Text( '${widget.study.seriesCount} series', style: InouText.mono, ), const SizedBox(width: 16), Text( _formatDate(widget.study.date), style: InouText.mono.copyWith(color: InouTheme.textMuted), ), const SizedBox(width: 8), Icon(Icons.arrow_forward, size: 16, color: InouTheme.accent), ], ), ), ), if (_expanded) Container( color: InouTheme.bg, child: Column( children: [ for (final series in widget.study.series) if (series.sliceCount > 0) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ const SizedBox(width: 32), Expanded( child: Text( series.description ?? series.modality, style: InouText.bodySmall, ), ), Text( '${series.sliceCount} ${series.sliceCount == 1 ? 'slice' : 'slices'}', style: InouText.mono, ), const SizedBox(width: 8), Icon(Icons.arrow_forward, size: 14, color: InouTheme.accent), ], ), ), ], ), ), const Divider(height: 1), ], ); } String _formatDate(String yyyymmdd) { if (yyyymmdd.length != 8) return yyyymmdd; return '${yyyymmdd.substring(4, 6)}/${yyyymmdd.substring(6, 8)}/${yyyymmdd.substring(0, 4)}'; } } class _LabsSection extends StatelessWidget { final List labs; const _LabsSection({required this.labs}); @override Widget build(BuildContext context) { return _DataCard( title: 'LABS', indicatorColor: InouTheme.indicatorLabs, summary: labs.isEmpty ? 'No lab data' : '${labs.length} results', child: labs.isEmpty ? null : Column( children: [ for (final lab in labs) _DataRow(item: lab), ], ), ); } } class _DocumentsSection extends StatelessWidget { final List documents; const _DocumentsSection({required this.documents}); @override Widget build(BuildContext context) { return _DataCard( title: 'RECORDS', indicatorColor: InouTheme.indicatorRecords, summary: '${documents.length} documents', child: Column( children: [ for (final doc in documents) _DataRow(item: doc, showType: true), ], ), ); } } class _ProceduresSection extends StatelessWidget { final List procedures; const _ProceduresSection({required this.procedures}); @override Widget build(BuildContext context) { return _DataCard( title: 'PROCEDURES & SURGERY', indicatorColor: InouTheme.danger, summary: '${procedures.length} procedures', child: Column( children: [ for (final proc in procedures) _DataRow(item: proc), ], ), ); } } class _AssessmentsSection extends StatelessWidget { final List assessments; const _AssessmentsSection({required this.assessments}); @override Widget build(BuildContext context) { return _DataCard( title: 'CLINICAL ASSESSMENTS', indicatorColor: const Color(0xFF7C3AED), summary: '${assessments.length} assessments', child: Column( children: [ for (final assessment in assessments) _DataRow(item: assessment), ], ), ); } } class _GeneticsSection extends StatelessWidget { final List categories; const _GeneticsSection({required this.categories}); @override Widget build(BuildContext context) { final totalShown = categories.fold(0, (sum, c) => sum + c.shown); final totalHidden = categories.fold(0, (sum, c) => sum + c.hidden); return _DataCard( title: 'GENETICS', indicatorColor: InouTheme.indicatorGenetics, summary: '$totalShown variants${totalHidden > 0 ? ' ($totalHidden hidden)' : ''}', trailing: totalHidden > 0 ? InouButton( text: 'Show all', size: ButtonSize.small, onPressed: () { // TODO: show warning modal }, ) : null, child: Column( children: [ for (final cat in categories.take(5)) if (cat.shown > 0) _GeneticCategoryRow(category: cat), if (categories.length > 5) _ShowMoreRow( text: 'Show all ${categories.length} categories', onTap: () {}, ), ], ), ); } } class _GeneticCategoryRow extends StatelessWidget { final GeneticCategory category; const _GeneticCategoryRow({required this.category}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ const SizedBox( width: 20, child: Text('+', style: TextStyle(color: InouTheme.textMuted)), ), const SizedBox(width: 12), Expanded( child: Text( category.displayName, style: InouText.body.copyWith(fontWeight: FontWeight.w500), ), ), Text( '${category.shown} variants${category.hidden > 0 ? ' (${category.hidden} hidden)' : ''}', style: InouText.bodySmall.copyWith(color: InouTheme.textMuted), ), ], ), ); } } class _UploadsSection extends StatelessWidget { final int count; final String size; final bool canEdit; const _UploadsSection({required this.count, required this.size, required this.canEdit}); @override Widget build(BuildContext context) { return _DataCard( title: 'UPLOADS', indicatorColor: InouTheme.indicatorUploads, summary: count == 0 ? 'No files' : '$count files, $size', trailing: InouButton( text: 'Manage', size: ButtonSize.small, variant: canEdit ? ButtonVariant.secondary : ButtonVariant.secondary, onPressed: canEdit ? () {} : null, ), ); } } class _MedicationsSection extends StatelessWidget { final List medications; const _MedicationsSection({required this.medications}); @override Widget build(BuildContext context) { return _DataCard( title: 'MEDICATIONS', indicatorColor: InouTheme.indicatorMedications, summary: '${medications.length} medications', child: Column( children: [ for (final med in medications) _DataRow(item: med), ], ), ); } } class _SymptomsSection extends StatelessWidget { final List symptoms; const _SymptomsSection({required this.symptoms}); @override Widget build(BuildContext context) { return _DataCard( title: 'SYMPTOMS', indicatorColor: const Color(0xFFF59E0B), summary: '${symptoms.length} symptoms', child: Column( children: [ for (final symptom in symptoms) _DataRow(item: symptom), ], ), ); } } class _HospitalizationsSection extends StatelessWidget { final List hospitalizations; const _HospitalizationsSection({required this.hospitalizations}); @override Widget build(BuildContext context) { return _DataCard( title: 'HOSPITALIZATIONS', indicatorColor: const Color(0xFFEF4444), summary: '${hospitalizations.length} hospitalizations', child: Column( children: [ for (final hosp in hospitalizations) _DataRow(item: hosp), ], ), ); } } class _TherapiesSection extends StatelessWidget { final List therapies; const _TherapiesSection({required this.therapies}); @override Widget build(BuildContext context) { return _DataCard( title: 'THERAPIES', indicatorColor: const Color(0xFF10B981), summary: '${therapies.length} therapies', child: Column( children: [ for (final therapy in therapies) _DataRow(item: therapy), ], ), ); } } class _VitalsSection extends StatelessWidget { @override Widget build(BuildContext context) { return _DataCard( title: 'VITALS', indicatorColor: InouTheme.indicatorVitals, summary: 'Track blood pressure, weight, temperature, and more', trailing: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: InouTheme.bg, border: Border.all(color: InouTheme.border), borderRadius: BorderRadius.circular(4), ), child: Text( 'COMING SOON', style: InouText.labelCaps.copyWith(color: InouTheme.textMuted), ), ), comingSoon: true, ); } } class _PrivacySection extends StatelessWidget { final List accessList; final String dossierId; final bool canManageAccess; const _PrivacySection({ required this.accessList, required this.dossierId, required this.canManageAccess, }); @override Widget build(BuildContext context) { return _DataCard( title: 'PRIVACY', indicatorColor: InouTheme.indicatorPrivacy, summary: '${accessList.length} people with access', child: Column( children: [ for (final access in accessList) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${access.name}${access.isSelf ? ' (you)' : ''}${access.isPending ? ' (pending)' : ''}', style: InouText.body.copyWith(fontWeight: FontWeight.w500), ), Text( '${access.relation}${access.canEdit ? ' · can edit' : ''}', style: InouText.bodySmall.copyWith(color: InouTheme.textMuted), ), ], ), ), if (canManageAccess && !access.isSelf) ...[ InouButton( text: 'Edit', size: ButtonSize.small, variant: ButtonVariant.secondary, onPressed: () {}, ), const SizedBox(width: 8), InouButton( text: 'Remove', size: ButtonSize.small, variant: ButtonVariant.danger, onPressed: () {}, ), ], ], ), ), // Privacy actions row Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: InouTheme.bg, border: Border(top: BorderSide(color: InouTheme.border)), ), child: Row( children: [ _PrivacyAction(text: 'Share access', onTap: () {}), const SizedBox(width: 24), if (canManageAccess) ...[ _PrivacyAction(text: 'Manage permissions', onTap: () {}), const SizedBox(width: 24), ], _PrivacyAction(text: 'View audit log', onTap: () {}), const SizedBox(width: 24), _PrivacyAction(text: 'Export data', onTap: () {}), ], ), ), ], ), ); } } class _PrivacyAction extends StatelessWidget { final String text; final VoidCallback onTap; const _PrivacyAction({required this.text, required this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Text( text, style: InouText.bodySmall.copyWith(color: InouTheme.accent), ), ); } } // ============================================ // SHARED WIDGETS // ============================================ class _DataCard extends StatelessWidget { final String title; final Color indicatorColor; final String summary; final Widget? trailing; final Widget? child; final bool comingSoon; const _DataCard({ required this.title, required this.indicatorColor, required this.summary, this.trailing, this.child, this.comingSoon = false, }); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: InouTheme.bgCard, border: Border.all(color: InouTheme.border), borderRadius: InouTheme.borderRadiusLg, ), child: Opacity( opacity: comingSoon ? 0.6 : 1.0, child: Column( children: [ // Header Padding( padding: const EdgeInsets.all(16), child: Row( children: [ // Indicator bar Container( width: 4, height: 32, decoration: BoxDecoration( color: indicatorColor, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 12), // Title and summary Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: InouText.labelCaps.copyWith( letterSpacing: 0.8, ), ), const SizedBox(height: 2), Text( summary, style: InouText.bodySmall.copyWith(color: InouTheme.textMuted), ), ], ), ), if (trailing != null) trailing!, ], ), ), // Content if (child != null) Container( decoration: BoxDecoration( border: Border(top: BorderSide(color: InouTheme.border)), ), child: child, ), ], ), ), ); } } class _DataRow extends StatelessWidget { final DataItem item; final bool showType; const _DataRow({required this.item, this.showType = false}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: InouTheme.border, style: BorderStyle.solid)), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.value, style: InouText.body.copyWith(fontWeight: FontWeight.w500), ), if (item.summary != null) Text( item.summary!, style: InouText.bodySmall.copyWith(color: InouTheme.textMuted), ), ], ), ), if (showType && item.type != null) ...[ Text( item.type!, style: InouText.bodySmall.copyWith(color: InouTheme.textMuted), ), const SizedBox(width: 16), ], if (item.date != null) Text( item.date!, style: InouText.mono.copyWith(color: InouTheme.textMuted), ), ], ), ); } } class _ShowMoreRow extends StatelessWidget { final String text; final VoidCallback onTap; const _ShowMoreRow({required this.text, required this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), alignment: Alignment.center, decoration: BoxDecoration( border: Border(top: BorderSide(color: InouTheme.border)), ), child: Text( text, style: InouText.bodySmall.copyWith(color: InouTheme.textMuted), ), ), ); } }