Squashed commit of the following:

commit bcfb4c1456
Merge: 1cbde57 975c87d
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Mon Nov 17 11:15:14 2025 +0700

    Merge pull request #147 from dikstub-rssa/feat/surat-kontrol-135

    Feat: Integration Rehab Medik - Surat Kontrol

commit 975c87d99a
Merge: f582090 1cbde57
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Mon Nov 17 10:58:10 2025 +0700

    Merge branch 'dev' into feat/surat-kontrol-135

commit 1cbde57cf9
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Sun Nov 16 00:44:53 2025 +0700

    dev: hotfix

    comps/pub/myui
    + updated data/types
    + updated data-table
    + updated nav-header
    + added toggle

    comps/pub/ui
    + updated button
    + updated toggle

commit ccabe0177b
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Fri Nov 14 16:39:21 2025 +0700

    dev: hotfix, added combobox objectsToItems

commit f582090d18
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Thu Nov 13 11:56:21 2025 +0700

    Fix: Refactor surat kontrol

commit 0d97ba9d25
Merge: 02508b2 bb8df3d
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Thu Nov 13 11:52:23 2025 +0700

    Merge pull request #164 from dikstub-rssa/feeat/pendaftaran-kemoterapi-141

    Feat: Pendaftaran Kemoterapi

commit bb8df3d53a
Merge: a592a0b 02508b2
Author: riefive <rie.five@gmail.com>
Date:   Thu Nov 13 10:14:17 2025 +0700

    Merge branch 'dev' of https://github.com/dikstub-rssa/simrs-fe into feeat/pendaftaran-kemoterapi-141

commit 02508b22de
Merge: 6b933de 295bb81
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Thu Nov 13 07:56:31 2025 +0700

    Merge pull request #162 from dikstub-rssa/fe-prescription-56

    Fe prescription 56

commit 295bb8120f
Merge: 8462eba 6b933de
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Thu Nov 13 07:45:48 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit 6b933de212
Merge: f2e98fc 471c846
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Wed Nov 12 07:13:11 2025 +0700

    Merge pull request #156 from dikstub-rssa/feat/cp-lab-order-48

    Feat/cp lab order 48

commit f2e98fc732
Merge: 2e899c6 9b281de
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Wed Nov 12 07:12:40 2025 +0700

    Merge pull request #158 from dikstub-rssa/feat/menu-structure

    Feat/menu structure

commit 471c846045
Merge: f676c8a 2e899c6
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Nov 12 07:11:46 2025 +0700

    Merge branch 'dev' into feat/cp-lab-order-48

commit 9b281de00b
Merge: 80383a5 2e899c6
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Nov 12 07:08:29 2025 +0700

    Merge branch 'dev' into feat/menu-structure

commit 2e899c6022
Merge: 8effefb b7d4fcf
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Wed Nov 12 07:04:47 2025 +0700

    Merge pull request #157 from dikstub-rssa/feat/encounter-status-107

    Feat/encounter status 107

commit 8effefb5ad
Merge: 3f63f19 8e7f9b1
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Wed Nov 12 07:04:17 2025 +0700

    Merge pull request #155 from dikstub-rssa/feat/radiology-order-54

    Feat/radiology order 54

commit 80383a5f0a
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Nov 12 06:52:31 2025 +0700

    feat/menu-structure: adjust page rehab

commit 93c9e74d08
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Nov 12 06:52:14 2025 +0700

    feat/menu-structure: adjust menu items all roles

commit f0d2bc4de1
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Nov 12 06:51:37 2025 +0700

    feat/menu-structure: update access control

commit 02c14089f1
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Nov 12 06:51:03 2025 +0700

    feat/menu-structure: update role switcher

commit a14c4a5d3c
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Tue Nov 11 14:21:58 2025 +0700

    Fix: Refactor Surat Kontrol CRUD {id} to {code}

commit e9e0e21d1b
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Tue Nov 11 12:30:43 2025 +0700

    feat/menu-structure: wip

commit 8e7f9b19e3
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Mon Nov 10 23:17:49 2025 +0700

    feat/radiology-order-54: upgraded mcu-order/list

commit 24313adef6
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Fri Nov 7 10:35:46 2025 +0700

    Fix: debug back btn in add, edit, detail content page

commit 59b44b5729
Merge: 99a61a0 db15ec9
Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
Date:   Fri Nov 7 09:11:10 2025 +0700

    Merge branch 'dev' into feat/surat-kontrol-135

commit 99a61a0bf2
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Thu Nov 6 08:06:01 2025 +0700

    Feat: add right & bottom label in input base component

commit 8462eba94b
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Nov 5 21:23:04 2025 +0700

    feat/prescription-56: wip

commit db48919325
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Wed Nov 5 13:53:43 2025 +0700

    Feat: add banner in List if requirement not met

commit bd57250f7e
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Wed Nov 5 13:26:48 2025 +0700

    Fix: refactor getDetail url param

commit a361922e32
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Wed Nov 5 13:19:07 2025 +0700

    Feat: Add & integrate add, edit, detail page

commit 331f4a6b20
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Tue Nov 4 16:56:08 2025 +0700

    Feat: Integrate Control Letter

commit a592a0be36
Author: riefive <rie.five@gmail.com>
Date:   Tue Nov 4 15:15:38 2025 +0700

    feat(cemo): add home encounter

commit be0a761170
Author: riefive <rie.five@gmail.com>
Date:   Tue Nov 4 13:23:52 2025 +0700

    feat(cemo): change flow admin

commit 64fe2524fb
Author: riefive <rie.five@gmail.com>
Date:   Tue Nov 4 12:02:31 2025 +0700

    feat(cemo): enhance admin mode functionality and update series handling

commit fb7731188d
Author: riefive <rie.five@gmail.com>
Date:   Mon Nov 3 15:52:35 2025 +0700

    feat(cemo): add mode adm + series

commit 89b2fb9cd9
Author: riefive <rie.five@gmail.com>
Date:   Mon Nov 3 15:03:56 2025 +0700

    feat(chemo): add page process and modify components

commit f676c8a4b9
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Nov 3 08:11:02 2025 +0700

    feat/cp-lab-order-48: wip

commit 69ffe6bd49
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Fri Oct 31 14:35:39 2025 +0700

    feat/radiology-order: added the page

commit d1369d513b
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 31 16:08:22 2025 +0700

    feat(cemo): add list verification

commit a9ab75fd98
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Fri Oct 31 14:35:05 2025 +0700

    feat/readiology-order: added mcu

commit 71d68e5a0e
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 31 14:49:21 2025 +0700

    feat(cemo): add dialog verification and list register

commit f8d906b6c2
Merge: 66872c9 5f9e441
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Fri Oct 31 14:48:21 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit 40d78a999a
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Fri Oct 31 14:35:05 2025 +0700

    feat/readiology-order: added mcu

commit b3502df0f8
Merge: 831749a 7119f67
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 31 13:12:11 2025 +0700

    Merge branch 'feat/fe-kemoterapi' into feeat/pendaftaran-kemoterapi-141

commit 7119f67402
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 31 13:09:59 2025 +0700

    feat(cemo): modify schema

commit 66872c95f8
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Fri Oct 31 07:56:36 2025 +0700

    feat/prescription-56: wip

commit 45cc019ec1
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 30 15:43:50 2025 +0700

    feat(cemo): layouting form

commit e866c0cf2a
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 30 14:41:52 2025 +0700

    feat(cemo): layouting protocol

commit dc4edc1dc0
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 29 15:58:52 2025 +0700

    feat(cemo): show list cemo

commit 3234853473
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 29 15:39:55 2025 +0700

    feat(cemo): add list of cemo

commit 67ee129f4b
Merge: 9919b4b 9e82d17
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Tue Oct 28 16:20:17 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit 2275f4dc99
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Mon Oct 27 14:01:58 2025 +0700

    Feat: add UI BPJS > Surat Kontrol

commit 89e0e7a2c8
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Mon Oct 27 10:21:59 2025 +0700

    Feat: add UI CRUD Surat Kontrol at Rehab Medik > kunjungan > Proses

commit 9919b4b896
Merge: 19a43bd e93e72a
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sat Oct 25 15:36:29 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit b7d4fcf939
Merge: eaac4aa e93e72a
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sat Oct 25 15:31:30 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit 19a43bd291
Merge: d90e400 3558672
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sat Oct 25 05:02:57 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit eaac4aab85
Merge: 72e8d43 3558672
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sat Oct 25 05:01:45 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit d90e40043c
Merge: 0c9f9de b90f0c1
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Fri Oct 24 12:40:59 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit 0c9f9deb7e
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Fri Oct 24 12:38:02 2025 +0700

    fe-prescription-56: wip

commit 729474a2a0
Merge: 7159bd6 ddd35d6
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Thu Oct 23 14:16:52 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit 72e8d431d6
Merge: 3f77d92 2a9b78a
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Oct 22 07:17:41 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit 3f77d927b6
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Tue Oct 21 22:48:34 2025 +0700

    feat/encounter: done

commit d8c861d60c
Merge: 6bdee66 27ab7c2
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Tue Oct 21 00:15:01 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit 7159bd6566
Merge: ccc9b0b be5768b
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 13 07:45:15 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit ccc9b0bda3
Merge: f94ccd7 cad7ac6
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 13 06:29:03 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit 6bdee66cc6
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 13 06:26:30 2025 +0700

    feat/encounter: wip

commit f7c53fc4e5
Merge: a7c7ef6 cad7ac6
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 13 06:24:45 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit a7c7ef6dd8
Merge: 89b051b f52e516
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sun Oct 12 13:27:06 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit 89b051b883
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sun Oct 12 13:18:46 2025 +0700

    feat/encounter-status-107: wip

