part of '../../app/app.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({ super.key, required this.token, required this.baseUrl, }); final String token; final String baseUrl; @override State createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { late final ApiClient _api; late Future> _future; final _homeSearch = TextEditingController(); int _selectedTab = 0; @override void initState() { super.initState(); _api = ApiClient(baseUrl: widget.baseUrl, token: widget.token); _future = _api.get('api/mobile/dashboard'); } @override void dispose() { _homeSearch.dispose(); super.dispose(); } void _reload() { setState(() { _future = _api.get('api/mobile/dashboard'); }); } Future _logout() async { await SessionStore.clear(); if (!mounted) { return; } Navigator.of( context, ).pushReplacement(MaterialPageRoute(builder: (_) => const LoginScreen())); } Future _searchAndOpenDetail(String query) async { final keyword = query.trim(); if (keyword.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Masukkan kata kunci sampel.')), ); return; } try { final data = await _api.get('api/mobile/examinations/search', { 'search': keyword, }); final item = asMap(data['item']); final id = (item['id'] as num?)?.toInt(); if (id == null) { throw ApiException('Data pemeriksaan tidak ditemukan.'); } if (!mounted) return; Navigator.of(context).push( MaterialPageRoute( builder: (_) => ExaminationDetailScreen( token: widget.token, baseUrl: widget.baseUrl, id: id, ), ), ); } on ApiException catch (error) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(error.message))); } } Future _scanAndSearch() async { final result = await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()), ); if (result == null || result.trim().isEmpty) { return; } _homeSearch.text = result.trim(); await _searchAndOpenDetail(result); } void _showSearchSheet() { showModalBottomSheet( context: context, showDragHandle: true, isScrollControlled: true, builder: (context) => SafeArea( child: Padding( padding: EdgeInsets.fromLTRB( 18, 0, 18, MediaQuery.viewInsetsOf(context).bottom + 18, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Cari Sampel', style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 12), TextField( controller: _homeSearch, autofocus: true, textInputAction: TextInputAction.search, onSubmitted: (value) { Navigator.of(context).pop(); _searchAndOpenDetail(value); }, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), labelText: 'No. Sampel / No. RM / Nama Pasien', border: const OutlineInputBorder(), suffixIcon: IconButton( onPressed: () async { Navigator.of(context).pop(); await _scanAndSearch(); }, icon: const Icon(Icons.qr_code_scanner_rounded), tooltip: 'Scan barcode', ), ), ), const SizedBox(height: 12), FilledButton.icon( onPressed: () { Navigator.of(context).pop(); _searchAndOpenDetail(_homeSearch.text); }, icon: const Icon(Icons.manage_search_rounded), label: const Text('Cari dan Buka Detail'), ), ], ), ), ), ); } void _openBook(Map book) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ExaminationListScreen( token: widget.token, baseUrl: widget.baseUrl, master: book['master']?.toString() ?? 'buku0', title: book['label']?.toString() ?? 'Pemeriksaan', ), ), ); } void _openEws(List warnings) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => EwsScreen( warnings: warnings, token: widget.token, baseUrl: widget.baseUrl, ), ), ); } void _openInitialWork(Map user, int notificationCount) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => InitialWorkScreen( token: widget.token, baseUrl: widget.baseUrl, user: user, notificationCount: notificationCount, ), ), ); } void _openSampleGroups( List books, Map user, int notificationCount, ) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => SampleGroupsScreen( books: books, token: widget.token, baseUrl: widget.baseUrl, user: user, notificationCount: notificationCount, ), ), ); } void _openAllSpecimens(Map user, int notificationCount) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => SpecimenRegisterScreen( token: widget.token, baseUrl: widget.baseUrl, master: 'ALL', title: 'Semua Spesimen', total: 0, user: user, notificationCount: notificationCount, ), ), ); } Future _openCriticalValues() async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => CriticalValuesScreen(token: widget.token, baseUrl: widget.baseUrl), ), ); if (mounted) { _reload(); } } void _showProfile(Map user) { showModalBottomSheet( context: context, showDragHandle: true, builder: (context) => SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ CircleAvatar( radius: 28, backgroundColor: const Color(0xFF0F766E), child: Text( _initials(user['nama']?.toString() ?? 'SP'), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w900, ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user['nama']?.toString() ?? '-', style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 2), Text( '${user['username'] ?? '-'} • ${user['previlage'] ?? '-'}', style: const TextStyle(color: Colors.black54), ), ], ), ), ], ), const SizedBox(height: 18), FilledButton.icon( onPressed: () { Navigator.of(context).pop(); _logout(); }, icon: const Icon(Icons.logout), label: const Text('Logout'), ), ], ), ), ), ); } void _handleBottomNav(int value, Map user) { setState(() => _selectedTab = value); if (value == 1) { _showSearchSheet(); } else if (value == 2) { _showProfile(user); setState(() => _selectedTab = 0); } } String _initials(String name) { final parts = name .trim() .split(RegExp(r'\s+')) .where((part) => part.isNotEmpty) .toList(); if (parts.isEmpty) { return 'SP'; } return parts.take(2).map((part) => part[0].toUpperCase()).join(); } @override Widget build(BuildContext context) { return Scaffold( body: RefreshIndicator( onRefresh: () async => _reload(), child: FutureBuilder>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const LoadingScreen(); } if (snapshot.hasError) { return ErrorView( message: _message(snapshot.error), onRetry: _reload, ); } final data = snapshot.data!; final user = asMap(data['user']); final summary = asMap(data['summary']); final warnings = asList(data['early_warning_groups']); final books = asList(data['books']); final notificationCount = (summary['criticalNotificationCount'] as num? ?? 0).toInt(); return ListView( padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), children: [ HomeTopBar( user: user, notificationCount: notificationCount, onNotifications: _openCriticalValues, onProfile: () => _showProfile(user), ), const SizedBox(height: 22), SampleSearchPanel( controller: _homeSearch, onSearch: () => _searchAndOpenDetail(_homeSearch.text), onScan: _scanAndSearch, ), const SizedBox(height: 24), const HomeSectionTitle(title: 'Menu Utama'), const SizedBox(height: 12), MainMenuGrid( books: books, onEwsTap: () => _openEws(warnings), onInitialWorkTap: () => _openInitialWork(user, notificationCount), onSpecimenTap: () => _openAllSpecimens(user, notificationCount), onBookTap: _openBook, ), const SizedBox(height: 24), EwsSummaryPanel( warnings: warnings, token: widget.token, baseUrl: widget.baseUrl, ), const SizedBox(height: 18), DetailSamplePanel( onTap: () => _openSampleGroups(books, user, notificationCount), total: summary['antrian_hari_ini'], ), const SizedBox(height: 12), ], ); }, ), ), bottomNavigationBar: FutureBuilder>( future: _future, builder: (context, snapshot) { final user = asMap(snapshot.data?['user']); return NavigationBar( selectedIndex: _selectedTab, onDestinationSelected: (value) => _handleBottomNav(value, user), destinations: const [ NavigationDestination( icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Beranda', ), NavigationDestination( icon: Icon(Icons.search), label: 'Cari sampel', ), NavigationDestination( icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profil', ), ], ); }, ), ); } } class HomeTopBar extends StatelessWidget { const HomeTopBar({ super.key, required this.user, required this.notificationCount, required this.onNotifications, required this.onProfile, }); final Map user; final int notificationCount; final VoidCallback onNotifications; final VoidCallback onProfile; @override Widget build(BuildContext context) { final initials = _initials(user['nama']?.toString() ?? 'SP'); return SafeArea( bottom: false, child: Row( children: [ Container( width: 66, height: 66, padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(18), ), child: Image.asset(kAppLogoAsset, fit: BoxFit.contain), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Mikrobiology Laboratory Information System', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: const Color(0xFF47517A), ), ), ], ), ), Stack( clipBehavior: Clip.none, children: [ IconButton( onPressed: onNotifications, icon: const Icon(Icons.notifications_none_rounded, size: 31), color: const Color(0xFF063B60), tooltip: 'Notifikasi nilai kritis', ), if (notificationCount > 0) Positioned( top: 6, right: 6, child: Container( padding: const EdgeInsets.all(5), constraints: const BoxConstraints(minWidth: 20), decoration: const BoxDecoration( color: Color(0xFFFF1D25), shape: BoxShape.circle, ), child: Text( notificationCount > 99 ? '99+' : '$notificationCount', textAlign: TextAlign.center, style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w800, ), ), ), ), ], ), const SizedBox(width: 8), InkWell( borderRadius: BorderRadius.circular(28), onTap: onProfile, child: Row( children: [ Container( width: 56, height: 56, alignment: Alignment.center, decoration: const BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [Color(0xFF10B981), Color(0xFF06B6D4)], ), ), child: Text( initials, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w900, fontSize: 20, ), ), ), const Icon(Icons.keyboard_arrow_down_rounded), ], ), ), ], ), ); } String _initials(String name) { final parts = name .trim() .split(RegExp(r'\s+')) .where((part) => part.isNotEmpty) .toList(); if (parts.isEmpty) { return 'SP'; } return parts.take(2).map((part) => part[0].toUpperCase()).join(); } } class SampleSearchPanel extends StatelessWidget { const SampleSearchPanel({ super.key, required this.controller, required this.onSearch, required this.onScan, }); final TextEditingController controller; final VoidCallback onSearch; final VoidCallback onScan; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFCFE0FF)), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: const Color(0xFF2563EB).withValues(alpha: 0.05), blurRadius: 18, offset: const Offset(0, 10), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon( Icons.manage_search_rounded, color: Color(0xFF0B7BFF), size: 38, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Cari sampel dengan cepat', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFF080D3D), ), ), const SizedBox(height: 3), Text( 'Cari dan pantau status sampel', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: const Color(0xFF47517A), ), ), ], ), ), const Icon( Icons.keyboard_arrow_up_rounded, color: Color(0xFF0B7BFF), ), ], ), const SizedBox(height: 18), Text( 'Masukkan kata kunci', style: Theme.of(context).textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w700, color: const Color(0xFF47517A), ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: controller, textInputAction: TextInputAction.search, onSubmitted: (_) => onSearch(), decoration: const InputDecoration( prefixIcon: Icon(Icons.search), hintText: 'Ketik No. Sampel, No. RM, Nama Pasien', border: OutlineInputBorder(), isDense: true, ), ), ), const SizedBox(width: 10), IconButton.outlined( onPressed: onScan, icon: const Icon(Icons.qr_code_scanner_rounded), tooltip: 'Scan barcode', ), const SizedBox(width: 10), SizedBox( width: 88, child: FilledButton( onPressed: onSearch, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text('Cari'), ), ), ], ), ], ), ); } } class HomeSectionTitle extends StatelessWidget { const HomeSectionTitle({super.key, required this.title}); final String title; @override Widget build(BuildContext context) { return Text( title, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFF080D3D), ), ); } } class MainMenuGrid extends StatelessWidget { const MainMenuGrid({ super.key, required this.books, required this.onEwsTap, required this.onInitialWorkTap, required this.onSpecimenTap, required this.onBookTap, }); final List books; final VoidCallback onEwsTap; final VoidCallback onInitialWorkTap; final VoidCallback onSpecimenTap; final ValueChanged> onBookTap; @override Widget build(BuildContext context) { final items = [ _HomeMenuItem( title: 'EWS', subtitle: 'Early Warning Score', icon: Icons.track_changes_rounded, color: const Color(0xFF0B7BFF), onTap: onEwsTap, ), _HomeMenuItem( title: 'Pengerjaan Awal', subtitle: 'Validasi & Verifikasi', icon: Icons.check_circle_outline_rounded, color: const Color(0xFF11A85B), onTap: onInitialWorkTap, ), _HomeMenuItem( title: 'Spesimen', subtitle: 'Catatan & Register', icon: Icons.menu_book_rounded, color: const Color(0xFFFF6B00), onTap: onSpecimenTap, ), ]; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: items.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 0.68, ), itemBuilder: (context, index) => _HomeMenuCard(item: items[index]), ); } } class _HomeMenuCard extends StatelessWidget { const _HomeMenuCard({required this.item}); final _HomeMenuItem item; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(14), onTap: item.onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE1E8F5)), borderRadius: BorderRadius.circular(14), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 46, height: 46, decoration: BoxDecoration( color: item.color.withValues(alpha: 0.13), borderRadius: BorderRadius.circular(16), ), child: Icon(item.icon, color: item.color, size: 29), ), const SizedBox(height: 8), Text( item.title, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w900, color: item.color, height: 1.05, ), ), const SizedBox(height: 6), Text( item.subtitle, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: const Color(0xFF47517A), fontWeight: FontWeight.w600, fontSize: 11, height: 1.2, ), ), ], ), ), ); } } class _HomeMenuItem { const _HomeMenuItem({ required this.title, required this.subtitle, required this.icon, required this.color, required this.onTap, }); final String title; final String subtitle; final IconData icon; final Color color; final VoidCallback onTap; } class EwsSummaryPanel extends StatelessWidget { const EwsSummaryPanel({ super.key, required this.warnings, required this.token, required this.baseUrl, }); final List warnings; final String token; final String baseUrl; @override Widget build(BuildContext context) { final totalTypes = warnings.length; final rows = warnings.take(3).map((item) => asMap(item)).toList(); return Container( decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE1E8F5)), borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 12, 14), child: Row( children: [ Expanded( child: Text( 'EWS - Early Warning System', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFF0B7BFF), ), ), ), const Icon( Icons.keyboard_arrow_up_rounded, color: Color(0xFF0B7BFF), ), ], ), ), const Divider(height: 1), Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: Text( 'Ringkasan per Jenis Spesimen', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFF080D3D), ), ), ), Text( 'Total $totalTypes jenis spesimen', style: const TextStyle( color: Color(0xFF47517A), fontWeight: FontWeight.w600, ), ), ], ), ), if (rows.isEmpty) const EmptyPanel(text: 'Tidak ada data Early Warning.') else ...rows.map( (row) => EwsSummaryRow(group: row, token: token, baseUrl: baseUrl), ), Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Wrap( spacing: 16, runSpacing: 8, children: const [ _LegendDot( color: Color(0xFFFF1D25), text: 'Melewati Target (TAT > Target)', ), _LegendDot(color: Color(0xFFFF8A00), text: 'Mendekati Target'), ], ), ), ], ), ); } } class EwsSummaryRow extends StatelessWidget { const EwsSummaryRow({ super.key, required this.group, required this.token, required this.baseUrl, }); final Map group; final String token; final String baseUrl; @override Widget build(BuildContext context) { final items = asList(group['items']); final total = (group['total'] as num? ?? items.length).toInt(); final late = _countByText(items, ['lewat', 'melewati', 'target']); final near = total - late > 0 ? total - late : 0; final title = group['subpoli']?.toString().isNotEmpty == true ? group['subpoli'].toString() : 'Tanpa Subpoli'; final subtitle = items.isEmpty ? 'Belum ada sampel' : asMap(items.first)['reques']?.toString() ?? 'Pemeriksaan aktif'; return InkWell( onTap: () { if (items.isEmpty) return; showModalBottomSheet( context: context, showDragHandle: true, builder: (context) => ListView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), children: [ Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 10), ...items.map( (item) => ExaminationCompactTile( item: asMap(item), token: token, baseUrl: baseUrl, ), ), ], ), ); }, child: Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 10), child: Row( children: [ _SpecimenIcon(title: title), const SizedBox(width: 12), Expanded( flex: 5, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title.toUpperCase(), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Color(0xFF080D3D), fontWeight: FontWeight.w900, ), ), const SizedBox(height: 5), Text( subtitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Color(0xFF47517A), fontWeight: FontWeight.w600, height: 1.35, ), ), ], ), ), _EwsCount( label: 'Melewati', value: late, color: const Color(0xFFFF1D25), ), _EwsCount( label: 'Mendekati', value: near, color: const Color(0xFFFF7A00), ), _EwsCount( label: 'Total', value: total, color: const Color(0xFF47517A), ), const Icon(Icons.chevron_right_rounded, color: Color(0xFF667095)), ], ), ), ); } int _countByText(List items, List keywords) { var count = 0; for (final item in items) { final text = asMap(item).values.join(' ').toLowerCase(); if (keywords.any(text.contains)) { count += 1; } } return count == 0 && items.isNotEmpty ? items.length : count; } } class _SpecimenIcon extends StatelessWidget { const _SpecimenIcon({required this.title}); final String title; @override Widget build(BuildContext context) { final lower = title.toLowerCase(); final color = lower.contains('darah') ? const Color(0xFF11A85B) : lower.contains('sputum') ? const Color(0xFF06A6D7) : const Color(0xFF8B2BEF); final icon = lower.contains('darah') ? Icons.water_drop_rounded : lower.contains('sputum') ? Icons.science_rounded : Icons.biotech_rounded; return Container( width: 58, height: 58, decoration: BoxDecoration( color: color.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: Icon(icon, color: color, size: 30), ); } } class _EwsCount extends StatelessWidget { const _EwsCount({ required this.label, required this.value, required this.color, }); final String label; final int value; final Color color; @override Widget build(BuildContext context) { return SizedBox( width: 66, child: Column( children: [ Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: color, fontSize: 10, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 6), Text( '$value', style: TextStyle( color: color, fontSize: 24, fontWeight: FontWeight.w900, ), ), ], ), ); } } class _LegendDot extends StatelessWidget { const _LegendDot({required this.color, required this.text}); final Color color; final String text; @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 8), Text( text, style: const TextStyle( color: Color(0xFF47517A), fontWeight: FontWeight.w600, ), ), ], ); } } class DetailSamplePanel extends StatelessWidget { const DetailSamplePanel({ super.key, required this.onTap, required this.total, }); final VoidCallback onTap; final Object? total; @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.circular(16), onTap: onTap, child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE1E8F5)), borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Detail Sampel', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFF080D3D), ), ), const SizedBox(height: 10), Text( 'Pilih jenis spesimen untuk melihat daftar sampel yang mendekati atau melewati target TAT.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: const Color(0xFF47517A), height: 1.5, ), ), const SizedBox(height: 8), BadgeLabel( text: '${total ?? 0} sampel hari ini', color: const Color(0xFF0B7BFF), ), ], ), ), const SizedBox(width: 12), Container( width: 86, height: 86, decoration: BoxDecoration( color: const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(18), ), child: const Icon( Icons.find_in_page_outlined, color: Color(0xFF0B7BFF), size: 52, ), ), ], ), ), ); } } class HeaderPanel extends StatelessWidget { const HeaderPanel({super.key, required this.user, required this.summary}); final Map user; final Map summary; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFF0F766E), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 54, height: 54, padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: Image.asset(kHospitalLogoAsset, fit: BoxFit.contain), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( kAppShortName, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.w800, ), ), const Text( kHospitalName, style: TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ), ], ), const SizedBox(height: 14), Text( user['nama']?.toString() ?? '-', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, ), ), Text( user['previlage']?.toString() ?? '-', style: const TextStyle(color: Colors.white70), ), const SizedBox(height: 16), Row( children: [ Expanded( child: SummaryItem(label: 'PPDS', value: summary['total_ppds']), ), Expanded( child: SummaryItem( label: 'Verifikasi', value: summary['butuh_verifikasi'], ), ), Expanded( child: SummaryItem( label: 'Hari Ini', value: summary['antrian_hari_ini'], ), ), ], ), ], ), ); } } class SummaryItem extends StatelessWidget { const SummaryItem({super.key, required this.label, required this.value}); final String label; final Object? value; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${value ?? 0}', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.w800, ), ), Text( label, style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], ); } } class SectionTitle extends StatelessWidget { const SectionTitle({super.key, required this.title, this.actionLabel}); final String title; final String? actionLabel; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ Expanded( child: Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800), ), ), if (actionLabel != null) BadgeLabel(text: actionLabel!, color: const Color(0xFFDC2626)), ], ), ); } } class EarlyWarningGroupCard extends StatelessWidget { const EarlyWarningGroupCard({ super.key, required this.group, required this.token, required this.baseUrl, }); final Map group; final String token; final String baseUrl; @override Widget build(BuildContext context) { final items = asList(group['items']); return Card( margin: const EdgeInsets.only(bottom: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: ExpansionTile( leading: const Icon( Icons.warning_amber_rounded, color: Color(0xFFDC2626), ), title: Text( group['subpoli']?.toString() ?? 'Tanpa Subpoli', style: const TextStyle(fontWeight: FontWeight.w700), ), subtitle: Text('${group['total'] ?? items.length} kasus warning'), children: items .map( (item) => ExaminationCompactTile( item: asMap(item), token: token, baseUrl: baseUrl, ), ) .toList(), ), ); } } class BookTile extends StatelessWidget { const BookTile({ super.key, required this.book, required this.token, required this.baseUrl, this.detailBuilder, }); final Map book; final String token; final String baseUrl; final Widget Function(Map book)? detailBuilder; @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.only(bottom: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: ListTile( leading: const CircleAvatar( backgroundColor: Color(0xFFE0F2F1), child: Icon(Icons.science_outlined, color: Color(0xFF0F766E)), ), title: Text(book['label']?.toString() ?? '-'), subtitle: Text('${book['total'] ?? 0} pemeriksaan aktif'), trailing: const Icon(Icons.chevron_right), onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => detailBuilder?.call(book) ?? ExaminationListScreen( token: token, baseUrl: baseUrl, master: book['master'].toString(), title: book['label']?.toString() ?? 'Pemeriksaan', ), ), ); }, ), ); } }