part of '../../app/app.dart'; class TbcExpertiseWizard extends StatefulWidget { const TbcExpertiseWizard({ super.key, required this.item, required this.user, required this.staffOptions, required this.textControllers, required this.selectValues, required this.multiValues, 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 textControllers; final Map selectValues; final Map> multiValues; 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() => _TbcExpertiseWizardState(); } class _TbcExpertiseWizardState extends State { late int _step; @override void initState() { super.initState(); _step = widget.initialStep.clamp(0, 6); } @override void didUpdateWidget(covariant TbcExpertiseWizard oldWidget) { super.didUpdateWidget(oldWidget); final nextStep = widget.initialStep.clamp(0, 6); if (nextStep != _step) { _step = nextStep; } } void _setStep(int step) { final nextStep = step.clamp(0, 6); 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: PatientStaffPanel( item: widget.item, user: widget.user, staffOptions: widget.staffOptions, staffValues: widget.staffValues, onChanged: widget.onChanged, ), ), Step( title: const Text('Riwayat & Identitas Nasional'), isActive: _step == 1, content: _historyAndIdentity(), ), Step( title: const Text('Jenis Pemeriksaan'), isActive: _step == 2, content: _testType(), ), Step( title: const Text('Hasil Pemeriksaan'), isActive: _step == 3, content: _examinationResultByType(), ), Step( title: const Text('Tes Kepekaan Antibiotik'), isActive: _step == 4, content: _antibioticSensitivity(), ), Step( title: const Text('Data Alat'), isActive: _step == 5, content: _toolData(), ), Step( title: const Text('Expertise'), isActive: _step == 6, content: _finalExpertise(), ), ], ); } Widget _historyAndIdentity() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CollapsibleExpertiseSection( index: 1, title: 'Jenis Terduga / Pasien TBC', completed: _hasAny([ 'jenis_pasien_tbc', 'id_noidsediaan', 'id_tanggalpengambilancontohuji', 'id_tanggalpengirimancontohuji', 'id_lamariwayatpengobatantb', ]), initiallyExpanded: !_hasAny(['jenis_pasien_tbc', 'id_noidsediaan']), children: [ _multiSelect( 'jenis_pasien_tbc', 'Jenis Terduga / Pasien TBC', const ['TBC SO', 'TBC RO', 'Anak', 'HIV', 'DM'], ), _text('id_noidsediaan', 'No. Identitas Sediaan'), _text( 'id_tanggalpengambilancontohuji', 'Tanggal Pengambilan Contoh Uji', ), _text( 'id_tanggalpengirimancontohuji', 'Tanggal Pengiriman Contoh Uji', ), _select( 'id_lamariwayatpengobatantb', 'Lama Riwayat Pengobatan Terduga/Pasien TBC', const [ 'Riwayat Pengobatan >= 5 tahun', 'Riwayat Pengobatan < 5 tahun', ], ), ], ), CollapsibleExpertiseSection( index: 2, title: 'Identitas Nasional', completed: _hasAny([ 'id_lokasianatomi', 'id_alasanpemeriksaan', 'id_noregfasyankes', 'id_noregkota', ]), children: [ _select('id_lokasianatomi', 'Lokasi Anatomi', const [ 'Paru', 'Ekstraparu', ]), _text('id_ekstraparu', 'Ekstraparu'), _select('id_alasanpemeriksaan', 'Alasan Pemeriksaan', const [ 'Diagnosis TBC', 'Diagnosis Baseline TBC', 'Akhir Pengobatan', ]), _text( 'id_bulankemajuantbfollow', 'Pemantauan kemajuan pengobatan (Bulan ke)', ), _text('id_bulanpemeriksaanulang', 'Pemeriksaan ulang (Bulan ke)'), _text( 'id_bulanpemeriksaansetelahselesai', 'Pemeriksaan setelah selesai pengobatan (Bulan ke)', ), _text('id_noregfasyankes', 'No.Reg.TBC/TBC RO Fasyankes'), _text('id_noregkota', 'No.Reg.TBC/TBC RO Kab/Kota'), ], ), CollapsibleExpertiseSection( index: 3, title: 'Contoh Uji dan Visual Dahak', completed: _hasAny([ 'id_contohuji', 'id_contohujilainnya', 'visual_dahak_sewaktu', 'visual_dahak_pagi', ]), children: [ _select('id_contohuji', 'Contoh Uji', const ['Dahak', 'Lainnya']), _text('id_contohujilainnya', 'Contoh Uji Lainnya'), _multiSelect('visual_dahak_sewaktu', 'Visual dahak Sewaktu', const [ 'Nanah Lendir', 'Bercak darah', 'Air liur', ]), _multiSelect('visual_dahak_pagi', 'Visual dahak Pagi', const [ 'Nanah Lendir', 'Bercak darah', 'Air liur', ]), ], ), ], ); } bool _hasAny(List names) { for (final name in names) { if ((widget.selectValues[name] ?? '').trim().isNotEmpty) { return true; } if ((widget.textControllers[name]?.text ?? '').trim().isNotEmpty) { return true; } if ((widget.multiValues[name] ?? const {}).isNotEmpty) { return true; } } return false; } Widget _testType() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CollapsibleExpertiseSection( index: 1, title: 'Jenis Pemeriksaan', completed: _hasAny(['id_jenispemeriksaantb']), initiallyExpanded: !_hasAny(['id_jenispemeriksaantb']), children: [ _select( 'id_jenispemeriksaantb', 'Jenis Pemeriksaan', _tbcTestTypes, ), ], ), ], ); } Widget _examinationResultByType() { final type = widget.selectValues['id_jenispemeriksaantb'] ?? ''; if (type == 'TCM XDR (Xpert)') { return _xdrResult(); } if (type == 'Biakan') { return _cultureResult(); } if (_tcmRifTypes.contains(type)) { return _tcmMtbRifResult(); } return _microscopicResult(); } Widget _microscopicResult() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle('Hasil Pemeriksaan Mikroskopis'), _text('hasilpemeriksaantmikroskopis_tglsewaktu', 'Tanggal Sewaktu'), _select( 'hasilpemeriksaantmikroskopis_hasilsewaktu', 'Hasil Sewaktu', _tbMicroscopeResults, ), _text('hasilpemeriksaantmikroskopis_tglpagi', 'Tanggal Pagi'), _select( 'hasilpemeriksaantmikroskopis_hasilpagi', 'Hasil Pagi', _tbMicroscopeResults, ), ], ); } Widget _tcmMtbRifResult() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle('Hasil Pemeriksaan TCM MTB Rif'), _text('hasilpemeriksaanttcmmtbrifxpert_tglsewaktu', 'Tanggal Sewaktu'), _text('hasilpemeriksaanttcmmtbrifxpert_kodeunik', 'Kode Unik Sewaktu'), _select( 'hasilpemeriksaanttcmmtbrifxpert_hasilsewaktu', 'Hasil Sewaktu', _tcmMtbRifResults, ), _text('hasilpemeriksaanttcmmtbrifxpert_tglpagi', 'Tanggal Pagi'), _text('hasilpemeriksaanttcmmtbrifxpert_kodeunikpagi', 'Kode Unik Pagi'), _select( 'hasilpemeriksaanttcmmtbrifxpert_hasilpagi', 'Hasil Pagi', _tcmMtbRifResults, ), ], ); } Widget _cultureResult() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle('Hasil Biakan'), _text('hasilpemeriksaantbbiakan_tgl', 'Tanggal Mikroskopis'), _select( 'hasilpemeriksaantbbiakan_pagi', 'Mikroskopis Pagi', _tbMicroscopeResults, ), _select( 'hasilpemeriksaantbbiakan_sewaktu', 'Mikroskopis Sewaktu', _tbMicroscopeResults, ), _text('hasilpemeriksaantbbiakan_tglbiakan', 'Tanggal Biakan'), _select( 'hasilpemeriksaantbbiakan_hasilbiakanmgit', 'Hasil Biakan (MGIT)', const ['Pos', 'Neg', 'NTM', 'KTM', 'TDL'], ), _select( 'hasilpemeriksaantbbiakan_hasilbiakanlj', 'Hasil Biakan (LJ)', const ['3+', '2+', '1+', '1-9', 'Neg', 'NTM', 'KTM', 'TDL'], ), ], ); } Widget _xdrResult() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle('TCM XDR (Xpert) - Pagi'), _text('hasilpemeriksaanttcmmtbxdrxpert_tglpagi', 'Tanggal Pagi'), _text('hasilpemeriksaanttcmmtbxdrxpert_kodeunikpagi', 'Kode Unik Pagi'), _xdrPanel('pagi'), const SizedBox(height: 12), _sectionTitle('TCM XDR (Xpert) - Sewaktu'), _text('hasilpemeriksaanttcmmtbxdrxpert_tglsewaktu', 'Tanggal Sewaktu'), _text('hasilpemeriksaanttcmmtbxdrxpert_kodeunik', 'Kode Unik Sewaktu'), _xdrPanel('sewaktu'), ], ); } Widget _xdrPanel(String prefix) { final normalized = prefix == 'pagi' ? 'pagi' : 'sewaktu'; final inhLowField = prefix == 'pagi' ? 'hasilpemeriksaanttcmmtbxdrxpert_paginhlow' : 'hasilpemeriksaanttcmmtbxdrxpert_sewaktunhlow'; return Card( margin: const EdgeInsets.only(bottom: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(12), child: Column( children: [ _select( 'hasilpemeriksaanttcmmtbxdrxpert_$normalized', 'TCM XDR', _xdrValidityResults, ), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}mtb', 'MTB', const ['Detected', 'Not Detected'], ), _select(inhLowField, 'H Low', _xdrSirResults), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}h', 'H', _xdrSirResults, ), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}fqlow', 'FQ Low', _xdrSirResults, ), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}fq', 'FQ', _xdrSirResults, ), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}amk', 'Amk', _xdrSirResults, ), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}km', 'Km', _xdrSirResults, ), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}cm', 'Cm', _xdrSirResults, ), _select( 'hasilpemeriksaanttcmmtbxdrxpert_${normalized}eto', 'Eto', _xdrSirResults, ), ], ), ), ); } Widget _antibioticSensitivity() { return AntibioticSensitivityPanel( textControllers: widget.textControllers, selectValues: widget.selectValues, onChanged: widget.onChanged, ); } Widget _toolData() { return ToolDataPanel( textControllers: widget.textControllers, selectValues: widget.selectValues, onChanged: widget.onChanged, ); } Widget _finalExpertise() { return 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'), ), ], ], ); } Widget _sectionTitle(String title, {bool compact = false}) { return Padding( padding: EdgeInsets.only(bottom: compact ? 8 : 12), child: Text( title, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w800, color: const Color(0xFF0F766E), ), ), ); } // 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: [ _sectionTitle(title, compact: true), _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 _multiSelect(String name, String label, List options) { final selected = widget.multiValues[name] ?? {}; return Padding( padding: const EdgeInsets.only(bottom: 12), child: CompactMultiSelectField( label: label, options: options, selected: selected, onChanged: (value) { widget.multiValues[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(), ), ), ); } } const List _tbcTestTypes = [ 'Mikroskopis', 'TCM MTB Rif (Xpert)', 'TCM XDR (Xpert)', 'TCM MTB Rif (Truenat)', 'TCM MTB Rif INH (BDMAX)', 'PCR Open System', 'LPA lini 1', 'LPA lini 2', 'Biakan', 'Paket standar uji kepekaan', ]; const Set _tcmRifTypes = { 'TCM MTB Rif (Xpert)', 'TCM MTB Rif (Truenat)', 'TCM MTB Rif INH (BDMAX)', 'LPA lini 1', 'LPA lini 2', }; const List _tbMicroscopeResults = ['+++', '++', '+', '1-9', 'Neg']; const List _tcmMtbRifResults = [ 'Neg', 'Rif Sen', 'Rif Res', 'Rif Indet', 'Invalid', 'Error', 'No result', ]; const List _xdrValidityResults = [ 'Valid', 'Invalid', 'No Result', 'Error', ]; const List _xdrSirResults = [ 'S', 'I', 'R', 'Invalid', 'Error', 'No Result', ];