Files
lis/mylis/lib/screens/expertise/tbc_expertise_wizard.dart
T
2026-07-21 19:08:27 +07:00

670 lines
20 KiB
Dart

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,
});
final Map<String, dynamic> item;
final Map<String, dynamic> user;
final Map<String, dynamic> staffOptions;
final Map<String, TextEditingController> textControllers;
final Map<String, String> selectValues;
final Map<String, Set<String>> multiValues;
final Map<String, String> staffValues;
final bool criticalValue;
final bool isSupervisor;
final bool saving;
final ValueChanged<bool> onCriticalChanged;
final VoidCallback onChanged;
final ValueChanged<String> onSave;
@override
State<TbcExpertiseWizard> createState() => _TbcExpertiseWizardState();
}
class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
int _step = 0;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 6));
}
void _previous() {
setState(() => _step = (_step - 1).clamp(0, 6));
}
@override
Widget build(BuildContext context) {
return Stepper(
currentStep: _step,
onStepTapped: (step) => setState(() => _step = step),
controlsBuilder: (context, details) {
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
children: [
OutlinedButton.icon(
onPressed: _step == 0 ? null : _previous,
icon: const Icon(Icons.chevron_left),
label: const Text('Previous'),
),
const SizedBox(width: 8),
FilledButton.icon(
onPressed: _step == 6 ? null : _next,
icon: const Icon(Icons.chevron_right),
label: const Text('Next'),
),
],
),
);
},
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: [
_sectionTitle('Jenis Terduga / Pasien TBC'),
_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',
],
),
_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'),
_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',
]),
],
);
}
Widget _testType() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Jenis Pemeriksaan'),
_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 Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Tes Kepekaan Antibiotik'),
Text(
'S: Sensitif, I: Intermediate, R: Resisten',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
for (final media in _cultureMedia)
Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(media.label, compact: true),
_select(
media.statusField,
'Status Pertumbuhan',
_growthOptions,
),
_text(media.noteField, 'Catatan'),
],
),
),
),
],
);
}
Widget _toolData() {
return Column(
children: [
_toolSection(
title: 'Vitek',
bacteriaField: 'bakteri',
sirField: 'bakterisir',
colonyField: 'id_bakterihitungkol',
colonyTextField: 'id_bakterihitungkolteks',
printField: 'id_baktericetak',
),
_toolSection(
title: 'MALDITOF',
bacteriaField: 'id_bakteri01',
antibioticField: 'id_antibiotikmanual1',
sirField: 'id_bakterisir01',
colonyField: 'id_bakterihitungkol01',
colonyTextField: 'id_bakterihitungkolteks01',
printField: 'id_bakteri01cetak',
),
_toolSection(
title: 'Bakteri Manual I',
bacteriaField: 'id_bakteri02',
antibioticField: 'id_antibiotikmanual2',
sirField: 'id_bakterisir02',
colonyField: 'id_bakterihitungkol02',
colonyTextField: 'id_bakterihitungkolteks02',
printField: 'id_bakteri02cetak',
),
],
);
}
Widget _finalExpertise() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
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),
),
),
);
}
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<String> 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<String>(
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<String> options) {
final selected = widget.multiValues[name] ?? <String>{};
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InputDecorator(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
child: Wrap(
spacing: 8,
runSpacing: 4,
children: options
.map(
(option) => FilterChip(
label: Text(option),
selected: selected.contains(option),
onSelected: (checked) {
if (checked) {
selected.add(option);
} else {
selected.remove(option);
}
widget.multiValues[name] = selected;
widget.onChanged();
},
),
)
.toList(),
),
),
);
}
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<String> _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<String> _tcmRifTypes = {
'TCM MTB Rif (Xpert)',
'TCM MTB Rif (Truenat)',
'TCM MTB Rif INH (BDMAX)',
'LPA lini 1',
'LPA lini 2',
};
const List<String> _tbMicroscopeResults = ['+++', '++', '+', '1-9', 'Neg'];
const List<String> _tcmMtbRifResults = [
'Neg',
'Rif Sen',
'Rif Res',
'Rif Indet',
'Invalid',
'Error',
'No result',
];
const List<String> _xdrValidityResults = [
'Valid',
'Invalid',
'No Result',
'Error',
];
const List<String> _xdrSirResults = [
'S',
'I',
'R',
'Invalid',
'Error',
'No Result',
];