commit 743c38804a
Merge: d6d60e3 f7b66d2
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sun Oct 12 11:53:03 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit d6d60e38d0
Merge: 9530cdd 18e00bf
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sun Oct 12 11:49:49 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit 18e00bf89a
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Sun Oct 12 11:40:53 2025 +0700

    dev: hotfix, text-size standardization

commit 9530cdd4f9
Merge: 0820cb6 0d1e469
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sun Oct 12 11:41:49 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit 0d1e469ece
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Sun Oct 12 11:40:53 2025 +0700

    dev: hotfix, text-size standardization

commit 0820cb653c
Merge: fff1ce0 867c1b4
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sat Oct 11 00:38:10 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit fff1ce0eb7
Merge: 1a3edd5 3a4b2aa
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sat Oct 11 00:35:16 2025 +0700

    Merge branch 'dev' into feat/encounter-status-107

commit 3a4b2aa6fb
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Sat Oct 11 00:25:44 2025 +0700

    dev: hotfix, moved combobox and datepicker

commit 1a3edd5a1e
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Fri Oct 10 23:58:44 2025 +0700

    dev: hotfix, moved combobox and datepicker

commit f94ccd707b
Merge: 0647675 51d1221
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Wed Oct 8 08:00:01 2025 +0700

    Merge branch 'feat/consultation-82' into fe-prescription-56

commit 06476756fb
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Wed Oct 8 07:58:48 2025 +0700

    fe-prescription-56: wip

commit fdbcfed87f
Merge: 4da896a bd66a88
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Tue Oct 7 03:10:19 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit bd66a8887d
Merge: 19e00fa ba61d05
Author: Munawwirul Jamal <57973347+munaja@users.noreply.github.com>
Date:   Tue Oct 7 03:07:46 2025 +0700

    Merge pull request #103 from dikstub-rssa/feat/fe-integrasi-org-src-72

    Feat - Integrasi Org Src

commit ba61d05257
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 12:42:08 2025 +0700

    fix: adjustment division app + flow

commit 8601d4a4fd
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 11:07:05 2025 +0700

    fix: remove shared handlers

commit fff5f2c11d
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 11:06:29 2025 +0700

    fix: update content list of specialist, subspecialist, etc

commit 301cb82803
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 11:00:14 2025 +0700

    fix: update list medicine

commit 3003ec9d80
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 10:45:15 2025 +0700

    fix: update list division + equipment

commit d1bcd6e66c
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 10:38:10 2025 +0700

    fix: update some service

commit 78ae8a8aa0
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 10:26:25 2025 +0700

    fix: medicine method and group

commit 8eaf95dd3e
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 10:20:05 2025 +0700

    fix: update service for unit and uom

commit 58c0dde377
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 10:14:51 2025 +0700

    fix: update handler for unit and uom

commit fe23c75aca
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 10:09:24 2025 +0700

    fix: update some service and handlers

commit 4da896a242
Merge: 285c3ee 19e00fa
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 6 09:55:24 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit ecdc5d80d9
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 09:51:31 2025 +0700

    fix: update device service and handler

commit 45ea70d415
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 09:44:48 2025 +0700

    fix: update crud base

commit 51ddb9d8b5
Merge: 42a54bb 19e00fa
Author: riefive <rie.five@gmail.com>
Date:   Mon Oct 6 09:40:22 2025 +0700

    fix: resolve conflict

commit 19e00fa143
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Mon Oct 6 08:26:08 2025 +0700

    dev: hotfix, moved encounter to pub/component

commit 285c3ee4e5
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 6 07:56:29 2025 +0700

    Merged Stash

commit 421159971e
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 6 04:41:21 2025 +0700

    feat/prescription-56: wip

commit 3a45de413d
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 6 04:31:08 2025 +0700

    Merge from Stash

commit e959c3ae61
Merge: 32c69af ad4695c
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Oct 6 04:21:06 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit ad4695c867
Author: Munawwirul Jamal <munawwirul.jamal@gmail.com>
Date:   Mon Oct 6 04:18:55 2025 +0700

    dev: hotfix, encounter content back nav

commit 42a54bbb3b
Merge: a7cbbee 55559a4
Author: riefive <rie.five@gmail.com>
Date:   Sat Oct 4 09:07:03 2025 +0700

    fix: solve conflict after pull

commit a7cbbeeda9
Author: riefive <rie.five@gmail.com>
Date:   Sat Oct 4 09:05:28 2025 +0700

    feat(division): fixing logic treeview

commit 71e0615ee1
Author: riefive <rie.five@gmail.com>
Date:   Sat Oct 4 08:49:38 2025 +0700

    feat(division): restructure division parent

commit f02903e756
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 15:00:10 2025 +0700

    feat(division): change parent id to number before integrate

commit 2e8667a780
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 14:47:02 2025 +0700

    feat(division): parent id to default null

commit e65e562690
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 12:45:05 2025 +0700

    feat(division): change form attribute

commit 9407501c49
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 11:03:19 2025 +0700

    feat(division): change compoent combobox to tree select

commit c5ba07a226
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 10:54:35 2025 +0700

    feat(division): create tree item converter for division

commit 8e7ce771b0
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 15:31:27 2025 +0700

    test: call division position

commit 7edab33427
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:49:55 2025 +0700

    fix: handler reset state

commit ce59eac86c
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:37:17 2025 +0700

    fix: list unit

commit 39d2869ffb
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:34:19 2025 +0700

    fix: list with params error

commit 3c046a4d82
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:12:10 2025 +0700

    fix: list integration

commit 6feb480a51
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 11:16:23 2025 +0700

    fix: change get encounter class to constants

commit d7d984810e
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 11:00:05 2025 +0700

    remove previous list + form from any features

commit a6377ef943
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 15:24:54 2025 +0700

    fix: includes for unit

commit b00b9b198e
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 15:17:31 2025 +0700

    fix: includes for medicine list

commit 4908f16770
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 14:56:11 2025 +0700

    fix: search on list file

commit 41405ae113
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 14:36:48 2025 +0700

    fix: resolve list organization source

commit 6b69e48bd6
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 13:05:36 2025 +0700

    feat(installation): add encounter list

commit 59847dce34
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 13:01:24 2025 +0700

    chore: add shared handlers

commit e78342829e
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 12:38:04 2025 +0700

    feat(installation): integrate api installation

commit 55559a4683
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 15:00:10 2025 +0700

    feat(division): change parent id to number before integrate

commit 2d8c751788
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 14:47:02 2025 +0700

    feat(division): parent id to default null

commit f374f9ef5b
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 12:45:05 2025 +0700

    feat(division): change form attribute

commit 1837afce6c
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 11:03:19 2025 +0700

    feat(division): change compoent combobox to tree select

commit 539a1cefb0
Author: riefive <rie.five@gmail.com>
Date:   Fri Oct 3 10:54:35 2025 +0700

    feat(division): create tree item converter for division

commit 32c69af4e1
Merge: 0752855 10bbee9
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Fri Oct 3 06:05:35 2025 +0700

    Merge branch 'feat/layout-cleaning' into fe-prescription-56

commit 757b8c0444
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 15:31:27 2025 +0700

    test: call division position

commit 378e6773b8
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:49:55 2025 +0700

    fix: handler reset state

commit 0e115eed5e
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:37:17 2025 +0700

    fix: list unit

commit d544d031c3
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:34:19 2025 +0700

    fix: list with params error

commit 693d8225bf
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 14:12:10 2025 +0700

    fix: list integration

commit 0752855808
Merge: f83dbfe c0557cc
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Thu Oct 2 12:46:54 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit fc3bda14f4
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 11:16:23 2025 +0700

    fix: change get encounter class to constants

commit 9603915fd7
Author: riefive <rie.five@gmail.com>
Date:   Thu Oct 2 11:00:05 2025 +0700

    remove previous list + form from any features

commit 546423bdfb
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 15:24:54 2025 +0700

    fix: includes for unit

commit db48233f6c
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 15:17:31 2025 +0700

    fix: includes for medicine list

commit 54a5aaa78f
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 14:56:11 2025 +0700

    fix: search on list file

commit cc41118570
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 14:36:48 2025 +0700

    fix: resolve list organization source

commit 6a7a9cda80
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 13:05:36 2025 +0700

    feat(installation): add encounter list

commit c96d738379
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 13:01:24 2025 +0700

    chore: add shared handlers

commit a48f375018
Author: riefive <rie.five@gmail.com>
Date:   Wed Oct 1 12:38:04 2025 +0700

    feat(installation): integrate api installation

commit f83dbfeae3
Merge: ba77ed1 f29eb38
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Wed Oct 1 04:02:05 2025 +0700

    Merge branch 'feat/layout-cleaning' into fe-prescription-56

commit ba77ed1bb5
Merge: 4fbd8ee 97d36f1
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Mon Sep 29 08:27:01 2025 +0700

    Merge branch 'dev' into fe-prescription-56

commit 4fbd8ee757
Author: Andrian Roshandy <andrianovsky95@gmail.com>
Date:   Sun Sep 28 07:10:32 2025 +0700

    feat/prescription-56: merapikan models
