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
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Vendored Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
+87 -104
View File
@@ -158,7 +158,7 @@ File: Main Css File
}
#topnav .navbar-custom {
background-color: #FEF0E1;
background-color: #02c0ce;
}
#topnav .navbar-toggle {
@@ -174,7 +174,7 @@ File: Main Css File
}
#topnav .navbar-toggle:hover span {
background-color: #FEF0E1;
background-color: #313a46;
}
#topnav .navbar-toggle:focus {
@@ -182,7 +182,7 @@ File: Main Css File
}
#topnav .navbar-toggle:focus span {
background-color: #FEF0E1;
background-color: #313a46;
}
#topnav .navbar-toggle .lines {
@@ -240,72 +240,40 @@ File: Main Css File
}
#topnav .navigation-menu > li > a {
display: flex; /* Menggunakan Flexbox untuk alignment */
align-items: center; /* Membuat item (ikon + teks) rata tengah secara vertikal */
justify-content: center; /* Membuat item (ikon + teks) rata tengah secara horizontal */
color: #2F2F2F;
display: block;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
font-size: 16px;
-webkit-transition: all .3s ease;
transition: all .3s ease;
-webkit-transition: all .5s ease;
transition: all .5s ease;
line-height: 20px;
padding: 20px 25px;
border-radius: 10px;
min-width: 130px; /* Atur lebar minimum untuk semua menu */
padding-left: 25px;
padding-right: 25px;
}
#topnav .navigation-menu > li > a:hover {
color: #ffffff;
}
#topnav .navigation-menu > li > a:focus {
color: #ffffff;
}
#topnav .navigation-menu > li > a:active {
color: #ffffff;
}
#topnav .navigation-menu > li > a i {
margin-right: 8px; /* Memberi jarak antara ikon dan teks */
}
#topnav .navigation-menu > li > a:hover,
#topnav .navigation-menu > li.active > a {
background-color: #FFA14F; /* Warna menu di sorot dan aktif */
color: #2F2F2F; /* Warna font menu di sorot dan aktif */
}
/* =========== Submenu =============*/
#topnav .navigation-menu > li .submenu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
border: 1px solid #e7e7e7;
padding: 15px 0;
list-style: none;
min-width: 200px;
visibility: hidden;
opacity: 0;
margin-top: 10px;
-webkit-transition: all .2s ease;
transition: all .2s ease;
background-color: #FEF0E1; /* Background submenu */
box-shadow: 0 0 24px 0 rgba(0, 0, 0, 0.06), 0 1px 0 0 rgba(0, 0, 0, 0.02);
text-align: left;
}
#topnav .navigation-menu > li:hover > .submenu {
visibility: visible;
opacity: 1;
margin-top: 0;
}
#topnav .navigation-menu .submenu li a {
display: block;
padding: 8px 25px;
clear: both;
font-weight: normal;
white-space: nowrap;
font-size: 15px;
color: #2F2F2F;
-webkit-transition: all .35s ease;
transition: all .35s ease;
margin-right: 8px;
-webkit-transition: all .5s ease;
transition: all .5s ease;
}
/* ATURAN HOVER YANG DIPERBAIKI */
#topnav .navigation-menu .submenu li a:hover,
#topnav .navigation-menu .submenu li a:focus,
#topnav .navigation-menu .submenu li a:active {
background-color: #FFA14F; /* Warna hover submenu */
color: #2F2F2F;
#topnav .navigation-menu > li > a:hover, #topnav .navigation-menu > li > a:focus {
background-color: transparent;
}
/*
Responsive Menu
*/
@@ -328,16 +296,33 @@ File: Main Css File
margin-right: 10px;
}
#topnav .navigation-menu > li:hover a {
color: #2F2F2F !important;
color: #ffffff;
}
#topnav .navigation-menu > li:hover a i {
color: #2F2F2F !important;
color: #ffffff;
}
#topnav .navigation-menu > li > ul > li.has-submenu:active > a,
#topnav .navigation-menu > li > ul > li.has-submenu:hover > a {
color: #2F2F2F !important;
color: #02c0ce;
}
#topnav .navigation-menu > li .submenu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
border: 1px solid #e7e7e7;
padding: 15px 0;
list-style: none;
min-width: 200px;
text-align: left;
visibility: hidden;
opacity: 0;
margin-top: 10px;
-webkit-transition: all .2s ease;
transition: all .2s ease;
background-color: #ffffff;
box-shadow: 0 0 24px 0 rgba(0, 0, 0, 0.06), 0 1px 0 0 rgba(0, 0, 0, 0.02);
}
#topnav .navigation-menu > li .submenu.megamenu {
white-space: nowrap;
width: auto;
@@ -393,7 +378,7 @@ File: Main Css File
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 500;
background-color: #FEF0E1; /* Background submenu */
color: #313a46;
}
#topnav .navbar-toggle {
display: none;
@@ -413,11 +398,10 @@ File: Main Css File
#topnav .navigation-menu {
float: none;
max-height: 400px;
text-align: center;
text-align: left;
}
#topnav .navigation-menu > li {
display: block;
text-align: center;
}
#topnav .navigation-menu > li > a {
color: #313a46;
@@ -446,8 +430,7 @@ File: Main Css File
color: #313a46;
}
#topnav .navigation-menu > li .submenu li a:hover {
background-color: #FFA14F;
color: #313a46;
color: #02c0ce;
}
#topnav .navigation-menu > li .submenu li.has-submenu > a:after {
content: "\F140";
@@ -502,10 +485,10 @@ File: Main Css File
overflow-y: auto;
}
#topnav .has-submenu.active a {
color: #FFA14F;
color: #02c0ce;
}
#topnav .has-submenu.active a:active, #topnav .has-submenu.active a:focus, #topnav .has-submenu.active a i {
color: #FFA14F;
color: #02c0ce;
}
}
@@ -1131,11 +1114,11 @@ code {
}
.bg-primary {
background-color: #FFA14F !important;
background-color: #2d7bf4 !important;
}
.bg-success {
background-color: #77C470 !important;
background-color: #0acf97 !important;
}
.bg-info {
@@ -1143,11 +1126,11 @@ code {
}
.bg-warning {
background-color: #FAA645 !important;
background-color: #f9bc0b !important;
}
.bg-danger {
background-color: #E8563C !important;
background-color: #f1556c !important;
}
.bg-muted {
@@ -1196,7 +1179,7 @@ code {
}
.text-warning {
color: #FAA645 !important;
color: #f9bc0b !important;
}
.text-success {
@@ -1233,7 +1216,7 @@ code {
}
.border-warning {
border-color: #FAA645 !important;
border-color: #f9bc0b !important;
}
.border-danger {
@@ -1265,12 +1248,12 @@ code {
}
.badge-custom {
background: #FFA14F;
background: #02c0ce;
color: #ffffff;
}
.badge-primary {
background-color: #FFA14F;
background-color: #2d7bf4;
}
.badge-secondary {
@@ -1286,7 +1269,7 @@ code {
}
.badge-warning {
background-color: #FAA645;
background-color: #f9bc0b;
color: #ffffff;
}
@@ -1679,15 +1662,15 @@ code {
}
.btn-warning {
background-color: #FAA645;
border: 1px solid #FAA645;
background-color: #f9bc0b;
border: 1px solid #f9bc0b;
}
.btn-warning:hover, .btn-warning:focus, .btn-warning:active, .btn-warning.active, .btn-warning.focus, .btn-warning:active,
.btn-warning:focus, .btn-warning:hover, .open > .dropdown-toggle.btn-warning, .btn-warning:not([disabled]):not(.disabled).active,
.btn-warning:not([disabled]):not(.disabled):active, .show > .btn-warning.dropdown-toggle {
background-color: #FAA645;
border-color: #FAA645;
background-color: #e5ac06;
border-color: #e5ac06;
}
.btn-warning.focus, .btn-warning:focus, .btn-outline-warning.focus, .btn-outline-warning:focus,
@@ -1802,13 +1785,13 @@ code {
}
.btn-outline-warning {
color: #FAA645;
border-color: #FAA645;
color: #f9bc0b;
border-color: #f9bc0b;
}
.btn-outline-warning:hover {
background-color: #FAA645;
border-color: #FAA645;
background-color: #f9bc0b;
border-color: #f9bc0b;
}
.btn-outline-danger {
@@ -2000,7 +1983,7 @@ code {
}
.ribbon-box .ribbon-warning {
background: #FAA645;
background: #f9bc0b;
}
.ribbon-box .ribbon-warning:before {
@@ -2143,7 +2126,7 @@ code {
}
.ribbon-box .ribbon-two-warning span {
background: #FAA645;
background: #f9bc0b;
}
.ribbon-box .ribbon-two-warning span:before {
@@ -2426,8 +2409,8 @@ code {
}
.checkbox-warning input[type="checkbox"]:checked + label::before {
background-color: #FAA645;
border-color: #FAA645;
background-color: #f9bc0b;
border-color: #f9bc0b;
}
.checkbox-warning input[type="checkbox"]:checked + label::after {
@@ -2601,15 +2584,15 @@ code {
}
.radio-warning input[type="radio"] + label::after {
background-color: #FAA645;
background-color: #f9bc0b;
}
.radio-warning input[type="radio"]:checked + label::before {
border-color: #FAA645;
border-color: #f9bc0b;
}
.radio-warning input[type="radio"]:checked + label::after {
background-color: #FAA645;
background-color: #f9bc0b;
}
.radio-success input[type="radio"] + label::after {
@@ -2682,7 +2665,7 @@ code {
}
.alert-warning {
color: #FAA645;
color: #f9bc0b;
background-color: #fffaeb;
border-color: #fbd76f;
}
@@ -2741,9 +2724,9 @@ code {
}
.jq-icon-warning {
background-color: #FAA645;
background-color: #f9bc0b;
color: #ffffff;
border-color: #FAA645;
border-color: #f9bc0b;
}
.jq-icon-error {
@@ -3146,7 +3129,7 @@ code {
}
.progress-bar-warning {
background-color: #FAA645;
background-color: #f9bc0b;
}
.progress-bar-danger {
@@ -3280,8 +3263,8 @@ code {
}
.swal2-icon.swal2-warning {
color: #FAA645;
border-color: #FAA645;
color: #f9bc0b;
border-color: #f9bc0b;
}
.swal2-icon.swal2-error {
@@ -3397,7 +3380,7 @@ code {
}
.ct-chart .ct-series.ct-series-g .ct-bar, .ct-chart .ct-series.ct-series-g .ct-line, .ct-chart .ct-series.ct-series-g .ct-point, .ct-chart .ct-series.ct-series-g .ct-slice-donut {
stroke: #FAA645;
stroke: #f9bc0b;
}
.ct-series-a .ct-area, .ct-series-a .ct-slice-pie {
@@ -3590,7 +3573,7 @@ code {
}
.message-list li.orange-dot .col-mail-1 .dot {
border-color: #FAA645;
border-color: #f9bc0b;
}
.message-list li.green-dot .col-mail-1 .dot {
View File
@@ -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,