This commit is contained in:
Dwi Swandhana
2026-07-22 10:01:55 +07:00
parent 9e4774bd5e
commit 5774d55f71
14 changed files with 292 additions and 36 deletions
+2 -5
View File
@@ -4951,9 +4951,6 @@
dlp = dlp || document.getElementById('dlp').value;
nofoto = nofoto || document.getElementById('nofoto').value;
idne = idne || document.getElementById('periksa_id').value;
$('#dlp').val(dlp);
$('#nofoto').val(nofoto);
$('#periksa_id').val(idne);
$('#template_jenis').val(dlp);
$('#divtemplate').hide();
$('.templatejawaban').hide();
@@ -7201,8 +7198,8 @@
$("#template_jenis").val(dataRecord.judul);
$("#dlp").val(dataRecord.judul);
var dlp = dataRecord.judul;
var nofoto = document.getElementById('nofoto').value || document.getElementById('nofoto').textContent;
var idne = document.getElementById('periksa_id').value || document.getElementById('periksa_id').textContent;
var nofoto = document.getElementById('nofoto').value;
var idne = document.getElementById('periksa_id').value;
openlamanekpertise(dlp, nofoto, id);
}
},
@@ -4989,9 +4989,6 @@
} else {
$('#divtemplate').hide();
}
$('#nofoto').val(nofoto).html(nofoto);
$('#periksa_id').val(idne);
$('#template_jenis').val(dlp);
$('#divreadonly').hide();
$('#divawal').hide();
$('#divperiksa').show();
@@ -7251,8 +7248,8 @@
$("#template_jenis").val(dataRecord.judul);
$("#dlp").val(dataRecord.judul);
var dlp = dataRecord.judul;
var nofoto = document.getElementById('nofoto').value || document.getElementById('nofoto').textContent;
var idne = document.getElementById('periksa_id').value || document.getElementById('periksa_id').textContent;
var nofoto = document.getElementById('nofoto').value;
var idne = document.getElementById('periksa_id').value;
openlamanekpertise(dlp, nofoto, id);
}
},
@@ -26,7 +26,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
}
void _reload() {
setState(() => _future = _api.get('api/mobile/dashboard'));
setState(() {
_future = _api.get('api/mobile/dashboard');
});
}
Future<void> _logout() async {
@@ -29,7 +29,9 @@ class _ExaminationDetailScreenState extends State<ExaminationDetailScreen> {
}
void _reload() {
setState(() => _future = _api.get('api/mobile/examinations/${widget.id}'));
setState(() {
_future = _api.get('api/mobile/examinations/${widget.id}');
});
}
Future<void> _launch(String? url) async {
@@ -45,7 +45,9 @@ class _ExaminationListScreenState extends State<ExaminationListScreen> {
}
void _reload() {
setState(() => _future = _load());
setState(() {
_future = _load();
});
}
@override
@@ -161,7 +161,11 @@ class _CciExpertiseWizardState extends State<CciExpertiseWizard> {
content: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
ExpertiseHtmlEditor(
controller: widget.textControllers['keterangan'] ??=
TextEditingController(),
label: 'Expertise',
),
CheckboxListTile(
value: widget.criticalValue,
onChanged: (value) => widget.onCriticalChanged(value ?? false),
@@ -177,7 +177,11 @@ class _CovidExpertiseWizardState extends State<CovidExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
ExpertiseHtmlEditor(
controller: widget.textControllers['keterangan'] ??=
TextEditingController(),
label: 'Expertise',
),
CheckboxListTile(
value: widget.criticalValue,
onChanged: (value) => widget.onCriticalChanged(value ?? false),
@@ -333,6 +333,222 @@ class CompactMultiSelectField extends StatelessWidget {
}
}
class ExpertiseHtmlEditor extends StatefulWidget {
const ExpertiseHtmlEditor({
super.key,
required this.controller,
this.label = 'Expertise',
});
final TextEditingController controller;
final String label;
@override
State<ExpertiseHtmlEditor> createState() => _ExpertiseHtmlEditorState();
}
class _ExpertiseHtmlEditorState extends State<ExpertiseHtmlEditor> {
bool _preview = false;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Text(
widget.label,
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w900),
),
const Spacer(),
SegmentedButton<bool>(
segments: const [
ButtonSegment(value: false, label: Text('Edit')),
ButtonSegment(value: true, label: Text('Preview')),
],
selected: {_preview},
onSelectionChanged: (value) =>
setState(() => _preview = value.first),
showSelectedIcon: false,
style: const ButtonStyle(
visualDensity: VisualDensity.compact,
),
),
],
),
const SizedBox(height: 10),
_EditorToolbar(
onBold: () => _wrapSelection('<b>', '</b>'),
onItalic: () => _wrapSelection('<i>', '</i>'),
onUnderline: () => _wrapSelection('<u>', '</u>'),
onParagraph: () => _wrapSelection('<p>', '</p>'),
onBullet: _insertBulletList,
onBreak: () => _insertText('<br>'),
onClear: _clearTags,
),
const SizedBox(height: 10),
if (_preview)
Container(
constraints: const BoxConstraints(minHeight: 210),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
border: Border.all(color: const Color(0xFFE2E8F0)),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_plainPreview(widget.controller.text),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
height: 1.45,
fontWeight: FontWeight.w600,
),
),
)
else
TextField(
controller: widget.controller,
minLines: 10,
maxLines: 16,
keyboardType: TextInputType.multiline,
decoration: const InputDecoration(
hintText: 'Tulis expertise...',
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
),
],
),
),
);
}
void _wrapSelection(String open, String close) {
final selection = widget.controller.selection;
final text = widget.controller.text;
if (!selection.isValid || selection.isCollapsed) {
_insertText('$open$close', cursorOffset: open.length);
return;
}
final selectedText = text.substring(selection.start, selection.end);
final replacement = '$open$selectedText$close';
widget.controller.value = TextEditingValue(
text: text.replaceRange(selection.start, selection.end, replacement),
selection: TextSelection.collapsed(
offset: selection.start + replacement.length,
),
);
}
void _insertText(String value, {int? cursorOffset}) {
final selection = widget.controller.selection;
final text = widget.controller.text;
final start = selection.isValid ? selection.start : text.length;
final end = selection.isValid ? selection.end : text.length;
widget.controller.value = TextEditingValue(
text: text.replaceRange(start, end, value),
selection: TextSelection.collapsed(
offset: start + (cursorOffset ?? value.length),
),
);
}
void _insertBulletList() {
final selection = widget.controller.selection;
final text = widget.controller.text;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final items = selectedText
.split('\n')
.where((line) => line.trim().isNotEmpty)
.map((line) => '<li>${line.trim()}</li>')
.join();
final replacement = '<ul>$items</ul>';
widget.controller.value = TextEditingValue(
text: text.replaceRange(selection.start, selection.end, replacement),
selection: TextSelection.collapsed(
offset: selection.start + replacement.length,
),
);
return;
}
_insertText('<ul><li></li></ul>', cursorOffset: '<ul><li>'.length);
}
void _clearTags() {
final clean = _plainPreview(widget.controller.text);
widget.controller.value = TextEditingValue(
text: clean,
selection: TextSelection.collapsed(offset: clean.length),
);
}
String _plainPreview(String value) {
return decodeHtmlEntities(
value
.replaceAll(RegExp(r'<\s*br\s*/?\s*>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'</\s*p\s*>', caseSensitive: false), '\n\n')
.replaceAll(RegExp(r'<\s*li\s*>', caseSensitive: false), '')
.replaceAll(RegExp(r'</\s*li\s*>', caseSensitive: false), '\n')
.replaceAll(RegExp(r'<[^>]*>'), ''),
).trim();
}
}
class _EditorToolbar extends StatelessWidget {
const _EditorToolbar({
required this.onBold,
required this.onItalic,
required this.onUnderline,
required this.onParagraph,
required this.onBullet,
required this.onBreak,
required this.onClear,
});
final VoidCallback onBold;
final VoidCallback onItalic;
final VoidCallback onUnderline;
final VoidCallback onParagraph;
final VoidCallback onBullet;
final VoidCallback onBreak;
final VoidCallback onClear;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 6,
runSpacing: 6,
children: [
_tool(Icons.format_bold, 'Bold', onBold),
_tool(Icons.format_italic, 'Italic', onItalic),
_tool(Icons.format_underlined, 'Underline', onUnderline),
_tool(Icons.notes, 'Paragraph', onParagraph),
_tool(Icons.format_list_bulleted, 'Bullet', onBullet),
_tool(Icons.keyboard_return, 'Line break', onBreak),
_tool(Icons.format_clear, 'Clear', onClear),
],
);
}
Widget _tool(IconData icon, String tooltip, VoidCallback onPressed) {
return IconButton.outlined(
tooltip: tooltip,
onPressed: onPressed,
icon: Icon(icon),
visualDensity: VisualDensity.compact,
);
}
}
class AntibioticSensitivityPanel extends StatefulWidget {
const AntibioticSensitivityPanel({
super.key,
@@ -1264,6 +1480,9 @@ class ExpertiseForm extends StatelessWidget {
}
final controller = textControllers[name] ??= TextEditingController();
if (name == 'keterangan') {
return ExpertiseHtmlEditor(controller: controller, label: label);
}
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
@@ -48,7 +48,9 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
}
void _reload() {
setState(() => _future = _load());
setState(() {
_future = _load();
});
}
void _prepareFields(Map<String, dynamic> data) {
@@ -106,7 +108,9 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
);
if (!mounted) return;
_showMessage('Template $dlp dipilih.');
setState(() => _future = _load());
setState(() {
_future = _load();
});
} on ApiException catch (error) {
_showMessage(error.message);
} catch (_) {
@@ -134,30 +138,35 @@ class _ExpertiseScreenState extends State<ExpertiseScreen> {
fields[entry.key] = entry.value.toList();
}
Map<String, dynamic> data;
try {
final data = await _api
.post('api/mobile/examinations/${widget.id}/expertise', {
'dlp': _currentDlp,
'action': action,
'fields': fields,
'keterangan': fields['keterangan']?.toString() ?? '',
'analis': _staffValues['analis'] ?? '0',
'ppds3': _staffValues['ppds3'] ?? '0',
'dokter': _staffValues['dokter'] ?? '0',
'nilai_kritis': _criticalValue ? '1' : '0',
});
if (!mounted) return;
_showMessage(data['message']?.toString() ?? 'Expertise tersimpan.');
setState(() => _future = _load());
data = await _api.post('api/mobile/examinations/${widget.id}/expertise', {
'dlp': _currentDlp,
'action': action,
'fields': fields,
'keterangan': fields['keterangan']?.toString() ?? '',
'analis': _staffValues['analis'] ?? '0',
'ppds3': _staffValues['ppds3'] ?? '0',
'dokter': _staffValues['dokter'] ?? '0',
'nilai_kritis': _criticalValue ? '1' : '0',
});
} on ApiException catch (error) {
_showMessage(error.message);
return;
} catch (_) {
_showMessage('Expertise tidak bisa disimpan.');
return;
} finally {
if (mounted) {
setState(() => _saving = false);
}
}
if (!mounted) return;
_showMessage(data['message']?.toString() ?? 'Expertise tersimpan.');
setState(() {
_future = _load();
});
}
int _wizardStep(String dlp) => _wizardSteps[dlp] ?? 0;
@@ -339,7 +339,11 @@ class _KulturExpertiseWizardState extends State<KulturExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
ExpertiseHtmlEditor(
controller: widget.textControllers['keterangan'] ??=
TextEditingController(),
label: 'Expertise',
),
CheckboxListTile(
value: widget.criticalValue,
onChanged: (value) => widget.onCriticalChanged(value ?? false),
@@ -167,7 +167,11 @@ class _LeptospiraExpertiseWizardState extends State<LeptospiraExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
ExpertiseHtmlEditor(
controller: widget.textControllers['keterangan'] ??=
TextEditingController(),
label: 'Expertise',
),
CheckboxListTile(
value: widget.criticalValue,
onChanged: (value) => widget.onCriticalChanged(value ?? false),
@@ -277,7 +277,11 @@ class _PewarnaanLangsungExpertiseWizardState
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
ExpertiseHtmlEditor(
controller: widget.textControllers['keterangan'] ??=
TextEditingController(),
label: 'Expertise',
),
CheckboxListTile(
value: widget.criticalValue,
onChanged: (value) => widget.onCriticalChanged(value ?? false),
@@ -403,7 +403,11 @@ class _TbcExpertiseWizardState extends State<TbcExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
ExpertiseHtmlEditor(
controller: widget.textControllers['keterangan'] ??=
TextEditingController(),
label: 'Expertise',
),
CheckboxListTile(
value: widget.criticalValue,
onChanged: (value) => widget.onCriticalChanged(value ?? false),
@@ -180,7 +180,11 @@ class _ViralLoadExpertiseWizardState extends State<ViralLoadExpertiseWizard> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_text('keterangan', 'Expertise', minLines: 8, maxLines: 12),
ExpertiseHtmlEditor(
controller: widget.textControllers['keterangan'] ??=
TextEditingController(),
label: 'Expertise',
),
CheckboxListTile(
value: widget.criticalValue,
onChanged: (value) => widget.onCriticalChanged(value ?? false),