part of '../../app/app.dart'; class CriticalValuesScreen extends StatefulWidget { const CriticalValuesScreen({ super.key, required this.token, required this.baseUrl, }); final String token; final String baseUrl; @override State createState() => _CriticalValuesScreenState(); } class _CriticalValuesScreenState extends State { late final ApiClient _api; late Future> _future; @override void initState() { super.initState(); _api = ApiClient(baseUrl: widget.baseUrl, token: widget.token); _future = _load(); } Future> _load() { return _api.get('api/mobile/critical-values'); } void _reload() { setState(() { _future = _load(); }); } Future _markRead(Map item) async { final method = ValueNotifier(''); final recipient = TextEditingController(); final reason = TextEditingController(); await showModalBottomSheet( context: context, showDragHandle: true, isScrollControlled: true, builder: (context) => SafeArea( child: Padding( padding: EdgeInsets.fromLTRB( 18, 0, 18, MediaQuery.viewInsetsOf(context).bottom + 18, ), child: SingleChildScrollView( child: ValueListenableBuilder( valueListenable: method, builder: (context, value, _) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Tindak Lanjut Nilai Kritis', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w900, ), ), const SizedBox(height: 6), Text( '${item['nofoto'] ?? '-'} • ${item['nmpasien'] ?? '-'}', style: const TextStyle(color: Colors.black54), ), const SizedBox(height: 14), DropdownButtonFormField( initialValue: value.isEmpty ? null : value, isExpanded: true, decoration: const InputDecoration( labelText: 'Melalui', helperText: 'Kosongkan jika belum dilaporkan', border: OutlineInputBorder(), ), items: const [ DropdownMenuItem(value: 'Telpon', child: Text('Telpon')), DropdownMenuItem( value: 'Whatshap', child: Text('Whatshap'), ), DropdownMenuItem(value: 'Email', child: Text('Email')), DropdownMenuItem( value: 'Ketemu di Jalan', child: Text('Ketemu di Jalan'), ), ], onChanged: (next) => method.value = next ?? '', ), const SizedBox(height: 12), TextField( controller: recipient, decoration: const InputDecoration( labelText: 'Penerima Laporan', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: reason, minLines: 2, maxLines: 4, decoration: const InputDecoration( labelText: 'Alasan belum dilaporkan/Pasien meninggal', hintText: 'Meninggal / Pindah Rumah Sakit / dll', border: OutlineInputBorder(), ), ), const SizedBox(height: 14), FilledButton.icon( onPressed: () async { try { await _api.post( 'api/mobile/critical-values/${item['id']}/mark-read', { 'caratindaklanjut': method.value, 'penerima_laporan': recipient.text.trim(), 'alasan_belum_laporkan': reason.text.trim(), }, ); if (!context.mounted) return; Navigator.of(context).pop(); _reload(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Sample nilai kritis sudah ditindaklanjuti.', ), ), ); } on ApiException catch (error) { if (!context.mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(error.message))); } }, icon: const Icon(Icons.done_all_outlined), label: const Text('Mark as Read'), ), ], ), ), ), ), ), ); recipient.dispose(); reason.dispose(); method.dispose(); } @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( body: 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 pending = asList(data['pending_items']); final followed = asList(data['followed_items']); return SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 0), child: _CriticalValuesHeader( pendingCount: pending.length, onRefresh: _reload, ), ), Padding( padding: const EdgeInsets.fromLTRB(18, 22, 18, 0), child: _CriticalValuesSummary( pendingCount: pending.length, followedCount: followed.length, ), ), Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 0), child: Container( height: 48, padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(12), ), child: const TabBar( indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, indicator: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(9)), ), labelColor: Color(0xFF0B7BFF), unselectedLabelColor: Color(0xFF47517A), labelStyle: TextStyle(fontWeight: FontWeight.w900), tabs: [ Tab(text: 'Belum Dilaporkan'), Tab(text: 'Riwayat'), ], ), ), ), Expanded( child: TabBarView( children: [ CriticalValueList( items: pending, emptyText: 'Tidak ada sample nilai kritis yang perlu dilaporkan.', onMarkRead: _markRead, ), CriticalValueList( items: followed, emptyText: 'Belum ada riwayat tindak lanjut.', ), ], ), ), ], ), ); }, ), ), ); } } class _CriticalValuesHeader extends StatelessWidget { const _CriticalValuesHeader({ required this.pendingCount, required this.onRefresh, }); final int pendingCount; final VoidCallback onRefresh; @override Widget build(BuildContext context) { return Row( children: [ IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back_ios_new_rounded), color: const Color(0xFF080D3D), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( 'Nilai Kritis', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.headlineSmall ?.copyWith( color: const Color(0xFF080D3D), fontWeight: FontWeight.w900, ), ), ), const SizedBox(width: 8), BadgeLabel( text: '$pendingCount', color: const Color(0xFFFF1D25), ), ], ), const SizedBox(height: 4), Text( 'Notifikasi & Tindak Lanjut', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: const Color(0xFF47517A), fontWeight: FontWeight.w700, ), ), ], ), ), IconButton( onPressed: onRefresh, icon: const Icon(Icons.refresh_rounded), color: const Color(0xFF063B60), tooltip: 'Refresh', ), ], ); } } class _CriticalValuesSummary extends StatelessWidget { const _CriticalValuesSummary({ required this.pendingCount, required this.followedCount, }); final int pendingCount; final int followedCount; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE1E8F5)), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: const Color(0xFF2563EB).withValues(alpha: 0.05), blurRadius: 18, offset: const Offset(0, 10), ), ], ), child: Row( children: [ Expanded( child: _CriticalSummaryItem( label: 'Belum Dilaporkan', value: pendingCount, color: const Color(0xFFFF1D25), ), ), Container(width: 1, height: 48, color: const Color(0xFFE1E8F5)), Expanded( child: _CriticalSummaryItem( label: 'Riwayat', value: followedCount, color: const Color(0xFF0B7BFF), ), ), ], ), ); } } class _CriticalSummaryItem extends StatelessWidget { const _CriticalSummaryItem({ required this.label, required this.value, required this.color, }); final String label; final int value; final Color color; @override Widget build(BuildContext context) { return Column( children: [ Text( '$value', style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: color, fontWeight: FontWeight.w900, ), ), const SizedBox(height: 4), Text( label, textAlign: TextAlign.center, style: const TextStyle( color: Color(0xFF47517A), fontWeight: FontWeight.w800, ), ), ], ); } } class CriticalValueList extends StatelessWidget { const CriticalValueList({ super.key, required this.items, required this.emptyText, this.onMarkRead, }); final List items; final String emptyText; final ValueChanged>? onMarkRead; @override Widget build(BuildContext context) { if (items.isEmpty) { return EmptyPanel(text: emptyText); } return RefreshIndicator( onRefresh: () async {}, child: ListView.separated( padding: const EdgeInsets.fromLTRB(16, 14, 16, 24), itemCount: items.length, separatorBuilder: (_, _) => const SizedBox(height: 10), itemBuilder: (context, index) { final item = asMap(items[index]); return CriticalValueCard( item: item, index: index, onMarkRead: onMarkRead == null ? null : () => onMarkRead!(item), ); }, ), ); } } class CriticalValueCard extends StatelessWidget { const CriticalValueCard({ super.key, required this.item, required this.index, this.onMarkRead, }); final Map item; final int index; final VoidCallback? onMarkRead; @override Widget build(BuildContext context) { final followed = item['followed_up_at']?.toString().isNotEmpty == true; return Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: const BorderSide(color: Color(0xFFE1E8F5)), ), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundColor: const Color( 0xFFFF1D25, ).withValues(alpha: 0.12), child: Text( '${index + 1}', style: const TextStyle( color: Color(0xFFFF1D25), fontWeight: FontWeight.w900, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item['nmpasien']?.toString() ?? '-', style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 3), Text( '${item['nofoto'] ?? '-'} / RM ${item['noregister'] ?? '-'}', style: const TextStyle(color: Colors.black54), ), ], ), ), StatusPill( status: followed ? item['tat_status_label']?.toString() ?? '-' : 'Belum Dilaporkan', ), ], ), const SizedBox(height: 12), Wrap( spacing: 10, runSpacing: 8, children: [ BadgeLabel( text: item['asalpasien']?.toString() ?? '-', color: const Color(0xFF0F766E), ), BadgeLabel( text: item['nm_spesimen']?.toString() ?? '-', color: const Color(0xFF2563EB), ), ], ), const SizedBox(height: 12), DetailRow( label: 'Waktu Set Nilai Kritis', value: _timeWithUser( item['critical_set_at'], item['critical_set_by_name'], ), ), if (followed) ...[ DetailRow( label: 'Waktu Ditindaklanjuti', value: _timeWithUser( item['followed_up_at'], item['followed_up_by_name'], ), ), DetailRow( label: 'Selisih Waktu', value: item['tat_duration_label'], ), DetailRow(label: 'Aksi', value: _followUpLabel(item)), ], if (onMarkRead != null) Align( alignment: Alignment.centerRight, child: FilledButton.icon( onPressed: onMarkRead, icon: const Icon(Icons.done_all_outlined), label: const Text('Mark as Read'), ), ), ], ), ), ); } String _timeWithUser(Object? time, Object? user) { final first = time?.toString().isNotEmpty == true ? time.toString() : '-'; final second = user?.toString().isNotEmpty == true ? '\noleh $user' : ''; return '$first$second'; } String _followUpLabel(Map item) { final status = item['follow_up_status']?.toString() ?? ''; if (status == 'reported') { return 'Dilaporkan\nMelalui: ${item['follow_up_method'] ?? '-'}\nPenerima: ${item['follow_up_recipient'] ?? '-'}'; } if (status == 'not_reported') { return 'Belum Dilaporkan\nAlasan: ${item['follow_up_reason'] ?? '-'}'; } return '-'; } }