From 5774d55f710fe11de031a46e7cc7743eeaa4291e Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Wed, 22 Jul 2026 10:01:55 +0700 Subject: [PATCH] update --- htdocs/resources/views/dokter/ppds.blade.php | 7 +- .../views/dokter/ppdsdeveloper.blade.php | 7 +- .../screens/dashboard/dashboard_screen.dart | 4 +- .../examination_detail_screen.dart | 4 +- .../examinations/examination_list_screen.dart | 4 +- .../expertise/cci_expertise_wizard.dart | 6 +- .../expertise/covid_expertise_wizard.dart | 6 +- .../lib/screens/expertise/expertise_form.dart | 219 ++++++++++++++++++ .../screens/expertise/expertise_screen.dart | 41 ++-- .../expertise/kultur_expertise_wizard.dart | 6 +- .../leptospira_expertise_wizard.dart | 6 +- .../pewarnaan_langsung_expertise_wizard.dart | 6 +- .../expertise/tbc_expertise_wizard.dart | 6 +- .../viral_load_expertise_wizard.dart | 6 +- 14 files changed, 292 insertions(+), 36 deletions(-) diff --git a/htdocs/resources/views/dokter/ppds.blade.php b/htdocs/resources/views/dokter/ppds.blade.php index c8423ab3..f0888a39 100644 --- a/htdocs/resources/views/dokter/ppds.blade.php +++ b/htdocs/resources/views/dokter/ppds.blade.php @@ -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); } }, diff --git a/htdocs/resources/views/dokter/ppdsdeveloper.blade.php b/htdocs/resources/views/dokter/ppdsdeveloper.blade.php index ea0dc7b5..740c153f 100644 --- a/htdocs/resources/views/dokter/ppdsdeveloper.blade.php +++ b/htdocs/resources/views/dokter/ppdsdeveloper.blade.php @@ -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); } }, diff --git a/mylis/lib/screens/dashboard/dashboard_screen.dart b/mylis/lib/screens/dashboard/dashboard_screen.dart index e46eb474..8668d0de 100644 --- a/mylis/lib/screens/dashboard/dashboard_screen.dart +++ b/mylis/lib/screens/dashboard/dashboard_screen.dart @@ -26,7 +26,9 @@ class _DashboardScreenState extends State { } void _reload() { - setState(() => _future = _api.get('api/mobile/dashboard')); + setState(() { + _future = _api.get('api/mobile/dashboard'); + }); } Future _logout() async { diff --git a/mylis/lib/screens/examinations/examination_detail_screen.dart b/mylis/lib/screens/examinations/examination_detail_screen.dart index ec7c35b3..8fa1575a 100644 --- a/mylis/lib/screens/examinations/examination_detail_screen.dart +++ b/mylis/lib/screens/examinations/examination_detail_screen.dart @@ -29,7 +29,9 @@ class _ExaminationDetailScreenState extends State { } void _reload() { - setState(() => _future = _api.get('api/mobile/examinations/${widget.id}')); + setState(() { + _future = _api.get('api/mobile/examinations/${widget.id}'); + }); } Future _launch(String? url) async { diff --git a/mylis/lib/screens/examinations/examination_list_screen.dart b/mylis/lib/screens/examinations/examination_list_screen.dart index c1677308..dea8551e 100644 --- a/mylis/lib/screens/examinations/examination_list_screen.dart +++ b/mylis/lib/screens/examinations/examination_list_screen.dart @@ -45,7 +45,9 @@ class _ExaminationListScreenState extends State { } void _reload() { - setState(() => _future = _load()); + setState(() { + _future = _load(); + }); } @override diff --git a/mylis/lib/screens/expertise/cci_expertise_wizard.dart b/mylis/lib/screens/expertise/cci_expertise_wizard.dart index 4529338a..8034f626 100644 --- a/mylis/lib/screens/expertise/cci_expertise_wizard.dart +++ b/mylis/lib/screens/expertise/cci_expertise_wizard.dart @@ -161,7 +161,11 @@ class _CciExpertiseWizardState extends State { 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), diff --git a/mylis/lib/screens/expertise/covid_expertise_wizard.dart b/mylis/lib/screens/expertise/covid_expertise_wizard.dart index 0105141d..178ff743 100644 --- a/mylis/lib/screens/expertise/covid_expertise_wizard.dart +++ b/mylis/lib/screens/expertise/covid_expertise_wizard.dart @@ -177,7 +177,11 @@ class _CovidExpertiseWizardState extends State { 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), diff --git a/mylis/lib/screens/expertise/expertise_form.dart b/mylis/lib/screens/expertise/expertise_form.dart index b4b55884..818b96ac 100644 --- a/mylis/lib/screens/expertise/expertise_form.dart +++ b/mylis/lib/screens/expertise/expertise_form.dart @@ -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 createState() => _ExpertiseHtmlEditorState(); +} + +class _ExpertiseHtmlEditorState extends State { + 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( + 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('', ''), + onItalic: () => _wrapSelection('', ''), + onUnderline: () => _wrapSelection('', ''), + onParagraph: () => _wrapSelection('

', '

'), + onBullet: _insertBulletList, + onBreak: () => _insertText('
'), + 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) => '
  • ${line.trim()}
  • ') + .join(); + final replacement = '
      $items
    '; + widget.controller.value = TextEditingValue( + text: text.replaceRange(selection.start, selection.end, replacement), + selection: TextSelection.collapsed( + offset: selection.start + replacement.length, + ), + ); + return; + } + _insertText('
    ', cursorOffset: '
    • '.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'', caseSensitive: false), '\n\n') + .replaceAll(RegExp(r'<\s*li\s*>', caseSensitive: false), '• ') + .replaceAll(RegExp(r'', 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( diff --git a/mylis/lib/screens/expertise/expertise_screen.dart b/mylis/lib/screens/expertise/expertise_screen.dart index fcd01461..c1233e73 100644 --- a/mylis/lib/screens/expertise/expertise_screen.dart +++ b/mylis/lib/screens/expertise/expertise_screen.dart @@ -48,7 +48,9 @@ class _ExpertiseScreenState extends State { } void _reload() { - setState(() => _future = _load()); + setState(() { + _future = _load(); + }); } void _prepareFields(Map data) { @@ -106,7 +108,9 @@ class _ExpertiseScreenState extends State { ); 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 { fields[entry.key] = entry.value.toList(); } + Map 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; diff --git a/mylis/lib/screens/expertise/kultur_expertise_wizard.dart b/mylis/lib/screens/expertise/kultur_expertise_wizard.dart index 64deca12..96193a1b 100644 --- a/mylis/lib/screens/expertise/kultur_expertise_wizard.dart +++ b/mylis/lib/screens/expertise/kultur_expertise_wizard.dart @@ -339,7 +339,11 @@ class _KulturExpertiseWizardState extends State { 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), diff --git a/mylis/lib/screens/expertise/leptospira_expertise_wizard.dart b/mylis/lib/screens/expertise/leptospira_expertise_wizard.dart index 9a0361f7..856a69be 100644 --- a/mylis/lib/screens/expertise/leptospira_expertise_wizard.dart +++ b/mylis/lib/screens/expertise/leptospira_expertise_wizard.dart @@ -167,7 +167,11 @@ class _LeptospiraExpertiseWizardState extends State { 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), diff --git a/mylis/lib/screens/expertise/pewarnaan_langsung_expertise_wizard.dart b/mylis/lib/screens/expertise/pewarnaan_langsung_expertise_wizard.dart index d84bca72..06161048 100644 --- a/mylis/lib/screens/expertise/pewarnaan_langsung_expertise_wizard.dart +++ b/mylis/lib/screens/expertise/pewarnaan_langsung_expertise_wizard.dart @@ -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), diff --git a/mylis/lib/screens/expertise/tbc_expertise_wizard.dart b/mylis/lib/screens/expertise/tbc_expertise_wizard.dart index 5071493c..cb0449a0 100644 --- a/mylis/lib/screens/expertise/tbc_expertise_wizard.dart +++ b/mylis/lib/screens/expertise/tbc_expertise_wizard.dart @@ -403,7 +403,11 @@ class _TbcExpertiseWizardState extends State { 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), diff --git a/mylis/lib/screens/expertise/viral_load_expertise_wizard.dart b/mylis/lib/screens/expertise/viral_load_expertise_wizard.dart index 1645cdbb..a80c1a99 100644 --- a/mylis/lib/screens/expertise/viral_load_expertise_wizard.dart +++ b/mylis/lib/screens/expertise/viral_load_expertise_wizard.dart @@ -180,7 +180,11 @@ class _ViralLoadExpertiseWizardState extends State { 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),