part of '../../app/app.dart'; class CciExpertiseWizard extends StatefulWidget { const CciExpertiseWizard({ super.key, required this.item, required this.user, required this.staffOptions, required this.optionSets, required this.textControllers, required this.selectValues, required this.staffValues, required this.criticalValue, required this.isSupervisor, required this.saving, required this.onCriticalChanged, required this.onChanged, required this.onSave, required this.initialStep, required this.onStepChanged, }); final Map item; final Map user; final Map staffOptions; final Map optionSets; final Map textControllers; final Map selectValues; final Map staffValues; final bool criticalValue; final bool isSupervisor; final bool saving; final ValueChanged onCriticalChanged; final VoidCallback onChanged; final ValueChanged onSave; final int initialStep; final ValueChanged onStepChanged; @override State createState() => _CciExpertiseWizardState(); } class _CciExpertiseWizardState extends State { late int _step; @override void initState() { super.initState(); _step = widget.initialStep.clamp(0, 3); } @override void didUpdateWidget(covariant CciExpertiseWizard oldWidget) { super.didUpdateWidget(oldWidget); final nextStep = widget.initialStep.clamp(0, 3); if (nextStep != _step) { _step = nextStep; } } void _setStep(int step) { final nextStep = step.clamp(0, 3); setState(() => _step = nextStep); widget.onStepChanged(nextStep); } @override Widget build(BuildContext context) { return ExpertiseWizardShell( currentStep: _step, onStepChanged: _setStep, steps: [ Step( title: const Text('Pasien & Petugas'), isActive: _step == 0, content: Column( children: [ PatientStaffPanel( item: widget.item, user: widget.user, staffOptions: widget.staffOptions, staffValues: widget.staffValues, onChanged: widget.onChanged, ), ], ), ), Step( title: const Text('CCI'), isActive: _step == 1, content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Pemeriksaan Candida Colonization Index', style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800), ), const SizedBox(height: 12), _select( 'id_sputum', 'Sputum', _options('jsonsputum', ['Pilih Salah Satu']), ), _select( 'id_swabtenggorok', 'Swab Tenggorok', _options('jsonswabtenggorok', ['Pilih Salah Satu']), ), _select( 'id_urine', 'Urine', _options('jsonurine', ['Pilih Salah Satu']), ), _select( 'id_swabperineum', 'Swab Perineum', _options('jsonswabperineum', ['Pilih Salah Satu']), ), ], ), ), Step( title: const Text('Data Alat'), isActive: _step == 2, content: ToolDataPanel( textControllers: widget.textControllers, selectValues: widget.selectValues, onChanged: widget.onChanged, ), ), Step( title: const Text('Final'), isActive: _step == 3, content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ExpertiseHtmlEditor( controller: widget.textControllers['keterangan'] ??= TextEditingController(), label: 'Expertise', ), CheckboxListTile( value: widget.criticalValue, onChanged: (value) => widget.onCriticalChanged(value ?? false), contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, title: const Text('Nilai Kritis'), ), FilledButton.icon( onPressed: widget.saving ? null : () => widget.onSave('Draft'), icon: widget.saving ? const SizedBox.square( dimension: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.save_outlined), label: const Text('Save as Draft'), ), if (widget.isSupervisor) ...[ OutlinedButton.icon( onPressed: widget.saving ? null : () => widget.onSave('preliminary'), icon: const Icon(Icons.done_all_outlined), label: const Text('Save Preliminary result'), ), OutlinedButton.icon( onPressed: widget.saving ? null : () => widget.onSave('verifikasi'), icon: const Icon(Icons.verified_outlined), label: const Text('Save Final Result'), ), ] else ...[ OutlinedButton.icon( onPressed: widget.saving ? null : () => widget.onSave('Permohonan Verifikasi'), icon: const Icon(Icons.send_outlined), label: const Text('Save and Send To SPV'), ), OutlinedButton.icon( onPressed: widget.saving ? null : () => widget.onSave('Permohonan Verifikasi Preliminary'), icon: const Icon(Icons.outgoing_mail), label: const Text('Kirim SPV Preliminary'), ), ], ], ), ), ], ); } List _options(String key, List fallback) { final values = asList( widget.optionSets[key], ).map((item) => item.toString()).where((item) => item.isNotEmpty).toList(); return values.isEmpty ? fallback : values; } // ignore: unused_element Widget _toolSection({ required String title, required String bacteriaField, String? antibioticField, required String sirField, required String colonyField, required String colonyTextField, required String printField, }) { return Card( margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of( context, ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800), ), const SizedBox(height: 10), _text(bacteriaField, 'Bakteri'), if (antibioticField != null) _text(antibioticField, 'Set Antibiotik'), _select(sirField, 'SIR', _sirOptions), _select(colonyField, 'Hitung Koloni', _colonyOptions), _text(colonyTextField, 'Hitung Koloni Lainnya'), _select(printField, 'Cetak', const ['YA', 'TIDAK']), ], ), ), ); } Widget _select(String name, String label, List options) { final current = widget.selectValues[name]; final effectiveOptions = [ if (current != null && current.isNotEmpty && !options.contains(current)) current, ...options, ]; return Padding( padding: const EdgeInsets.only(bottom: 12), child: DropdownButtonFormField( initialValue: current != null && current.isNotEmpty && effectiveOptions.contains(current) ? current : null, isExpanded: true, decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), ), items: effectiveOptions .map( (option) => DropdownMenuItem(value: option, child: Text(option)), ) .toList(), onChanged: (value) { widget.selectValues[name] = value ?? ''; widget.onChanged(); }, ), ); } Widget _text( String name, String label, { int minLines = 1, int maxLines = 1, }) { final controller = widget.textControllers[name] ??= TextEditingController(); return Padding( padding: const EdgeInsets.only(bottom: 12), child: TextField( controller: controller, minLines: minLines, maxLines: maxLines, decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), ), ), ); } } class PatientStaffPanel extends StatelessWidget { const PatientStaffPanel({ super.key, required this.item, required this.user, required this.staffOptions, required this.staffValues, required this.onChanged, }); final Map item; final Map user; final Map staffOptions; final Map staffValues; final VoidCallback onChanged; @override Widget build(BuildContext context) { final status = item['status']?.toString() ?? '-'; final genderAge = '${item['jkpasien'] ?? '-'}, ${item['usia'] ?? '-'}'; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( radius: 24, backgroundColor: const Color( 0xFF0F766E, ).withValues(alpha: 0.12), child: const Icon( Icons.person_outline, color: Color(0xFF0F766E), ), ), 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( 'RM ${_textValue(item['noregister'])} ยท Lab ${_textValue(item['nofoto'])}', style: Theme.of(context).textTheme.bodySmall ?.copyWith(color: Colors.black54), ), ], ), ), StatusPill(status: status), ], ), const SizedBox(height: 14), _InfoGrid( items: [ _InfoItem('Order', item['reques'], Icons.receipt_long), _InfoItem( 'Dokter Pengirim', item['klinisi'] ?? item['nmdokter'], Icons.local_hospital_outlined, ), _InfoItem( 'Asal Pasien', item['asalpasien'], Icons.apartment_outlined, ), _InfoItem('Gender, Age', genderAge, Icons.badge_outlined), _InfoItem('Phone', item['tlppasien'], Icons.call_outlined), _InfoItem( 'Tanggal Registrasi', item['tanggalregis'] ?? item['daftar'], Icons.event_available_outlined, ), _InfoItem( 'Tanggal Pengambilan', item['tanggalsampel'], Icons.science_outlined, ), _InfoItem( 'Cara Pengambilan', item['pengambilan'], Icons.medical_services_outlined, ), _InfoItem( 'Asal Pengambilan', item['asalpengirim'], Icons.output_outlined, ), _InfoItem( 'Spesimen', item['kesimpulan'] ?? item['nm_spesimen'], Icons.biotech_outlined, ), ], ), const SizedBox(height: 8), _InfoTile( label: 'Address', value: item['alamatpasien'], icon: Icons.place_outlined, ), ], ), ), ), const SizedBox(height: 12), Card( margin: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Petugas', style: Theme.of( context, ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w900), ), const SizedBox(height: 12), _staffSelect('analis', 'ATLM', asList(staffOptions['analis'])), _staffSelect('ppds3', 'PPDS', asList(staffOptions['ppds'])), _staffSelect('dokter', 'SPV', asList(staffOptions['dokters'])), ], ), ), ), const SizedBox(height: 12), _InfoTile( label: 'Klinis/Diagnosis', value: item['klinis'], icon: Icons.assignment_outlined, ), ], ); } Widget _staffSelect(String name, String label, List users) { final current = staffValues[name]; final items = users.map(asMap).toList(); final hasCurrent = items.any((item) => item['id'].toString() == current); return Padding( padding: const EdgeInsets.only(bottom: 12), child: DropdownButtonFormField( initialValue: hasCurrent ? current : null, isExpanded: true, decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.account_circle_outlined), ), items: [ const DropdownMenuItem(value: '0', child: Text('Pilih')), ...items.map( (item) => DropdownMenuItem( value: item['id'].toString(), child: Text(item['nama']?.toString() ?? '-'), ), ), ], onChanged: (value) { staffValues[name] = value ?? '0'; onChanged(); }, ), ); } String _textValue(Object? value) { final text = value?.toString() ?? ''; return text.trim().isEmpty ? '-' : decodeHtmlEntities(text.trim()); } } class _InfoGrid extends StatelessWidget { const _InfoGrid({required this.items}); final List<_InfoItem> items; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final columns = constraints.maxWidth >= 520 ? 2 : 1; return Wrap( spacing: 10, runSpacing: 10, children: [ for (final item in items) SizedBox( width: columns == 1 ? constraints.maxWidth : (constraints.maxWidth - 10) / 2, child: _InfoTile( label: item.label, value: item.value, icon: item.icon, compact: true, ), ), ], ); }, ); } } class _InfoTile extends StatelessWidget { const _InfoTile({ required this.label, required this.value, required this.icon, this.compact = false, }); final String label; final Object? value; final IconData icon; final bool compact; @override Widget build(BuildContext context) { final cleanValue = _clean(value); return Container( padding: EdgeInsets.all(compact ? 10 : 12), decoration: BoxDecoration( color: const Color(0xFFF8FAFC), border: Border.all(color: const Color(0xFFE2E8F0)), borderRadius: BorderRadius.circular(8), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 18, color: const Color(0xFF0F766E)), const SizedBox(width: 9), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: Colors.black54, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 3), Text( cleanValue, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w800, color: Colors.black87, ), ), ], ), ), ], ), ); } String _clean(Object? value) { final text = value?.toString() ?? ''; return text.trim().isEmpty ? '-' : decodeHtmlEntities(text.trim()); } } class _InfoItem { const _InfoItem(this.label, this.value, this.icon); final String label; final Object? value; final IconData icon; } const List _sirOptions = [ 'ESBL', 'MRSA', 'MDR', 'XDR', 'PDR', 'Carbapenem Resistant', 'MDR Carbapenem Resistant', 'XDR Carbapenem Resistant', ]; const List _colonyOptions = [ '>= 10^5 CFU/ml Urine (Bakteriuria bermakna)', '>= 10^5 CFU/ml Urine (Candiduria bermakna)', '>= 10^3 CFU/ml Urine (Bakteriuria bermakna)', '8 x 10^3 CFU/ml Urine (Bakteriuria bermakna)', 'lainnya', ];