This commit is contained in:
hasyim_kai
2025-11-18 13:26:58 +07:00
parent 270869b928
commit 9d93afed2b
151 changed files with 7702 additions and 270 deletions
@@ -0,0 +1,220 @@
<script setup lang="ts">
import type { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
import type { Summary } from '~/components/pub/my-ui/summary-card/type'
// #region Imports
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Filter from '~/components/pub/my-ui/nav-header/filter.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import SummaryCard from '~/components/pub/my-ui/summary-card/summary-card.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { getPatients, removePatient } from '~/services/patient.service'
import FilterDialog from '~/components/pub/my-ui/nav-header/filter-dialog.vue'
// #endregion
// #region State
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getPatients({ ...params, includes: ['person', 'person-Addresses'] }),
entityName: 'patient',
})
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
isFormEntryDialogOpen.value = true
},
onInput: (val: string) => {
searchInput.value = val
},
onClear: () => {
searchInput.value = ''
},
}
const refExportNav: RefExportNav = {
onExportCsv: () => {
// open filter modal
console.log(`Export CSV Clicked`)
},
}
const isFormEntryDialogOpen = ref(false)
const isHistoryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
const summaryLoading = ref(false)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: "Surat Kontrol",
icon: 'i-lucide-newspaper',
}
const filterPrep: HeaderPrep = {
title: "Surat Kontrol",
icon: 'i-lucide-newspaper',
}
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
getPatientSummary()
})
// #endregion
// #region Functions
async function getPatientSummary() {
try {
summaryLoading.value = true
await new Promise((resolve) => setTimeout(resolve, 500))
} catch (error) {
console.error('Error fetching patient summary:', error)
} finally {
summaryLoading.value = false
}
}
function handleFiltering() {
console.log('Confirmed action: Filter')
}
// Handle confirmation result
async function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
if (action === 'delete' && record?.id) {
try {
const result = await removePatient(record.id)
if (result.success) {
console.log('Patient deleted successfully')
// Refresh the list
await fetchData()
} else {
console.error('Failed to delete patient:', result)
// Handle error - show error message to user
}
} catch (error) {
console.error('Error deleting patient:', error)
// Handle error - show error message to user
}
}
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
function exportCsv() {
console.log('Ekspor CSV dipilih')
// tambahkan logic untuk generate CSV
}
function exportExcel() {
console.log('Ekspor Excel dipilih')
// tambahkan logic untuk generate Excel
}
// #endregion
// #region Provide
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Watchers
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showProcess:
navigateTo('https://google.com', { external: true, open: { target: '_blank' } });
break
case ActionEvents.showDetail:
isHistoryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<Header :prep="{ ...headerPrep }" />
<!-- Disable dulu, ayahab kalo diminta beneran -->
<!-- <div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
<template v-if="summaryLoading">
<SummaryCard
v-for="n in 4"
:key="n"
is-skeleton
/>
</template>
<template v-else>
<SummaryCard
v-for="card in summaryData"
:key="card.title"
:stat="card"
/>
</template>
</div>
</div>
-->
<FilterDialog :prep="{ ...filterPrep }"
:ref-search-nav="refSearchNav"
:ref-export-nav="refExportNav"
:enable-search="false"
:enable-date-range="false"/>
<AppBpjsControlLetterList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog
v-model:open="isFormEntryDialogOpen"
title="Filter"
>
<AppBpjsControlLetterFilter @submit="handleFiltering" />
</Dialog>
<Dialog
v-model:open="isHistoryDialogOpen"
title="Log History Surat Kontrol">
<AppBpjsControlLetterCommonHistoryDialog />
</Dialog>
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.firstName">
<strong>Nama:</strong>
{{ record.firstName }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.cellphone }}
</p>
</div>
</template>
</RecordConfirmation>
</template>
@@ -0,0 +1,150 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { format } from 'date-fns'
import { id as localeID } from 'date-fns/locale'
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
import { Button } from '~/components/pub/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '~/components/pub/ui/popover'
import Input from '~/components/pub/ui/input/Input.vue'
import RangeCalendar from '~/components/pub/ui/range-calendar/RangeCalendar.vue'
import AppChemotherapyListAdmin from '~/components/app/chemotherapy/list-admin.vue'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Sample data - replace with actual API call
import { sampleRows, type ChemotherapyData } from '~/components/app/chemotherapy/sample'
const route = useRoute()
const recId = ref(0)
const recAction = ref('')
const recItem = ref<any>(null)
const search = ref('')
const mode = route.params.mode as string || 'admin'
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: null,
to: null,
})
// Format date range for display
const dateRangeDisplay = computed(() => {
if (dateRange.value.from && dateRange.value.to) {
return `${format(dateRange.value.from, 'dd MMMM yyyy', { locale: localeID })} - ${format(dateRange.value.to, 'dd MMMM yyyy', { locale: localeID })}`
}
return '12 Agustus 2025 - 32 Agustus 2025' // Default display
})
// Filter + search (client-side)
const filtered = computed(() => {
const q = search.value.trim().toLowerCase()
return sampleRows.filter((r: ChemotherapyData) => {
if (q) {
return r.nama.toLowerCase().includes(q) || r.noRm.toLowerCase().includes(q)
}
return true
})
})
// Pagination meta
const paginationMeta = reactive<PaginationMeta>({
recordCount: filtered.value.length,
page: 1,
pageSize: 10,
totalPage: Math.ceil(filtered.value.length / 10),
hasNext: false,
hasPrev: false,
})
function handlePageChange(page: number) {
paginationMeta.page = page
paginationMeta.hasNext = page < paginationMeta.totalPage
paginationMeta.hasPrev = page > 1
}
function handleFilter() {
// TODO: Implement filter logic
console.log('Filter clicked', { search: search.value, dateRange: dateRange.value })
}
// Provide proses handler for action button
function handleProses(rec: any) {
// Navigate to verification page with record
navigateTo(`/outpation-action/chemotherapy/verification?id=${rec.id}`)
}
watch([recId, recAction], () => {
switch (recAction.value) {
case 'Process':
navigateTo(`/outpation-action/chemotherapy/${mode}/${recId.value}/verification`)
break
case 'Verification':
break
}
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('proses-handler', handleProses)
</script>
<template>
<div class="mx-auto max-w-full">
<!-- Header Section -->
<div class="border-b p-6">
<h1 class="text-2xl font-semibold">Administrasi Pasien Rawat Jalan Kemoterapi</h1>
<p class="mt-1 text-sm text-gray-500">
Manajemen pendaftaran serta monitoring terapi pasien tindakan rawat jalan
</p>
</div>
<!-- Search and Filter Bar -->
<div class="flex flex-wrap items-center gap-3 border-b p-4">
<!-- Search Input -->
<div class="relative">
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
v-model="search"
placeholder="Cari Nama /No.RM"
class="w-64 pl-9"
/>
</div>
<!-- Date Range Picker -->
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
class="h-10 w-72 justify-start border-gray-300 bg-white text-left font-normal"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ dateRangeDisplay }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar />
</PopoverContent>
</Popover>
<!-- Filter Button -->
<Button
variant="outline"
class="ml-auto border-orange-500 bg-orange-50 text-orange-600 hover:bg-orange-100"
@click="handleFilter"
>
<FilterIcon class="mr-2 h-4 w-4" />
Filter
</Button>
</div>
<!-- Data Table -->
<div class="overflow-x-auto p-4">
<AppChemotherapyListAdmin
:data="filtered"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</div>
</template>
@@ -0,0 +1,75 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
// Components
import AppChemotherapyList from '~/components/app/chemotherapy/list.vue'
// Samples
import { sampleRows, type ChemotherapyData } from '~/components/app/chemotherapy/sample'
const search = ref('')
const dateFrom = ref('')
const dateTo = ref('')
// filter + pencarian sederhana (client-side)
const filtered = computed(() => {
const q = search.value.trim().toLowerCase()
return sampleRows.filter((r: ChemotherapyData) => {
if (q) {
return r.nama.toLowerCase().includes(q) || r.noRm.toLowerCase().includes(q) || r.dokter.toLowerCase().includes(q)
}
return true
})
})
</script>
<template>
<div class="mx-auto max-w-full">
<div class="border-b p-6">
<h1 class="text-2xl font-semibold">Daftar Kunjungan Rawat Jalan Kemoterapi</h1>
<p class="mt-1 text-sm text-gray-500">
Manajemen pendaftaran serta monitoring terapi pasien tindakan rawat jalan
</p>
</div>
<div class="flex flex-wrap items-center gap-3 border-b p-4">
<div class="flex items-center gap-2">
<input
v-model="search"
placeholder="Cari Nama / No.RM"
class="w-64 rounded border px-3 py-2"
/>
</div>
<div class="flex items-center gap-2">
<input
v-model="dateFrom"
type="date"
class="rounded border px-3 py-2"
/>
<span class="text-sm text-gray-500">-</span>
<input
v-model="dateTo"
type="date"
class="rounded border px-3 py-2"
/>
</div>
<button class="ml-auto rounded bg-orange-500 px-3 py-2 text-white hover:bg-orange-600">Filter</button>
</div>
<div class="overflow-x-auto p-4">
<AppChemotherapyList
:data="filtered"
:pagination-meta="{
recordCount: 2,
page: 1,
pageSize: 10,
totalPage: 1,
hasPrev: false,
hasNext: false,
}"
/>
</div>
</div>
</template>
@@ -0,0 +1,7 @@
<script setup lang="ts">
import EncounterHome from '~/components/content/encounter/home.vue'
</script>
<template>
<EncounterHome classes="chemotherapy" />
</template>
@@ -0,0 +1,106 @@
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
// Types
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Components
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
import ProtocolList from '~/components/app/chemotherapy/list.protocol.vue'
// Services
import { getDetail } from '~/services/encounter.service'
const route = useRoute()
const router = useRouter()
// activeTab selalu sinkron dengan query param
const activeTab = computed({
get: () => (route.query?.tab && typeof route.query.tab === 'string' ? route.query.tab : 'status'),
set: (val: string) => {
router.replace({ path: route.path, query: { tab: val } })
},
})
// Dummy data so AppEncounterQuickInfo can render in development/storybook
// Replace with real API result when available (see commented fetch below)
const data = ref<any>({
patient: {
number: 'RM-2025-0001',
person: {
name: 'John Doe',
birthDate: '1980-01-01T00:00:00Z',
gender_code: 'M',
addresses: [
{ address: 'Jl. Contoh No.1, Jakarta' }
],
frontTitle: '',
endTitle: ''
}
},
visitDate: new Date().toISOString(),
unit: { name: 'Onkologi' },
responsible_doctor: null,
appointment_doctor: { employee: { person: { name: 'Dr. Clara Smith', frontTitle: 'Dr.', endTitle: 'Sp.OG' } } }
})
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
const protocolRows = [
{
number: '1',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'I',
periode: 'Siklus I',
kehadiran: 'Hadir',
action: '',
},
{
number: '2',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'II',
periode: 'Siklus II',
kehadiran: 'Tidak Hadir',
action: '',
},
]
const paginationMeta: PaginationMeta = {
recordCount: protocolRows.length,
page: 1,
pageSize: 10,
totalPage: 1,
hasNext: false,
hasPrev: false,
}
const tabs: TabItem[] = [
{ value: 'chemotherapy-protocol', label: 'Protokol Kemoterapi', component: ProtocolList, props: { data: protocolRows, paginationMeta } },
{ value: 'chemotherapy-medicine', label: 'Protokol Obat Kemoterapi' },
]
onMounted(async () => {
// const id = typeof route.query.id == 'string' ? parseInt(route.query.id) : 0
// const dataRes = await getDetail(id, {
// includes:
// 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
// })
// const dataResBody = dataRes.body ?? null
// data.value = dataResBody?.data ?? null
})
</script>
<template>
<div class="w-full">
<div class="mb-4">
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
</div>
<AppEncounterQuickInfo :data="data" />
<CompTab
:data="tabs"
:initial-active-tab="activeTab"
@change-tab="activeTab = $event"
/>
</div>
</template>
@@ -0,0 +1,241 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { format } from 'date-fns'
import { id as localeID } from 'date-fns/locale'
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
import { Button } from '~/components/pub/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '~/components/pub/ui/popover'
import Input from '~/components/pub/ui/input/Input.vue'
import RangeCalendar from '~/components/pub/ui/range-calendar/RangeCalendar.vue'
import AppChemotherapyListVerification from '~/components/app/chemotherapy/list-verification.vue'
import DialogVerification from '~/components/app/chemotherapy/dialog-verification.vue'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
const route = useRoute()
const router = useRouter()
// Sample patient data - replace with actual API call
const patientData = ref({
noRm: 'RM21123',
nama: 'Ahmad Sutanto',
jenisPembayaran: 'PKS',
noBilling: '18291822',
tanggalLahir: '23 April 1992',
usia: '33 tahun',
jenisKelamin: 'Laki-Laki (L)',
diagnosis: 'C34.9 - Karsinoma Paru',
klinik: 'Penyakit Dalam',
})
// Sample schedule data - replace with actual API call
const scheduleData = ref([
{
id: 1,
tanggalMasuk: '12 Agustus 2025',
pjBerkasRm: 'TPP Rawat Jalan',
dokter: 'Dr. Andreas Sutaji',
jenisRuangan: 'Ruang Tindakan',
jenisTindakan: 'KEMOTERAPI',
tanggalJadwalTindakan: '-',
status: 'belum_terverifikasi',
tanggalPemeriksaan: '2025-08-12',
},
])
const search = ref('')
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: null,
to: null,
})
// Format date range for display
const dateRangeDisplay = computed(() => {
if (dateRange.value.from && dateRange.value.to) {
return `${format(dateRange.value.from, 'dd MMMM yyyy', { locale: localeID })} - ${format(dateRange.value.to, 'dd MMMM yyyy', { locale: localeID })}`
}
return '12 Agustus 2025 - 32 Agustus 2025' // Default display
})
// Filter + search (client-side)
const filtered = computed(() => {
const q = search.value.trim().toLowerCase()
return scheduleData.value.filter((r: any) => {
if (q) {
return r.dokter.toLowerCase().includes(q) || patientData.value.noRm.toLowerCase().includes(q)
}
return true
})
})
// Pagination meta
const paginationMeta = reactive<PaginationMeta>({
recordCount: filtered.value.length,
page: 1,
pageSize: 10,
totalPage: Math.ceil(filtered.value.length / 10),
hasNext: false,
hasPrev: false,
})
function handlePageChange(page: number) {
paginationMeta.page = page
paginationMeta.hasNext = page < paginationMeta.totalPage
paginationMeta.hasPrev = page > 1
}
function handleFilter() {
// TODO: Implement filter logic
console.log('Filter clicked', { search: search.value, dateRange: dateRange.value })
}
// Dialog verification state
const isDialogVerificationOpen = ref(false)
const selectedSchedule = ref<any>(null)
// Provide verify handler for verify button
function handleVerify(rec: any) {
selectedSchedule.value = rec
isDialogVerificationOpen.value = true
}
provide('verify-handler', handleVerify)
function handleDialogSubmit(data: { tanggalPemeriksaan: string | undefined; jadwalTanggalPemeriksaan: string | undefined }) {
// TODO: Implement submit logic
console.log('Verification submitted', data)
isDialogVerificationOpen.value = false
// Refresh data after verification
}
function handleBackToAdmin() {
router.push('/outpation-action/chemotherapy/admin')
}
</script>
<template>
<div class="mx-auto max-w-full">
<!-- Back Button -->
<div class="mb-4">
<Button
variant="outline"
class="flex items-center gap-2 rounded-full border border-orange-400 bg-orange-50 px-3 py-1 text-sm font-medium text-orange-600 hover:bg-orange-100"
@click="handleBackToAdmin"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Kembali ke Administrasi Kunjungan
</Button>
</div>
<!-- Data Pasien Section -->
<div class="mb-6 rounded-md border bg-white p-4 shadow-sm">
<h3 class="mb-4 text-lg font-semibold">Data Pasien:</h3>
<div class="grid grid-cols-2 gap-6">
<!-- Left Column -->
<div class="space-y-3">
<div class="flex">
<span class="w-48 font-medium">No. RM:</span>
<span>{{ patientData.noRm }}</span>
</div>
<div class="flex">
<span class="w-48 font-medium">Nama:</span>
<span>{{ patientData.nama }}</span>
</div>
<div class="flex">
<span class="w-48 font-medium">Jenis Pembayaran:</span>
<span>{{ patientData.jenisPembayaran }}</span>
</div>
<div class="flex">
<span class="w-48 font-medium">No Billing:</span>
<span>{{ patientData.noBilling }}</span>
</div>
</div>
<!-- Right Column -->
<div class="space-y-3">
<div class="flex">
<span class="w-48 font-medium">Tanggal Lahir / Usia:</span>
<span>{{ patientData.tanggalLahir }} / {{ patientData.usia }}</span>
</div>
<div class="flex">
<span class="w-48 font-medium">Jenis Kelamin:</span>
<span>{{ patientData.jenisKelamin }}</span>
</div>
<div class="flex">
<span class="w-48 font-medium">Diagnosis:</span>
<span>{{ patientData.diagnosis }}</span>
</div>
<div class="flex">
<span class="w-48 font-medium">Klinik:</span>
<span>{{ patientData.klinik }}</span>
</div>
</div>
</div>
</div>
<!-- Header Section -->
<div class="border-b p-6">
<h1 class="text-2xl font-semibold">Verifikasi Jadwal Pasien</h1>
<p class="mt-1 text-sm text-gray-500">
Pantau riwayat masuk, dokter penanggung jawab, dan status pasien secara real-time.
</p>
</div>
<!-- Search and Filter Bar -->
<div class="flex flex-wrap items-center gap-3 border-b p-4">
<!-- Search Input -->
<div class="relative">
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
v-model="search"
placeholder="Cari Nama /No.RM"
class="w-64 pl-9"
/>
</div>
<!-- Date Range Picker -->
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
class="h-10 w-72 justify-start border-gray-300 bg-white text-left font-normal"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{ dateRangeDisplay }}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar />
</PopoverContent>
</Popover>
<!-- Filter Button -->
<Button
variant="outline"
class="ml-auto border-orange-500 bg-orange-50 text-orange-600 hover:bg-orange-100"
@click="handleFilter"
>
<FilterIcon class="mr-2 h-4 w-4" />
Filter
</Button>
</div>
<!-- Data Table -->
<div class="overflow-x-auto p-4">
<AppChemotherapyListVerification
:data="filtered"
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
<!-- Dialog Verification -->
<DialogVerification
v-model:open="isDialogVerificationOpen"
:tanggal-pemeriksaan="selectedSchedule?.tanggalPemeriksaan"
:jadwal-tanggal-pemeriksaan="selectedSchedule?.jadwalTanggalPemeriksaan"
@submit="handleDialogSubmit"
/>
</div>
</template>
@@ -0,0 +1,133 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import type { ExposedForm } from '~/types/form'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import { ControlLetterSchema } from '~/schemas/control-letter.schema'
import { handleActionSave,} from '~/handlers/control-letter.handler'
import { toast } from '~/components/pub/ui/toast'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import { type ControlLetter } from '~/models/control-letter'
// #region Props & Emits
const props = defineProps<{
callbackUrl?: string
}>()
// form related state
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const controlLetterForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
const router = useRouter()
const isConfirmationOpen = ref(false)
const selectedUnitId = ref<number|null>(null)
const selectedSpecialistId = ref<number|null>(null)
const selectedSubSpecialistId = ref<number|null>(null)
// #endregion
// #region Lifecycle Hooks
// #endregion
// #region Functions
function goBack() {
router.go(-1)
}
async function handleConfirmAdd() {
const controlLetter: ControlLetter = await composeFormData()
let createdControlLetterId = 0
const response = await handleActionSave(
controlLetter,
() => { },
() => { },
toast,
)
const data = (response?.body?.data ?? null)
if (!data) return
createdControlLetterId = data.id
// // If has callback provided redirect to callback with patientData
if (props.callbackUrl) {
navigateTo(props.callbackUrl + '?control-letter-id=' + controlLetter.id)
}
goBack()
}
async function composeFormData(): Promise<ControlLetter> {
const [controlLetter,] = await Promise.all([
controlLetterForm.value?.validate(),
])
const results = [controlLetter]
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
if (!allValid) return Promise.reject('Form validation failed')
const formData = controlLetter?.values
formData.encounter_id = encounterId
return new Promise((resolve) => resolve(formData))
}
// #endregion region
// #region Utilities & event handlers
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
isConfirmationOpen.value = true
}
if (eventType === 'back') {
if (props.callbackUrl) {
await navigateTo(props.callbackUrl)
return
}
goBack()
}
}
function handleCancelAdd() {
isConfirmationOpen.value = false
}
provide("selectedUnitId", selectedUnitId);
provide("selectedSpecialistId", selectedSpecialistId);
provide("selectedSubSpecialistId", selectedSubSpecialistId);
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Tambah Surat Kontrol</div>
<AppControlLetterEntryForm
ref="controlLetterForm"
:schema="ControlLetterSchema"
:selected-unit-id="selectedUnitId"
:selected-specialist-id="selectedSpecialistId"
:selected-sub-specialist-id="selectedSubSpecialistId"
/>
<div class="my-2 flex justify-end py-2">
<Action :enable-draft="false" @click="handleActionClick" />
</div>
<Confirmation v-model:open="isConfirmationOpen"
title="Simpan Data"
message="Apakah Anda yakin ingin menyimpan data ini?"
confirm-text="Simpan"
@confirm="handleConfirmAdd"
@cancel="handleCancelAdd" />
</template>
<style scoped>
/* component style */
</style>
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { withBase } from '~/models/_base'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import type { Patient } from '~/models/patient'
import type { Person } from '~/models/person'
import { getDetail } from '~/services/control-letter.service'
// Components
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import type { ControlLetter } from '~/models/control-letter'
// #region Props & Emits
const props = defineProps<{
}>()
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const controlLetterId = typeof route.params.control_letter_id == 'string' ? parseInt(route.params.control_letter_id) : 0
const controlLetter = ref<ControlLetter | null>(null)
const headerPrep: HeaderPrep = {
title: 'Detail Surat Kontrol',
icon: 'i-lucide-newspaper',
}
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
const result = await getDetail(controlLetterId, {
includes: "unit,specialist,subspecialist,doctor-employee-person",
})
if (result.success) {
controlLetter.value = result.body?.data
}
})
// #endregion
// #region Functions
function goBack() {
router.go(-1)
}
// #endregion region
// #region Utilities & event handlers
function handleAction(type: string) {
switch (type) {
case 'edit':
// TODO: Handle edit action
navigateTo({
name: 'rehab-encounter-id-control-letter-control_letter_id-edit',
params: { id: encounterId, "control_letter_id": controlLetterId },
})
break
case 'back':
goBack()
break
}
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<Header :prep="headerPrep" :ref-search-nav="headerPrep.refSearchNav" />
<AppControlLetterPreview :instance="controlLetter" @click="handleAction" />
</template>
@@ -0,0 +1,162 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import type { Patient, genPatientProps } from '~/models/patient'
import type { ExposedForm } from '~/types/form'
import type { PatientBase } from '~/models/patient'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import { genPatient } from '~/models/patient'
import { PatientSchema } from '~/schemas/patient.schema'
import { PersonAddressRelativeSchema } from '~/schemas/person-address-relative.schema'
import { PersonAddressSchema } from '~/schemas/person-address.schema'
import { PersonContactListSchema } from '~/schemas/person-contact.schema'
import { PersonFamiliesSchema } from '~/schemas/person-family.schema'
import { ResponsiblePersonSchema } from '~/schemas/person-relative.schema'
import { uploadAttachment } from '~/services/patient.service'
import { getDetail, update } from '~/services/control-letter.service'
import {
// for form entry
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
} from '~/handlers/control-letter.handler'
import { toast } from '~/components/pub/ui/toast'
import { withBase } from '~/models/_base'
import type { Person } from '~/models/person'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import type { ControlLetter } from '~/models/control-letter'
import { ControlLetterSchema } from '~/schemas/control-letter.schema'
import { formatDateYyyyMmDd } from '~/lib/date'
// #region Props & Emits
const props = defineProps<{
callbackUrl?: string
}>()
// form related state
const controlLetterForm = ref<ExposedForm<any> | null>(null)
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const controlLetterId = typeof route.params.control_letter_id == 'string' ? parseInt(route.params.control_letter_id) : 0
const isConfirmationOpen = ref(false)
const controlLetter = ref({})
const selectedUnitId = ref<number|null>(null)
const selectedSpecialistId = ref<number|null>(null)
const selectedSubSpecialistId = ref<number|null>(null)
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
const result = await getDetail(controlLetterId)
if (result.success) {
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
selectedUnitId.value = responseData?.unit_code
selectedSpecialistId.value = responseData?.specialist_code
selectedSubSpecialistId.value = responseData?.subspecialist_code
controlLetter.value = responseData
controlLetterForm.value?.setValues(responseData)
}
})
// #endregion
// #region Functions
function goBack() {
router.go(-1)
}
async function handleConfirmAdd() {
const response = await handleActionEdit(
controlLetterId,
await composeFormData(),
() => { },
() => { },
toast,
)
goBack()
}
async function composeFormData(): Promise<ControlLetter> {
const [controlLetter,] = await Promise.all([
controlLetterForm.value?.validate(),
])
const results = [controlLetter]
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
if (!allValid) return Promise.reject('Form validation failed')
const formData = controlLetter?.values
formData.encounter_id = encounterId
return new Promise((resolve) => resolve(formData))
}
// #endregion region
// #region Utilities & event handlers
async function handleActionClick(eventType: string) {
if (eventType === 'submit') {
isConfirmationOpen.value = true
}
if (eventType === 'back') {
if (props.callbackUrl) {
await navigateTo(props.callbackUrl)
return
}
goBack()
}
}
function handleCancelAdd() {
isConfirmationOpen.value = false
}
provide("selectedUnitId", selectedUnitId);
provide("selectedSpecialistId", selectedSpecialistId);
provide("selectedSubSpecialistId", selectedSubSpecialistId);
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Update Surat Kontrol</div>
<AppControlLetterEntryForm
ref="controlLetterForm"
:schema="ControlLetterSchema"
:selected-unit-id="selectedUnitId"
:selected-specialist-id="selectedSpecialistId"
:selected-sub-specialist-id="selectedSubSpecialistId"
/>
<div class="my-2 flex justify-end py-2">
<Action :enable-draft="false" @click="handleActionClick" />
</div>
<Confirmation
v-model:open="isConfirmationOpen"
title="Simpan Data"
message="Apakah Anda yakin ingin menyimpan data ini?"
confirm-text="Simpan"
@confirm="handleConfirmAdd"
@cancel="handleCancelAdd"
/>
</template>
<style scoped>
/* component style */
</style>
@@ -0,0 +1,176 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
// #region Imports
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { getList, remove } from '~/services/control-letter.service'
import { toast } from '~/components/pub/ui/toast'
import type { Encounter } from '~/models/encounter'
import WarningAlert from '~/components/pub/my-ui/alert/warning-alert.vue'
// #endregion
// #region State
const props = defineProps<{
encounter?: Encounter
}>()
const route = useRoute()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
entityName: 'control-letter',
})
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (val: string) => {
searchInput.value = val
},
onClear: () => {
searchInput.value = ''
},
}
const isRecordConfirmationOpen = ref(false)
const summaryLoading = ref(false)
const isRequirementsMet = ref(true)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: "Surat Kontrol",
icon: 'i-lucide-newspaper',
addNav: {
label: "Surat Kontrol",
onClick: () => navigateTo({
name: 'rehab-encounter-id-control-letter-add',
params: { id: encounterId },
}),
},
}
// #endregion
// #region Lifecycle Hooks
onMounted(() => {
getListData()
})
// #endregion
// #region Functions
async function getListData() {
try {
summaryLoading.value = true
await new Promise((resolve) => setTimeout(resolve, 500))
} catch (error) {
console.error('Error fetching Data:', error)
} finally {
summaryLoading.value = false
}
}
// Handle confirmation result
async function handleConfirmDelete(record: any, action: string) {
if (action === 'delete' && record?.id) {
try {
const result = await remove(record.id)
if (result.success) {
toast({ title: 'Berhasil', description: 'Data berhasil dihapus', variant: 'default' })
await fetchData()
} else {
toast({ title: 'Gagal', description: `Data gagal dihapus`, variant: 'destructive' })
}
} catch (error) {
toast({ title: 'Gagal', description: `Something went wrong`, variant: 'destructive' })
}
}
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
// #region Provide
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// #endregion
// #region Watchers
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
navigateTo({
name: 'rehab-encounter-id-control-letter-control_letter_id',
params: { id: encounterId, "control_letter_id": recId.value },
})
break
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
navigateTo({
name: 'rehab-encounter-id-control-letter-control_letter_id-edit',
params: { id: encounterId, "control_letter_id": recId.value },
})
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<WarningAlert v-if="!isRequirementsMet"
class="mb-5"
text="Syarat pembuatan surat kontrol belum terpenuhi"
:description="[
'Lanjutan Penatalaksanaan Pasien harus pulang/KRS.',
'Status Resume Medis harus tervalidasi.'
]" />
<div v-else>
<Header v-model:search="searchInput"
:prep="{ ...headerPrep }"
:ref-search-nav="refSearchNav"
@search="handleSearch" />
<AppControlLetterList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<RecordConfirmation v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation">
<template #default="{ record }">
<div class="text-sm">
<p>
<strong>ID:</strong>
{{ record?.id }}
</p>
<p v-if="record?.firstName">
<strong>Nama:</strong>
{{ record.firstName }}
</p>
<p v-if="record?.code">
<strong>Kode:</strong>
{{ record.cellphone }}
</p>
</div>
</template>
</RecordConfirmation>
</div>
</template>
@@ -0,0 +1,155 @@
<script setup lang="ts">
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
// mcu src category
import ScrCategorySwitcher from '~/components/app/mcu-src-category/switcher.vue'
import { getList as getMcuCategoryList } from '~/services/mcu-src-category.service'
// mcu src
import { type McuSrc } from '~/models/mcu-src'
import { getList as getMcuSrcList } from '~/services/mcu-src.service'
import McuSrcPicker from '~/components/app/mcu-src/picker-accordion.vue'
// mcu order
import { getDetail } from '~/services/mcu-order.service'
import Detail from '~/components/app/mcu-order/detail.vue'
// mcu order item, manually not using composable
import {
getList as getMcuOrderItemList,
create as createMcuOrderItem,
remove as removeMcuOrderItem,
} from '~/services/mcu-order-item.service'
import { type McuOrderItem } from '~/models/mcu-order-item'
import ItemListEntry from '~/components/app/mcu-order-item/list-entry.vue'
// props
const props = defineProps<{
encounter_id: number
}>()
// declaration & flows
// MCU Order
const { getQueryParam } = useQueryParam()
const id = getQueryParam('id')
const dataRes = await getDetail(
typeof id === 'string' ? parseInt(id) : 0,
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
)
const data = dataRes.body?.data
// MCU items
const items = ref<McuOrderItem[]>([])
// MCU Categories
const mcuSrcCategoryRes = await getMcuCategoryList()
const mcuSrcCategories = mcuSrcCategoryRes.body?.data
const selectedMcuSrcCategory_code = ref('')
// MCU Sources
const mcuSrcs = ref<McuSrc[]>([])
// const {
// data: items,
// fetchData: getItems,
// } = usePaginatedList<McuOrderItem> ({
// fetchFn: async ({ page, search }) => {
// const result = await getMcuOrderItemList({ 'mcu-order-id': id, search, page })
// if (result.success) {
// items.value = result.body.data
// }
// return { success: result.success || false, body: result.body || {} }
// },
// entityName: 'mcu-order-item',
// })
const { backToList } = useQueryCRUDMode()
const headerPrep: HeaderPrep = {
title: 'Detail dan List Item Order Radiologi ',
icon: 'i-lucide-box',
}
const pickerDialogOpen = ref(false)
onMounted(async () => {
await getItems()
})
watch(selectedMcuSrcCategory_code, async () => {
const res = await getMcuSrcList({ 'mcu-src-category-code': selectedMcuSrcCategory_code.value })
mcuSrcs.value = res.body?.data
})
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
if (type === 'back') {
backToList()
}
}
function requestItem() {
pickerDialogOpen.value = true
}
async function pickItem(item: McuSrc) {
const exItem = items.value.find(e => e.mcuSrc_id === item.id)
if (exItem) {
await removeMcuOrderItem(exItem.id)
await getItems()
} else {
const intId = parseInt(id?.toString() || '0')
await createMcuOrderItem({
mcuOrder_id: intId,
mcuSrc_id: item.id,
})
await getItems()
}
}
async function getItems() {
const itemsRes = await getMcuOrderItemList({ 'mcu-order-id': id, includes: 'mcuSrc,mcuSrc-mcuSrcCategory' })
if (itemsRes.success) {
items.value = itemsRes.body.data
} else {
items.value = []
}
}
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<Detail :data="data" />
<ItemListEntry
:data="items"
@requestItem="requestItem"/>
<Separator class="my-5" />
<div class="w-full flex justify-center">
<Nav @click="navClick" />
</div>
<Dialog
v-model:open="pickerDialogOpen"
title="Pilih Item"
size="2xl"
prevent-outside
>
<ScrCategorySwitcher :data="mcuSrcCategories" v-model="selectedMcuSrcCategory_code" />
<McuSrcPicker v-model="items" :data-source="mcuSrcs" @pick="pickItem" />
<Separator />
<NavOk @click="() => pickerDialogOpen = false" class="justify-center" />
</Dialog>
</template>
@@ -0,0 +1,169 @@
<script setup lang="ts">
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionRemove,
} from '~/handlers/mcu-order.handler'
// Apps
import { getList, getDetail } from '~/services/mcu-order.service'
import List from '~/components/app/mcu-order/list.vue'
import type { McuOrder } from '~/models/mcu-order'
const route = useRoute()
const { setQueryParams } = useQueryParam()
const title = ref('')
const plainEid = route.params.id
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0 // here the
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMyList,
} = usePaginatedList<McuOrder>({
fetchFn: async ({ page, search }) => {
const result = await getList({
search,
page,
'scope-code': "cp-lab",
'encounter-id': encounter_id,
includes: 'doctor,doctor-employee,doctor-employee-person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'mcu-order'
})
const headerPrep: HeaderPrep = {
title: 'Order Lab PK',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getMyDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getMyDetail(recId.value)
title.value = 'Detail Order Lab PK'
isReadonly.value = true
break
case ActionEvents.showEdit:
getMyDetail(recId.value)
title.value = 'Edit Order Lab PK'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
watch([isFormEntryDialogOpen], async () => {
if (isFormEntryDialogOpen.value) {
isFormEntryDialogOpen.value = false;
const saveResp = await handleActionSave({ encounter_id, scope_code: "cp-lab" }, getMyList, () =>{}, toast)
if (saveResp.success) {
setQueryParams({
'mode': 'entry',
'id': saveResp.body?.data?.id.toString()
})
}
}
})
onMounted(async () => {
})
function cancel(data: McuOrder) {
recId.value = data.id
recItem.value = data
isRecordConfirmationOpen.value = true
}
function edit(data: McuOrder) {
setQueryParams({
'mode': 'entry',
'id': data.id.toString()
})
recItem.value = data
}
function submit(data: McuOrder) {
}
</script>
<template>
<Header :prep="{ ...headerPrep }" />
<List
v-if="!isLoading.dataListLoading"
:data="data"
:pagination-meta="paginationMeta"
@cancel="cancel"
@edit="edit"
@submit="submit"
/>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getMyList, toast)"
@cancel=""
/>
</template>
@@ -0,0 +1,16 @@
<script setup lang="ts">
//
import List from './list.vue'
import Entry from './entry.vue'
const props = defineProps<{
encounter_id: number
}>()
const { mode } = useQueryCRUDMode()
</script>
<template>
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
<Entry v-else :encounter_id="encounter_id" />
</template>
+190
View File
@@ -0,0 +1,190 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getDetail } from '~/services/encounter.service'
import type { TabItem } from '~/components/pub/my-ui/comp-tab/type'
import CompTab from '~/components/pub/my-ui/comp-tab/comp-tab.vue'
// PLASE ORDER BY TAB POSITION
import Status from '~/components/content/encounter/status.vue'
import AssesmentFunctionList from '~/components/content/assesment-function/list.vue'
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
import PrescriptionList from '~/components/content/prescription/list.vue'
import Consultation from '~/components/content/consultation/list.vue'
import ProtocolList from '~/components/app/chemotherapy/list.protocol.vue'
import MedicineProtocolList from '~/components/app/chemotherapy/list.medicine.vue'
const route = useRoute()
const router = useRouter()
const props = defineProps<{
classes?: string
}>()
// activeTab selalu sinkron dengan query param
const activeTab = computed({
get: () => (route.query?.tab && typeof route.query.tab === 'string' ? route.query.tab : 'status'),
set: (val: string) => {
router.replace({ path: route.path, query: { tab: val } })
},
})
const id = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
// const dataRes = await getDetail(id, {
// includes:
// 'patient,patient-person,patient-person-addresses,unit,Appointment_Doctor,Appointment_Doctor-employee,Appointment_Doctor-employee-person',
// })
// const dataResBody = dataRes.body ?? null
// const data = dataResBody?.data ?? null
// Dummy data so AppEncounterQuickInfo can render in development/storybook
// Replace with real API result when available (see commented fetch below)
const data = ref<any>({
patient: {
number: 'RM-2025-0001',
person: {
name: 'John Doe',
birthDate: '1980-01-01T00:00:00Z',
gender_code: 'M',
addresses: [{ address: 'Jl. Contoh No.1, Jakarta' }],
frontTitle: '',
endTitle: '',
},
},
visitDate: new Date().toISOString(),
unit: { name: 'Onkologi' },
responsible_doctor: null,
appointment_doctor: { employee: { person: { name: 'Dr. Clara Smith', frontTitle: 'Dr.', endTitle: 'Sp.OG' } } },
})
// Dummy rows for ProtocolList (matches keys expected by list-cfg.protocol)
const protocolRows = [
{
number: '1',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'I',
periode: 'Siklus I',
kehadiran: 'Hadir',
action: '',
},
{
number: '2',
tanggal: new Date().toISOString().substring(0, 10),
siklus: 'II',
periode: 'Siklus II',
kehadiran: 'Tidak Hadir',
action: '',
},
]
const paginationMeta = {
recordCount: protocolRows.length,
page: 1,
pageSize: 10,
totalPage: 1,
hasNext: false,
hasPrev: false,
}
const tabsRaws: TabItem[] = [
{
value: 'status',
label: 'Status Masuk/Keluar',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: Status,
props: { encounter: data },
},
{
value: 'early-medical-assessment',
label: 'Pengkajian Awal Medis',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: EarlyMedicalAssesmentList,
},
{
value: 'rehab-medical-assessment',
label: 'Pengkajian Awal Medis Rehabilitasi Medis',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: EarlyMedicalRehabList,
},
{
value: 'function-assessment',
label: 'Asesmen Fungsi',
groups: ['ambulatory', 'rehabilitation'],
component: AssesmentFunctionList,
},
{ value: 'therapy-protocol', groups: ['ambulatory', 'rehabilitation'], label: 'Protokol Terapi' },
{
value: 'chemotherapy-protocol',
label: 'Protokol Kemoterapi',
groups: ['chemotherapy'],
component: ProtocolList,
props: { data: protocolRows, paginationMeta },
},
{
value: 'chemotherapy-medicine',
label: 'Protokol Obat Kemoterapi',
groups: ['chemotherapy'],
component: MedicineProtocolList,
props: { data: protocolRows, paginationMeta },
},
{ value: 'report', label: 'Laporan Tindakan', groups: ['chemotherapy'] },
{ value: 'patient-note', label: 'CPRJ', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{
value: 'education-assessment',
label: 'Asesmen Kebutuhan Edukasi',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
},
{ value: 'consent', label: 'General Consent', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'patient-note', label: 'CPRJ', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{
value: 'prescription',
label: 'Order Obat',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: PrescriptionList,
},
{ value: 'device', label: 'Order Alkes', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-radiology', label: 'Order Radiologi', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-lab-pc', label: 'Order Lab PK', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-lab-pa', label: 'Order Lab PA', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'medical-action', label: 'Order Ruang Tindakan', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'mcu-result', label: 'Hasil Penunjang', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{
value: 'consultation',
label: 'Konsultasi',
groups: ['ambulatory', 'rehabilitation', 'chemotherapy'],
component: Consultation,
props: { encounter: data },
},
{ value: 'resume', label: 'Resume', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'control', label: 'Surat Kontrol', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'screening', label: 'Skrinning MPP', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung', groups: ['ambulatory', 'rehabilitation'] },
{ value: 'price-list', label: 'Tarif Tindakan', groups: ['ambulatory', 'rehabilitation', 'chemotherapy'] },
]
const tabs = computed(() => {
return tabsRaws
.filter((tab: TabItem) => tab.groups ? tab.groups.some((group: string) => props.classes?.includes(group)) : true)
.map((tab: TabItem) => {
return { ...tab, props: { ...tab.props, encounter: data } }
})
})
</script>
<template>
<div class="w-full">
<div class="mb-4">
<PubMyUiNavContentBa label="Kembali ke Daftar Kunjungan" />
</div>
<AppEncounterQuickInfo :data="data" />
<CompTab
:data="tabs"
:initial-active-tab="activeTab"
@change-tab="activeTab = $event"
/>
</div>
</template>
+8 -5
View File
@@ -14,8 +14,11 @@ import Status from '~/components/content/encounter/status.vue'
import AssesmentFunctionList from '~/components/content/soapi/entry.vue'
import EarlyMedicalAssesmentList from '~/components/content/soapi/entry.vue'
import EarlyMedicalRehabList from '~/components/content/soapi/entry.vue'
import PrescriptionList from '~/components/content/prescription/list.vue'
import Prescription from '~/components/content/prescription/main.vue'
import CpLabOrder from '~/components/content/cp-lab-order/main.vue'
import Radiology from '~/components/content/radiology-order/main.vue'
import Consultation from '~/components/content/consultation/list.vue'
import ControlLetterList from '~/components/content/control-letter/list.vue'
const route = useRoute()
const router = useRouter()
@@ -60,17 +63,17 @@ const tabs: TabItem[] = [
{ value: 'education-assessment', label: 'Asesmen Kebutuhan Edukasi' },
{ value: 'consent', label: 'General Consent' },
{ value: 'patient-note', label: 'CPRJ' },
{ value: 'prescription', label: 'Order Obat', component: PrescriptionList },
{ value: 'prescription', label: 'Order Obat', component: Prescription, props: { encounter_id: data.id } },
{ value: 'device', label: 'Order Alkes' },
{ value: 'mcu-radiology', label: 'Order Radiologi' },
{ value: 'mcu-lab-pc', label: 'Order Lab PK' },
{ value: 'mcu-radiology', label: 'Order Radiologi', component: Radiology, props: { encounter_id: data.id } },
{ value: 'mcu-lab-cp', label: 'Order Lab PK', component: CpLabOrder, props: { encounter_id: data.id } },
{ value: 'mcu-lab-micro', label: 'Order Lab Mikro' },
{ value: 'mcu-lab-pa', label: 'Order Lab PA' },
{ value: 'medical-action', label: 'Order Ruang Tindakan' },
{ value: 'mcu-result', label: 'Hasil Penunjang' },
{ value: 'consultation', label: 'Konsultasi', component: Consultation, props: { encounter: data } },
{ value: 'resume', label: 'Resume' },
{ value: 'control', label: 'Surat Kontrol' },
{ value: 'control', label: 'Surat Kontrol', component: ControlLetterList, props: { encounter: data } },
{ value: 'screening', label: 'Skrinning MPP' },
{ value: 'supporting-document', label: 'Upload Dokumen Pendukung' },
]
@@ -0,0 +1,132 @@
<script setup lang="ts">
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
import type { HeaderPrep } from '~/components/pub/my-ui/data/types'
import { getDetail } from '~/services/prescription.service'
import Detail from '~/components/app/prescription/detail.vue'
import { getList as getPrescriptionItemList } from '~/services/prescription-item.service'
import ItemListEntry from '~/components/app/prescription-item/list-entry.vue'
import { type PrescriptionItem } from '~/models/prescription-item'
import MixItemEntry from '~/components/app/prescription-item/mix-entry.vue'
import { create } from '~/services/prescription-item.service';
import NonMixItemEntry from '~/components/app/prescription-item/non-mix-entry.vue'
import {
recItem,
} from '~/handlers/prescription-item.handler'
// props
const props = defineProps<{
encounter_id: number
}>()
// declaration & flows
// const route = useRoute()
const { getQueryParam } = useQueryParam()
const id = getQueryParam('id')
const dataRes = await getDetail(
typeof id === 'string' ? parseInt(id) : 0,
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
)
const data = dataRes.body?.data || null
const items = ref(data?.items || [])
const {
data: prescriptionItems,
fetchData: getMyList,
} = usePaginatedList<PrescriptionItem> ({
fetchFn: async ({ page, search }) => {
const result = await getPrescriptionItemList({ 'prescription-id': id, search, page })
if (result.success) {
data.value = result.body.data
}
return { success: result.success || false, body: result.body || {} }
},
entityName: 'prescription-item',
})
const { backToList } = useQueryCRUDMode()
const headerPrep: HeaderPrep = {
title: 'Tambah Order Obat / Resep',
icon: 'i-lucide-box',
}
const mixDialogOpen = ref(false)
const nonMixDialogOpen = ref(false)
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
if (type === 'back') {
backToList()
}
}
function addItem(mode: 'mix' | 'non-mix') {
if (mode === 'mix') {
mixDialogOpen.value = true
} else if (mode === 'non-mix') {
nonMixDialogOpen.value = true
}
}
function saveMix() {
create({data})
}
function saveNonMix(data: PrescriptionItem) {
create({data})
}
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<Detail :data="data" />
<ItemListEntry
:data="prescriptionItems"
@add="addItem"/>
<Separator class="my-5" />
<div class="w-full flex justify-center">
<Nav @click="navClick" />
</div>
<Dialog
v-model:open="mixDialogOpen"
:title="recItem?.id ? 'Edit Racikan' : 'Tambah Racikan'"
size="xl"
prevent-outside
>
<MixItemEntry
:data="data"
:items="items"
@close="mixDialogOpen = false"
@save="saveMix"
/>
</Dialog>
<Dialog
v-model:open="nonMixDialogOpen"
:title="recItem?.id ? 'Edit Non Racikan' : 'Tambah Non Racikan'"
size="xl"
prevent-outside
>
<NonMixItemEntry
:data="data"
:items="items"
@close="mixDialogOpen = false"
@save="saveNonMix"
/>
</Dialog>
</template>
+139 -45
View File
@@ -1,37 +1,82 @@
<script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import PrescriptionItemListEntry from '~/components/app/prescription-item/list-entry.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
const data = ref([])
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionRemove,
handleActionSave,
} from '~/handlers/prescription.handler'
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
// filter patient list
},
onClear: () => {
// clear url param
},
}
// Services
import { getList, getDetail } from '~/services/prescription.service'
import List from '~/components/app/prescription/list.vue'
import type { Prescription } from '~/models/prescription'
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
const props = defineProps<{
encounter_id: number
}>()
const route = useRoute()
const { setQueryParams } = useQueryParam()
const title = ref('')
const plainEid = route.params.id
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0
const {
data,
isLoading,
paginationMeta,
searchInput,
fetchData: getMyList,
} = usePaginatedList<Prescription>({
fetchFn: async ({ page, search }) => {
const result = await getList({
search,
page,
'encounter-id': encounter_id,
includes: 'doctor,doctor-employee,doctor-employee-person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'prescription'
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const headerPrep: HeaderPrep = {
title: 'Resep Obat',
icon: 'i-lucide-panel-bottom',
title: 'Order Obat',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
onClick: () => navigateTo('/tools-equipment-src/equipment/add'),
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
@@ -40,33 +85,82 @@ provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
onMounted(() => {
getMaterialList()
const getMyDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getMyDetail(recId.value)
title.value = 'Detail Konsultasi'
isReadonly.value = true
break
case ActionEvents.showEdit:
getMyDetail(recId.value)
title.value = 'Edit Konsultasi'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
break
}
})
async function getMaterialList() {
isLoading.dataListLoading = true
watch([isFormEntryDialogOpen], async () => {
if (isFormEntryDialogOpen.value) {
isFormEntryDialogOpen.value = false;
const saveResp = await handleActionSave({ encounter_id }, getMyList, () =>{}, toast)
if (saveResp.success) {
setQueryParams({
'mode': 'entry',
'id': saveResp.body?.data?.id.toString()
})
}
}
})
// const resp = await xfetch('/api/v1/material')
// if (resp.success) {
// data.value = (resp.body as Record<string, any>).data
// }
isLoading.dataListLoading = false
function cancel(data: Prescription) {
recId.value = data.id
recItem.value = data
isRecordConfirmationOpen.value = true
}
function edit(data: Prescription) {
setQueryParams({
'mode': 'entry',
'id': data.id.toString()
})
recItem.value = data
}
function submit(data: Prescription) {
}
</script>
<template>
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
<Header :prep="{ ...headerPrep }" />
<List
v-if="!isLoading.dataListLoading"
:data="data"
:pagination-meta="paginationMeta"
@cancel="cancel"
@edit="edit"
@submit="submit"
/>
<AppPrescriptionList v-if="!isLoading.dataListLoading" />
<AppPrescriptionEntry />
<PrescriptionItemListEntry :data=[] />
<div>
<Button>
Tambah
</Button>
</div>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getMyList, toast)"
@cancel=""
>
</RecordConfirmation>
</template>
@@ -0,0 +1,16 @@
<script setup lang="ts">
//
import List from './list.vue'
import Entry from './entry.vue'
const props = defineProps<{
encounter_id: number
}>()
const { mode } = useQueryCRUDMode()
</script>
<template>
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
<Entry v-else :encounter_id="encounter_id" />
</template>
@@ -0,0 +1,155 @@
<script setup lang="ts">
import Nav from '~/components/pub/my-ui/nav-footer/ba-de-su.vue'
import NavOk from '~/components/pub/my-ui/nav-footer/ok.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { useQueryCRUDMode } from '~/composables/useQueryCRUD'
import { type HeaderPrep } from '~/components/pub/my-ui/data/types'
// mcu src category
import ScrCategorySwitcher from '~/components/app/mcu-src-category/switcher.vue'
import { getList as getMcuCategoryList } from '~/services/mcu-src-category.service'
// mcu src
import { type McuSrc } from '~/models/mcu-src'
import { getList as getMcuSrcList } from '~/services/mcu-src.service'
import McuSrcPicker from '~/components/app/mcu-src/picker-accordion.vue'
// mcu order
import { getDetail } from '~/services/mcu-order.service'
import Detail from '~/components/app/mcu-order/detail.vue'
// mcu order item, manually not using composable
import {
getList as getMcuOrderItemList,
create as createMcuOrderItem,
remove as removeMcuOrderItem,
} from '~/services/mcu-order-item.service'
import { type McuOrderItem } from '~/models/mcu-order-item'
import ItemListEntry from '~/components/app/mcu-order-item/list-entry.vue'
// props
const props = defineProps<{
encounter_id: number
}>()
// declaration & flows
// MCU Order
const { getQueryParam } = useQueryParam()
const id = getQueryParam('id')
const dataRes = await getDetail(
typeof id === 'string' ? parseInt(id) : 0,
{ includes: 'encounter,doctor,doctor-employee,doctor-employee-person' }
)
const data = dataRes.body?.data
// MCU items
const items = ref<McuOrderItem[]>([])
// MCU Categories
const mcuSrcCategoryRes = await getMcuCategoryList()
const mcuSrcCategories = mcuSrcCategoryRes.body?.data
const selectedMcuSrcCategory_code = ref('')
// MCU Sources
const mcuSrcs = ref<McuSrc[]>([])
// const {
// data: items,
// fetchData: getItems,
// } = usePaginatedList<McuOrderItem> ({
// fetchFn: async ({ page, search }) => {
// const result = await getMcuOrderItemList({ 'mcu-order-id': id, search, page })
// if (result.success) {
// items.value = result.body.data
// }
// return { success: result.success || false, body: result.body || {} }
// },
// entityName: 'mcu-order-item',
// })
const { backToList } = useQueryCRUDMode()
const headerPrep: HeaderPrep = {
title: 'Detail dan List Item Order Radiologi ',
icon: 'i-lucide-box',
}
const pickerDialogOpen = ref(false)
onMounted(async () => {
await getItems()
})
watch(selectedMcuSrcCategory_code, async () => {
const res = await getMcuSrcList({ 'mcu-src-category-code': selectedMcuSrcCategory_code.value })
mcuSrcs.value = res.body?.data
})
function navClick(type: 'back' | 'delete' | 'draft' | 'submit') {
if (type === 'back') {
backToList()
}
}
function requestItem() {
pickerDialogOpen.value = true
}
async function pickItem(item: McuSrc) {
const exItem = items.value.find(e => e.mcuSrc_id === item.id)
if (exItem) {
await removeMcuOrderItem(exItem.id)
await getItems()
} else {
const intId = parseInt(id?.toString() || '0')
await createMcuOrderItem({
mcuOrder_id: intId,
mcuSrc_id: item.id,
})
await getItems()
}
}
async function getItems() {
const itemsRes = await getMcuOrderItemList({ 'mcu-order-id': id, includes: 'mcuSrc,mcuSrc-mcuSrcCategory' })
if (itemsRes.success) {
items.value = itemsRes.body.data
} else {
items.value = []
}
}
</script>
<template>
<Header
:prep="headerPrep"
:ref-search-nav="headerPrep.refSearchNav"
class="mb-4 xl:mb-5"
/>
<Detail :data="data" />
<ItemListEntry
:data="items"
@requestItem="requestItem"/>
<Separator class="my-5" />
<div class="w-full flex justify-center">
<Nav @click="navClick" />
</div>
<Dialog
v-model:open="pickerDialogOpen"
title="Pilih Item"
size="2xl"
prevent-outside
>
<ScrCategorySwitcher :data="mcuSrcCategories" v-model="selectedMcuSrcCategory_code" />
<McuSrcPicker v-model="items" :data-source="mcuSrcs" @pick="pickItem" />
<Separator />
<NavOk @click="() => pickerDialogOpen = false" class="justify-center" />
</Dialog>
</template>
@@ -0,0 +1,169 @@
<script setup lang="ts">
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
// Handlers
import {
recId,
recAction,
recItem,
isReadonly,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
handleActionSave,
handleActionRemove,
} from '~/handlers/mcu-order.handler'
// Apps
import { getList, getDetail } from '~/services/mcu-order.service'
import List from '~/components/app/mcu-order/list.vue'
import type { McuOrder } from '~/models/mcu-order'
const route = useRoute()
const { setQueryParams } = useQueryParam()
const title = ref('')
const plainEid = route.params.id
const encounter_id = (plainEid && typeof plainEid == 'string') ? parseInt(plainEid) : 0 // here the
const {
data,
isLoading,
paginationMeta,
searchInput,
handlePageChange,
handleSearch,
fetchData: getMyList,
} = usePaginatedList<McuOrder>({
fetchFn: async ({ page, search }) => {
const result = await getList({
search,
page,
'scope-code': "rad",
'encounter-id': encounter_id,
includes: 'doctor,doctor-employee,doctor-employee-person',
})
return { success: result.success || false, body: result.body || {} }
},
entityName: 'mcu-order'
})
const headerPrep: HeaderPrep = {
title: 'Order Radiologi',
icon: 'i-lucide-box',
refSearchNav: {
placeholder: 'Cari (min. 3 karakter)...',
minLength: 3,
debounceMs: 500,
showValidationFeedback: true,
onInput: (value: string) => {
searchInput.value = value
},
onClick: () => {},
onClear: () => {},
},
addNav: {
label: 'Tambah',
icon: 'i-lucide-plus',
onClick: () => {
recItem.value = null
recId.value = 0
isFormEntryDialogOpen.value = true
isReadonly.value = false
},
},
}
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
const getMyDetail = async (id: number | string) => {
const result = await getDetail(id)
if (result.success) {
const currentValue = result.body?.data || {}
recItem.value = currentValue
isFormEntryDialogOpen.value = true
}
}
// Watch for row actions when recId or recAction changes
watch([recId, recAction], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
getMyDetail(recId.value)
title.value = 'Detail Order Radiologi'
isReadonly.value = true
break
case ActionEvents.showEdit:
getMyDetail(recId.value)
title.value = 'Edit Order Radiologi'
isReadonly.value = false
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
watch([isFormEntryDialogOpen], async () => {
if (isFormEntryDialogOpen.value) {
isFormEntryDialogOpen.value = false;
const saveResp = await handleActionSave({ encounter_id }, getMyList, () =>{}, toast)
if (saveResp.success) {
setQueryParams({
'mode': 'entry',
'id': saveResp.body?.data?.id.toString()
})
}
}
})
onMounted(async () => {
})
function cancel(data: McuOrder) {
recId.value = data.id
recItem.value = data
isRecordConfirmationOpen.value = true
}
function edit(data: McuOrder) {
setQueryParams({
'mode': 'entry',
'id': data.id.toString()
})
recItem.value = data
}
function submit(data: McuOrder) {
}
</script>
<template>
<Header :prep="{ ...headerPrep }" />
<List
v-if="!isLoading.dataListLoading"
:data="data"
:pagination-meta="paginationMeta"
@cancel="cancel"
@edit="edit"
@submit="submit"
/>
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
action="delete"
:record="recItem"
@confirm="() => handleActionRemove(recId, getMyList, toast)"
@cancel=""
/>
</template>
@@ -0,0 +1,16 @@
<script setup lang="ts">
//
import List from './list.vue'
import Entry from './entry.vue'
const props = defineProps<{
encounter_id: number
}>()
const { mode } = useQueryCRUDMode()
</script>
<template>
<List v-if="mode === 'list'" :encounter_id="encounter_id" />
<Entry v-else :encounter_id="encounter_id" />
</template>