update
This commit is contained in:
@@ -386,6 +386,7 @@ class MobileApiController extends Controller
|
|||||||
'preliminary' => 'Preliminary Results',
|
'preliminary' => 'Preliminary Results',
|
||||||
'Permohonan Verifikasi' => 'Expertise Saved (Un Verified)',
|
'Permohonan Verifikasi' => 'Expertise Saved (Un Verified)',
|
||||||
'Permohonan Verifikasi Preliminary' => 'Otor Maldi (Un Verified)',
|
'Permohonan Verifikasi Preliminary' => 'Otor Maldi (Un Verified)',
|
||||||
|
'statuspewarnaanlsg' => $this->resolvePeriksaHierarchyStatus($row->status, 'Pewarnaan Langsung'),
|
||||||
default => 'Pemeriksaan awal',
|
default => 'Pemeriksaan awal',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -424,6 +425,7 @@ class MobileApiController extends Controller
|
|||||||
'preliminary' => 'Preliminary Results',
|
'preliminary' => 'Preliminary Results',
|
||||||
'Permohonan Verifikasi' => 'Kirim ke SPV',
|
'Permohonan Verifikasi' => 'Kirim ke SPV',
|
||||||
'Permohonan Verifikasi Preliminary' => 'Kirim Preliminary ke SPV',
|
'Permohonan Verifikasi Preliminary' => 'Kirim Preliminary ke SPV',
|
||||||
|
'statuspewarnaanlsg' => 'Pewarnaan Langsung',
|
||||||
default => 'Draft',
|
default => 'Draft',
|
||||||
},
|
},
|
||||||
'verifikasi' => in_array($action, ['verifikasi', 'preliminary'], true) ? 'Accepted' : '',
|
'verifikasi' => in_array($action, ['verifikasi', 'preliminary'], true) ? 'Accepted' : '',
|
||||||
@@ -435,6 +437,7 @@ class MobileApiController extends Controller
|
|||||||
'preliminary' => 'Preliminary Results Saved',
|
'preliminary' => 'Preliminary Results Saved',
|
||||||
'Permohonan Verifikasi' => 'Permohonan Verifikasi Terkirim',
|
'Permohonan Verifikasi' => 'Permohonan Verifikasi Terkirim',
|
||||||
'Permohonan Verifikasi Preliminary' => 'Permohonan Verifikasi Preliminary Terkirim',
|
'Permohonan Verifikasi Preliminary' => 'Permohonan Verifikasi Preliminary Terkirim',
|
||||||
|
'statuspewarnaanlsg' => 'Pewarnaan Langsung tersimpan',
|
||||||
default => 'Draft Expertise Saved',
|
default => 'Draft Expertise Saved',
|
||||||
},
|
},
|
||||||
'item' => $this->examinationPayload($row->fresh()),
|
'item' => $this->examinationPayload($row->fresh()),
|
||||||
@@ -958,6 +961,123 @@ class MobileApiController extends Controller
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function resolvePeriksaHierarchyStatus($currentStatus, $proposedStatus): string
|
||||||
|
{
|
||||||
|
$currentStatus = trim((string) $currentStatus);
|
||||||
|
$proposedStatus = trim((string) $proposedStatus);
|
||||||
|
|
||||||
|
if ($proposedStatus === '') {
|
||||||
|
return $currentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentRank = $this->periksaHierarchyStatusRank($currentStatus);
|
||||||
|
$proposedRank = $this->periksaHierarchyStatusRank($proposedStatus);
|
||||||
|
|
||||||
|
if ($currentStatus === '') {
|
||||||
|
return $proposedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($proposedRank === null) {
|
||||||
|
return $currentRank === null ? $proposedStatus : $currentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentRank === null || $proposedRank >= $currentRank) {
|
||||||
|
return $proposedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $currentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function periksaHierarchyStatusRank($status): ?int
|
||||||
|
{
|
||||||
|
$normalized = $this->normalizePeriksaStatus($status);
|
||||||
|
|
||||||
|
if ($normalized === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized === 'penerimaan sampel') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($normalized, ['pemeriksaan awal', 'pemeriksaan sampel', 'proses analisis sampel'], true)) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($normalized, 'data bd di terima') || str_starts_with($normalized, 'data bd diterima')) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized === 'pewarnaan langsung') {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isProsesingKoloniMediaStatus($normalized)) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($normalized, 'data malditof diterima') || str_starts_with($normalized, 'data malditof di terima')) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($normalized, 'otor maldi (un verified)')) {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($normalized, 'preliminary results')) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($normalized, 'data vitek di terima') || str_starts_with($normalized, 'data vitek diterima')) {
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($normalized, 'id/ast pending result') || $normalized === 'sedang id+ast') {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($normalized, 'expertise saved (un verified)')
|
||||||
|
|| in_array($normalized, ['expertise', 'selesai', 'final result'], true)) {
|
||||||
|
return 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizePeriksaStatus($status): string
|
||||||
|
{
|
||||||
|
$status = strtolower(trim((string) $status));
|
||||||
|
$status = str_replace(['_', '-'], ' ', $status);
|
||||||
|
$status = preg_replace('/\s+/', ' ', $status);
|
||||||
|
|
||||||
|
return trim($status);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isProsesingKoloniMediaStatus(string $normalizedStatus): bool
|
||||||
|
{
|
||||||
|
$exactStatuses = [
|
||||||
|
'tidak ada pertumbuhan',
|
||||||
|
'tidak lanjut identifikasi',
|
||||||
|
'tidak tindak lanjut identifikasi',
|
||||||
|
'inkubasi lanjutan',
|
||||||
|
'sub kultur',
|
||||||
|
'menunggu kultur yg lain',
|
||||||
|
'menunggu kultur yang lain',
|
||||||
|
'proses identifikasi dan uji kepekaan',
|
||||||
|
'sedang malditof',
|
||||||
|
'pertumbuhan primer',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (in_array($normalizedStatus, $exactStatuses, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_starts_with($normalizedStatus, 'prosesing koloni media')
|
||||||
|
|| str_starts_with($normalizedStatus, 'processing koloni media')
|
||||||
|
|| str_starts_with($normalizedStatus, 'proses identifikasi')
|
||||||
|
|| str_starts_with($normalizedStatus, 'proses uji kepekaan');
|
||||||
|
}
|
||||||
|
|
||||||
public function earlyWarning(Request $request)
|
public function earlyWarning(Request $request)
|
||||||
{
|
{
|
||||||
$this->requireUser($request);
|
$this->requireUser($request);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
if (trim($rows->komponen) == 'bakteri') {$bakteri = $rows->isidata; }
|
if (trim($rows->komponen) == 'bakteri') {$bakteri = $rows->isidata; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($periksa->kd_spesimen == 'KULTUR JMR'){
|
if ($periksa->kd_spesimen == 'KULTUR JMR' OR $periksa->kd_spesimen == 'JAMUR'){
|
||||||
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
||||||
} else {
|
} else {
|
||||||
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
$namakuman = '';
|
$namakuman = '';
|
||||||
$namakuman1 = '';
|
$namakuman1 = '';
|
||||||
$namakuman2 = '';
|
$namakuman2 = '';
|
||||||
if ($periksa->kd_spesimen == 'KULTUR JMR'){
|
if ($periksa->kd_spesimen == 'KULTUR JMR' OR $periksa->kd_spesimen == 'JAMUR'){
|
||||||
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
||||||
} else {
|
} else {
|
||||||
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
$namakuman = '';
|
$namakuman = '';
|
||||||
$namakuman1 = '';
|
$namakuman1 = '';
|
||||||
$namakuman2 = '';
|
$namakuman2 = '';
|
||||||
if ($periksa->kd_spesimen == 'KULTUR JMR'){
|
if ($periksa->kd_spesimen == 'KULTUR JMR' OR $periksa->kd_spesimen == 'JAMUR'){
|
||||||
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
||||||
} else {
|
} else {
|
||||||
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
</table>
|
</table>
|
||||||
@php
|
@php
|
||||||
$cekketerangan = explode('/table', $keterangan);
|
$cekketerangan = explode('/table', $keterangan);
|
||||||
if ($periksa->kd_spesimen == 'KULTUR JMR'){
|
if ($periksa->kd_spesimen == 'KULTUR JMR' OR $periksa->kd_spesimen == 'JAMUR'){
|
||||||
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
||||||
} else {
|
} else {
|
||||||
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
||||||
|
|||||||
@@ -333,7 +333,7 @@
|
|||||||
$namakuman = '';
|
$namakuman = '';
|
||||||
$namakuman1 = '';
|
$namakuman1 = '';
|
||||||
$namakuman2 = '';
|
$namakuman2 = '';
|
||||||
if ($periksa->kd_spesimen == 'KULTUR JMR'){
|
if ($periksa->kd_spesimen == 'KULTUR JMR' OR $periksa->kd_spesimen == 'JAMUR'){
|
||||||
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
||||||
} else {
|
} else {
|
||||||
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
$namakuman = '';
|
$namakuman = '';
|
||||||
$namakuman1 = '';
|
$namakuman1 = '';
|
||||||
$namakuman2 = '';
|
$namakuman2 = '';
|
||||||
if ($periksa->kd_spesimen == 'KULTUR JMR'){
|
if ($periksa->kd_spesimen == 'KULTUR JMR' OR $periksa->kd_spesimen == 'JAMUR'){
|
||||||
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
||||||
} else {
|
} else {
|
||||||
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
if (trim($rows->komponen) == 'iggigm_interpretasi') {$iggigm_interpretasi = $rows->isidata; }
|
if (trim($rows->komponen) == 'iggigm_interpretasi') {$iggigm_interpretasi = $rows->isidata; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($periksa->kd_spesimen == 'KULTUR JMR'){
|
if ($periksa->kd_spesimen == 'KULTUR JMR' OR $periksa->kd_spesimen == 'JAMUR'){
|
||||||
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
$teksmikroorganisme = 'Jamur yang ditemukan : ';
|
||||||
} else {
|
} else {
|
||||||
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
$teksmikroorganisme = 'Bakteri yang ditemukan : ';
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http/io_client.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'app/app.dart';
|
import 'app/app.dart';
|
||||||
export 'app/app.dart';
|
export 'app/app.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
HttpOverrides.global = _InternalCertificateHttpOverrides();
|
||||||
runApp(const MyLisApp());
|
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.onCriticalChanged,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
|
required this.initialStep,
|
||||||
|
required this.onStepChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
@@ -31,47 +33,42 @@ class CciExpertiseWizard extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onCriticalChanged;
|
final ValueChanged<bool> onCriticalChanged;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
final int initialStep;
|
||||||
|
final ValueChanged<int> onStepChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CciExpertiseWizard> createState() => _CciExpertiseWizardState();
|
State<CciExpertiseWizard> createState() => _CciExpertiseWizardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CciExpertiseWizardState extends State<CciExpertiseWizard> {
|
class _CciExpertiseWizardState extends State<CciExpertiseWizard> {
|
||||||
int _step = 0;
|
late int _step;
|
||||||
|
|
||||||
void _next() {
|
@override
|
||||||
setState(() => _step = (_step + 1).clamp(0, 3));
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_step = widget.initialStep.clamp(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previous() {
|
@override
|
||||||
setState(() => _step = (_step - 1).clamp(0, 3));
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stepper(
|
return ExpertiseWizardShell(
|
||||||
currentStep: _step,
|
currentStep: _step,
|
||||||
onStepTapped: (step) => setState(() => _step = step),
|
onStepChanged: _setStep,
|
||||||
controlsBuilder: (context, details) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: _step == 0 ? null : _previous,
|
|
||||||
icon: const Icon(Icons.chevron_left),
|
|
||||||
label: const Text('Previous'),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: _step == 3 ? null : _next,
|
|
||||||
icon: const Icon(Icons.chevron_right),
|
|
||||||
label: const Text('Next'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
steps: [
|
steps: [
|
||||||
Step(
|
Step(
|
||||||
title: const Text('Pasien & Petugas'),
|
title: const Text('Pasien & Petugas'),
|
||||||
@@ -339,60 +336,136 @@ class PatientStaffPanel extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final status = item['status']?.toString() ?? '-';
|
||||||
|
final genderAge = '${item['jkpasien'] ?? '-'}, ${item['usia'] ?? '-'}';
|
||||||
return Column(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Card(
|
Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(14),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
DetailRow(label: 'Order', value: item['reques']),
|
Row(
|
||||||
DetailRow(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
label: 'Dokter Pengirim',
|
children: [
|
||||||
value: item['klinisi'] ?? item['nmdokter'],
|
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']),
|
const SizedBox(height: 14),
|
||||||
DetailRow(label: 'No. Lokal Lab', value: item['nofoto']),
|
_InfoGrid(
|
||||||
DetailRow(label: 'Register Number', value: item['noregister']),
|
items: [
|
||||||
DetailRow(label: 'Name', value: item['nmpasien']),
|
_InfoItem('Order', item['reques'], Icons.receipt_long),
|
||||||
DetailRow(
|
_InfoItem(
|
||||||
label: 'Gender, Age',
|
'Dokter Pengirim',
|
||||||
value: '${item['jkpasien'] ?? '-'}, ${item['usia'] ?? '-'}',
|
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']),
|
const SizedBox(height: 8),
|
||||||
DetailRow(label: 'Address', value: item['alamatpasien']),
|
_InfoTile(
|
||||||
DetailRow(
|
label: 'Address',
|
||||||
label: 'Tanggal Registrasi',
|
value: item['alamatpasien'],
|
||||||
value: item['tanggalregis'] ?? item['daftar'],
|
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),
|
const SizedBox(height: 12),
|
||||||
_staffSelect('analis', 'ATLM', asList(staffOptions['analis'])),
|
Card(
|
||||||
_staffSelect('ppds3', 'PPDS', asList(staffOptions['ppds'])),
|
margin: EdgeInsets.zero,
|
||||||
_staffSelect('dokter', 'SPV', asList(staffOptions['dokters'])),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
DetailRow(label: 'Klinis/Diagnosis', value: item['klinis']),
|
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(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.account_circle_outlined),
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
const DropdownMenuItem(value: '0', child: Text('Pilih')),
|
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 = [
|
const List<String> _sirOptions = [
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class CovidExpertiseWizard extends StatefulWidget {
|
|||||||
required this.onCriticalChanged,
|
required this.onCriticalChanged,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
|
required this.initialStep,
|
||||||
|
required this.onStepChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
@@ -29,47 +31,42 @@ class CovidExpertiseWizard extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onCriticalChanged;
|
final ValueChanged<bool> onCriticalChanged;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
final int initialStep;
|
||||||
|
final ValueChanged<int> onStepChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CovidExpertiseWizard> createState() => _CovidExpertiseWizardState();
|
State<CovidExpertiseWizard> createState() => _CovidExpertiseWizardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CovidExpertiseWizardState extends State<CovidExpertiseWizard> {
|
class _CovidExpertiseWizardState extends State<CovidExpertiseWizard> {
|
||||||
int _step = 0;
|
late int _step;
|
||||||
|
|
||||||
void _next() {
|
@override
|
||||||
setState(() => _step = (_step + 1).clamp(0, 4));
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_step = widget.initialStep.clamp(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previous() {
|
@override
|
||||||
setState(() => _step = (_step - 1).clamp(0, 4));
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stepper(
|
return ExpertiseWizardShell(
|
||||||
currentStep: _step,
|
currentStep: _step,
|
||||||
onStepTapped: (step) => setState(() => _step = step),
|
onStepChanged: _setStep,
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
steps: [
|
steps: [
|
||||||
Step(
|
Step(
|
||||||
title: const Text('Pasien & Petugas'),
|
title: const Text('Pasien & Petugas'),
|
||||||
@@ -136,49 +133,10 @@ class _CovidExpertiseWizardState extends State<CovidExpertiseWizard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _antibioticSensitivity() {
|
Widget _antibioticSensitivity() {
|
||||||
return Column(
|
return AntibioticSensitivityPanel(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
textControllers: widget.textControllers,
|
||||||
children: [
|
selectValues: widget.selectValues,
|
||||||
_sectionTitle('Tes Kepekaan Antibiotik'),
|
onChanged: widget.onChanged,
|
||||||
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'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
|
|||||||
final Map<String, String> _selectValues = {};
|
final Map<String, String> _selectValues = {};
|
||||||
final Map<String, Set<String>> _multiValues = {};
|
final Map<String, Set<String>> _multiValues = {};
|
||||||
final Map<String, String> _staffValues = {};
|
final Map<String, String> _staffValues = {};
|
||||||
|
final Map<String, int> _wizardSteps = {};
|
||||||
String _currentDlp = '';
|
String _currentDlp = '';
|
||||||
bool _criticalValue = false;
|
bool _criticalValue = false;
|
||||||
bool _saving = 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 {
|
Future<void> _setTemplate(String dlp) async {
|
||||||
@@ -113,6 +119,9 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _save(String action) async {
|
Future<void> _save(String action) async {
|
||||||
|
if (action == 'statuspewarnaanlsg') {
|
||||||
|
_wizardSteps[_currentDlp] = 1;
|
||||||
|
}
|
||||||
setState(() => _saving = true);
|
setState(() => _saving = true);
|
||||||
final fields = <String, dynamic>{};
|
final fields = <String, dynamic>{};
|
||||||
for (final entry in _textControllers.entries) {
|
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) {
|
void _showMessage(String message) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
@@ -188,201 +203,233 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
|
|||||||
_prepareFields(data);
|
_prepareFields(data);
|
||||||
final isSupervisor = _isSupervisor(user['previlage']?.toString());
|
final isSupervisor = _isSupervisor(user['previlage']?.toString());
|
||||||
|
|
||||||
return ListView(
|
final summaryCard = Card(
|
||||||
padding: const EdgeInsets.all(16),
|
shape: RoundedRectangleBorder(
|
||||||
children: [
|
borderRadius: BorderRadius.circular(8),
|
||||||
Card(
|
),
|
||||||
shape: RoundedRectangleBorder(
|
child: Padding(
|
||||||
borderRadius: BorderRadius.circular(8),
|
padding: const EdgeInsets.all(14),
|
||||||
),
|
child: Column(
|
||||||
child: Padding(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.all(14),
|
children: [
|
||||||
child: Column(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
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: [
|
children: [
|
||||||
Text(
|
StatusPill(status: item['status']?.toString() ?? 'NEW'),
|
||||||
item['nmpasien']?.toString() ?? '-',
|
const SizedBox(width: 8),
|
||||||
style: Theme.of(context).textTheme.titleMedium
|
if (_currentDlp.isNotEmpty)
|
||||||
?.copyWith(fontWeight: FontWeight.w800),
|
BadgeLabel(
|
||||||
),
|
text: _currentDlp,
|
||||||
const SizedBox(height: 6),
|
color: const Color(0xFF0F766E),
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
),
|
||||||
if (_currentDlp.isEmpty)
|
);
|
||||||
TemplatePicker(
|
|
||||||
|
final expertiseContent = _currentDlp.isEmpty
|
||||||
|
? TemplatePicker(
|
||||||
templates: asList(data['templates']),
|
templates: asList(data['templates']),
|
||||||
saving: _saving,
|
saving: _saving,
|
||||||
onUse: _setTemplate,
|
onUse: _setTemplate,
|
||||||
)
|
)
|
||||||
else
|
: _currentDlp == 'CCI'
|
||||||
_currentDlp == 'CCI'
|
? CciExpertiseWizard(
|
||||||
? 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,
|
item: item,
|
||||||
user: user,
|
user: user,
|
||||||
staffOptions: asMap(data['staff_options']),
|
staffOptions: asMap(data['staff_options']),
|
||||||
optionSets: asMap(data['option_sets']),
|
|
||||||
textControllers: _textControllers,
|
|
||||||
selectValues: _selectValues,
|
|
||||||
staffValues: _staffValues,
|
staffValues: _staffValues,
|
||||||
criticalValue: _criticalValue,
|
|
||||||
isSupervisor: isSupervisor,
|
|
||||||
saving: _saving,
|
|
||||||
onCriticalChanged: (value) =>
|
|
||||||
setState(() => _criticalValue = value),
|
|
||||||
onChanged: () => setState(() {}),
|
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.onCriticalChanged,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
|
required this.initialStep,
|
||||||
|
required this.onStepChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
@@ -33,47 +35,42 @@ class KulturExpertiseWizard extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onCriticalChanged;
|
final ValueChanged<bool> onCriticalChanged;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
final int initialStep;
|
||||||
|
final ValueChanged<int> onStepChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<KulturExpertiseWizard> createState() => _KulturExpertiseWizardState();
|
State<KulturExpertiseWizard> createState() => _KulturExpertiseWizardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
|
class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
|
||||||
int _step = 0;
|
late int _step;
|
||||||
|
|
||||||
void _next() {
|
@override
|
||||||
setState(() => _step = (_step + 1).clamp(0, 5));
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_step = widget.initialStep.clamp(0, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previous() {
|
@override
|
||||||
setState(() => _step = (_step - 1).clamp(0, 5));
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stepper(
|
return ExpertiseWizardShell(
|
||||||
currentStep: _step,
|
currentStep: _step,
|
||||||
onStepTapped: (step) => setState(() => _step = step),
|
onStepChanged: _setStep,
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
steps: [
|
steps: [
|
||||||
Step(
|
Step(
|
||||||
title: const Text('Pasien & Petugas'),
|
title: const Text('Pasien & Petugas'),
|
||||||
@@ -172,10 +169,32 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
|
|||||||
_options('jsonpewarnaangiemsa', _giemsaFallback),
|
_options('jsonpewarnaangiemsa', _giemsaFallback),
|
||||||
),
|
),
|
||||||
_text('id_pewarnaangiesmaoptional', 'Pewarnaan Giemsa Manual'),
|
_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() {
|
Widget _nugentScore() {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@@ -276,53 +295,10 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _antibioticSensitivity() {
|
Widget _antibioticSensitivity() {
|
||||||
return Column(
|
return AntibioticSensitivityPanel(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
textControllers: widget.textControllers,
|
||||||
children: [
|
selectValues: widget.selectValues,
|
||||||
_sectionTitle('C. Tes Kepekaan Antibiotik'),
|
onChanged: widget.onChanged,
|
||||||
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)),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,32 +488,14 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
|
|||||||
final selected = widget.multiValues[name] ?? <String>{};
|
final selected = widget.multiValues[name] ?? <String>{};
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: InputDecorator(
|
child: CompactMultiSelectField(
|
||||||
decoration: InputDecoration(
|
label: label,
|
||||||
labelText: label,
|
options: options,
|
||||||
border: const OutlineInputBorder(),
|
selected: selected,
|
||||||
),
|
onChanged: (value) {
|
||||||
child: Wrap(
|
widget.multiValues[name] = value;
|
||||||
spacing: 8,
|
widget.onChanged();
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -572,24 +530,10 @@ class _KirbyAntibiotic {
|
|||||||
final String sirField;
|
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> _scoreOptions = ['0', '1', '2', '3', '4'];
|
||||||
|
|
||||||
const List<String> _antibioticSirOptions = ['S', 'I', 'R'];
|
const List<String> _antibioticSirOptions = ['S', 'I', 'R'];
|
||||||
|
|
||||||
const List<String> _growthOptions = [
|
|
||||||
'Ada Pertumbuhan',
|
|
||||||
'Tidak Ada Pertumbuhan',
|
|
||||||
'Pertumbuhan Primer',
|
|
||||||
];
|
|
||||||
|
|
||||||
const List<String> _kirbyZoneOptions = [
|
const List<String> _kirbyZoneOptions = [
|
||||||
'Tidak dilakukan',
|
'Tidak dilakukan',
|
||||||
'1',
|
'1',
|
||||||
@@ -643,30 +587,6 @@ const List<_KirbyAntibiotic> _kirbyAntibiotics = [
|
|||||||
_KirbyAntibiotic('SCF', 'id_kbscf', 'id_sirkbscf'),
|
_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 = [
|
const List<String> _kohFallback = [
|
||||||
'Ditemukan morfologi Hifa',
|
'Ditemukan morfologi Hifa',
|
||||||
'Ditemukan morfologi Budding Cell',
|
'Ditemukan morfologi Budding Cell',
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class LeptospiraExpertiseWizard extends StatefulWidget {
|
|||||||
required this.onCriticalChanged,
|
required this.onCriticalChanged,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
|
required this.initialStep,
|
||||||
|
required this.onStepChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
@@ -29,6 +31,8 @@ class LeptospiraExpertiseWizard extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onCriticalChanged;
|
final ValueChanged<bool> onCriticalChanged;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
final int initialStep;
|
||||||
|
final ValueChanged<int> onStepChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LeptospiraExpertiseWizard> createState() =>
|
State<LeptospiraExpertiseWizard> createState() =>
|
||||||
@@ -36,41 +40,34 @@ class LeptospiraExpertiseWizard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LeptospiraExpertiseWizardState extends State<LeptospiraExpertiseWizard> {
|
class _LeptospiraExpertiseWizardState extends State<LeptospiraExpertiseWizard> {
|
||||||
int _step = 0;
|
late int _step;
|
||||||
|
|
||||||
void _next() {
|
@override
|
||||||
setState(() => _step = (_step + 1).clamp(0, 4));
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_step = widget.initialStep.clamp(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previous() {
|
@override
|
||||||
setState(() => _step = (_step - 1).clamp(0, 4));
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stepper(
|
return ExpertiseWizardShell(
|
||||||
currentStep: _step,
|
currentStep: _step,
|
||||||
onStepTapped: (step) => setState(() => _step = step),
|
onStepChanged: _setStep,
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
steps: [
|
steps: [
|
||||||
Step(
|
Step(
|
||||||
title: const Text('Pasien & Petugas'),
|
title: const Text('Pasien & Petugas'),
|
||||||
@@ -126,49 +123,10 @@ class _LeptospiraExpertiseWizardState extends State<LeptospiraExpertiseWizard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _antibioticSensitivity() {
|
Widget _antibioticSensitivity() {
|
||||||
return Column(
|
return AntibioticSensitivityPanel(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
textControllers: widget.textControllers,
|
||||||
children: [
|
selectValues: widget.selectValues,
|
||||||
_sectionTitle('Tes Kepekaan Antibiotik'),
|
onChanged: widget.onChanged,
|
||||||
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'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class PewarnaanLangsungExpertiseWizard extends StatefulWidget {
|
|||||||
required this.onCriticalChanged,
|
required this.onCriticalChanged,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
|
required this.initialStep,
|
||||||
|
required this.onStepChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
@@ -33,6 +35,8 @@ class PewarnaanLangsungExpertiseWizard extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onCriticalChanged;
|
final ValueChanged<bool> onCriticalChanged;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
final int initialStep;
|
||||||
|
final ValueChanged<int> onStepChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PewarnaanLangsungExpertiseWizard> createState() =>
|
State<PewarnaanLangsungExpertiseWizard> createState() =>
|
||||||
@@ -41,41 +45,34 @@ class PewarnaanLangsungExpertiseWizard extends StatefulWidget {
|
|||||||
|
|
||||||
class _PewarnaanLangsungExpertiseWizardState
|
class _PewarnaanLangsungExpertiseWizardState
|
||||||
extends State<PewarnaanLangsungExpertiseWizard> {
|
extends State<PewarnaanLangsungExpertiseWizard> {
|
||||||
int _step = 0;
|
late int _step;
|
||||||
|
|
||||||
void _next() {
|
@override
|
||||||
setState(() => _step = (_step + 1).clamp(0, 4));
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_step = widget.initialStep.clamp(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previous() {
|
@override
|
||||||
setState(() => _step = (_step - 1).clamp(0, 4));
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stepper(
|
return ExpertiseWizardShell(
|
||||||
currentStep: _step,
|
currentStep: _step,
|
||||||
onStepTapped: (step) => setState(() => _step = step),
|
onStepChanged: _setStep,
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
steps: [
|
steps: [
|
||||||
Step(
|
Step(
|
||||||
title: const Text('Pasien & Petugas'),
|
title: const Text('Pasien & Petugas'),
|
||||||
@@ -175,10 +172,32 @@ class _PewarnaanLangsungExpertiseWizardState
|
|||||||
maxLines: 8,
|
maxLines: 8,
|
||||||
),
|
),
|
||||||
_nugentScore(),
|
_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() {
|
Widget _nugentScore() {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@@ -214,49 +233,10 @@ class _PewarnaanLangsungExpertiseWizardState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _antibioticSensitivity() {
|
Widget _antibioticSensitivity() {
|
||||||
return Column(
|
return AntibioticSensitivityPanel(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
textControllers: widget.textControllers,
|
||||||
children: [
|
selectValues: widget.selectValues,
|
||||||
_sectionTitle('Tes Kepekaan Antibiotik'),
|
onChanged: widget.onChanged,
|
||||||
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'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,32 +420,14 @@ class _PewarnaanLangsungExpertiseWizardState
|
|||||||
final selected = widget.multiValues[name] ?? <String>{};
|
final selected = widget.multiValues[name] ?? <String>{};
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: InputDecorator(
|
child: CompactMultiSelectField(
|
||||||
decoration: InputDecoration(
|
label: label,
|
||||||
labelText: label,
|
options: options,
|
||||||
border: const OutlineInputBorder(),
|
selected: selected,
|
||||||
),
|
onChanged: (value) {
|
||||||
child: Wrap(
|
widget.multiValues[name] = value;
|
||||||
spacing: 8,
|
widget.onChanged();
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class TbcExpertiseWizard extends StatefulWidget {
|
|||||||
required this.onCriticalChanged,
|
required this.onCriticalChanged,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
|
required this.initialStep,
|
||||||
|
required this.onStepChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
@@ -31,47 +33,42 @@ class TbcExpertiseWizard extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onCriticalChanged;
|
final ValueChanged<bool> onCriticalChanged;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
final int initialStep;
|
||||||
|
final ValueChanged<int> onStepChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TbcExpertiseWizard> createState() => _TbcExpertiseWizardState();
|
State<TbcExpertiseWizard> createState() => _TbcExpertiseWizardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
|
class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
|
||||||
int _step = 0;
|
late int _step;
|
||||||
|
|
||||||
void _next() {
|
@override
|
||||||
setState(() => _step = (_step + 1).clamp(0, 6));
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_step = widget.initialStep.clamp(0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previous() {
|
@override
|
||||||
setState(() => _step = (_step - 1).clamp(0, 6));
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stepper(
|
return ExpertiseWizardShell(
|
||||||
currentStep: _step,
|
currentStep: _step,
|
||||||
onStepTapped: (step) => setState(() => _step = step),
|
onStepChanged: _setStep,
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
steps: [
|
steps: [
|
||||||
Step(
|
Step(
|
||||||
title: const Text('Pasien & Petugas'),
|
title: const Text('Pasien & Petugas'),
|
||||||
@@ -362,38 +359,10 @@ class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _antibioticSensitivity() {
|
Widget _antibioticSensitivity() {
|
||||||
return Column(
|
return AntibioticSensitivityPanel(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
textControllers: widget.textControllers,
|
||||||
children: [
|
selectValues: widget.selectValues,
|
||||||
_sectionTitle('Tes Kepekaan Antibiotik'),
|
onChanged: widget.onChanged,
|
||||||
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'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,32 +536,14 @@ class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
|
|||||||
final selected = widget.multiValues[name] ?? <String>{};
|
final selected = widget.multiValues[name] ?? <String>{};
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: InputDecorator(
|
child: CompactMultiSelectField(
|
||||||
decoration: InputDecoration(
|
label: label,
|
||||||
labelText: label,
|
options: options,
|
||||||
border: const OutlineInputBorder(),
|
selected: selected,
|
||||||
),
|
onChanged: (value) {
|
||||||
child: Wrap(
|
widget.multiValues[name] = value;
|
||||||
spacing: 8,
|
widget.onChanged();
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class ViralLoadExpertiseWizard extends StatefulWidget {
|
|||||||
required this.onCriticalChanged,
|
required this.onCriticalChanged,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
|
required this.initialStep,
|
||||||
|
required this.onStepChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Map<String, dynamic> item;
|
final Map<String, dynamic> item;
|
||||||
@@ -31,6 +33,8 @@ class ViralLoadExpertiseWizard extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onCriticalChanged;
|
final ValueChanged<bool> onCriticalChanged;
|
||||||
final VoidCallback onChanged;
|
final VoidCallback onChanged;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
final int initialStep;
|
||||||
|
final ValueChanged<int> onStepChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ViralLoadExpertiseWizard> createState() =>
|
State<ViralLoadExpertiseWizard> createState() =>
|
||||||
@@ -38,41 +42,34 @@ class ViralLoadExpertiseWizard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ViralLoadExpertiseWizardState extends State<ViralLoadExpertiseWizard> {
|
class _ViralLoadExpertiseWizardState extends State<ViralLoadExpertiseWizard> {
|
||||||
int _step = 0;
|
late int _step;
|
||||||
|
|
||||||
void _next() {
|
@override
|
||||||
setState(() => _step = (_step + 1).clamp(0, 4));
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_step = widget.initialStep.clamp(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previous() {
|
@override
|
||||||
setState(() => _step = (_step - 1).clamp(0, 4));
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stepper(
|
return ExpertiseWizardShell(
|
||||||
currentStep: _step,
|
currentStep: _step,
|
||||||
onStepTapped: (step) => setState(() => _step = step),
|
onStepChanged: _setStep,
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
steps: [
|
steps: [
|
||||||
Step(
|
Step(
|
||||||
title: const Text('Pasien & Petugas'),
|
title: const Text('Pasien & Petugas'),
|
||||||
@@ -139,49 +136,10 @@ class _ViralLoadExpertiseWizardState extends State<ViralLoadExpertiseWizard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _antibioticSensitivity() {
|
Widget _antibioticSensitivity() {
|
||||||
return Column(
|
return AntibioticSensitivityPanel(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
textControllers: widget.textControllers,
|
||||||
children: [
|
selectValues: widget.selectValues,
|
||||||
_sectionTitle('Tes Kepekaan Antibiotik'),
|
onChanged: widget.onChanged,
|
||||||
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'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
);
|
);
|
||||||
} on ApiException catch (error) {
|
} on ApiException catch (error) {
|
||||||
_showMessage(error.message);
|
_showMessage(error.message);
|
||||||
} catch (_) {
|
} catch (error, stackTrace) {
|
||||||
_showMessage('Tidak bisa terhubung ke server MyLIS.');
|
debugPrint('Login MyLIS gagal tidak terduga: $error');
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
_showMessage('Tidak bisa terhubung ke server MyLIS. Penyebab: $error');
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _loading = false);
|
if (mounted) setState(() => _loading = false);
|
||||||
}
|
}
|
||||||
@@ -93,8 +95,10 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
});
|
});
|
||||||
} on ApiException catch (error) {
|
} on ApiException catch (error) {
|
||||||
setState(() => _pingStatus = error.message);
|
setState(() => _pingStatus = error.message);
|
||||||
} catch (_) {
|
} catch (error, stackTrace) {
|
||||||
setState(() => _pingStatus = 'Koneksi server gagal.');
|
debugPrint('Ping MyLIS gagal tidak terduga: $error');
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
setState(() => _pingStatus = 'Koneksi server gagal. Penyebab: $error');
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _pinging = false);
|
if (mounted) setState(() => _pinging = false);
|
||||||
}
|
}
|
||||||
@@ -120,29 +124,34 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Row(
|
||||||
child: Image.asset(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
kAppLogoAsset,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
height: 96,
|
children: [
|
||||||
fit: BoxFit.contain,
|
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),
|
const SizedBox(height: 18),
|
||||||
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),
|
|
||||||
Text(
|
Text(
|
||||||
kHospitalName,
|
kHospitalName,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -152,6 +161,13 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
kAppLongName,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Login petugas',
|
'Login petugas',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -261,6 +277,15 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
context,
|
context,
|
||||||
).textTheme.bodySmall?.copyWith(color: Colors.black45),
|
).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,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,31 +25,171 @@ class ApiClient {
|
|||||||
String path,
|
String path,
|
||||||
Map<String, dynamic> body,
|
Map<String, dynamic> body,
|
||||||
) async {
|
) async {
|
||||||
final response = await http.post(
|
final uri = _uri(path);
|
||||||
_uri(path),
|
final client = _clientFor(uri);
|
||||||
headers: _headers,
|
try {
|
||||||
body: jsonEncode(body),
|
final response = await client
|
||||||
);
|
.post(uri, headers: _headers, body: jsonEncode(body))
|
||||||
return _decode(response);
|
.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(
|
Future<Map<String, dynamic>> get(
|
||||||
String path, [
|
String path, [
|
||||||
Map<String, String>? query,
|
Map<String, String>? query,
|
||||||
]) async {
|
]) async {
|
||||||
final response = await http.get(_uri(path, query), headers: _headers);
|
final uri = _uri(path, query);
|
||||||
return _decode(response);
|
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) {
|
http.Client _clientFor(Uri uri) {
|
||||||
final decoded = response.body.isEmpty
|
final client = HttpClient()
|
||||||
? <String, dynamic>{}
|
..badCertificateCallback = (certificate, host, port) {
|
||||||
: jsonDecode(response.body);
|
debugPrint(
|
||||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
'MyLIS menerima sertifikat internal untuk $host:$port '
|
||||||
return decoded is Map<String, dynamic> ? decoded : {'data': decoded};
|
'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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,3 +138,32 @@ List<dynamic> asList(Object? value) => value is List ? value : <dynamic>[];
|
|||||||
String _message(Object? error) => error is ApiException
|
String _message(Object? error) => error is ApiException
|
||||||
? error.message
|
? error.message
|
||||||
: 'Terjadi kesalahan saat memuat data.';
|
: '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('>', '>')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', "'")
|
||||||
|
.replaceAll(''', "'");
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user