662 lines
18 KiB
Dart
662 lines
18 KiB
Dart
part of '../../app/app.dart';
|
|
|
|
class KulturExpertiseWizard extends StatefulWidget {
|
|
const KulturExpertiseWizard({
|
|
super.key,
|
|
required this.item,
|
|
required this.user,
|
|
required this.staffOptions,
|
|
required this.optionSets,
|
|
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, dynamic> optionSets;
|
|
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<KulturExpertiseWizard> createState() => _KulturExpertiseWizardState();
|
|
}
|
|
|
|
class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
|
|
late int _step;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_step = widget.initialStep.clamp(0, 5);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant KulturExpertiseWizard oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
final nextStep = widget.initialStep.clamp(0, 5);
|
|
if (nextStep != _step) {
|
|
_step = nextStep;
|
|
}
|
|
}
|
|
|
|
void _setStep(int step) {
|
|
final nextStep = step.clamp(0, 5);
|
|
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('Sediaan Langsung'),
|
|
isActive: _step == 1,
|
|
content: _directSmear(),
|
|
),
|
|
Step(
|
|
title: const Text('Biakan Kultur'),
|
|
isActive: _step == 2,
|
|
content: _cultureGrowth(),
|
|
),
|
|
Step(
|
|
title: const Text('Tes Kepekaan Antibiotik'),
|
|
isActive: _step == 3,
|
|
content: _antibioticSensitivity(),
|
|
),
|
|
Step(
|
|
title: const Text('Data Alat'),
|
|
isActive: _step == 4,
|
|
content: _toolData(),
|
|
),
|
|
Step(
|
|
title: const Text('Expertise'),
|
|
isActive: _step == 5,
|
|
content: _finalExpertise(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _directSmear() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
CollapsibleExpertiseSection(
|
|
index: 1,
|
|
title: 'Pewarnaan Gram',
|
|
completed: _hasAny([
|
|
'id_selepitel',
|
|
'id_selradang',
|
|
'id_mikroorganisme',
|
|
'id_mikroorganismeoptional',
|
|
]),
|
|
initiallyExpanded: !_hasAny(['id_selepitel', 'id_selradang']),
|
|
children: [
|
|
_select('id_selepitel', 'Sel Epitel', _options('jsonselepitel')),
|
|
_select('id_selradang', 'Sel Radang', _options('jsonselradang')),
|
|
_select(
|
|
'id_mikroorganisme',
|
|
'Mikroorganisme',
|
|
_options('jsonmikroorganisme'),
|
|
),
|
|
_multiSelect(
|
|
'id_mikroorganismeoptional',
|
|
'Bisa memilih lebih dari satu',
|
|
_options('jsonmikroorganismeoptional'),
|
|
),
|
|
],
|
|
),
|
|
_nugentScore(),
|
|
CollapsibleExpertiseSection(
|
|
index: 2,
|
|
title: 'Pewarnaan Ziehl Neelsen',
|
|
completed: _hasAny([
|
|
'id_pewarnaanziehlnielsen',
|
|
'id_pewarnaanziehlnielsensewaktu',
|
|
]),
|
|
children: [
|
|
_select(
|
|
'id_pewarnaanziehlnielsen',
|
|
'Pagi',
|
|
_options('jsonpewarnaanziehlnielsen'),
|
|
),
|
|
_select(
|
|
'id_pewarnaanziehlnielsensewaktu',
|
|
'Sewaktu',
|
|
_options('jsonpewarnaanziehlnielsen'),
|
|
),
|
|
],
|
|
),
|
|
CollapsibleExpertiseSection(
|
|
index: 3,
|
|
title: 'Pewarnaan KOH',
|
|
completed: _hasAny(['id_pewarnaankoh', 'id_pewarnaankohoptional']),
|
|
children: [
|
|
_select(
|
|
'id_pewarnaankoh',
|
|
'Pewarnaan KOH',
|
|
_options('jsonpewarnaankoh', _kohFallback),
|
|
),
|
|
_text('id_pewarnaankohoptional', 'Pewarnaan KOH Manual'),
|
|
],
|
|
),
|
|
CollapsibleExpertiseSection(
|
|
index: 4,
|
|
title: 'Pewarnaan Neisser',
|
|
completed: _hasAny(['id_pewarnaanneisser']),
|
|
children: [
|
|
_select(
|
|
'id_pewarnaanneisser',
|
|
'Pewarnaan Neisser',
|
|
_options('jsonpewarnaanneisser'),
|
|
),
|
|
],
|
|
),
|
|
CollapsibleExpertiseSection(
|
|
index: 5,
|
|
title: 'Pewarnaan Negatif',
|
|
completed: _hasAny(['id_pewarnaannegatif']),
|
|
children: [
|
|
_select(
|
|
'id_pewarnaannegatif',
|
|
'Pewarnaan Negatif',
|
|
_options('jsonpewarnaannegatif'),
|
|
),
|
|
],
|
|
),
|
|
CollapsibleExpertiseSection(
|
|
index: 6,
|
|
title: 'Pewarnaan Spora',
|
|
completed: _hasAny(['id_pewarnaanspora']),
|
|
children: [
|
|
_select(
|
|
'id_pewarnaanspora',
|
|
'Pewarnaan Spora',
|
|
_options('jsonpewarnaanspora'),
|
|
),
|
|
],
|
|
),
|
|
CollapsibleExpertiseSection(
|
|
index: 7,
|
|
title: 'Pewarnaan Giemsa',
|
|
completed: _hasAny([
|
|
'id_pewarnaangiesma',
|
|
'id_pewarnaangiesmaoptional',
|
|
]),
|
|
children: [
|
|
_select(
|
|
'id_pewarnaangiesma',
|
|
'Pewarnaan Giemsa',
|
|
_options('jsonpewarnaangiemsa', _giemsaFallback),
|
|
),
|
|
_text('id_pewarnaangiesmaoptional', 'Pewarnaan Giemsa Manual'),
|
|
],
|
|
),
|
|
_saveDirectSmearButton(),
|
|
],
|
|
);
|
|
}
|
|
|
|
bool _hasAny(List<String> 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 <String>{}).isNotEmpty) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Widget _saveDirectSmearButton() {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 4, bottom: 12),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton.icon(
|
|
onPressed: widget.saving
|
|
? null
|
|
: () => widget.onSave('statuspewarnaanlsg'),
|
|
icon: widget.saving
|
|
? const SizedBox.square(
|
|
dimension: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.save_outlined),
|
|
label: const Text('SIMPAN PEWARNAAN LANGSUNG'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _nugentScore() {
|
|
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('Nugent Score', compact: true),
|
|
_select(
|
|
'id_jumlahlactobacillus',
|
|
'Lactobacillus - Jumlah Per Lapang',
|
|
_options('jsonjumlahlactobacillus'),
|
|
),
|
|
_select('id_lactobacillus', 'Lactobacillus - Skor', _scoreOptions),
|
|
_select(
|
|
'id_jumlahgardnerella',
|
|
'Gardnerella - Jumlah Per Lapang',
|
|
_options('jsonjumlahgardnerella'),
|
|
),
|
|
_select('id_gardnerella', 'Gardnerella - Skor', _scoreOptions),
|
|
_select(
|
|
'id_jumlahmobiluncus',
|
|
'Mobiluncus - Jumlah Per Lapang',
|
|
_options('jsonjumlahmobiluncus'),
|
|
),
|
|
_select('id_mobiluncus', 'Mobiluncus - Skor', _scoreOptions),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _cultureGrowth() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_sectionTitle('B. Biakan Kultur'),
|
|
const BadgeLabel(
|
|
text: 'Khusus Kultur Darah dan Cairan Steril',
|
|
color: Color(0xFFDC2626),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_text('bd_result', 'BD Result'),
|
|
_text('bd_result_date', 'BD Result Date'),
|
|
_sectionTitle('Pertumbuhan Koloni (Kultur Primer)', compact: true),
|
|
_kirbyBauer(),
|
|
_select(
|
|
'id_biakankultur',
|
|
'Biakan Kultur (Aerob / Anaerob)',
|
|
_options('jsonbiakankultur'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _kirbyBauer() {
|
|
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('Kirby Bauer', compact: true),
|
|
for (final antibiotic in _kirbyAntibiotics)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _select(
|
|
antibiotic.zoneField,
|
|
antibiotic.label,
|
|
_kirbyZoneOptions,
|
|
dense: true,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
SizedBox(
|
|
width: 120,
|
|
child: _select(
|
|
antibiotic.sirField,
|
|
'SIR',
|
|
_antibioticSirOptions,
|
|
dense: true,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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'),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
List<String> _options(
|
|
String key, [
|
|
List<String> fallback = const ['Pilih Salah Satu'],
|
|
]) {
|
|
final values = asList(
|
|
widget.optionSets[key],
|
|
).map((item) => item.toString()).where((item) => item.isNotEmpty).toList();
|
|
return values.isEmpty ? fallback : values;
|
|
}
|
|
|
|
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, {
|
|
bool dense = false,
|
|
}) {
|
|
final current = widget.selectValues[name];
|
|
final effectiveOptions = [
|
|
if (current != null && current.isNotEmpty && !options.contains(current))
|
|
current,
|
|
...options,
|
|
];
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: dense ? 0 : 12),
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue:
|
|
current != null &&
|
|
current.isNotEmpty &&
|
|
effectiveOptions.contains(current)
|
|
? current
|
|
: null,
|
|
isExpanded: true,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
border: const OutlineInputBorder(),
|
|
isDense: dense,
|
|
),
|
|
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(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _KirbyAntibiotic {
|
|
const _KirbyAntibiotic(this.label, this.zoneField, this.sirField);
|
|
|
|
final String label;
|
|
final String zoneField;
|
|
final String sirField;
|
|
}
|
|
|
|
const List<String> _scoreOptions = ['0', '1', '2', '3', '4'];
|
|
|
|
const List<String> _antibioticSirOptions = ['S', 'I', 'R'];
|
|
|
|
const List<String> _kirbyZoneOptions = [
|
|
'Tidak dilakukan',
|
|
'1',
|
|
'2',
|
|
'3',
|
|
'4',
|
|
'5',
|
|
'6',
|
|
'7',
|
|
'8',
|
|
'9',
|
|
'10',
|
|
'11',
|
|
'12',
|
|
'13',
|
|
'14',
|
|
'15',
|
|
'16',
|
|
'17',
|
|
'18',
|
|
'19',
|
|
'20',
|
|
'21',
|
|
'22',
|
|
'23',
|
|
'24',
|
|
'25',
|
|
'26',
|
|
'27',
|
|
'28',
|
|
'29',
|
|
'30',
|
|
'31',
|
|
'32',
|
|
'33',
|
|
'34',
|
|
'35',
|
|
'36',
|
|
'37',
|
|
'38',
|
|
'39',
|
|
'40',
|
|
'41',
|
|
'42',
|
|
];
|
|
|
|
const List<_KirbyAntibiotic> _kirbyAntibiotics = [
|
|
_KirbyAntibiotic('AMC', 'id_kbamc', 'id_sirkbamc'),
|
|
_KirbyAntibiotic('AK', 'id_kbak', 'id_sirkbak'),
|
|
_KirbyAntibiotic('FOS', 'id_kbfos', 'id_sirkbfos'),
|
|
_KirbyAntibiotic('SCF', 'id_kbscf', 'id_sirkbscf'),
|
|
];
|
|
|
|
const List<String> _kohFallback = [
|
|
'Ditemukan morfologi Hifa',
|
|
'Ditemukan morfologi Budding Cell',
|
|
'Ditemukan morfologi Conidia',
|
|
'Ditemukan morfologi Hifa dan Budding Cell',
|
|
'Ditemukan morfologi Hifa dan Conidia',
|
|
'Ditemukan morfologi Budding Cell dan Conidia',
|
|
'Tidak ditemukan morfologi Budding Cell, Hifa dan Conidia',
|
|
'Tidak ditemukan morfologi Budding Cell dan Hifa',
|
|
'lainnya',
|
|
];
|
|
|
|
const List<String> _giemsaFallback = [
|
|
'Ditemukan morfologi Inclusion Bodies dan Reticulate Bodies',
|
|
'Tidak ditemukan morfologi Inclusion Bodies dan Reticulate Bodies',
|
|
'Ditemukan morfologi kista tropozoid',
|
|
'Tidak ditemukan morfologi kista tropozoid',
|
|
'lainnya',
|
|
];
|