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