601 lines
17 KiB
Dart
601 lines
17 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,
|
|
required this.initialStep,
|
|
required this.onStepChanged,
|
|
});
|
|
|
|
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;
|
|
final int initialStep;
|
|
final ValueChanged<int> onStepChanged;
|
|
|
|
@override
|
|
State<TbcExpertiseWizard> createState() => _TbcExpertiseWizardState();
|
|
}
|
|
|
|
class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
|
|
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: [
|
|
_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 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<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: 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<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',
|
|
];
|