Files
lis/mylis/lib/screens/expertise/kultur_expertise_wizard.dart
T
2026-07-22 14:06:01 +07:00

589 lines
16 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: [
_sectionTitle('A. Sediaan Langsung'),
_sectionTitle('1. Pewarna Gram', compact: true),
_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(),
_sectionTitle('2. Pewarnaan Ziehl Neelsen', compact: true),
_select(
'id_pewarnaanziehlnielsen',
'Pagi',
_options('jsonpewarnaanziehlnielsen'),
),
_select(
'id_pewarnaanziehlnielsensewaktu',
'Sewaktu',
_options('jsonpewarnaanziehlnielsen'),
),
_select(
'id_pewarnaankoh',
'3. Pewarnaan KOH',
_options('jsonpewarnaankoh', _kohFallback),
),
_text('id_pewarnaankohoptional', 'Pewarnaan KOH Manual'),
_select(
'id_pewarnaanneisser',
'4. Pewarnaan Neisser',
_options('jsonpewarnaanneisser'),
),
_select(
'id_pewarnaannegatif',
'5. Pewarnaan Negatif',
_options('jsonpewarnaannegatif'),
),
_select(
'id_pewarnaanspora',
'6. Pewarnaan Spora',
_options('jsonpewarnaanspora'),
),
_select(
'id_pewarnaangiesma',
'7. Pewarnaan Giemsa',
_options('jsonpewarnaangiemsa', _giemsaFallback),
),
_text('id_pewarnaangiesmaoptional', 'Pewarnaan Giemsa Manual'),
_saveDirectSmearButton(),
],
);
}
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',
];