This commit is contained in:
Dwi Swandhana
2026-07-22 09:07:43 +07:00
parent 92dc35396b
commit 9e4774bd5e
22 changed files with 2272 additions and 850 deletions
+3
View File
@@ -1,7 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
+17
View File
@@ -1,8 +1,25 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'app/app.dart';
export 'app/app.dart';
void main() {
HttpOverrides.global = _InternalCertificateHttpOverrides();
runApp(const MyLisApp());
}
class _InternalCertificateHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback = (certificate, host, port) {
debugPrint(
'MyLIS menerima sertifikat internal untuk $host:$port '
'issuer=${certificate.issuer}',
);
return true;
};
}
}
@@ -16,6 +16,8 @@ class CciExpertiseWizard extends StatefulWidget {
required this.onCriticalChanged,
required this.onChanged,
required this.onSave,
required this.initialStep,
required this.onStepChanged,
});
final Map<String, dynamic> item;
@@ -31,47 +33,42 @@ class CciExpertiseWizard extends StatefulWidget {
final ValueChanged<bool> onCriticalChanged;
final VoidCallback onChanged;
final ValueChanged<String> onSave;
final int initialStep;
final ValueChanged<int> onStepChanged;
@override
State<CciExpertiseWizard> createState() => _CciExpertiseWizardState();
}
class _CciExpertiseWizardState extends State<CciExpertiseWizard> {
int _step = 0;
late int _step;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 3));
@override
void initState() {
super.initState();
_step = widget.initialStep.clamp(0, 3);
}
void _previous() {
setState(() => _step = (_step - 1).clamp(0, 3));
@override
void didUpdateWidget(covariant CciExpertiseWizard oldWidget) {
super.didUpdateWidget(oldWidget);
final nextStep = widget.initialStep.clamp(0, 3);
if (nextStep != _step) {
_step = nextStep;
}
}
void _setStep(int step) {
final nextStep = step.clamp(0, 3);
setState(() => _step = nextStep);
widget.onStepChanged(nextStep);
}
@override
Widget build(BuildContext context) {
return Stepper(
return ExpertiseWizardShell(
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'),
),
],
),
);
},
onStepChanged: _setStep,
steps: [
Step(
title: const Text('Pasien & Petugas'),
@@ -339,60 +336,136 @@ class PatientStaffPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final status = item['status']?.toString() ?? '-';
final genderAge = '${item['jkpasien'] ?? '-'}, ${item['usia'] ?? '-'}';
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(12),
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DetailRow(label: 'Order', value: item['reques']),
DetailRow(
label: 'Dokter Pengirim',
value: item['klinisi'] ?? item['nmdokter'],
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 24,
backgroundColor: const Color(
0xFF0F766E,
).withValues(alpha: 0.12),
child: const Icon(
Icons.person_outline,
color: Color(0xFF0F766E),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['nmpasien']?.toString() ?? '-',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w900),
),
const SizedBox(height: 3),
Text(
'RM ${_textValue(item['noregister'])} · Lab ${_textValue(item['nofoto'])}',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: Colors.black54),
),
],
),
),
StatusPill(status: status),
],
),
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'] ?? '-'}',
const SizedBox(height: 14),
_InfoGrid(
items: [
_InfoItem('Order', item['reques'], Icons.receipt_long),
_InfoItem(
'Dokter Pengirim',
item['klinisi'] ?? item['nmdokter'],
Icons.local_hospital_outlined,
),
_InfoItem(
'Asal Pasien',
item['asalpasien'],
Icons.apartment_outlined,
),
_InfoItem('Gender, Age', genderAge, Icons.badge_outlined),
_InfoItem('Phone', item['tlppasien'], Icons.call_outlined),
_InfoItem(
'Tanggal Registrasi',
item['tanggalregis'] ?? item['daftar'],
Icons.event_available_outlined,
),
_InfoItem(
'Tanggal Pengambilan',
item['tanggalsampel'],
Icons.science_outlined,
),
_InfoItem(
'Cara Pengambilan',
item['pengambilan'],
Icons.medical_services_outlined,
),
_InfoItem(
'Asal Pengambilan',
item['asalpengirim'],
Icons.output_outlined,
),
_InfoItem(
'Spesimen',
item['kesimpulan'] ?? item['nm_spesimen'],
Icons.biotech_outlined,
),
],
),
DetailRow(label: 'Phone', value: item['tlppasien']),
DetailRow(label: 'Address', value: item['alamatpasien']),
DetailRow(
label: 'Tanggal Registrasi',
value: item['tanggalregis'] ?? item['daftar'],
const SizedBox(height: 8),
_InfoTile(
label: 'Address',
value: item['alamatpasien'],
icon: Icons.place_outlined,
),
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']),
Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Petugas',
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w900),
),
const SizedBox(height: 12),
_staffSelect('analis', 'ATLM', asList(staffOptions['analis'])),
_staffSelect('ppds3', 'PPDS', asList(staffOptions['ppds'])),
_staffSelect('dokter', 'SPV', asList(staffOptions['dokters'])),
],
),
),
),
const SizedBox(height: 12),
_InfoTile(
label: 'Klinis/Diagnosis',
value: item['klinis'],
icon: Icons.assignment_outlined,
),
],
);
}
@@ -409,6 +482,7 @@ class PatientStaffPanel extends StatelessWidget {
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.account_circle_outlined),
),
items: [
const DropdownMenuItem(value: '0', child: Text('Pilih')),
@@ -426,6 +500,113 @@ class PatientStaffPanel extends StatelessWidget {
),
);
}
String _textValue(Object? value) {
final text = value?.toString() ?? '';
return text.trim().isEmpty ? '-' : decodeHtmlEntities(text.trim());
}
}
class _InfoGrid extends StatelessWidget {
const _InfoGrid({required this.items});
final List<_InfoItem> items;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth >= 520 ? 2 : 1;
return Wrap(
spacing: 10,
runSpacing: 10,
children: [
for (final item in items)
SizedBox(
width: columns == 1
? constraints.maxWidth
: (constraints.maxWidth - 10) / 2,
child: _InfoTile(
label: item.label,
value: item.value,
icon: item.icon,
compact: true,
),
),
],
);
},
);
}
}
class _InfoTile extends StatelessWidget {
const _InfoTile({
required this.label,
required this.value,
required this.icon,
this.compact = false,
});
final String label;
final Object? value;
final IconData icon;
final bool compact;
@override
Widget build(BuildContext context) {
final cleanValue = _clean(value);
return Container(
padding: EdgeInsets.all(compact ? 10 : 12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
border: Border.all(color: const Color(0xFFE2E8F0)),
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: const Color(0xFF0F766E)),
const SizedBox(width: 9),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Colors.black54,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 3),
Text(
cleanValue,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w800,
color: Colors.black87,
),
),
],
),
),
],
),
);
}
String _clean(Object? value) {
final text = value?.toString() ?? '';
return text.trim().isEmpty ? '-' : decodeHtmlEntities(text.trim());
}
}
class _InfoItem {
const _InfoItem(this.label, this.value, this.icon);
final String label;
final Object? value;
final IconData icon;
}
const List<String> _sirOptions = [
@@ -15,6 +15,8 @@ class CovidExpertiseWizard extends StatefulWidget {
required this.onCriticalChanged,
required this.onChanged,
required this.onSave,
required this.initialStep,
required this.onStepChanged,
});
final Map<String, dynamic> item;
@@ -29,47 +31,42 @@ class CovidExpertiseWizard extends StatefulWidget {
final ValueChanged<bool> onCriticalChanged;
final VoidCallback onChanged;
final ValueChanged<String> onSave;
final int initialStep;
final ValueChanged<int> onStepChanged;
@override
State<CovidExpertiseWizard> createState() => _CovidExpertiseWizardState();
}
class _CovidExpertiseWizardState extends State<CovidExpertiseWizard> {
int _step = 0;
late int _step;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 4));
@override
void initState() {
super.initState();
_step = widget.initialStep.clamp(0, 4);
}
void _previous() {
setState(() => _step = (_step - 1).clamp(0, 4));
@override
void didUpdateWidget(covariant CovidExpertiseWizard oldWidget) {
super.didUpdateWidget(oldWidget);
final nextStep = widget.initialStep.clamp(0, 4);
if (nextStep != _step) {
_step = nextStep;
}
}
void _setStep(int step) {
final nextStep = step.clamp(0, 4);
setState(() => _step = nextStep);
widget.onStepChanged(nextStep);
}
@override
Widget build(BuildContext context) {
return Stepper(
return ExpertiseWizardShell(
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 == 4 ? null : _next,
icon: const Icon(Icons.chevron_right),
label: const Text('Next'),
),
],
),
);
},
onStepChanged: _setStep,
steps: [
Step(
title: const Text('Pasien & Petugas'),
@@ -136,49 +133,10 @@ class _CovidExpertiseWizardState extends State<CovidExpertiseWizard> {
}
Widget _antibioticSensitivity() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Tes Kepekaan Antibiotik'),
Text(
'S: Sensitif, I: Intermediate, R: Resisten',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
BadgeLabel(text: 'Media BAP', color: Color(0xFF2563EB)),
BadgeLabel(text: 'Media CAP', color: Color(0xFFDC2626)),
BadgeLabel(text: 'Mc Conkey', color: Color(0xFF0891B2)),
BadgeLabel(text: 'Media Jamur', color: Color(0xFFD97706)),
],
),
const SizedBox(height: 12),
for (final media in _cultureMedia)
Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(media.label, compact: true),
_select(
media.statusField,
'Status Pertumbuhan',
_growthOptions,
),
_text(media.noteField, 'Catatan'),
],
),
),
),
],
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
selectValues: widget.selectValues,
onChanged: widget.onChanged,
);
}
File diff suppressed because it is too large Load Diff
+228 -181
View File
@@ -23,6 +23,7 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
final Map<String, String> _selectValues = {};
final Map<String, Set<String>> _multiValues = {};
final Map<String, String> _staffValues = {};
final Map<String, int> _wizardSteps = {};
String _currentDlp = '';
bool _criticalValue = false;
bool _saving = false;
@@ -89,6 +90,11 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
);
}
}
for (final name in kAntibioticMediaRowFields) {
_textControllers[name] = TextEditingController(
text: components[name]?.toString() ?? '',
);
}
}
Future<void> _setTemplate(String dlp) async {
@@ -113,6 +119,9 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
}
Future<void> _save(String action) async {
if (action == 'statuspewarnaanlsg') {
_wizardSteps[_currentDlp] = 1;
}
setState(() => _saving = true);
final fields = <String, dynamic>{};
for (final entry in _textControllers.entries) {
@@ -151,6 +160,12 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
}
}
int _wizardStep(String dlp) => _wizardSteps[dlp] ?? 0;
void _setWizardStep(String dlp, int step) {
_wizardSteps[dlp] = step;
}
void _showMessage(String message) {
ScaffoldMessenger.of(
context,
@@ -188,201 +203,233 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
_prepareFields(data);
final isSupervisor = _isSupervisor(user['previlage']?.toString());
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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: [
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),
),
],
),
StatusPill(status: item['status']?.toString() ?? 'NEW'),
const SizedBox(width: 8),
if (_currentDlp.isNotEmpty)
BadgeLabel(
text: _currentDlp,
color: const Color(0xFF0F766E),
),
],
),
),
],
),
const SizedBox(height: 14),
if (_currentDlp.isEmpty)
TemplatePicker(
),
);
final expertiseContent = _currentDlp.isEmpty
? TemplatePicker(
templates: asList(data['templates']),
saving: _saving,
onUse: _setTemplate,
)
else
_currentDlp == 'CCI'
? CciExpertiseWizard(
: _currentDlp == 'CCI'
? CciExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
initialStep: _wizardStep(_currentDlp),
onStepChanged: (step) => _setWizardStep(_currentDlp, step),
)
: _currentDlp == 'Kultur'
? KulturExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
initialStep: _wizardStep(_currentDlp),
onStepChanged: (step) => _setWizardStep(_currentDlp, step),
)
: _currentDlp == 'Pewarna Langsung'
? PewarnaanLangsungExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
initialStep: _wizardStep(_currentDlp),
onStepChanged: (step) => _setWizardStep(_currentDlp, step),
)
: _currentDlp == 'Viral Load'
? ViralLoadExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
initialStep: _wizardStep(_currentDlp),
onStepChanged: (step) => _setWizardStep(_currentDlp, step),
)
: _currentDlp == 'IgM IgG Leptospira'
? LeptospiraExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
initialStep: _wizardStep(_currentDlp),
onStepChanged: (step) => _setWizardStep(_currentDlp, step),
)
: _currentDlp == 'PCR COVID'
? CovidExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
initialStep: _wizardStep(_currentDlp),
onStepChanged: (step) => _setWizardStep(_currentDlp, step),
)
: _currentDlp == 'TBC'
? TbcExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
initialStep: _wizardStep(_currentDlp),
onStepChanged: (step) => _setWizardStep(_currentDlp, step),
)
: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
PatientStaffPanel(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
)
: _currentDlp == 'Kultur'
? KulturExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
)
: _currentDlp == 'Pewarna Langsung'
? PewarnaanLangsungExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
)
: _currentDlp == 'Viral Load'
? ViralLoadExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
optionSets: asMap(data['option_sets']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
)
: _currentDlp == 'IgM IgG Leptospira'
? LeptospiraExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
)
: _currentDlp == 'PCR COVID'
? CovidExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
textControllers: _textControllers,
selectValues: _selectValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
)
: _currentDlp == 'TBC'
? TbcExpertiseWizard(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
staffValues: _staffValues,
criticalValue: _criticalValue,
isSupervisor: isSupervisor,
saving: _saving,
onCriticalChanged: (value) =>
setState(() => _criticalValue = value),
onChanged: () => setState(() {}),
onSave: _save,
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
PatientStaffPanel(
item: item,
user: user,
staffOptions: asMap(data['staff_options']),
staffValues: _staffValues,
onChanged: () => setState(() {}),
),
const SizedBox(height: 12),
ExpertiseForm(
dlp: _currentDlp,
fields: asList(data['fields']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
onChanged: () => setState(() {}),
),
const SizedBox(height: 12),
ExpertiseActions(
isSupervisor: isSupervisor,
saving: _saving,
onSave: _save,
),
],
),
],
const SizedBox(height: 12),
ExpertiseForm(
dlp: _currentDlp,
fields: asList(data['fields']),
textControllers: _textControllers,
selectValues: _selectValues,
multiValues: _multiValues,
onChanged: () => setState(() {}),
),
const SizedBox(height: 12),
ExpertiseActions(
isSupervisor: isSupervisor,
saving: _saving,
onSave: _save,
),
],
),
);
if (_currentDlp.isEmpty) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
summaryCard,
const SizedBox(height: 14),
expertiseContent,
],
);
}
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Column(
children: [
summaryCard,
const SizedBox(height: 14),
Expanded(child: expertiseContent),
],
),
);
},
),
@@ -17,6 +17,8 @@ class KulturExpertiseWizard extends StatefulWidget {
required this.onCriticalChanged,
required this.onChanged,
required this.onSave,
required this.initialStep,
required this.onStepChanged,
});
final Map<String, dynamic> item;
@@ -33,47 +35,42 @@ class KulturExpertiseWizard extends StatefulWidget {
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> {
int _step = 0;
late int _step;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 5));
@override
void initState() {
super.initState();
_step = widget.initialStep.clamp(0, 5);
}
void _previous() {
setState(() => _step = (_step - 1).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 Stepper(
return ExpertiseWizardShell(
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 == 5 ? null : _next,
icon: const Icon(Icons.chevron_right),
label: const Text('Next'),
),
],
),
);
},
onStepChanged: _setStep,
steps: [
Step(
title: const Text('Pasien & Petugas'),
@@ -172,10 +169,32 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
_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),
@@ -276,53 +295,10 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
}
Widget _antibioticSensitivity() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('C. Tes Kepekaan Antibiotik'),
Text(
'S: Sensitif, I: Intermediate, R: Resisten',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
_mediaStatusSummary(),
const SizedBox(height: 12),
for (final media in _cultureMedia)
Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(media.label, compact: true),
_select(
media.statusField,
'Status Pertumbuhan',
_growthOptions,
),
_text(media.noteField, 'Catatan'),
],
),
),
),
],
);
}
Widget _mediaStatusSummary() {
return Wrap(
spacing: 8,
runSpacing: 8,
children: const [
BadgeLabel(text: 'Media BAP', color: Color(0xFF2563EB)),
BadgeLabel(text: 'Media CAP', color: Color(0xFFDC2626)),
BadgeLabel(text: 'Mc Conkey', color: Color(0xFF0891B2)),
BadgeLabel(text: 'Media Jamur', color: Color(0xFFD97706)),
],
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
selectValues: widget.selectValues,
onChanged: widget.onChanged,
);
}
@@ -512,32 +488,14 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
final selected = widget.multiValues[name] ?? <String>{};
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InputDecorator(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
child: Wrap(
spacing: 8,
runSpacing: 4,
children: options
.map(
(option) => FilterChip(
label: Text(option),
selected: selected.contains(option),
onSelected: (checked) {
if (checked) {
selected.add(option);
} else {
selected.remove(option);
}
widget.multiValues[name] = selected;
widget.onChanged();
},
),
)
.toList(),
),
child: CompactMultiSelectField(
label: label,
options: options,
selected: selected,
onChanged: (value) {
widget.multiValues[name] = value;
widget.onChanged();
},
),
);
}
@@ -572,24 +530,10 @@ class _KirbyAntibiotic {
final String sirField;
}
class _CultureMedia {
const _CultureMedia(this.label, this.statusField, this.noteField);
final String label;
final String statusField;
final String noteField;
}
const List<String> _scoreOptions = ['0', '1', '2', '3', '4'];
const List<String> _antibioticSirOptions = ['S', 'I', 'R'];
const List<String> _growthOptions = [
'Ada Pertumbuhan',
'Tidak Ada Pertumbuhan',
'Pertumbuhan Primer',
];
const List<String> _kirbyZoneOptions = [
'Tidak dilakukan',
'1',
@@ -643,30 +587,6 @@ const List<_KirbyAntibiotic> _kirbyAntibiotics = [
_KirbyAntibiotic('SCF', 'id_kbscf', 'id_sirkbscf'),
];
const List<_CultureMedia> _cultureMedia = [
_CultureMedia('Media BAP', 'media_bap_status', 'media_bap_note'),
_CultureMedia('Media CAP', 'media_cap_status', 'media_cap_note'),
_CultureMedia(
'Media Mc Conkey',
'media_mcconkey_status',
'media_mcconkey_note',
),
_CultureMedia('Kultur Jamur R1', 'media_sdar1_status', 'media_sdar1_note'),
_CultureMedia('Kultur Jamur R2', 'media_sdar2_status', 'media_sdar2_note'),
_CultureMedia('Kultur Jamur I1', 'media_sdai1_status', 'media_sdai1_note'),
_CultureMedia('Kultur Jamur I2', 'media_sdai2_status', 'media_sdai2_note'),
_CultureMedia(
'Media Selektif Lainnya',
'media_sellainnya_status',
'media_sellainnya_note',
),
_CultureMedia(
'Pemeriksaan Tambahan Lainnya',
'media_tamlainnya_status',
'media_tamlainnya_note',
),
];
const List<String> _kohFallback = [
'Ditemukan morfologi Hifa',
'Ditemukan morfologi Budding Cell',
@@ -15,6 +15,8 @@ class LeptospiraExpertiseWizard extends StatefulWidget {
required this.onCriticalChanged,
required this.onChanged,
required this.onSave,
required this.initialStep,
required this.onStepChanged,
});
final Map<String, dynamic> item;
@@ -29,6 +31,8 @@ class LeptospiraExpertiseWizard extends StatefulWidget {
final ValueChanged<bool> onCriticalChanged;
final VoidCallback onChanged;
final ValueChanged<String> onSave;
final int initialStep;
final ValueChanged<int> onStepChanged;
@override
State<LeptospiraExpertiseWizard> createState() =>
@@ -36,41 +40,34 @@ class LeptospiraExpertiseWizard extends StatefulWidget {
}
class _LeptospiraExpertiseWizardState extends State<LeptospiraExpertiseWizard> {
int _step = 0;
late int _step;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 4));
@override
void initState() {
super.initState();
_step = widget.initialStep.clamp(0, 4);
}
void _previous() {
setState(() => _step = (_step - 1).clamp(0, 4));
@override
void didUpdateWidget(covariant LeptospiraExpertiseWizard oldWidget) {
super.didUpdateWidget(oldWidget);
final nextStep = widget.initialStep.clamp(0, 4);
if (nextStep != _step) {
_step = nextStep;
}
}
void _setStep(int step) {
final nextStep = step.clamp(0, 4);
setState(() => _step = nextStep);
widget.onStepChanged(nextStep);
}
@override
Widget build(BuildContext context) {
return Stepper(
return ExpertiseWizardShell(
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 == 4 ? null : _next,
icon: const Icon(Icons.chevron_right),
label: const Text('Next'),
),
],
),
);
},
onStepChanged: _setStep,
steps: [
Step(
title: const Text('Pasien & Petugas'),
@@ -126,49 +123,10 @@ class _LeptospiraExpertiseWizardState extends State<LeptospiraExpertiseWizard> {
}
Widget _antibioticSensitivity() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Tes Kepekaan Antibiotik'),
Text(
'S: Sensitif, I: Intermediate, R: Resisten',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
BadgeLabel(text: 'Media BAP', color: Color(0xFF2563EB)),
BadgeLabel(text: 'Media CAP', color: Color(0xFFDC2626)),
BadgeLabel(text: 'Mc Conkey', color: Color(0xFF0891B2)),
BadgeLabel(text: 'Media Jamur', color: Color(0xFFD97706)),
],
),
const SizedBox(height: 12),
for (final media in _cultureMedia)
Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(media.label, compact: true),
_select(
media.statusField,
'Status Pertumbuhan',
_growthOptions,
),
_text(media.noteField, 'Catatan'),
],
),
),
),
],
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
selectValues: widget.selectValues,
onChanged: widget.onChanged,
);
}
@@ -17,6 +17,8 @@ class PewarnaanLangsungExpertiseWizard extends StatefulWidget {
required this.onCriticalChanged,
required this.onChanged,
required this.onSave,
required this.initialStep,
required this.onStepChanged,
});
final Map<String, dynamic> item;
@@ -33,6 +35,8 @@ class PewarnaanLangsungExpertiseWizard extends StatefulWidget {
final ValueChanged<bool> onCriticalChanged;
final VoidCallback onChanged;
final ValueChanged<String> onSave;
final int initialStep;
final ValueChanged<int> onStepChanged;
@override
State<PewarnaanLangsungExpertiseWizard> createState() =>
@@ -41,41 +45,34 @@ class PewarnaanLangsungExpertiseWizard extends StatefulWidget {
class _PewarnaanLangsungExpertiseWizardState
extends State<PewarnaanLangsungExpertiseWizard> {
int _step = 0;
late int _step;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 4));
@override
void initState() {
super.initState();
_step = widget.initialStep.clamp(0, 4);
}
void _previous() {
setState(() => _step = (_step - 1).clamp(0, 4));
@override
void didUpdateWidget(covariant PewarnaanLangsungExpertiseWizard oldWidget) {
super.didUpdateWidget(oldWidget);
final nextStep = widget.initialStep.clamp(0, 4);
if (nextStep != _step) {
_step = nextStep;
}
}
void _setStep(int step) {
final nextStep = step.clamp(0, 4);
setState(() => _step = nextStep);
widget.onStepChanged(nextStep);
}
@override
Widget build(BuildContext context) {
return Stepper(
return ExpertiseWizardShell(
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 == 4 ? null : _next,
icon: const Icon(Icons.chevron_right),
label: const Text('Next'),
),
],
),
);
},
onStepChanged: _setStep,
steps: [
Step(
title: const Text('Pasien & Petugas'),
@@ -175,10 +172,32 @@ class _PewarnaanLangsungExpertiseWizardState
maxLines: 8,
),
_nugentScore(),
_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),
@@ -214,49 +233,10 @@ class _PewarnaanLangsungExpertiseWizardState
}
Widget _antibioticSensitivity() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Tes Kepekaan Antibiotik'),
Text(
'S: Sensitif, I: Intermediate, R: Resisten',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
BadgeLabel(text: 'Media BAP', color: Color(0xFF2563EB)),
BadgeLabel(text: 'Media CAP', color: Color(0xFFDC2626)),
BadgeLabel(text: 'Mc Conkey', color: Color(0xFF0891B2)),
BadgeLabel(text: 'Media Jamur', color: Color(0xFFD97706)),
],
),
const SizedBox(height: 12),
for (final media in _cultureMedia)
Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(media.label, compact: true),
_select(
media.statusField,
'Status Pertumbuhan',
_growthOptions,
),
_text(media.noteField, 'Catatan'),
],
),
),
),
],
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
selectValues: widget.selectValues,
onChanged: widget.onChanged,
);
}
@@ -440,32 +420,14 @@ class _PewarnaanLangsungExpertiseWizardState
final selected = widget.multiValues[name] ?? <String>{};
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InputDecorator(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
child: Wrap(
spacing: 8,
runSpacing: 4,
children: options
.map(
(option) => FilterChip(
label: Text(option),
selected: selected.contains(option),
onSelected: (checked) {
if (checked) {
selected.add(option);
} else {
selected.remove(option);
}
widget.multiValues[name] = selected;
widget.onChanged();
},
),
)
.toList(),
),
child: CompactMultiSelectField(
label: label,
options: options,
selected: selected,
onChanged: (value) {
widget.multiValues[name] = value;
widget.onChanged();
},
),
);
}
@@ -16,6 +16,8 @@ class TbcExpertiseWizard extends StatefulWidget {
required this.onCriticalChanged,
required this.onChanged,
required this.onSave,
required this.initialStep,
required this.onStepChanged,
});
final Map<String, dynamic> item;
@@ -31,47 +33,42 @@ class TbcExpertiseWizard extends StatefulWidget {
final ValueChanged<bool> onCriticalChanged;
final VoidCallback onChanged;
final ValueChanged<String> onSave;
final int initialStep;
final ValueChanged<int> onStepChanged;
@override
State<TbcExpertiseWizard> createState() => _TbcExpertiseWizardState();
}
class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
int _step = 0;
late int _step;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 6));
@override
void initState() {
super.initState();
_step = widget.initialStep.clamp(0, 6);
}
void _previous() {
setState(() => _step = (_step - 1).clamp(0, 6));
@override
void didUpdateWidget(covariant TbcExpertiseWizard oldWidget) {
super.didUpdateWidget(oldWidget);
final nextStep = widget.initialStep.clamp(0, 6);
if (nextStep != _step) {
_step = nextStep;
}
}
void _setStep(int step) {
final nextStep = step.clamp(0, 6);
setState(() => _step = nextStep);
widget.onStepChanged(nextStep);
}
@override
Widget build(BuildContext context) {
return Stepper(
return ExpertiseWizardShell(
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 == 6 ? null : _next,
icon: const Icon(Icons.chevron_right),
label: const Text('Next'),
),
],
),
);
},
onStepChanged: _setStep,
steps: [
Step(
title: const Text('Pasien & Petugas'),
@@ -362,38 +359,10 @@ class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
}
Widget _antibioticSensitivity() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Tes Kepekaan Antibiotik'),
Text(
'S: Sensitif, I: Intermediate, R: Resisten',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
for (final media in _cultureMedia)
Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(media.label, compact: true),
_select(
media.statusField,
'Status Pertumbuhan',
_growthOptions,
),
_text(media.noteField, 'Catatan'),
],
),
),
),
],
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
selectValues: widget.selectValues,
onChanged: widget.onChanged,
);
}
@@ -567,32 +536,14 @@ class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
final selected = widget.multiValues[name] ?? <String>{};
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InputDecorator(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
child: Wrap(
spacing: 8,
runSpacing: 4,
children: options
.map(
(option) => FilterChip(
label: Text(option),
selected: selected.contains(option),
onSelected: (checked) {
if (checked) {
selected.add(option);
} else {
selected.remove(option);
}
widget.multiValues[name] = selected;
widget.onChanged();
},
),
)
.toList(),
),
child: CompactMultiSelectField(
label: label,
options: options,
selected: selected,
onChanged: (value) {
widget.multiValues[name] = value;
widget.onChanged();
},
),
);
}
@@ -16,6 +16,8 @@ class ViralLoadExpertiseWizard extends StatefulWidget {
required this.onCriticalChanged,
required this.onChanged,
required this.onSave,
required this.initialStep,
required this.onStepChanged,
});
final Map<String, dynamic> item;
@@ -31,6 +33,8 @@ class ViralLoadExpertiseWizard extends StatefulWidget {
final ValueChanged<bool> onCriticalChanged;
final VoidCallback onChanged;
final ValueChanged<String> onSave;
final int initialStep;
final ValueChanged<int> onStepChanged;
@override
State<ViralLoadExpertiseWizard> createState() =>
@@ -38,41 +42,34 @@ class ViralLoadExpertiseWizard extends StatefulWidget {
}
class _ViralLoadExpertiseWizardState extends State<ViralLoadExpertiseWizard> {
int _step = 0;
late int _step;
void _next() {
setState(() => _step = (_step + 1).clamp(0, 4));
@override
void initState() {
super.initState();
_step = widget.initialStep.clamp(0, 4);
}
void _previous() {
setState(() => _step = (_step - 1).clamp(0, 4));
@override
void didUpdateWidget(covariant ViralLoadExpertiseWizard oldWidget) {
super.didUpdateWidget(oldWidget);
final nextStep = widget.initialStep.clamp(0, 4);
if (nextStep != _step) {
_step = nextStep;
}
}
void _setStep(int step) {
final nextStep = step.clamp(0, 4);
setState(() => _step = nextStep);
widget.onStepChanged(nextStep);
}
@override
Widget build(BuildContext context) {
return Stepper(
return ExpertiseWizardShell(
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 == 4 ? null : _next,
icon: const Icon(Icons.chevron_right),
label: const Text('Next'),
),
],
),
);
},
onStepChanged: _setStep,
steps: [
Step(
title: const Text('Pasien & Petugas'),
@@ -139,49 +136,10 @@ class _ViralLoadExpertiseWizardState extends State<ViralLoadExpertiseWizard> {
}
Widget _antibioticSensitivity() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle('Tes Kepekaan Antibiotik'),
Text(
'S: Sensitif, I: Intermediate, R: Resisten',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
BadgeLabel(text: 'Media BAP', color: Color(0xFF2563EB)),
BadgeLabel(text: 'Media CAP', color: Color(0xFFDC2626)),
BadgeLabel(text: 'Mc Conkey', color: Color(0xFF0891B2)),
BadgeLabel(text: 'Media Jamur', color: Color(0xFFD97706)),
],
),
const SizedBox(height: 12),
for (final media in _cultureMedia)
Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionTitle(media.label, compact: true),
_select(
media.statusField,
'Status Pertumbuhan',
_growthOptions,
),
_text(media.noteField, 'Catatan'),
],
),
),
),
],
return AntibioticSensitivityPanel(
textControllers: widget.textControllers,
selectValues: widget.selectValues,
onChanged: widget.onChanged,
);
}
+51 -26
View File
@@ -64,8 +64,10 @@ class _LoginScreenState extends State<LoginScreen> {
);
} on ApiException catch (error) {
_showMessage(error.message);
} catch (_) {
_showMessage('Tidak bisa terhubung ke server MyLIS.');
} catch (error, stackTrace) {
debugPrint('Login MyLIS gagal tidak terduga: $error');
debugPrintStack(stackTrace: stackTrace);
_showMessage('Tidak bisa terhubung ke server MyLIS. Penyebab: $error');
} finally {
if (mounted) setState(() => _loading = false);
}
@@ -93,8 +95,10 @@ class _LoginScreenState extends State<LoginScreen> {
});
} on ApiException catch (error) {
setState(() => _pingStatus = error.message);
} catch (_) {
setState(() => _pingStatus = 'Koneksi server gagal.');
} catch (error, stackTrace) {
debugPrint('Ping MyLIS gagal tidak terduga: $error');
debugPrintStack(stackTrace: stackTrace);
setState(() => _pingStatus = 'Koneksi server gagal. Penyebab: $error');
} finally {
if (mounted) setState(() => _pinging = false);
}
@@ -120,29 +124,34 @@ class _LoginScreenState extends State<LoginScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Image.asset(
kAppLogoAsset,
height: 96,
fit: BoxFit.contain,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Image.asset(
kAppLogoAsset,
height: 78,
fit: BoxFit.contain,
),
),
const SizedBox(width: 18),
Container(
width: 1,
height: 58,
color: const Color(0xFFE2E8F0),
),
const SizedBox(width: 18),
Flexible(
child: Image.asset(
kHospitalLogoAsset,
height: 78,
fit: BoxFit.contain,
),
),
],
),
const SizedBox(height: 16),
Text(
kAppLongName,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 12),
Center(
child: Image.asset(
kHospitalLogoAsset,
height: 54,
fit: BoxFit.contain,
),
),
const SizedBox(height: 8),
const SizedBox(height: 18),
Text(
kHospitalName,
textAlign: TextAlign.center,
@@ -152,6 +161,13 @@ class _LoginScreenState extends State<LoginScreen> {
),
),
const SizedBox(height: 8),
Text(
kAppLongName,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
'Login petugas',
textAlign: TextAlign.center,
@@ -261,6 +277,15 @@ class _LoginScreenState extends State<LoginScreen> {
context,
).textTheme.bodySmall?.copyWith(color: Colors.black45),
),
const SizedBox(height: 18),
Text(
'Pembuat aplikasi: Duidev Software House | CV Swandhana Copyright @ 2026',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
],
),
),
+156 -16
View File
@@ -25,31 +25,171 @@ class ApiClient {
String path,
Map<String, dynamic> body,
) async {
final response = await http.post(
_uri(path),
headers: _headers,
body: jsonEncode(body),
);
return _decode(response);
final uri = _uri(path);
final client = _clientFor(uri);
try {
final response = await client
.post(uri, headers: _headers, body: jsonEncode(body))
.timeout(const Duration(seconds: 15));
return _decode(response, uri);
} catch (error, stackTrace) {
throw _connectionException(uri, error, stackTrace);
} finally {
client.close();
}
}
Future<Map<String, dynamic>> get(
String path, [
Map<String, String>? query,
]) async {
final response = await http.get(_uri(path, query), headers: _headers);
return _decode(response);
final uri = _uri(path, query);
final client = _clientFor(uri);
try {
final response = await client
.get(uri, headers: _headers)
.timeout(const Duration(seconds: 15));
return _decode(response, uri);
} catch (error, stackTrace) {
throw _connectionException(uri, error, stackTrace);
} finally {
client.close();
}
}
Map<String, dynamic> _decode(http.Response response) {
final decoded = response.body.isEmpty
? <String, dynamic>{}
: jsonDecode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
return decoded is Map<String, dynamic> ? decoded : {'data': decoded};
http.Client _clientFor(Uri uri) {
final client = HttpClient()
..badCertificateCallback = (certificate, host, port) {
debugPrint(
'MyLIS menerima sertifikat internal untuk $host:$port '
'issuer=${certificate.issuer}',
);
return true;
};
return IOClient(client);
}
Map<String, dynamic> _decode(http.Response response, Uri uri) {
final bodyPreview = response.body.length > 240
? '${response.body.substring(0, 240)}...'
: response.body;
if (response.statusCode < 200 || response.statusCode >= 300) {
Object? decoded;
if (_looksLikeJson(response)) {
try {
decoded = response.body.isEmpty
? <String, dynamic>{}
: jsonDecode(response.body);
} catch (_) {
decoded = null;
}
}
final message = decoded is Map ? decoded['message'] : null;
debugPrint(
'MyLIS API gagal: ${response.request?.method ?? '-'} $uri '
'HTTP ${response.statusCode}; body=$bodyPreview',
);
throw ApiException(
message?.toString() ??
'Server mengembalikan HTTP ${response.statusCode}. '
'URL: $uri. Kemungkinan request masuk ke virtual host/proxy yang salah. '
'Detail awal respons: $bodyPreview',
);
}
final message = decoded is Map ? decoded['message'] : null;
throw ApiException(message?.toString() ?? 'Koneksi ke server gagal.');
final Object decoded;
try {
decoded = response.body.isEmpty
? <String, dynamic>{}
: jsonDecode(response.body);
} catch (error, stackTrace) {
debugPrint(
'MyLIS API decode gagal: ${response.request?.method ?? '-'} $uri '
'HTTP ${response.statusCode}; error=$error',
);
debugPrintStack(stackTrace: stackTrace);
throw ApiException(
'Respons server tidak bisa dibaca. '
'URL: $uri. Status HTTP: ${response.statusCode}.',
);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
final cleanDecoded = _decodeHtmlEntitiesIn(decoded);
return cleanDecoded is Map<String, dynamic>
? cleanDecoded
: {'data': cleanDecoded};
}
return {'data': _decodeHtmlEntitiesIn(decoded)};
}
bool _looksLikeJson(http.Response response) {
final contentType = response.headers['content-type']?.toLowerCase() ?? '';
final body = response.body.trimLeft();
return contentType.contains('json') ||
body.startsWith('{') ||
body.startsWith('[');
}
ApiException _connectionException(
Uri uri,
Object error,
StackTrace stackTrace,
) {
if (error is ApiException) {
return error;
}
final reason = _connectionReason(error);
debugPrint('MyLIS koneksi gagal: $uri; $reason; error=$error');
debugPrintStack(stackTrace: stackTrace);
return ApiException('Koneksi server gagal. URL: $uri. Penyebab: $reason');
}
String _connectionReason(Object error) {
if (error is TimeoutException) {
return 'timeout lebih dari 15 detik. Periksa jaringan, DNS lokal, atau server Laravel belum aktif.';
}
if (error is http.ClientException) {
final message = error.message.toLowerCase();
if (message.contains('certificate') ||
message.contains('handshake') ||
message.contains('tls')) {
return 'sertifikat HTTPS lokal belum dipercaya oleh device/simulator.';
}
if (message.contains('failed host lookup') ||
message.contains('nodename') ||
message.contains('name or service')) {
return 'host tidak ditemukan. Periksa URL atau DNS/hosts lokal.';
}
if (message.contains('connection refused')) {
return 'server menolak koneksi. Periksa apakah Laravel/web server aktif.';
}
if (message.contains('connection closed')) {
return 'koneksi ditutup oleh server.';
}
return 'client HTTP gagal: ${error.message}';
}
if (error is FormatException) {
return 'format URL atau respons tidak valid: ${error.message}';
}
return error.toString();
}
Object? _decodeHtmlEntitiesIn(Object? value) {
if (value is String) {
return decodeHtmlEntities(value);
}
if (value is List) {
return value.map(_decodeHtmlEntitiesIn).toList();
}
if (value is Map) {
return value.map(
(key, item) => MapEntry(key.toString(), _decodeHtmlEntitiesIn(item)),
);
}
return value;
}
}
+29
View File
@@ -138,3 +138,32 @@ List<dynamic> asList(Object? value) => value is List ? value : <dynamic>[];
String _message(Object? error) => error is ApiException
? error.message
: 'Terjadi kesalahan saat memuat data.';
String decodeHtmlEntities(String value) {
var result = value;
for (var i = 0; i < 3; i += 1) {
final previous = result;
result = result
.replaceAll(RegExp(r'<\s*br\s*/?\s*>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'</\s*p\s*>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'<[^>]*>'), '')
.replaceAll('&gt;', '>')
.replaceAll('&lt;', '<')
.replaceAll('&amp;', '&')
.replaceAll('&quot;', '"')
.replaceAll('&#039;', "'")
.replaceAll('&apos;', "'");
result = result.replaceAllMapped(RegExp(r'&#(\d+);'), (match) {
final code = int.tryParse(match.group(1) ?? '');
return code == null ? match.group(0)! : String.fromCharCode(code);
});
result = result.replaceAllMapped(RegExp(r'&#x([0-9a-fA-F]+);'), (match) {
final code = int.tryParse(match.group(1) ?? '', radix: 16);
return code == null ? match.group(0)! : String.fromCharCode(code);
});
if (result == previous) {
break;
}
}
return result;
}