449 lines
14 KiB
Dart
449 lines
14 KiB
Dart
part of '../../app/app.dart';
|
|
|
|
class CciExpertiseWizard extends StatefulWidget {
|
|
const CciExpertiseWizard({
|
|
super.key,
|
|
required this.item,
|
|
required this.user,
|
|
required this.staffOptions,
|
|
required this.optionSets,
|
|
required this.textControllers,
|
|
required this.selectValues,
|
|
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, dynamic> optionSets;
|
|
final Map<String, TextEditingController> textControllers;
|
|
final Map<String, String> selectValues;
|
|
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<CciExpertiseWizard> createState() => _CciExpertiseWizardState();
|
|
}
|
|
|
|
class _CciExpertiseWizardState extends State<CciExpertiseWizard> {
|
|
int _step = 0;
|
|
|
|
void _next() {
|
|
setState(() => _step = (_step + 1).clamp(0, 3));
|
|
}
|
|
|
|
void _previous() {
|
|
setState(() => _step = (_step - 1).clamp(0, 3));
|
|
}
|
|
|
|
@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 == 3 ? null : _next,
|
|
icon: const Icon(Icons.chevron_right),
|
|
label: const Text('Next'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
steps: [
|
|
Step(
|
|
title: const Text('Pasien & Petugas'),
|
|
isActive: _step == 0,
|
|
content: Column(
|
|
children: [
|
|
PatientStaffPanel(
|
|
item: widget.item,
|
|
user: widget.user,
|
|
staffOptions: widget.staffOptions,
|
|
staffValues: widget.staffValues,
|
|
onChanged: widget.onChanged,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Step(
|
|
title: const Text('CCI'),
|
|
isActive: _step == 1,
|
|
content: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Pemeriksaan Candida Colonization Index',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_select(
|
|
'id_sputum',
|
|
'Sputum',
|
|
_options('jsonsputum', ['Pilih Salah Satu']),
|
|
),
|
|
_select(
|
|
'id_swabtenggorok',
|
|
'Swab Tenggorok',
|
|
_options('jsonswabtenggorok', ['Pilih Salah Satu']),
|
|
),
|
|
_select(
|
|
'id_urine',
|
|
'Urine',
|
|
_options('jsonurine', ['Pilih Salah Satu']),
|
|
),
|
|
_select(
|
|
'id_swabperineum',
|
|
'Swab Perineum',
|
|
_options('jsonswabperineum', ['Pilih Salah Satu']),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Step(
|
|
title: const Text('Data Alat'),
|
|
isActive: _step == 2,
|
|
content: 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',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Step(
|
|
title: const Text('Final'),
|
|
isActive: _step == 3,
|
|
content: 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'),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
List<String> _options(String key, List<String> fallback) {
|
|
final values = asList(
|
|
widget.optionSets[key],
|
|
).map((item) => item.toString()).where((item) => item.isNotEmpty).toList();
|
|
return values.isEmpty ? fallback : values;
|
|
}
|
|
|
|
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: [
|
|
Text(
|
|
title,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_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 _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 PatientStaffPanel extends StatelessWidget {
|
|
const PatientStaffPanel({
|
|
super.key,
|
|
required this.item,
|
|
required this.user,
|
|
required this.staffOptions,
|
|
required this.staffValues,
|
|
required this.onChanged,
|
|
});
|
|
|
|
final Map<String, dynamic> item;
|
|
final Map<String, dynamic> user;
|
|
final Map<String, dynamic> staffOptions;
|
|
final Map<String, String> staffValues;
|
|
final VoidCallback onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
Card(
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
children: [
|
|
DetailRow(label: 'Order', value: item['reques']),
|
|
DetailRow(
|
|
label: 'Dokter Pengirim',
|
|
value: item['klinisi'] ?? item['nmdokter'],
|
|
),
|
|
DetailRow(label: 'Asal Pasien', value: item['asalpasien']),
|
|
DetailRow(label: 'No. Lokal Lab', value: item['nofoto']),
|
|
DetailRow(label: 'Register Number', value: item['noregister']),
|
|
DetailRow(label: 'Name', value: item['nmpasien']),
|
|
DetailRow(
|
|
label: 'Gender, Age',
|
|
value: '${item['jkpasien'] ?? '-'}, ${item['usia'] ?? '-'}',
|
|
),
|
|
DetailRow(label: 'Phone', value: item['tlppasien']),
|
|
DetailRow(label: 'Address', value: item['alamatpasien']),
|
|
DetailRow(
|
|
label: 'Tanggal Registrasi',
|
|
value: item['tanggalregis'] ?? item['daftar'],
|
|
),
|
|
DetailRow(
|
|
label: 'Tanggal Pengambilan',
|
|
value: item['tanggalsampel'],
|
|
),
|
|
DetailRow(
|
|
label: 'Cara Pengambilan',
|
|
value: item['pengambilan'],
|
|
),
|
|
DetailRow(
|
|
label: 'Asal Pengambilan',
|
|
value: item['asalpengirim'],
|
|
),
|
|
DetailRow(
|
|
label: 'Spesimen',
|
|
value: item['kesimpulan'] ?? item['nm_spesimen'],
|
|
),
|
|
DetailRow(label: 'Status', value: item['status']),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_staffSelect('analis', 'ATLM', asList(staffOptions['analis'])),
|
|
_staffSelect('ppds3', 'PPDS', asList(staffOptions['ppds'])),
|
|
_staffSelect('dokter', 'SPV', asList(staffOptions['dokters'])),
|
|
DetailRow(label: 'Klinis/Diagnosis', value: item['klinis']),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _staffSelect(String name, String label, List<dynamic> users) {
|
|
final current = staffValues[name];
|
|
final items = users.map(asMap).toList();
|
|
final hasCurrent = items.any((item) => item['id'].toString() == current);
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue: hasCurrent ? current : null,
|
|
isExpanded: true,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
border: const OutlineInputBorder(),
|
|
),
|
|
items: [
|
|
const DropdownMenuItem(value: '0', child: Text('Pilih')),
|
|
...items.map(
|
|
(item) => DropdownMenuItem(
|
|
value: item['id'].toString(),
|
|
child: Text(item['nama']?.toString() ?? '-'),
|
|
),
|
|
),
|
|
],
|
|
onChanged: (value) {
|
|
staffValues[name] = value ?? '0';
|
|
onChanged();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const List<String> _sirOptions = [
|
|
'ESBL',
|
|
'MRSA',
|
|
'MDR',
|
|
'XDR',
|
|
'PDR',
|
|
'Carbapenem Resistant',
|
|
'MDR Carbapenem Resistant',
|
|
'XDR Carbapenem Resistant',
|
|
];
|
|
|
|
const List<String> _colonyOptions = [
|
|
'>= 10^5 CFU/ml Urine (Bakteriuria bermakna)',
|
|
'>= 10^5 CFU/ml Urine (Candiduria bermakna)',
|
|
'>= 10^3 CFU/ml Urine (Bakteriuria bermakna)',
|
|
'8 x 10^3 CFU/ml Urine (Bakteriuria bermakna)',
|
|
'lainnya',
|
|
];
|