This commit is contained in:
Dwi Swandhana
2026-08-04 05:55:29 +07:00
parent 2bf4cac343
commit d369d3754b
15 changed files with 936 additions and 354 deletions

No files matched your search

@@ -107,31 +107,60 @@ class _CovidExpertiseWizardState extends State<CovidExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('PCR COVID'),
for (final virus in _covidVirusFields)
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(virus.label, compact: true),
_select(virus.resultField, virus.label, _covidResultOptions),
_text(virus.unitField, 'Satuan'),
_text(virus.referenceField, 'Nilai Rujukan'),
],
for (var i = 0; i < _covidVirusFields.length; i += 1)
CollapsibleExpertiseSection(
index: i + 1,
title: _covidVirusFields[i].label,
completed: _hasAny([
_covidVirusFields[i].resultField,
_covidVirusFields[i].unitField,
_covidVirusFields[i].referenceField,
]),
initiallyExpanded:
i == 0 &&
!_hasAny([
_covidVirusFields[i].resultField,
_covidVirusFields[i].unitField,
]),
children: [
_select(
_covidVirusFields[i].resultField,
_covidVirusFields[i].label,
_covidResultOptions,
),
),
_text(_covidVirusFields[i].unitField, 'Satuan'),
_text(_covidVirusFields[i].referenceField, 'Nilai Rujukan'),
],
),
_text('covid_interpretasi', 'Interpretasi', minLines: 6, maxLines: 9),
CollapsibleExpertiseSection(
index: _covidVirusFields.length + 1,
title: 'Interpretasi',
completed: _hasAny(['covid_interpretasi']),
children: [
_text(
'covid_interpretasi',
'Interpretasi',
minLines: 6,
maxLines: 9,
),
],
),
],
);
}
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;
}
}
return false;
}
Widget _antibioticSensitivity() {
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
+169 -6
View File
@@ -27,7 +27,7 @@ class ExpertiseWizardShell extends StatelessWidget {
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 10),
padding: const EdgeInsets.fromLTRB(18, 18, 18, 10),
child: _WizardHeader(
currentStep: safeStep,
steps: steps,
@@ -40,14 +40,17 @@ class ExpertiseWizardShell extends StatelessWidget {
duration: const Duration(milliseconds: 180),
child: Padding(
key: ValueKey<int>(safeStep),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 18),
padding: const EdgeInsets.fromLTRB(18, 0, 18, 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w800),
?.copyWith(
fontWeight: FontWeight.w900,
color: const Color(0xFF080D3D),
),
),
const SizedBox(height: 12),
Align(
@@ -99,7 +102,7 @@ class _WizardHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 46,
height: 50,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: steps.length,
@@ -113,6 +116,19 @@ class _WizardHeader extends StatelessWidget {
onSelected: (_) => onStepChanged(index),
showCheckmark: false,
visualDensity: VisualDensity.compact,
selectedColor: const Color(0xFFE0F2F1),
backgroundColor: Colors.white,
side: BorderSide(
color: selected
? const Color(0xFF0F766E)
: const Color(0xFFE1E8F5),
),
labelStyle: TextStyle(
color: selected
? const Color(0xFF0F766E)
: const Color(0xFF47517A),
fontWeight: FontWeight.w800,
),
);
},
),
@@ -161,7 +177,7 @@ class _WizardBottomToolbar extends StatelessWidget {
children: [
OutlinedButton.icon(
onPressed: canPrevious ? onPrevious : null,
icon: const Icon(Icons.chevron_left),
icon: const Icon(Icons.chevron_left_rounded),
label: const Text('Previous'),
),
const SizedBox(width: 12),
@@ -178,7 +194,7 @@ class _WizardBottomToolbar extends StatelessWidget {
const SizedBox(width: 12),
FilledButton.icon(
onPressed: canNext ? onNext : null,
icon: const Icon(Icons.chevron_right),
icon: const Icon(Icons.chevron_right_rounded),
label: const Text('Next'),
),
],
@@ -188,6 +204,153 @@ class _WizardBottomToolbar extends StatelessWidget {
}
}
class CollapsibleExpertiseSection extends StatefulWidget {
const CollapsibleExpertiseSection({
super.key,
required this.index,
required this.title,
required this.children,
this.completed = false,
this.initiallyExpanded = false,
this.color,
});
final int index;
final String title;
final List<Widget> children;
final bool completed;
final bool initiallyExpanded;
final Color? color;
@override
State<CollapsibleExpertiseSection> createState() =>
_CollapsibleExpertiseSectionState();
}
class _CollapsibleExpertiseSectionState
extends State<CollapsibleExpertiseSection> {
late bool _expanded;
@override
void initState() {
super.initState();
_expanded = widget.initiallyExpanded;
}
@override
Widget build(BuildContext context) {
final color = widget.color ?? _sectionColor(widget.index);
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFE1E8F5)),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: const Color(0xFF2563EB).withValues(alpha: 0.04),
blurRadius: 14,
offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () => setState(() => _expanded = !_expanded),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 44,
height: 44,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.11),
shape: BoxShape.circle,
),
child: Text(
'${widget.index}.',
style: TextStyle(
color: color,
fontSize: 20,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(width: 14),
Expanded(
child: Text(
widget.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF080D3D),
fontWeight: FontWeight.w900,
),
),
),
if (widget.completed) ...[
Container(
width: 34,
height: 34,
decoration: const BoxDecoration(
color: Color(0xFF0F766E),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_rounded,
color: Colors.white,
size: 22,
),
),
const SizedBox(width: 10),
],
Icon(
_expanded
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
color: const Color(0xFF0F172A),
size: 30,
),
],
),
),
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.children,
),
),
crossFadeState: _expanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 180),
),
],
),
);
}
Color _sectionColor(int index) {
const colors = [
Color(0xFF0F766E),
Color(0xFF0B7BFF),
Color(0xFF7C3AED),
Color(0xFFFF6B00),
Color(0xFF0891B2),
Color(0xFFDB2777),
];
return colors[(index - 1).abs() % colors.length];
}
}
class CompactMultiSelectField extends StatelessWidget {
const CompactMultiSelectField({
super.key,
+152 -47
View File
@@ -189,16 +189,6 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Expertise'),
actions: [
IconButton(
onPressed: _saving ? null : _reload,
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
),
],
),
body: FutureBuilder<Map<String, dynamic>>(
future: _future,
builder: (context, snapshot) {
@@ -217,41 +207,9 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
_prepareFields(data);
final isSupervisor = _isSupervisor(user['previlage']?.toString());
final summaryCard = Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['nmpasien']?.toString() ?? '-',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
Text(
'${item['nofoto'] ?? '-'} / RM ${item['noregister'] ?? '-'}',
),
Text(item['reques']?.toString() ?? '-'),
const SizedBox(height: 8),
Row(
children: [
StatusPill(status: item['status']?.toString() ?? 'NEW'),
const SizedBox(width: 8),
if (_currentDlp.isNotEmpty)
BadgeLabel(
text: _currentDlp,
color: const Color(0xFF0F766E),
),
],
),
],
),
),
final summaryCard = _ExpertiseSummaryCard(
item: item,
dlp: _currentDlp,
);
final expertiseContent = _currentDlp.isEmpty
@@ -426,8 +384,18 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
if (_currentDlp.isEmpty) {
return ListView(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.fromLTRB(18, 18, 18, 24),
children: [
SafeArea(
bottom: false,
child: _ExpertiseHeader(
title: 'Expertise',
subtitle: 'Pilih Template',
saving: _saving,
onRefresh: _reload,
),
),
const SizedBox(height: 22),
summaryCard,
const SizedBox(height: 14),
expertiseContent,
@@ -436,9 +404,19 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
}
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
padding: const EdgeInsets.fromLTRB(18, 18, 18, 0),
child: Column(
children: [
SafeArea(
bottom: false,
child: _ExpertiseHeader(
title: 'Expertise',
subtitle: _currentDlp,
saving: _saving,
onRefresh: _reload,
),
),
const SizedBox(height: 22),
summaryCard,
const SizedBox(height: 14),
Expanded(child: expertiseContent),
@@ -450,3 +428,130 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
);
}
}
class _ExpertiseHeader extends StatelessWidget {
const _ExpertiseHeader({
required this.title,
required this.subtitle,
required this.saving,
required this.onRefresh,
});
final String title;
final String subtitle;
final bool saving;
final VoidCallback onRefresh;
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_new_rounded),
color: const Color(0xFF080D3D),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: const Color(0xFF080D3D),
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 4),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: const Color(0xFF47517A),
fontWeight: FontWeight.w700,
),
),
],
),
),
IconButton(
onPressed: saving ? null : onRefresh,
icon: const Icon(Icons.refresh_rounded),
color: const Color(0xFF063B60),
tooltip: 'Refresh',
),
],
);
}
}
class _ExpertiseSummaryCard extends StatelessWidget {
const _ExpertiseSummaryCard({required this.item, required this.dlp});
final Map<String, dynamic> item;
final String dlp;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFFE1E8F5)),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF2563EB).withValues(alpha: 0.05),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['nmpasien']?.toString() ?? '-',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: const Color(0xFF080D3D),
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
BadgeLabel(
text: item['nofoto']?.toString() ?? '-',
color: const Color(0xFF0B7BFF),
),
BadgeLabel(
text: 'RM ${item['noregister'] ?? '-'}',
color: const Color(0xFF667095),
),
StatusPill(status: item['status']?.toString() ?? 'NEW'),
if (dlp.isNotEmpty)
BadgeLabel(text: dlp, color: const Color(0xFF0F766E)),
],
),
const SizedBox(height: 12),
Text(
item['reques']?.toString() ?? '-',
style: const TextStyle(
color: Color(0xFF47517A),
fontWeight: FontWeight.w800,
height: 1.35,
),
),
],
),
);
}
}
@@ -116,64 +116,137 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
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'),
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(),
_sectionTitle('2. Pewarnaan Ziehl Neelsen', compact: true),
_select(
'id_pewarnaanziehlnielsen',
'Pagi',
_options('jsonpewarnaanziehlnielsen'),
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'),
),
],
),
_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'),
],
),
_select(
'id_pewarnaankoh',
'3. Pewarnaan KOH',
_options('jsonpewarnaankoh', _kohFallback),
CollapsibleExpertiseSection(
index: 4,
title: 'Pewarnaan Neisser',
completed: _hasAny(['id_pewarnaanneisser']),
children: [
_select(
'id_pewarnaanneisser',
'Pewarnaan Neisser',
_options('jsonpewarnaanneisser'),
),
],
),
_text('id_pewarnaankohoptional', 'Pewarnaan KOH Manual'),
_select(
'id_pewarnaanneisser',
'4. Pewarnaan Neisser',
_options('jsonpewarnaanneisser'),
CollapsibleExpertiseSection(
index: 5,
title: 'Pewarnaan Negatif',
completed: _hasAny(['id_pewarnaannegatif']),
children: [
_select(
'id_pewarnaannegatif',
'Pewarnaan Negatif',
_options('jsonpewarnaannegatif'),
),
],
),
_select(
'id_pewarnaannegatif',
'5. Pewarnaan Negatif',
_options('jsonpewarnaannegatif'),
CollapsibleExpertiseSection(
index: 6,
title: 'Pewarnaan Spora',
completed: _hasAny(['id_pewarnaanspora']),
children: [
_select(
'id_pewarnaanspora',
'Pewarnaan Spora',
_options('jsonpewarnaanspora'),
),
],
),
_select(
'id_pewarnaanspora',
'6. 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'),
],
),
_select(
'id_pewarnaangiesma',
'7. 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),
@@ -108,20 +108,58 @@ class _LeptospiraExpertiseWizardState extends State<LeptospiraExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('IgM IgG Leptospira'),
_select('igg_parameter', 'IgG Leptospira', const [
'Positif',
'Negatif',
]),
_select('igm_parameter', 'IgM Leptospira', const [
'Positif',
'Negatif',
]),
_text('iggigm_interpretasi', 'Interpretasi', minLines: 6, maxLines: 9),
CollapsibleExpertiseSection(
index: 1,
title: 'IgG Leptospira',
completed: _hasAny(['igg_parameter']),
initiallyExpanded: !_hasAny(['igg_parameter', 'igm_parameter']),
children: [
_select('igg_parameter', 'IgG Leptospira', const [
'Positif',
'Negatif',
]),
],
),
CollapsibleExpertiseSection(
index: 2,
title: 'IgM Leptospira',
completed: _hasAny(['igm_parameter']),
children: [
_select('igm_parameter', 'IgM Leptospira', const [
'Positif',
'Negatif',
]),
],
),
CollapsibleExpertiseSection(
index: 3,
title: 'Interpretasi',
completed: _hasAny(['iggigm_interpretasi']),
children: [
_text(
'iggigm_interpretasi',
'Interpretasi',
minLines: 6,
maxLines: 9,
),
],
),
],
);
}
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;
}
}
return false;
}
Widget _antibioticSensitivity() {
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
@@ -113,63 +113,128 @@ class _PewarnaanLangsungExpertiseWizardState
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('A. Sediaan Langsung'),
_sectionTitle('1. Pewarnaan Gram', compact: true),
_select('lsg_selepitel', 'Sel Epitel', _options('jsonselepitel')),
_select('lsg_selradang', 'Sel Radang', _options('jsonselradang')),
_select(
'lsg_mikroorganisme',
'Mikroorganisme',
_options('jsonmikroorganisme'),
CollapsibleExpertiseSection(
index: 1,
title: 'Pewarnaan Gram',
completed: _hasAny([
'lsg_selepitel',
'lsg_selradang',
'lsg_mikroorganisme',
'lsg_mikroorganismeoptional',
]),
initiallyExpanded: !_hasAny(['lsg_selepitel', 'lsg_selradang']),
children: [
_select('lsg_selepitel', 'Sel Epitel', _options('jsonselepitel')),
_select('lsg_selradang', 'Sel Radang', _options('jsonselradang')),
_select(
'lsg_mikroorganisme',
'Mikroorganisme',
_options('jsonmikroorganisme'),
),
_multiSelect(
'lsg_mikroorganismeoptional',
'Bisa memilih lebih dari satu',
_options('jsonmikroorganismeoptional'),
),
],
),
_multiSelect(
'lsg_mikroorganismeoptional',
'Bisa memilih lebih dari satu',
_options('jsonmikroorganismeoptional'),
CollapsibleExpertiseSection(
index: 2,
title: 'Pewarnaan Ziehl Neelsen',
completed: _hasAny([
'lsg_pewarnaanziehlnielsen',
'lsg_pewarnaanziehlnielsensewaktu',
]),
children: [
_select(
'lsg_pewarnaanziehlnielsen',
'Pagi',
_options('jsonpewarnaanziehlnielsen'),
),
_select(
'lsg_pewarnaanziehlnielsensewaktu',
'Sewaktu',
_options('jsonpewarnaanziehlnielsen'),
),
],
),
_sectionTitle('2. Pewarnaan Ziehl Neelsen', compact: true),
_select(
'lsg_pewarnaanziehlnielsen',
'Pagi',
_options('jsonpewarnaanziehlnielsen'),
CollapsibleExpertiseSection(
index: 3,
title: 'Pewarnaan KOH',
completed: _hasAny(['lsg_pewarnaankoh', 'lsg_pewarnaankohoptional']),
children: [
_select(
'lsg_pewarnaankoh',
'Pewarnaan KOH',
_options('jsonpewarnaankoh', _kohFallback),
),
_text('lsg_pewarnaankohoptional', 'Pewarnaan KOH Manual'),
],
),
_select(
'lsg_pewarnaanziehlnielsensewaktu',
'Sewaktu',
_options('jsonpewarnaanziehlnielsen'),
CollapsibleExpertiseSection(
index: 4,
title: 'Pewarnaan Neisser',
completed: _hasAny(['lsg_pewarnaanneisser']),
children: [
_select(
'lsg_pewarnaanneisser',
'Pewarnaan Neisser',
_options('jsonpewarnaanneisser'),
),
],
),
_select(
'lsg_pewarnaankoh',
'3. Pewarnaan KOH',
_options('jsonpewarnaankoh', _kohFallback),
CollapsibleExpertiseSection(
index: 5,
title: 'Pewarnaan Negatif',
completed: _hasAny(['lsg_pewarnaannegatif']),
children: [
_select(
'lsg_pewarnaannegatif',
'Pewarnaan Negatif',
_options('jsonpewarnaannegatif'),
),
],
),
_text('lsg_pewarnaankohoptional', 'Pewarnaan KOH Manual'),
_select(
'lsg_pewarnaanneisser',
'4. Pewarnaan Neisser',
_options('jsonpewarnaanneisser'),
CollapsibleExpertiseSection(
index: 6,
title: 'Pewarnaan Spora',
completed: _hasAny(['lsg_pewarnaanspora']),
children: [
_select(
'lsg_pewarnaanspora',
'Pewarnaan Spora',
_options('jsonpewarnaanspora'),
),
],
),
_select(
'lsg_pewarnaannegatif',
'5. Pewarnaan Negatif',
_options('jsonpewarnaannegatif'),
CollapsibleExpertiseSection(
index: 7,
title: 'Pewarnaan Giemsa',
completed: _hasAny([
'lsg_pewarnaangiesma',
'lsg_pewarnaangiesmaoptional',
]),
children: [
_select(
'lsg_pewarnaangiesma',
'Pewarnaan Giemsa',
_options('jsonpewarnaangiemsa', _giemsaFallback),
),
_text('lsg_pewarnaangiesmaoptional', 'Pewarnaan Giemsa Manual'),
],
),
_select(
'lsg_pewarnaangiesma',
'6. Pewarnaan Giemsa',
_options('jsonpewarnaangiemsa', _giemsaFallback),
),
_text('lsg_pewarnaangiesmaoptional', 'Pewarnaan Giemsa Manual'),
_select(
'lsg_pewarnaanspora',
'7. Pewarnaan Spora',
_options('jsonpewarnaanspora'),
),
_text(
'lsg_pewarnaanlain',
'8. Pewarnaan Lain-Lain',
minLines: 5,
maxLines: 8,
CollapsibleExpertiseSection(
index: 8,
title: 'Pewarnaan Lain-Lain',
completed: _hasAny(['lsg_pewarnaanlain']),
children: [
_text(
'lsg_pewarnaanlain',
'Pewarnaan Lain-Lain',
minLines: 5,
maxLines: 8,
),
],
),
_nugentScore(),
_saveDirectSmearButton(),
@@ -177,6 +242,21 @@ class _PewarnaanLangsungExpertiseWizardState
);
}
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),
@@ -119,71 +119,135 @@ class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
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',
CollapsibleExpertiseSection(
index: 1,
title: 'Jenis Terduga / Pasien TBC',
completed: _hasAny([
'jenis_pasien_tbc',
'id_noidsediaan',
'id_tanggalpengambilancontohuji',
'id_tanggalpengirimancontohuji',
'id_lamariwayatpengobatantb',
]),
initiallyExpanded: !_hasAny(['jenis_pasien_tbc', 'id_noidsediaan']),
children: [
_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)',
CollapsibleExpertiseSection(
index: 2,
title: 'Identitas Nasional',
completed: _hasAny([
'id_lokasianatomi',
'id_alasanpemeriksaan',
'id_noregfasyankes',
'id_noregkota',
]),
children: [
_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'),
],
),
_text('id_bulanpemeriksaanulang', 'Pemeriksaan ulang (Bulan ke)'),
_text(
'id_bulanpemeriksaansetelahselesai',
'Pemeriksaan setelah selesai pengobatan (Bulan ke)',
CollapsibleExpertiseSection(
index: 3,
title: 'Contoh Uji dan Visual Dahak',
completed: _hasAny([
'id_contohuji',
'id_contohujilainnya',
'visual_dahak_sewaktu',
'visual_dahak_pagi',
]),
children: [
_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',
]),
],
),
_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',
]),
],
);
}
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 _testType() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Jenis Pemeriksaan'),
_select('id_jenispemeriksaantb', 'Jenis Pemeriksaan', _tbcTestTypes),
CollapsibleExpertiseSection(
index: 1,
title: 'Jenis Pemeriksaan',
completed: _hasAny(['id_jenispemeriksaantb']),
initiallyExpanded: !_hasAny(['id_jenispemeriksaantb']),
children: [
_select(
'id_jenispemeriksaantb',
'Jenis Pemeriksaan',
_tbcTestTypes,
),
],
),
],
);
}
@@ -110,31 +110,78 @@ class _ViralLoadExpertiseWizardState extends State<ViralLoadExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Parameter Viral Load'),
_select(
'vl_parameter01',
'Parameter',
_options('jsonvlparameter', const ['HIV RNA', 'HCV RNA', 'HBV DNA']),
CollapsibleExpertiseSection(
index: 1,
title: 'Parameter Viral Load',
completed: _hasAny([
'vl_parameter01',
'vl_hasil01',
'vl_hasil01teks',
'vl_satuan01',
'vl_rujukan01',
]),
initiallyExpanded: !_hasAny(['vl_parameter01', 'vl_hasil01']),
children: [
_select(
'vl_parameter01',
'Parameter',
_options('jsonvlparameter', const [
'HIV RNA',
'HCV RNA',
'HBV DNA',
]),
),
_select('vl_hasil01', 'Hasil', _viralLoadResultOptions),
_text('vl_hasil01teks', 'Hasil Manual'),
_select('vl_satuan01', 'Satuan', const ['copies/mL', 'IU/mL']),
_select('vl_rujukan01', 'Nilai Rujukan', const [
'40-10.000.000',
'10-100.000.000',
]),
],
),
CollapsibleExpertiseSection(
index: 2,
title: 'Parameter 02',
completed: _hasAny([
'vl_parameter02',
'vl_hasil02',
'vl_hasil02teks',
'vl_satuan02',
'vl_rujukan02',
]),
children: [
_select('vl_parameter02', 'Parameter 02', const ['LOG']),
_select('vl_hasil02', 'Hasil 02', _viralLoadLogResultOptions),
_text('vl_hasil02teks', 'Hasil 02 Manual'),
_select('vl_satuan02', 'Satuan 02', const ['LOG']),
_select('vl_rujukan02', 'Nilai Rujukan 02', const ['1,6-7', ' ']),
],
),
CollapsibleExpertiseSection(
index: 3,
title: 'Catatan Viral Load',
completed: _hasAny(['viralload']),
children: [
_text('viralload', 'Catatan Viral Load', minLines: 6, maxLines: 9),
],
),
_select('vl_hasil01', 'Hasil', _viralLoadResultOptions),
_text('vl_hasil01teks', 'Hasil Manual'),
_select('vl_satuan01', 'Satuan', const ['copies/mL', 'IU/mL']),
_select('vl_rujukan01', 'Nilai Rujukan', const [
'40-10.000.000',
'10-100.000.000',
]),
const SizedBox(height: 8),
_sectionTitle('Parameter 02', compact: true),
_select('vl_parameter02', 'Parameter 02', const ['LOG']),
_select('vl_hasil02', 'Hasil 02', _viralLoadLogResultOptions),
_text('vl_hasil02teks', 'Hasil 02 Manual'),
_select('vl_satuan02', 'Satuan 02', const ['LOG']),
_select('vl_rujukan02', 'Nilai Rujukan 02', const ['1,6-7', ' ']),
_text('viralload', 'Catatan Viral Load', minLines: 6, maxLines: 9),
],
);
}
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;
}
}
return false;
}
Widget _antibioticSensitivity() {
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,