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,90 @@
<script setup lang="ts">
import { ActionEvents, type LinkItem, type ListItemDto } from '~/components/pub/my-ui/data/types';
const props = defineProps<{
rec: ListItemDto
}>()
const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
const linkItems: LinkItem[] = [
{
label: 'Print',
onClick: () => {
print()
},
icon: 'i-lucide-printer',
},
{
label: 'Log History',
onClick: () => {
history()
},
icon: 'i-lucide-logs',
},
{
label: 'Hapus',
onClick: () => {
del()
},
icon: 'i-lucide-trash',
},
]
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showProcess
recItem.value = props.rec
}
function history() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
>
<Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup>
<DropdownMenuItem
v-for="item in linkItems"
:key="item.label"
class="hover:bg-gray-100 dark:hover:bg-slate-700"
@click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
>
<Icon :name="item.icon ?? ''" />
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
import Field from '~/components/pub/my-ui/form/field.vue'
import Label from '~/components/pub/my-ui/form/label.vue'
import Select from '~/components/pub/my-ui/form/select.vue'
import { Form } from '~/components/pub/ui/form'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import SelectOriginPolyclinic from '~/components/app/bpjs/control-letter/_common/select-origin-polyclinic.vue'
import SelectDestinationPolyclinic from '~/components/app/bpjs/control-letter/_common/select-destination-polyclinic.vue'
import { cn } from '~/lib/utils'
const props = defineProps()
const items = reactive([
{ id: 1, description: 'Shipped from warehouse', createdAt: new Date(Date.now() - 86400000 * 2) },
{ id: 2, description: 'In transit to distribution center', createdAt: new Date(Date.now() - 86400000) },
{ id: 3, description: 'Out for delivery (Current)', createdAt: new Date() },
])
const itemsCount = computed(() => items.length || 0)
</script>
<template>
<ul :class="cn('pb-5 flex flex-col min-h-[30rem]', '')">
<li v-for="(item, index) in items" :key="item.id" class="flex gap-3 items-start">
<div class="flex flex-col items-center">
<div class="h-5 w-5 rounded-full border-2 border-gray-300 flex items-center justify-center">
<div :class="cn('dark:bg-white border-gray-300 rounded-full p-1.5',
index === 0 ? 'bg-green-500' : 'bg-transparent'
)">
</div>
</div>
<hr v-if="index !== itemsCount - 1" class="h-8 w-0.5 bg-gray-300 dark:bg-gray-300" aria-hidden="true">
</div>
<div class="flex justify-between items-center min-w-96">
<div class="max-w-80">
<time :class="cn('font-medium text-gray-800 dark:text-gray-100', '')">
{{ item?.createdAt.toLocaleDateString('id-ID') }}
</time>
<h1 :class="cn('text-gray-500 dark:text-gray-400', '')">{{ item.description }}</h1>
</div>
</div>
</li>
</ul>
</template>
@@ -0,0 +1,104 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { DateRange } from 'radix-vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: new Date(),
to: new Date(),
})
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
})
const value = ref({
start: new CalendarDate(2022, 1, 20),
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
}) as Ref<DateRange>
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn('w-full bg-white border-gray-400 justify-start text-left font-normal', !value && 'text-muted-foreground')"
>
<CalendarIcon class="mr-2 h-4 w-4" />
<template v-if="value.start">
<template v-if="value.end">
{{ df.format(value.start.toDate(getLocalTimeZone())) }} -
{{ df.format(value.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else> Pick a date </template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar
v-model="value"
initial-focus
:number-of-months="2"
@update:start-value="(startDate) => (value.start = startDate)"
/>
</PopoverContent>
</Popover>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Generate job options from constants, sama seperti pola genderCodes
const jobOptions = mapToComboboxOptList(occupationCodes)
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="jobOptions"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,128 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import FieldGroup from '~/components/pub/my-ui/form/field-group.vue'
import Field from '~/components/pub/my-ui/form/field.vue'
import Label from '~/components/pub/my-ui/form/label.vue'
import Select from '~/components/pub/my-ui/form/select.vue'
import { Form } from '~/components/pub/ui/form'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import SelectOriginPolyclinic from '~/components/app/bpjs/control-letter/_common/select-origin-polyclinic.vue'
import SelectDestinationPolyclinic from '~/components/app/bpjs/control-letter/_common/select-destination-polyclinic.vue'
import { cn } from '~/lib/utils'
import SelectDateRange from './_common/select-date-range.vue'
interface InstallationFormData {
name: string
code: string
encounterClassCode: string
}
const props = defineProps<{
installation: {
msg: {
placeholder: string
}
items: {
value: string
label: string
code: string
}[]
}
schema: any
initialValues?: Partial<InstallationFormData>
errors?: FormErrors
}>()
const emit = defineEmits<{
submit: [values: InstallationFormData, resetForm: () => void]
reset: [resetForm: () => void]
}>()
const formSchema = toTypedSchema(props.schema)
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
const formData: InstallationFormData = {
name: values.name || '',
code: values.code || '',
encounterClassCode: values.encounterClassCode || '',
}
emit('submit', formData, resetForm)
}
// Form cancel handler
function onResetForm({ resetForm }: { resetForm: () => void }) {
emit('reset', resetForm)
}
const items = ref([
{ label: 'Rujukan Internal', value: 'ri' },
{ label: 'SEP Rujukan', value: 'sr' },
])
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm }"
as=""
keep-values
:validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-7 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<SelectDateRange
field-name="releaseDate"
label="Tanggal Penerbitan"
placeholder="Tanggal Penerbitan"
:errors="errors"
is-required
/>
<SelectDateRange
field-name="controlPlanDate"
label="Tanggal Rencana Kontrol"
placeholder="Tanggal Rencana Kontrol"
:errors="errors"
is-required
/>
<InputBase
field-name="patientName"
label="Nama Pasien"
placeholder="Nama Pasien"
/>
<InputBase
field-name="cardNumber"
label="Nomor Kartu"
placeholder="Nomor Kartu"
/>
<InputBase
field-name="sepNumber"
label="Nomor SEP"
placeholder="Nomor SEP"
/>
<SelectOriginPolyclinic
field-name="originPolyclinic"
label="Poliklinik Asal"
placeholder="Pilih Poliklinik Asal"
:errors="errors"
is-required
/>
<SelectDestinationPolyclinic
field-name="destinationPolyclinic"
label="Poliklinik Tujuan"
placeholder="Pilih Poliklinik Tujuan"
:errors="errors"
is-required
/>
</div>
</div>
<div class="my-2 flex items-center gap-3 justify-end">
<Button @click="onResetForm" variant="secondary">Reset</Button>
<Button @click="onSubmitForm">Terapkan</Button>
</div>
</form>
</Form>
</template>
@@ -0,0 +1,108 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import type { Patient } from '~/models/patient'
import { defineAsyncComponent } from 'vue'
import { educationCodes, genderCodes } from '~/lib/constants'
import { calculateAge } from '~/lib/utils'
const action = defineAsyncComponent(() => import('./_common/dropdown-action.vue'))
const statusBadge = defineAsyncComponent(() => import('~/components/pub/my-ui/badge/status-badge.vue'))
export const config: Config = {
cols: [{}, {}, {}, {},{}, {}, {}, {}, {}, {width: 90},{width: 10},],
headers: [
[
{ label: 'No Surat' },
{ label: 'No MR' },
{ label: 'Nama' },
{ label: 'Tgl Rencana Kontrol' },
{ label: 'Tgl Penerbitan' },
{ label: 'Klinik Asal' },
{ label: 'Klinik Tujuan' },
{ label: 'DPJP' },
{ label: 'No SEP Asal' },
{ label: 'Status' },
{ label: 'Action' },
],
],
keys: ['birth_date', 'number', 'person.name', 'birth_date', 'birth_date',
'birth_date', 'number', 'person.name', 'birth_date', 'status', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
patientId: (rec: unknown): unknown => {
const patient = rec as Patient
return patient.number
},
identity_number: (rec: unknown): unknown => {
const { person } = rec as Patient
if (person.nationality == 'WNA') {
return person.passportNumber
}
return person.residentIdentityNumber || '-'
},
birth_date: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.birthDate == 'object' && person.birthDate) {
return (person.birthDate as Date).toLocaleDateString('id-ID')
} else if (typeof person.birthDate == 'string') {
return (person.birthDate as string).substring(0, 10)
}
return person.birthDate
},
patient_age: (rec: unknown): unknown => {
const { person } = rec as Patient
return calculateAge(person.birthDate)
},
gender: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.gender_code == 'number' && person.gender_code >= 0) {
return person.gender_code
} else if (typeof person.gender_code === 'string' && person.gender_code) {
return genderCodes[person.gender_code] || '-'
}
return '-'
},
education: (rec: unknown): unknown => {
const { person } = rec as Patient
if (typeof person.education_code == 'number' && person.education_code >= 0) {
return person.education_code
} else if (typeof person.education_code === 'string' && person.education_code) {
return educationCodes[person.education_code] || '-'
}
return '-'
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
status(rec, idx) {
return {
idx,
rec: rec as object,
component: statusBadge,
}
},
},
htmls: {
patient_address(_rec) {
return '-'
},
},
}
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,30 @@
<script setup lang="ts">
import Button from '~/components/pub/ui/button/Button.vue'
const props = defineProps<{
rec: any
idx?: number
}>()
// Try to get proses handler from parent via inject
const prosesHandler = inject<(rec: any) => void>('proses-handler', null)
function handleProses() {
if (prosesHandler) {
prosesHandler(props.rec)
}
}
</script>
<template>
<div class="flex justify-center">
<Button
type="button"
class="border-orange-500 bg-orange-500 text-white hover:bg-orange-600"
@click="handleProses"
>
Proses
</Button>
</div>
</template>
@@ -0,0 +1,164 @@
<script setup lang="ts">
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '~/components/pub/ui/table'
import Input from '~/components/pub/ui/input/Input.vue'
import DatepickerSingle from '~/components/pub/my-ui/datepicker/datepicker-single.vue'
import Button from '~/components/pub/ui/button/Button.vue'
import { format, parseISO } from 'date-fns'
import { id as localeID } from 'date-fns/locale'
interface Props {
open?: boolean
tanggalPemeriksaan?: string | Date
jadwalTanggalPemeriksaan?: string | Date
isLoading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
open: false,
tanggalPemeriksaan: undefined,
jadwalTanggalPemeriksaan: undefined,
isLoading: false,
})
const emit = defineEmits<{
'update:open': [value: boolean]
'update:date': [value: string | undefined]
'update:schedule': [value: string | undefined]
submit: [data: { tanggalPemeriksaan: string | undefined; jadwalTanggalPemeriksaan: string | undefined }]
}>()
// Local state for jadwal tanggal pemeriksaan
const jadwalTanggal = ref<string | undefined>(
props.jadwalTanggalPemeriksaan
? typeof props.jadwalTanggalPemeriksaan === 'string'
? props.jadwalTanggalPemeriksaan
: format(props.jadwalTanggalPemeriksaan, 'yyyy-MM-dd')
: undefined,
)
// Watch for external changes
watch(
() => props.jadwalTanggalPemeriksaan,
(newValue) => {
if (newValue) {
jadwalTanggal.value =
typeof newValue === 'string' ? newValue : format(newValue, 'yyyy-MM-dd')
} else {
jadwalTanggal.value = undefined
}
},
)
// Watch local changes
watch(jadwalTanggal, (newValue) => {
emit('update:schedule', newValue)
})
// Format date for display
const formattedTanggalPemeriksaan = computed(() => {
if (!props.tanggalPemeriksaan) return ''
try {
const date =
props.tanggalPemeriksaan instanceof Date
? props.tanggalPemeriksaan
: parseISO(props.tanggalPemeriksaan)
return format(date, 'dd MMMM yyyy', { locale: localeID })
} catch {
return props.tanggalPemeriksaan.toString()
}
})
// Handle submit
function handleSubmit() {
emit('submit', {
tanggalPemeriksaan: props.tanggalPemeriksaan
? typeof props.tanggalPemeriksaan === 'string'
? props.tanggalPemeriksaan
: format(props.tanggalPemeriksaan, 'yyyy-MM-dd')
: undefined,
jadwalTanggalPemeriksaan: jadwalTanggal.value,
})
}
// Table data for Jadwal Ruang Tindakan
const scheduleData = [
{
no: 1,
namaJenis: 'Ruang Tindakan',
jenisPemeriksaan: 'KEMOTERAPI',
},
]
</script>
<template>
<Dialog
:open="open"
size="lg"
title="Verifikasi Jadwal Pasien"
@update:open="$emit('update:open', $event)"
>
<div class="space-y-6 py-4">
<!-- Jadwal Ruang Tindakan Section -->
<div class="space-y-3">
<h4 class="text-base font-semibold">Jadwal Ruang Tindakan</h4>
<div class="overflow-hidden rounded-md border">
<Table>
<TableHeader class="bg-gray-50">
<TableRow>
<TableHead class="font-semibold">NO</TableHead>
<TableHead class="font-semibold">NAMA JENIS</TableHead>
<TableHead class="font-semibold">JENIS PEMERIKSAAN</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(row, index) in scheduleData" :key="index">
<TableCell>{{ row.no }}</TableCell>
<TableCell>{{ row.namaJenis }}</TableCell>
<TableCell>{{ row.jenisPemeriksaan }}</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<!-- Tanggal Pemeriksaan Section -->
<div class="space-y-2">
<label class="text-sm font-medium">Tanggal Pemeriksaan</label>
<Input
:model-value="formattedTanggalPemeriksaan"
:disabled="true"
class="bg-gray-100 text-gray-700"
readonly
/>
</div>
<!-- Jadwal Tanggal Pemeriksaan Section -->
<div class="space-y-2">
<label class="text-sm font-medium">
Jadwal Tanggal Pemeriksaan
<span class="text-red-500">*</span>
</label>
<DatepickerSingle
v-model="jadwalTanggal"
placeholder="Pilih Jadwal"
:disabled="isLoading"
/>
</div>
<!-- Action Button -->
<div class="flex justify-end pt-4">
<Button
type="button"
:disabled="isLoading || !jadwalTanggal"
class="bg-gradient-to-r from-orange-500 to-orange-400 hover:from-orange-600 hover:to-orange-500 text-white shadow-md"
@click="handleSubmit"
>
<Icon name="i-lucide-calendar-check" class="mr-2 h-4 w-4" />
Simpan Jadwal
</Button>
</div>
</div>
</Dialog>
</template>
@@ -0,0 +1,63 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from '~/components/pub/my-ui/data/types'
const props = defineProps<{
rec: ListItemDto
}>()
const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
const linkItems: LinkItem[] = [
{
label: 'Proses',
onClick: () => {
process()
},
icon: 'i-lucide-pencil',
},
]
function process() {
recId.value = props.rec.id || 0
recAction.value = 'Process'
recItem.value = props.rec
}
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
size="lg"
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
>
<Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup>
<DropdownMenuItem
v-for="item in linkItems"
:key="item.label"
class="hover:bg-gray-100 dark:hover:bg-slate-700"
@click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
>
<Icon :name="item.icon ?? ''" />
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
@@ -0,0 +1,302 @@
<script setup lang="ts">
// Components
import Block from '~/components/pub/my-ui/doc-entry/block.vue'
import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
import Field from '~/components/pub/my-ui/doc-entry/field.vue'
import Label from '~/components/pub/my-ui/doc-entry/label.vue'
import Input from '~/components/pub/ui/input/Input.vue'
import Button from '~/components/pub/ui/button/Button.vue'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import DatepickerSingle from '~/components/pub/my-ui/datepicker/datepicker-single.vue'
// Helpers
import type z from 'zod'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { chemotherapySchema } from "~/schemas/chemotherapy.schema"
interface Props {
values?: any
isLoading?: boolean
isReadonly?: boolean
}
const props = defineProps<Props>()
const isLoading = props.isLoading !== undefined ? props.isLoading : false
const isReadonly = props.isReadonly !== undefined ? props.isReadonly : false
const items = [
{ value: 'item-1', label: 'Item 1' },
{ value: 'item-2', label: 'Item 2' },
{ value: 'item-3', label: 'Item 3' },
]
const emit = defineEmits<{
submit: [values: any, resetForm: () => void]
cancel: [resetForm: () => void]
}>()
const { defineField, errors, meta } = useForm({
validationSchema: toTypedSchema(chemotherapySchema),
initialValues: {
namaPasien: '',
tanggalLahir: '',
noRM: '',
alamat: '',
beratBadan: '',
tinggiBadan: '',
diagnosa: '',
siklus: '',
periodeAwal: '',
periodeAkhir: '',
tanggalKemoterapi: '',
dokterKRJ: '',
},
})
// Define form fields
const [namaPasien, namaPasienAttrs] = defineField('namaPasien')
const [tanggalLahir, tanggalLahirAttrs] = defineField('tanggalLahir')
const [noRM, noRMAttrs] = defineField('noRM')
const [alamat, alamatAttrs] = defineField('alamat')
const [beratBadan, beratBadanAttrs] = defineField('beratBadan')
const [tinggiBadan, tinggiBadanAttrs] = defineField('tinggiBadan')
const [diagnosa, diagnosaAttrs] = defineField('diagnosa')
const [siklus, siklusAttrs] = defineField('siklus')
const [periodeAwal, periodeAwalAttrs] = defineField('periodeAwal')
const [periodeAkhir, periodeAkhirAttrs] = defineField('periodeAkhir')
const [tanggalKemoterapi, tanggalKemoterapiAttrs] = defineField('tanggalKemoterapi')
const [dokterKRJ, dokterKRJAttrs] = defineField('dokterKRJ')
// Set initial values if provided
if (props.values) {
// Object.entries(props.values).forEach(([key, value]) => {
// if (value !== undefined) {
// const field = defineField(key)[0]
// field.value = value
// }
// })
}
const resetForm = () => {
// Object.keys(meta.value.initialValues).forEach((key) => {
// const field = defineField(key)[0]
// field.value = ''
// })
}
function onSubmitForm() {
const formData = {
namaPasien: namaPasien.value,
tanggalLahir: tanggalLahir.value,
noRM: noRM.value,
alamat: alamat.value,
beratBadan: beratBadan.value,
tinggiBadan: tinggiBadan.value,
diagnosa: diagnosa.value,
siklus: siklus.value,
periodeAwal: periodeAwal.value,
periodeAkhir: periodeAkhir.value,
tanggalKemoterapi: tanggalKemoterapi.value,
dokterKRJ: dokterKRJ.value,
}
emit('submit', formData, resetForm)
}
function onCancelForm() {
emit('cancel', resetForm)
}
</script>
<template>
<form @submit.prevent>
<!-- Data Pasien Section -->
<div class="mb-6">
<h3 class="mb-4 text-lg font-semibold">Data Pasien</h3>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
>
<Cell>
<Label height="compact">Nama Pasien</Label>
<Field :errMessage="errors.namaPasien">
<Input
v-model="namaPasien"
v-bind="namaPasienAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan nama pasien"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Tanggal Lahir</Label>
<Field :errMessage="errors.tanggalLahir">
<DatePicker
v-model="tanggalLahir"
v-bind="tanggalLahirAttrs"
:disabled="isLoading || isReadonly"
placeholder="Pilih tanggal lahir"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">No. RM</Label>
<Field :errMessage="errors.noRM">
<Input
v-model="noRM"
v-bind="noRMAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan nomor RM"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Alamat</Label>
<Field :errMessage="errors.alamat">
<Input
v-model="alamat"
v-bind="alamatAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan alamat"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Berat Badan</Label>
<Field :errMessage="errors.beratBadan">
<Input
v-model="beratBadan"
v-bind="beratBadanAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan berat badan"
type="number"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Tinggi Badan</Label>
<Field :errMessage="errors.tinggiBadan">
<Input
v-model="tinggiBadan"
v-bind="tinggiBadanAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan tinggi badan"
type="number"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Diagnosa</Label>
<Field :errMessage="errors.diagnosa">
<Combobox
id="diagnose"
v-model="diagnosa"
v-bind="diagnosaAttrs"
:items="items"
:is-disabled="isLoading || isReadonly"
placeholder="Tentukan diagnosa pasien"
search-placeholder="Cari diagnosa"
empty-message="Diagnosa tidak ditemukan"
/>
</Field>
</Cell>
</Block>
</div>
<!-- Protokol Kemoterapi Section -->
<div class="mb-6">
<h3 class="mb-4 text-lg font-semibold">Protokol Kemoterapi</h3>
<Block
labelSize="thin"
class="!mb-2.5 !pt-0 xl:!mb-3"
>
<Cell>
<Label height="compact">Siklus</Label>
<Field :errMessage="errors.siklus">
<Input
v-model="siklus"
v-bind="siklusAttrs"
:disabled="isLoading || isReadonly"
placeholder="Masukkan siklus"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Periode</Label>
<div class="flex items-center gap-4">
<Field
:errMessage="errors.periodeAwal"
class="flex-1"
>
<DatepickerSingle
v-model="periodeAwal"
v-bind="periodeAwalAttrs"
:disabled="isLoading || isReadonly"
placeholder="Mulai Periode"
/>
</Field>
<span>Sampai</span>
<Field
:errMessage="errors.periodeAkhir"
class="flex-1"
>
<DatepickerSingle
v-model="periodeAkhir"
v-bind="periodeAkhirAttrs"
:disabled="isLoading || isReadonly"
placeholder="Akhir Periode"
/>
</Field>
</div>
</Cell>
<Cell>
<Label height="compact">Tanggal Kemoterapi</Label>
<Field :errMessage="errors.tanggalKemoterapi">
<DatepickerSingle
v-model="tanggalKemoterapi"
v-bind="tanggalKemoterapiAttrs"
:disabled="isLoading || isReadonly"
placeholder="Pilih tanggal kemoterapi"
/>
</Field>
</Cell>
<Cell>
<Label height="compact">Dokter Ruang Tindakan</Label>
<Field :errMessage="errors.dokterKRJ">
<Combobox
id="doctor"
v-model="dokterKRJ"
v-bind="dokterKRJAttrs"
:items="items"
:is-disabled="isLoading || isReadonly"
placeholder="Pilih dokter"
search-placeholder="Cari dokter"
empty-message="Dokter tidak ditemukan"
/>
</Field>
</Cell>
</Block>
</div>
<!-- Form Actions -->
<div class="flex justify-end gap-2 py-2">
<Button
type="button"
variant="secondary"
class="w-[120px]"
@click="onCancelForm"
>
Kembali
</Button>
<Button
v-if="!isReadonly"
type="button"
class="w-[120px]"
:disabled="isLoading || !meta.valid"
@click="onSubmitForm"
>
Simpan
</Button>
</div>
</form>
</template>
@@ -0,0 +1,37 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg.admin'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,80 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('./dropdown-action-process.vue'))
export const config: Config = {
cols: [
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 50 },
],
headers: [
[
{ label: 'TANGGAL' },
{ label: 'NO. RM' },
{ label: 'NO. BILL' },
{ label: 'JK' },
{ label: 'ALAMAT' },
{ label: 'KLINIK ASAL' },
{ label: 'NAMA DOKTER' },
{ label: 'CARA BAYAR' },
{ label: 'RUJUKAN' },
{ label: 'KET. RUJUKAN' },
{ label: 'ASAL' },
{ label: '' },
],
],
keys: [
'tanggal',
'noRm',
'noBill',
'jk',
'alamat',
'klinik',
'dokter',
'caraBayar',
'rujukan',
'ketRujukan',
'asal',
'action',
],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,69 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [
{ width: 60 },
{ width: 200 },
{ width: 100 },
{ width: 100 },
{ width: 150 },
{ width: 80 },
{ width: 200 },
{ width: 120 },
],
headers: [
[
{ label: 'NO.' },
{ label: 'NAMA OBAT' },
{ label: 'DOSIS' },
{ label: 'SATUAN' },
{ label: 'RUTE PEMBERIAN' },
{ label: 'HARI' },
{ label: 'CATATAN' },
{ label: '' },
],
],
keys: [
'number',
'namaObat',
'dosis',
'satuan',
'rute',
'hari',
'catatan',
'action',
],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,62 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 50 },
],
headers: [
[
{ label: 'NO.' },
{ label: 'TANGGAL' },
{ label: 'SIKLUS' },
{ label: 'PERIODE KEMOTERAPI' },
{ label: 'KEHADIRAN' },
{ label: '' },
],
],
keys: [
'number',
'tanggal',
'siklus',
'periode',
'kehadiran',
'action',
],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,80 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 120 },
{ width: 50 },
],
headers: [
[
{ label: 'TANGGAL' },
{ label: 'NO. RM' },
{ label: 'NO. BILL' },
{ label: 'JK' },
{ label: 'ALAMAT' },
{ label: 'KLINIK ASAL' },
{ label: 'NAMA DOKTER' },
{ label: 'CARA BAYAR' },
{ label: 'RUJUKAN' },
{ label: 'KET. RUJUKAN' },
{ label: 'ASAL' },
{ label: '' },
],
],
keys: [
'tanggal',
'noRm',
'noBill',
'jk',
'alamat',
'klinik',
'dokter',
'caraBayar',
'rujukan',
'ketRujukan',
'asal',
'action',
],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
},
components: {
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,78 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
const verifyButton = defineAsyncComponent(() => import('./verify-button.vue'))
export const config: Config = {
cols: [
{ width: 120 },
{ width: 150 },
{ width: 150 },
{ width: 150 },
{ width: 150 },
{ width: 180 },
{ width: 150 },
{ width: 100 },
],
headers: [
[
{ label: 'TANGGAL MASUK' },
{ label: 'PJ BERKAS RM' },
{ label: 'DOKTER' },
{ label: 'JENIS RUANGAN' },
{ label: 'JENIS TINDAKAN' },
{ label: 'TANGGAL JADWAL TINDAKAN' },
{ label: 'STATUS' },
{ label: 'AKSI' },
],
],
keys: [
'tanggalMasuk',
'pjBerkasRm',
'dokter',
'jenisRuangan',
'jenisTindakan',
'tanggalJadwalTindakan',
'status',
'action',
],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
parent: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return recX.parent?.name || '-'
},
},
components: {
status(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: statusBadge,
}
return res
},
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: verifyButton,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,99 @@
import type { Config, RecComponent } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import { format, parseISO } from 'date-fns'
import { id as localeID } from 'date-fns/locale'
type VisitDto = any
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
const verifyButton = defineAsyncComponent(() => import('./verify-button.vue'))
export const config: Config = {
cols: [
{ width: 150 }, // TANGGAL MASUK
{ width: 180 }, // PJ BERKAS RM
{ width: 200 }, // DOKTER
{ width: 150 }, // JENIS RUANGAN
{ width: 150 }, // JENIS TINDAKAN
{ width: 180 }, // TANGGAL JADWAL TINDAKAN
{ width: 150 }, // STATUS
{ width: 120 }, // AKSI
],
headers: [
[
{ label: 'TANGGAL MASUK' },
{ label: 'PJ BERKAS RM' },
{ label: 'DOKTER' },
{ label: 'JENIS RUANGAN' },
{ label: 'JENIS TINDAKAN' },
{ label: 'TANGGAL JADWAL TINDAKAN' },
{ label: 'STATUS' },
{ label: 'AKSI' },
],
],
keys: [
'tanggal_masuk',
'pj_berkas_rm',
'dokter',
'jenis_ruangan',
'jenis_tindakan',
'tanggal_jadwal_tindakan',
'status',
'action',
],
delKeyNames: [
{ key: 'id', label: 'ID' },
{ key: 'nama', label: 'Nama' },
],
parses: {
tanggal_masuk: (rec: unknown): string => {
const recX = rec as VisitDto
if (!recX.tanggal_masuk) return '-'
try {
const date = typeof recX.tanggal_masuk === 'string' ? parseISO(recX.tanggal_masuk) : recX.tanggal_masuk
return format(date, 'dd MMMM yyyy', { locale: localeID })
} catch {
return recX.tanggal_masuk.toString()
}
},
tanggal_jadwal_tindakan: (rec: unknown): string => {
const recX = rec as VisitDto
if (!recX.tanggal_jadwal_tindakan) return '-'
try {
const date =
typeof recX.tanggal_jadwal_tindakan === 'string'
? parseISO(recX.tanggal_jadwal_tindakan)
: recX.tanggal_jadwal_tindakan
return format(date, 'dd MMMM yyyy', { locale: localeID })
} catch {
return recX.tanggal_jadwal_tindakan.toString()
}
},
},
components: {
status(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: statusBadge,
}
return res
},
action(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: verifyButton,
}
return res
},
},
htmls: {},
}
@@ -0,0 +1,37 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg.verification'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,76 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg.medicine'
const searchQuery = ref('')
function handleSearch(event: Event) {
const target = event.target as HTMLInputElement
searchQuery.value = target.value
// TODO: Implement search logic here
// You can emit an event to parent or filter data directly
}
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<!-- Title and Search Section -->
<div class="flex flex-col items-start">
<div class="flex items-center justify-between w-full">
<div>
<h2 class="mb-1 text-xl font-semibold">Protokol Obat Kemoterapi</h2>
<p class="mb-4 text-sm text-gray-500">Daftar obat-obatan yang digunakan dalam protokol kemoterapi.</p>
</div>
<button class="rounded bg-orange-500 px-3 py-2 text-white hover:bg-orange-600">
<i class="ri-add-line"></i>
Tambah Obat Kemoterapi
</button>
</div>
<div class="relative mt-10 w-72">
<input
v-model="searchQuery"
type="text"
placeholder="Cari obat..."
class="w-full rounded-md border px-4 py-2 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
@input="handleSearch"
/>
<span class="absolute right-3 top-2.5 text-gray-400">
<i class="ri-search-line"></i>
</span>
</div>
</div>
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -0,0 +1,75 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg.protocol'
const searchQuery = ref('')
function handleSearch(event: Event) {
const target = event.target as HTMLInputElement
searchQuery.value = target.value
// TODO: Implement search logic here
// You can emit an event to parent or filter data directly
}
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<!-- Title and Search Section -->
<div class="flex flex-col items-start">
<div class="flex items-center justify-between w-full">
<div>
<h2 class="mb-1 text-xl font-semibold">Protokol Kemoterapi</h2>
<p class="mb-4 text-sm text-gray-500">Rangkaian prosedur kemoterapi yang terintegrasi dan konsisten.</p>
</div>
<button class="rounded bg-orange-500 px-3 py-2 text-white hover:bg-orange-600">
<i class="ri-add-line"></i>
Tambah Protokol Kemoterapi
</button>
</div>
<div class="relative mt-10 w-72">
<input
v-model="searchQuery"
type="text"
placeholder="Cari protokol..."
class="w-full rounded-md border px-4 py-2 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
@input="handleSearch"
/>
<span class="absolute right-3 top-2.5 text-gray-400">
<i class="ri-search-line"></i>
</span>
</div>
</div>
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -0,0 +1,71 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg.protocol'
const searchQuery = ref('')
function handleSearch(event: Event) {
const target = event.target as HTMLInputElement
searchQuery.value = target.value
// TODO: Implement search logic here
// You can emit an event to parent or filter data directly
}
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<!-- Title and Search Section -->
<div class="flex flex-col items-start">
<div class="flex items-center justify-between w-full">
<div>
<h2 class="mb-1 text-xl font-semibold">Daftar Kunjungan Rawat Jalan Kemoterapi</h2>
<p class="mb-4 text-sm text-gray-500">Manajemen pendaftaran serta monitoring terapi pasien tindakan rawat jalan.</p>
</div>
</div>
<div class="relative mt-10 w-72">
<input
v-model="searchQuery"
type="text"
placeholder="Cari jadwal pemeriksaan..."
class="w-full rounded-md border px-4 py-2 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
@input="handleSearch"
/>
<span class="absolute right-3 top-2.5 text-gray-400">
<i class="ri-search-line"></i>
</span>
</div>
</div>
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView
:pagination-meta="paginationMeta"
@page-change="handlePageChange"
/>
</div>
</template>
@@ -0,0 +1,45 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg.visit'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
verify: [rec: any]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
// Provide verify handler to child components via provide/inject
function handleVerify(rec: any) {
emit('verify', rec)
}
provide('verify-handler', handleVerify)
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
// Components
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Types
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
// Configs
import { config } from './list-cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
+49
View File
@@ -0,0 +1,49 @@
export type ChemotherapyData = {
id: number
tanggal: string
noRm: string
noBill: string
nama: string
jk: string
alamat: string
klinik: string
dokter: string
caraBayar: string
rujukan: string
ketRujukan: string
asal: string
}
export const sampleRows: ChemotherapyData[] = [
{
id: 1,
tanggal: '12 Agustus 2025',
noRm: 'RM23311224',
noBill: '-',
nama: 'Ahmad Baidowi',
jk: 'L',
alamat: 'Jl Jaksa Agung S. No. 9',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
{
id: 2,
tanggal: '11 Agustus 2025',
noRm: 'RM23455667',
noBill: '-',
nama: 'Abraham Sulaiman',
jk: 'L',
alamat: 'Purwantoro, Blimbing',
klinik: 'Penyakit dalam',
dokter: 'Dr. Andreas Sutaji',
caraBayar: 'JKN',
rujukan: 'Faskes BPJS',
ketRujukan: 'RUMAH SAKIT - RS Lawang Medika - Malang',
asal: 'Rawat Jalan Reguler',
},
// tambahkan lebih banyak baris contoh jika perlu
]
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
const props = defineProps<{
rec: any
idx?: number
}>()
const statusMap: Record<string, { text: string; variant: 'default' | 'secondary' | 'fresh' | 'positive' | 'negative' | 'warning' | 'destructive' | 'outline' }> = {
'belum_terverifikasi': { text: 'Belum Terverifikasi', variant: 'secondary' },
'terverifikasi': { text: 'Terverifikasi', variant: 'positive' },
'ditolak': { text: 'Ditolak', variant: 'destructive' },
}
const statusInfo = computed(() => {
const status = props.rec.status?.toLowerCase() || props.rec.status_code?.toLowerCase() || 'belum_terverifikasi'
return statusMap[status] || statusMap['belum_terverifikasi']
})
</script>
<template>
<div class="flex justify-center">
<Badge :variant="statusInfo.variant">
{{ statusInfo.text }}
</Badge>
</div>
</template>
@@ -0,0 +1,31 @@
<script setup lang="ts">
import Button from '~/components/pub/ui/button/Button.vue'
const props = defineProps<{
rec: any
idx?: number
}>()
// Try to get verify handler from parent via inject
const verifyHandler = inject<(rec: any) => void>('verify-handler', null)
function handleVerify() {
if (verifyHandler) {
verifyHandler(props.rec)
}
}
</script>
<template>
<div class="flex justify-center">
<Button
type="button"
variant="outline"
class="border-orange-500 bg-orange-50 text-orange-600 hover:bg-orange-100 hover:text-orange-700"
@click="handleVerify"
>
Verifikasi
</Button>
</div>
</template>
@@ -0,0 +1,116 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { differenceInDays, differenceInMonths, differenceInYears, parseISO } from 'date-fns'
import { Input } from '~/components/pub/ui/input'
import { cn } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'birthDate',
label = 'Tanggal Lahir',
placeholder = 'Pilih tanggal lahir',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
// Reactive variables for age calculation
const patientAge = ref<string>('Masukkan tanggal lahir')
// Function to calculate age with years, months, and days
function calculateAge(birthDate: string | Date | undefined): string {
if (!birthDate) {
return 'Masukkan tanggal lahir'
}
try {
let dateObj: Date
if (typeof birthDate === 'string') {
dateObj = parseISO(birthDate)
} else {
dateObj = birthDate
}
const today = new Date()
// Calculate years, months, and days
const totalYears = differenceInYears(today, dateObj)
// Calculate remaining months after years
const yearsPassed = new Date(dateObj)
yearsPassed.setFullYear(yearsPassed.getFullYear() + totalYears)
const remainingMonths = differenceInMonths(today, yearsPassed)
// Calculate remaining days after years and months
const monthsPassed = new Date(yearsPassed)
monthsPassed.setMonth(monthsPassed.getMonth() + remainingMonths)
const remainingDays = differenceInDays(today, monthsPassed)
// Format the result
const parts = []
if (totalYears > 0) parts.push(`${totalYears} Tahun`)
if (remainingMonths > 0) parts.push(`${remainingMonths} Bulan`)
if (remainingDays > 0) parts.push(`${remainingDays} Hari`)
return parts.length > 0 ? parts.join(' ') : '0 Hari'
} catch {
return 'Masukkan tanggal lahir'
}
}
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Input
id="birthDate"
type="date"
min="1900-01-01"
v-bind="componentField"
:placeholder="placeholder"
@update:model-value="
(value: string | number) => {
const dateStr = typeof value === 'number' ? String(value) : value
patientAge = calculateAge(dateStr)
}
"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import { getValueLabelList as getDoctorLabelList } from '~/services/doctor.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { Item } from '~/components/pub/my-ui/combobox'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const doctors = ref<Array<Item>>([])
async function fetchDpjp(specialistId: string, subspecialistId: string) {
doctors.value = await getDoctorLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
includes: 'employee-person',
// "unit-id": parseInt(unitId),
"specialist-code": String(specialistId),
"subspecialist-code": String(subspecialistId),
}, true)
}
// const selectedUnitId = inject<Ref<string | null>>("selectedUnitId")!
const selectedSpecialistId = inject<Ref<string | null>>("selectedSpecialistId")!
const selectedSubSpecialistId = inject<Ref<string | null>>("selectedSubSpecialistId")!
// function handleDpjpChange(selected: string) {
// selectedDpjpId.value = selected ?? null
// }
watch([ selectedSpecialistId, selectedSubSpecialistId], () => {
if (selectedSpecialistId.value && selectedSubSpecialistId.value) {
console.log(`Select Doctor`)
fetchDpjp( selectedSpecialistId.value, selectedSubSpecialistId.value)
}
})
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="doctors"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
:is-disabled="selectedSubSpecialistId === null"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import type { Item } from '~/components/pub/my-ui/combobox'
import { getValueLabelList as getSpecialistLabelList } from '~/services/specialist.service'
import { getValueLabelList as getSubspecialistLabelList } from '~/services/subspecialist.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const specialists = ref<Array<Item>>([])
async function fetchSpecialists(unitId: string) {
specialists.value = await getSpecialistLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
"unit-id": String(unitId),
}, true)
}
const selectedUnitId = inject<Ref<string | null>>("selectedUnitId")!
const selectedSpecialistId = inject<Ref<string | null>>("selectedSpecialistId")!
function handleSpecialistChange(selected: string) {
selectedSpecialistId.value = selected ?? null
}
watch([selectedUnitId], () => {
if (selectedUnitId.value) {
fetchSpecialists(selectedUnitId.value)
}
})
</script>
<template>
<DE.Block :class="cn('select-field-group', fieldGroupClass, containerClass)">
<div>
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
Spesialis
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="specialists"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
@update:model-value="handleSpecialistChange"
:is-disabled="selectedUnitId === null"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</div>
</DE.Block>
</template>
@@ -0,0 +1,97 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import type { Item } from '~/components/pub/my-ui/combobox'
import { getValueLabelList as getSubspecialistLabelList } from '~/services/subspecialist.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const subspecialists = ref<Array<Item>>([])
async function fetchSubSpecialists(specialistId: string) {
subspecialists.value = await getSubspecialistLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
specialistCode: 0,
"specialist-code": String(specialistId),
}, true)
}
const selectedSpecialistId = inject<Ref<string | null>>("selectedSpecialistId")!
const selectedSubSpecialistId = inject<Ref<string | null>>("selectedSubSpecialistId")!
function handleSubSpecialistChange(selected: string) {
selectedSubSpecialistId.value = selected ?? null
}
watch([selectedSpecialistId], () => {
if (selectedSpecialistId.value) {
fetchSubSpecialists(selectedSpecialistId.value)
}
})
</script>
<template>
<DE.Block :class="cn('select-field-group', fieldGroupClass, containerClass)">
<div>
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
Sub Spesialis
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="subspecialists"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
@update:model-value="handleSubSpecialistChange"
:is-disabled="selectedSpecialistId === null"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</div>
</DE.Block>
</template>
@@ -0,0 +1,85 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn, mapToComboboxOptList } from '~/lib/utils'
import { occupationCodes } from '~/lib/constants'
import { getValueLabelList as getUnitLabelList } from '~/services/unit.service'
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { Item } from '~/components/pub/my-ui/combobox'
const props = defineProps<{
fieldName?: string
label?: string
placeholder?: string
errors?: FormErrors
class?: string
selectClass?: string
fieldGroupClass?: string
labelClass?: string
isRequired?: boolean
}>()
const {
fieldName = 'job',
label = 'Pekerjaan',
placeholder = 'Pilih pekerjaan',
errors,
class: containerClass,
fieldGroupClass,
labelClass,
} = props
const units = ref<Array<Item>>([])
async function fetchData() {
units.value = await getUnitLabelList({}, true)
}
const selectedUnitId = inject<Ref<string | null>>("selectedUnitId")!
function handleDataChange(selected: string) {
selectedUnitId.value = selected ?? null
}
onMounted(() => {
fetchData()
})
</script>
<template>
<DE.Cell :class="cn('select-field-group', fieldGroupClass, containerClass)">
<DE.Label
:label-for="fieldName"
:class="cn('select-field-label', labelClass)"
:is-required="isRequired"
>
{{ label }}
</DE.Label>
<DE.Field
:id="fieldName"
:errors="errors"
:class="cn('select-field-wrapper')"
>
<FormField
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormControl>
<Combobox
class="focus:ring-0 focus:ring-offset-0"
:id="fieldName"
v-bind="componentField"
:items="units"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
@update:model-value="handleDataChange"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,94 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { Form } from '~/components/pub/ui/form'
import SelectDate from './_common/select-date.vue'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import SelectSpeciality from './_common/select-specialist.vue'
import SelectDpjp from './_common/select-dpjp.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import SelectUnit from './_common/select-unit.vue'
import SelectSubspecialist from './_common/select-subspecialist.vue'
import SelectSpecialist from './_common/select-specialist.vue'
const props = defineProps<{
schema: any
initialValues?: any
errors?: FormErrors
selectedUnitId?: number | null
selectedSpecialistId?: number | null
selectedSubSpecialistId?: number | null
}>()
const formSchema = toTypedSchema(props.schema)
const formRef = ref()
defineExpose({
validate: () => formRef.value?.validate(),
resetForm: () => formRef.value?.resetForm(),
setValues: (values: any, shouldValidate = true) => formRef.value?.setValues(values, shouldValidate),
values: computed(() => formRef.value?.values),
})
</script>
<template>
<Form
ref="formRef"
v-slot="{ values }"
as=""
keep-values
:validation-schema="formSchema"
:validate-on-mount="false"
validation-mode="onSubmit"
:initial-values="initialValues ? initialValues : {}"
>
<DE.Block :col-count="2" :cell-flex="false">
<InputBase
field-name="sepStatus"
label="Status Sep"
placeholder="Status Sep"
:is-disabled="true"
/>
<SelectDate
field-name="date"
label="Tanggal Rencana Kontrol"
:errors="errors"
is-required
/>
<DE.Cell :col-span="2">
<DE.Block :col-count="4" :cell-flex="false">
<SelectUnit
field-name="unit_code"
label="Unit"
placeholder="Pilih Unit"
:errors="errors"
is-required
/>
<SelectSpecialist
field-name="specialist_code"
label="Spesialis/Sub Spesialis"
placeholder="Pilih Spesialis/Sub Spesialis"
:errors="errors"
is-required
/>
<SelectSubspecialist
field-name="subspecialist_code"
label="Spesialis/Sub Spesialis"
placeholder="Pilih Spesialis/Sub Spesialis"
:errors="errors"
is-required
/>
<SelectDpjp
field-name="doctor_code"
label="DPJP"
placeholder="Pilih DPJP"
:errors="errors"
is-required
/>
</DE.Block>
</DE.Cell>
</DE.Block>
</Form>
</template>
@@ -0,0 +1,64 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import type { Patient } from '~/models/patient'
import { defineAsyncComponent } from 'vue'
import { educationCodes, genderCodes } from '~/lib/constants'
import { calculateAge } from '~/lib/utils'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const config: Config = {
cols: [{width: 180}, {}, {}, {}, {}, {width: 30},],
headers: [
[
{ label: 'Tgl Rencana Kontrol' },
{ label: 'Spesialis' },
{ label: 'Sub Spesialis' },
{ label: 'DPJP' },
{ label: 'Status SEP' },
{ label: 'Action' },
],
],
keys: ['date', 'specialist.name', 'subspecialist.name', 'doctor.employee.person.name', 'sep_status', 'action'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
date: (rec: unknown): unknown => {
const date = (rec as any).date
if (typeof date == 'object' && date) {
return (date as Date).toLocaleDateString('id-ID')
} else if (typeof date == 'string') {
return (date as string).substring(0, 10)
}
return date
},
specialist_subspecialist: (rec: unknown): unknown => {
return '-'
},
dpjp: (rec: unknown): unknown => {
// const { person } = rec as Patient
return '-'
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {
sep_status(_rec) {
return 'SEP Internal'
},
},
}
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { config } from './list.cfg'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
import { cn, } from '~/lib/utils'
import type { ControlLetter } from '~/models/control-letter'
// #region Props & Emits
const props = defineProps<{
instance: ControlLetter | null
}>()
const emit = defineEmits<{
(e: 'click', type: string): void
}>()
// #endregion
// #region State & Computed
// #endregion
// Computed addresses from nested data
// #endregion
// #region Lifecycle Hooks
// #endregion
// #region Functions
// #endregion region
// #region Utilities & event handlers
function onClick(type: string) {
emit('click', type)
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<div :class="cn('min-h-[50vh] space-y-2',)">
<DetailRow label="Tgl Rencana Kontrol">{{ props.instance?.date ? new Date(props.instance?.date).toLocaleDateString('id-ID') : '-' }}</DetailRow>
<DetailRow label="Unit">{{ props.instance?.unit.name || '-' }}</DetailRow>
<DetailRow label="Spesialis">{{ props.instance?.specialist.name || '-' }}</DetailRow>
<DetailRow label="Sub Spesialis">{{ props.instance?.subspecialist.name || '-' }}</DetailRow>
<DetailRow label="DPJP">{{ props.instance?.doctor.employee.person.name || '-' }}</DetailRow>
<DetailRow label="Status SEP">{{ 'SEP INTERNAL' }}</DetailRow>
</div>
<div class="border-t-1 my-2 flex justify-end border-t-slate-300 py-2">
<PubMyUiNavFooterBaEd @click="onClick" />
</div>
</template>
<style scoped></style>
@@ -0,0 +1,43 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
const input = defineAsyncComponent(() => import('~/components/pub/ui/input/Input.vue'))
export const config: Config = {
cols: [{}, {}, { classVal: '!p-0.5' }, { width: 50 }],
headers: [
[
{ label: 'Nama' },
{ label: 'Jenis' },
{ label: 'Catatan' },
{ label: '' },
],
],
keys: ['mcuSrc.name', 'mcuSrc.mcuSrcCategory.name', 'note'],
delKeyNames: [
{ key: 'mcuSrc.name', label: 'Nama' },
],
components: {
note(rec, idx) {
return {
idx,
rec: rec as object,
component: input,
}
},
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {},
}
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { config } from './list-entry.cfg'
import type { McuOrderItem } from '~/models/mcu-order-item';
defineProps<{
data: McuOrderItem[]
}>()
const emit = defineEmits<{
requestItem: []
}>()
</script>
<template>
<PubMyUiDataTable class="border mb-3 2xl:mb-4"
v-bind="config"
:rows="data"
/>
<div class="-mx-1 [&_button]:mx-1">
<Button @click="emit('requestItem')">
<Icon name="i-lucide-plus" />
Pilih Item
</Button>
</div>
</template>
@@ -0,0 +1,18 @@
import type { Config } from '~/components/pub/my-ui/data-table'
export const config: Config = {
cols: [{}, {}],
headers: [
[
{ label: 'Nama' },
{ label: 'Jenis' },
],
],
keys: ['mcuSrc.name', 'mcuSrcCategory.name'],
delKeyNames: [
{ key: 'mcuSrc.name', label: 'Nama' },
],
}
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { McuOrderItem } from '~/models/mcu-order-item';
import { config } from './list.cfg'
defineProps<{
data: McuOrderItem[]
}>()
const emit = defineEmits<{
tambah: [mode: string]
}>()
</script>
<template>
<PubMyUiDataTable class="border"
v-bind="config"
:rows="data"
/>
</template>
+32
View File
@@ -0,0 +1,32 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { McuOrder } from '~/models/mcu-order';
const props = defineProps<{
data: McuOrder
}>()
</script>
<template>
<div class="text-sm 2xl:text-base font-semibold mb-3">
Order {{ data?.createdAt?.substring(0, 10) }} - {{ data.status_code }}
</div>
<div class="max-w-[1000px]">
<DE.Block mode="preview" :col-count=5 class="!mb-3">
<DE.Cell :col-span="2">
<DE.Label class="font-semibold">DPJP</DE.Label>
<DE.Field>
{{ data?.doctor?.employee?.person?.name || '.........' }}
</DE.Field>
</DE.Cell>
<DE.Cell></DE.Cell>
<DE.Cell :col-span="2">
<DE.Label class="font-semibold">PPDS</DE.Label>
<DE.Field>
...........
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
</template>
+64
View File
@@ -0,0 +1,64 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry';
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import Nav from '~/components/pub/my-ui/nav-footer/ca-ed-su.vue'
import type { McuOrder } from '~/models/mcu-order';
import McuOrderItems from '~/components/app/mcu-order-item/list.vue';
interface Props {
data: McuOrder[]
paginationMeta: PaginationMeta
}
const props = defineProps<Props>()
const emit = defineEmits<{
cancel: [data: any]
edit: [data: any],
submit: [data: any]
}>()
function navClick(type: 'cancel' | 'edit' | 'submit', data: McuOrder): void {
if (type === 'cancel') {
emit('cancel', data)
} else if (type === 'edit') {
emit('edit', data)
} else if (type === 'submit') {
emit('submit', data)
}
}
</script>
<template>
<div v-if="data.length == 0" class="p-10 text-center">
<div class="mb-4 xl:mb-5">Belum Ada Data</div>
</div>
<template v-for="item, idx in data">
<div :class="'text-sm 2xl:text-base font-semibold ' + (item.status_code == 'new' ? 'mb-2' : 'mb-2')">
Order #{{ data.length - idx }} - {{ item.createdAt?.substring(0, 10) }} - {{ item.status_code }}
</div>
<DE.Block mode="preview" :col-count=7 class="!mb-3">
<DE.Cell :col-span="3">
<DE.Label :class="'font-semibold ' + (item.status_code == 'new' ? 'pt-2' : '')">DPJP</DE.Label>
<DE.Field :class="item.status_code == 'new' ? 'pt-2' : ''">
{{ item.doctor?.employee?.person?.name || '........' }}
</DE.Field>
</DE.Cell>
<DE.Cell :col-span="3">
<DE.Label :class="'font-semibold ' + (item.status_code == 'new' ? 'pt-2' : '')">PPDS</DE.Label>
<DE.Field :class="item.status_code == 'new' ? 'pt-2' : ''">
...........
</DE.Field>
</DE.Cell>
<div class="flex justify-end" >
<Nav
v-if="item.status_code == 'new'"
:small-mode="true"
:default-class="'flex gap-1'"
@click="(type) => { navClick(type, item) }"
/>
</div>
</DE.Block>
<McuOrderItems :data="item.items || []" @click="console.log('click')" class="!mb-5" />
</template>
</template>
+64
View File
@@ -0,0 +1,64 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry';
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import Nav from '~/components/pub/my-ui/nav-footer/ca-ed-su.vue'
import type { McuOrder } from '~/models/mcu-order';
import McuOrderItems from '~/components/app/mcu-order-item/list.vue';
interface Props {
data: McuOrder[]
paginationMeta: PaginationMeta
}
const props = defineProps<Props>()
const emit = defineEmits<{
cancel: [data: any]
edit: [data: any],
submit: [data: any]
}>()
function navClick(type: 'cancel' | 'edit' | 'submit', data: McuOrder): void {
if (type === 'cancel') {
emit('cancel', data)
} else if (type === 'edit') {
emit('edit', data)
} else if (type === 'submit') {
emit('submit', data)
}
}
</script>
<template>
<div v-if="data.length == 0" class="p-10 text-center">
<div class="mb-4 xl:mb-5">Belum Ada Data</div>
</div>
<template v-for="item, idx in data">
<div :class="'text-sm 2xl:text-base font-semibold ' + (item.status_code == 'new' ? 'mb-2' : 'mb-2')">
Order #{{ data.length - idx }} - {{ item.createdAt?.substring(0, 10) }} - {{ item.status_code }}
</div>
<DE.Block mode="preview" :col-count=7 class="!mb-3">
<DE.Cell :col-span="3">
<DE.Label :class="'font-semibold ' + (item.status_code == 'new' ? 'pt-2' : '')">DPJP</DE.Label>
<DE.Field :class="item.status_code == 'new' ? 'pt-2' : ''">
{{ item.doctor?.employee?.person?.name || '........' }}
</DE.Field>
</DE.Cell>
<DE.Cell :col-span="3">
<DE.Label :class="'font-semibold ' + (item.status_code == 'new' ? 'pt-2' : '')">PPDS</DE.Label>
<DE.Field :class="item.status_code == 'new' ? 'pt-2' : ''">
...........
</DE.Field>
</DE.Cell>
<div class="flex justify-end" >
<Nav
v-if="item.status_code == 'new'"
:small-mode="true"
:default-class="'flex gap-1'"
@click="(type) => { navClick(type, item) }"
/>
</div>
</DE.Block>
<McuOrderItems :data="item.items || []" @click="console.log('click')" class="!mb-5" />
</template>
</template>
@@ -0,0 +1,35 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { McuSrcCategory } from '~/models/mcu-src-category';
const model = defineModel()
const props = defineProps<{
data: McuSrcCategory[]
}>()
const emit = defineEmits<{
pick: [category: McuSrcCategory]
}>()
if (!model.value && props.data.length > 0) {
model.value = props.data[0]?.code
}
function pick(category: McuSrcCategory) {
model.value = category.code
emit('pick', category)
}
</script>
<template>
<div class="mb-5">
<div class="font-semibold mb-1.5">
Kategori
</div>
<div class="-mx-1 [&_button]:mx-1 ">
<Button v-for="item, idx in data" :variant="model === item.code ? 'default' : 'outline'" @click="pick(item)">
{{ item.name }}
</Button>
</div>
</div>
</template>
@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { McuSrc } from '~/models/mcu-src';
import type { McuOrderItem } from '~/models/mcu-order-item';
const data = defineModel({ type: Array as PropType<McuOrderItem[]>, required: true })
defineProps<{
dataSource: McuSrc[]
// data: number[]
}>()
const emit = defineEmits<{
pick: [item: McuSrc]
}>()
function pick(item: McuSrc) {
emit('pick', item)
// if (data.value.some(e => e.mcuSrc_id === item.id)) {
// const pos = data.value.map(e => e.mcuSrc_id).indexOf(item.id)
// data.value.splice(pos, 1)
// } else {
// data.value.push({
// id: 0,
// mcuOrder_id: 0,
// mcuSrc_id: item.id,
// createdAt: "",
// updatedAt: "",
// })
// }
}
</script>
<template>
<div class="mb-5">
<div class="font-semibold mb-1.5">
Daftar Item
</div>
<div class="grid lg:grid-cols-4 2xl:grid-cols-5 gap-2 [&_button]:w-full">
<div v-for="item, idx in dataSource" :key="idx" class="flex gap-2">
<Button
:variant="data.some(e => e.mcuSrc_id === item.id) ? 'default' : 'outline'"
type="button"
@click="pick(item)"
>
{{ item.name }}
</Button>
</div>
</div>
</div>
</template>
@@ -0,0 +1,50 @@
import type {
Col,
KeyLabel,
RecComponent,
RecStrFuncComponent,
RecStrFuncUnknown,
Th,
} from '~/components/pub/my-ui/data/types'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [{}, {}, {}, {}, {}, {}, { width: 50 }]
export const header: Th[][] = [
[
{ label: 'Nama' },
{ label: "Dosis" },
{ label: 'Satuan' },
{ label: '' },
],
]
export const keys = ['name', 'dose', 'uom.name', 'action']
export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
]
export const funcParsed: RecStrFuncUnknown = {
group: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineGroup_code || '-'
},
}
export const funcComponent: RecStrFuncComponent = {
action: (rec: unknown, idx: number): RecComponent => {
const res: RecComponent = {
idx,
rec: rec as object,
component: action,
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {}
@@ -0,0 +1,35 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-entry'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubBaseDataTable
:rows="data"
:cols="cols"
:header="header"
:keys="keys"
:func-parsed="funcParsed"
:func-html="funcHtml"
:func-component="funcComponent"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -1,14 +1,30 @@
<script setup lang="ts">
import type { PrescriptionItem } from '~/models/prescription-item';
import { config } from './list-entry.cfg'
defineProps<{
data: any[]
data: PrescriptionItem[]
}>()
const emit = defineEmits<{
add: [mode: 'mix' | 'non-mix']
}>()
</script>
<template>
<PubMyUiDataTable
<PubMyUiDataTable class="border mb-3 2xl:mb-4"
v-bind="config"
:rows="data"
/>
<div class="-mx-1 [&_button]:mx-1">
<Button @click="emit('add', 'mix')">
<Icon name="i-lucide-plus" />
Tambah Racikan
</Button>
<Button @click="emit('add', 'non-mix')">
<Icon name="i-lucide-plus" />
Tambah Non Racikan
</Button>
</div>
</template>
@@ -0,0 +1,41 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
export const config: Config = {
cols: [{}, {}, {}, {}, {}, {}],
headers: [
[
{ label: 'Nama' },
{ label: 'Bentuk' },
{ label: 'Freq' },
{ label: 'Dosis' },
{ label: 'Interval' },
{ label: 'Total' },
],
],
keys: ['name', 'uom_code', 'frequency', 'multiplier', 'interval', 'total'],
delKeyNames: [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
],
parses: {
cateogry: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineCategory?.name || '-'
},
group: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineGroup?.name || '-'
},
method: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineMethod?.name || '-'
},
unit: (rec: unknown): unknown => {
return (rec as SmallDetailDto).medicineUnit?.name || '-'
},
},
}
@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { PrescriptionItem } from '~/models/prescription-item';
import { config } from './list.cfg'
defineProps<{
data: PrescriptionItem[]
}>()
const emit = defineEmits<{
tambah: [mode: string]
}>()
</script>
<template>
<PubMyUiDataTable class="border mb-2 2xl:mb-3"
v-bind="config"
:rows="data"
/>
</template>
@@ -0,0 +1,113 @@
<script setup lang="ts">
import { LucidePlus } from 'lucide-vue-next';
import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue';
import * as Table from '~/components/pub/ui/table'
import Nav from '~/components/pub/my-ui/nav-footer/cl-sa.vue'
import { genBase } from '~/models/_base';
import { genMedicine } from '~/models/medicine';
import type { MedicinemixItem } from '~/models/medicinemix-item';
import type { PrescriptionItem } from '~/models/prescription-item';
const props = defineProps<{
data: PrescriptionItem
items: MedicinemixItem[]
}>()
type ClickType = 'close' | 'save'
const emit = defineEmits<{
close: [],
save: [data: PrescriptionItem, items: MedicinemixItem[]],
}>()
function navClick(type: ClickType) {
if (type === 'close') {
emit('close')
} else if (type === 'save') {
emit('save', props.data, props.items)
}
}
function addItem() {
props.items.push({
...genBase(),
medicineMix_id: 0,
medicine_id: 0,
medicine: genMedicine(),
dose: 0,
uom_code: '',
})
}
</script>
<template>
<DE.Block :colCount="5" :cellFlex="false">
<DE.Cell :colSpan="5">
<DE.Label>Nama</DE.Label>
<DE.Field><Input :value="data.medicineMix?.name" /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Frequensi</DE.Label>
<DE.Field><Input v-model="data.frequency" /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Dosis</DE.Label>
<DE.Field><Input v-model="data.dose" /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Sediaan</DE.Label>
<DE.Field><Input :value="data.medicineMix?.uom_code" /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Total</DE.Label>
<DE.Field><Input /></DE.Field>
</DE.Cell>
<DE.Cell :colSpan="5">
<DE.Label>Cara Pakai</DE.Label>
<DE.Field><Input /></DE.Field>
</DE.Cell>
</DE.Block>
<div class="text-sm 2xl:text-base font-semibold !mb-3">Daftar Obat</div>
<Table.Table class="border mb-3 2xl:mb-4">
<Table.TableHeader class="[&_th]:h-8 [&_th]:2xl:h-9">
<Table.TableRow>
<Table.TableHead>Nama</Table.TableHead>
<Table.TableHead class="w-24">Dosis</Table.TableHead>
<Table.TableHead class="w-24">Satuan</Table.TableHead>
<Table.TableHead class="w-20">..</Table.TableHead>
</Table.TableRow>
</Table.TableHeader>
<Table.TableBody class="[&_td]:p-0.6">
<Table.TableRow v-if="items.length > 0" v-for="item in items">
<Table.TableCell>
<Input v-model="item.medicine.name" />
</Table.TableCell>
<Table.TableCell>
<Input v-model="item.dose" />
</Table.TableCell>
<Table.TableCell>
<Input />
</Table.TableCell>
</Table.TableRow>
<Table.TableRow v-else>
<Table.TableCell colspan="4" class="!p-5 text-center">
Belum ada data
</Table.TableCell>
</Table.TableRow>
</Table.TableBody>
</Table.Table>
<div>
<Button @click="addItem">
<LucidePlus />
Tambah
</Button>
</div>
<Separator class="my-5" />
<div class="flex justify-center">
<Nav @click="navClick" />
</div>
</template>
@@ -0,0 +1,90 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import Separator from '~/components/pub/ui/separator/Separator.vue'
import Nav from '~/components/pub/my-ui/nav-footer/cl-sa.vue'
import { bigTimeUnitCodes } from '~/lib/constants'
import type { PrescriptionItem } from '~/models/prescription-item'
const props = defineProps<{
data: PrescriptionItem
}>()
type ClickType = 'close' | 'save'
type Item = {
value: string
label: string
}
const bigTimeUnitCodeItems: Item[] = []
if(!props.data.intervalUnit_code) {
props.data.intervalUnit_code = 'day'
}
Object.keys(bigTimeUnitCodes).forEach((key) => {
bigTimeUnitCodeItems.push({
value: key,
label: bigTimeUnitCodes[key] || '',
})
})
const emit = defineEmits<{
close: [],
save: [data: PrescriptionItem],
}>()
function navClick(type: ClickType) {
if (type === 'close') {
emit('close')
} else if (type === 'save') {
emit('save', props.data)
}
}
</script>
<template>
<DE.Block :colCount="5" :cellFlex="false">
<DE.Cell :colSpan="5">
<DE.Label>Nama</DE.Label>
<DE.Field><Input :value="data.medicineMix?.name" /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Frequensi</DE.Label>
<DE.Field><Input type="number" v-model.number="data.frequency" /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Dosis</DE.Label>
<DE.Field><Input type="number" v-model.number="data.dose" /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Sediaan</DE.Label>
<DE.Field><Input :value="data.medicineMix?.uom_code" readonly /></DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Interval</DE.Label>
<DE.Field>
<Select
v-model="data.intervalUnit_code"
:items="bigTimeUnitCodeItems"
/>
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Total</DE.Label>
<DE.Field>
<Input v-model="data.quantity" />
</DE.Field>
</DE.Cell>
<DE.Cell :colSpan="5">
<DE.Label>Cara Pakai</DE.Label>
<DE.Field><Input /></DE.Field>
</DE.Cell>
</DE.Block>
<Separator class="my-5" />
<div class="flex justify-center">
<Nav @click="navClick" />
</div>
</template>
@@ -0,0 +1,32 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { Prescription } from '~/models/prescription'
const props = defineProps<{
data: Prescription
}>()
</script>
<template>
<div class="text-sm 2xl:text-base font-semibold mb-3">
Order {{ data.issuedAt?.substring(0, 10) || data.createdAt?.substring(0, 10) }} - {{ data.status_code }}
</div>
<div class="max-w-[1000px]">
<DE.Block mode="preview" :col-count=5 class="!mb-3">
<DE.Cell :col-span="2">
<DE.Label class="font-semibold">DPJP</DE.Label>
<DE.Field>
{{ data.doctor?.employee?.person?.name || '.........' }}
</DE.Field>
</DE.Cell>
<DE.Cell></DE.Cell>
<DE.Cell :col-span="2">
<DE.Label class="font-semibold">PPDS</DE.Label>
<DE.Field>
...........
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
</template>
+33 -29
View File
@@ -1,32 +1,36 @@
<template>
<div>
<PubMyUiDocEntryBlock mode="preview" :colCount=3>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>DPJP</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell />
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>Tgl Order</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>DPJP</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell />
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>Status</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
</PubMyUiDocEntryBlock>
<div class="md:grid md:grid-cols-2 font-semibold">
<div class="md:pe-10">
<PubMyUiDocEntryBlock>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>Tgl Order</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>Status</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
</PubMyUiDocEntryBlock>
</div>
<div class="md:ps-10">
<PubMyUiDocEntryBlock>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel position="dynamic">DPJP</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel position="dynamic">PPDS</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
</PubMyUiDocEntryBlock>
</div>
</div>
</template>
@@ -0,0 +1,88 @@
<script setup lang="ts">
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import type { Prescription } from '~/models/prescription'
import * as DE from '~/components/pub/my-ui/doc-entry';
import PrescriptionItemList from '~/components/app/prescription-item/list-entry.vue';
interface Props {
data: Prescription[]
isLoading: boolean
paginationMeta?: PaginationMeta
}
defineProps<Props>()
</script>
<template>
<div v-if="isLoading" class="p-10 text-center">
Memuat data..
</div>
<div v-else-if="data && data.length == 0" class="p-10 text-center">
<div class="mb-4 xl:mb-5">Belum Ada Data</div>
<!-- <div>
<Button>
<Icon name="i-lucide-plus" class="me-2 align-middle" />
Tambah Order
</Button>
</div> -->
</div>
<div v-else v-for="(item, idx) in data">
<Separator class="my-5" />
<div class="md:grid md:grid-cols-2 font-semibold">
<div>
<DE.Block mode="preview">
<DE.Cell>
<DE.Label>Order #{{ data.length - idx }}</DE.Label>
<DE.Field>
2025-01-01
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>Status</DE.Label>
<DE.Field>
{{ item.status_code }}
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
<div>
<DE.Block mode="preview">
<DE.Cell>
<DE.Label>DPJP</DE.Label>
<DE.Field>
{{ item.doctor?.employee?.person.name }}
</DE.Field>
</DE.Cell>
<DE.Cell>
<DE.Label>PPDS</DE.Label>
<DE.Field>
{{ item.specialistIntern?.person.name }}
</DE.Field>
</DE.Cell>
</DE.Block>
</div>
</div>
<PrescriptionItemList :data="item.items || []" />
</div>
</template>
<!-- <Separator class="my-4 xl:my-5" />
<AppPrescriptionEntry />
<div class="flex content-center mb-3">
<div class="me-auto pt-2">
<div class="font-semibold md:text-sm xl:text-base">Daftar Obat</div>
</div>
<div>
<Button @click="addMedicine" class="me-2">
<Icon name="i-lucide-plus" />
Tambah Non Racikan
</Button>
<Button @click="addMedicineMix">
<Icon name="i-lucide-plus" />
Tambah Racikan
</Button>
</div>
</div>
<PrescriptionItemListEntry :data=[] /> -->
@@ -0,0 +1,56 @@
<script setup lang="ts">
// import { Block, Cell } from '~/components/pub/my-ui/doc-entry/index'
// import Block from '~/components/pub/my-ui/doc-entry/block.vue'
// import Cell from '~/components/pub/my-ui/doc-entry/cell.vue'
</script>
<template>
<div class="p-10 text-center">
<div class="mb-4 xl:mb-5">Belum Ada Data</div>
<div>
<Button>
<Icon name="i-lucide-plus" class="me-2 align-middle" />
Tambah Order
</Button>
</div>
</div>
<Separator class="my-5" />
<div class="md:grid md:grid-cols-2 font-semibold">
<div>
<PubCustomUiDocEntryBlock mode="preview">
<PubCustomUiDocEntryCell>
<PubCustomUiDocEntryLabel>Order #1</PubCustomUiDocEntryLabel>
<PubCustomUiDocEntryColon />
<PubCustomUiDocEntryField>
2025-01-01
</PubCustomUiDocEntryField>
</PubCustomUiDocEntryCell>
<PubCustomUiDocEntryCell>
<PubCustomUiDocEntryLabel>Status</PubCustomUiDocEntryLabel>
<PubCustomUiDocEntryColon />
<PubCustomUiDocEntryField>
Status
</PubCustomUiDocEntryField>
</PubCustomUiDocEntryCell>
</PubCustomUiDocEntryBlock>
</div>
<div>
<PubCustomUiDocEntryBlock mode="preview">
<PubCustomUiDocEntryCell>
<PubCustomUiDocEntryLabel>DPJP</PubCustomUiDocEntryLabel>
<PubCustomUiDocEntryColon />
<PubCustomUiDocEntryField>
Nama Dokter
</PubCustomUiDocEntryField>
</PubCustomUiDocEntryCell>
<PubCustomUiDocEntryCell>
<PubCustomUiDocEntryLabel>PPDS</PubCustomUiDocEntryLabel>
<PubCustomUiDocEntryColon />
<PubCustomUiDocEntryField>
Nama PPDS
</PubCustomUiDocEntryField>
</PubCustomUiDocEntryCell>
</PubCustomUiDocEntryBlock>
</div>
</div>
</template>
+62 -32
View File
@@ -1,5 +1,37 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry';
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import Nav from '~/components/pub/my-ui/nav-footer/ca-ed-su.vue'
import type { Prescription } from '~/models/prescription';
import PrescriptionItem from '~/components/app/prescription-item/list.vue';
import { add } from 'date-fns';
interface Props {
data: Prescription[]
paginationMeta: PaginationMeta
}
const props = defineProps<Props>()
const emit = defineEmits<{
cancel: [data: any]
edit: [data: any],
submit: [data: any]
}>()
function navClick(type: 'cancel' | 'edit' | 'submit', data: Prescription): void {
if (type === 'cancel') {
emit('cancel', data)
} else if (type === 'edit') {
emit('edit', data)
} else if (type === 'submit') {
emit('submit', data)
}
}
</script>
<template>
<div class="p-10 text-center">
<div v-if="data.length == 0" class="p-10 text-center">
<div class="mb-4 xl:mb-5">Belum Ada Data</div>
<div>
<Button>
@@ -8,35 +40,33 @@
</Button>
</div>
</div>
<Separator class="my-5" />
<div>
<PubMyUiDocEntryBlock mode="preview" :colCount=3>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>DPJP</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell />
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>Tgl Order</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>DPJP</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
<PubMyUiDocEntryCell />
<PubMyUiDocEntryCell>
<PubMyUiDocEntryLabel>Status</PubMyUiDocEntryLabel>
<PubMyUiDocEntryField>
<Input />
</PubMyUiDocEntryField>
</PubMyUiDocEntryCell>
</PubMyUiDocEntryBlock>
</div>
<template v-for="item, idx in data">
<div :class="'text-sm 2xl:text-base font-semibold ' + (item.status_code == 'new' ? 'mb-2' : 'mb-2')">
Order #{{ data.length - idx }} - {{ item.issuedAt?.substring(0, 10) || item.createdAt?.substring(0, 10) }} - {{ item.status_code }}
</div>
<DE.Block mode="preview" :col-count="7" class="!mb-3">
<DE.Cell :col-span="3">
<DE.Label :class="'font-semibold ' + (item.status_code == 'new' ? 'pt-2' : '')">DPJP</DE.Label>
<DE.Field :class="item.status_code == 'new' ? 'pt-2' : ''">
{{ item.doctor?.employee?.person?.name || '-' }}
</DE.Field>
</DE.Cell>
<DE.Cell :col-span="3">
<DE.Label :class="'font-semibold ' + (item.status_code == 'new' ? 'pt-2' : '')">PPDS</DE.Label>
<DE.Field :class="item.status_code == 'new' ? 'pt-2' : ''">
...........
</DE.Field>
</DE.Cell>
<div class="flex justify-end" >
<Nav
v-if="item.status_code == 'new'"
:small-mode="true"
:default-class="'flex gap-1'"
@click="(type) => { navClick(type, item) }"
/>
</div>
</DE.Block>
<PrescriptionItem :data="item.items || []" @click="console.log('click')" class="mb-10" />
<!-- <div v-if="idx < data.length - 1" class="my-8 -mx-4 border-t border-t-slate-300" /> -->
</template>
</template>
@@ -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>
+38 -8
View File
@@ -1,5 +1,12 @@
<script setup lang="ts">
const navMenu = ref([])
type NavGroup = {
heading?: string
items: any[]
}
const { getActiveRole } = useUserStore()
const navMenu = ref<NavGroup[]>([])
const teams: {
name: string
@@ -12,7 +19,14 @@ const teams: {
plan: 'Saiful Anwar Hospital',
},
]
const sidebar = {
type Sidebar = {
collapsible: 'offcanvas' | 'icon' | 'none'
side: 'left' | 'right'
variant: 'sidebar' | 'floating' | 'inset'
}
const sidebar: Sidebar = {
collapsible: 'offcanvas', // 'offcanvas' | 'icon' | 'none'
side: 'left', // 'left' | 'right'
variant: 'sidebar', // 'sidebar' | 'floating' | 'inset'
@@ -29,18 +43,34 @@ onMounted(async () => {
await setMenu()
})
async function setMenu() {
const activeRole = getActiveRole()
const activeRoleParts = activeRole ? activeRole.split('|') : []
const role = activeRoleParts[0]+(activeRoleParts.length > 1 ? `-${activeRoleParts[1]}` : '')
try {
const res = await fetch(`/side-menu-items/${role.toLowerCase()}.json`)
const rawMenu = await res.text()
navMenu.value = JSON.parse(rawMenu)
} catch (e) {
const res = await fetch(`/side-menu-items/blank.json`)
const rawMenu = await res.text()
navMenu.value = JSON.parse(rawMenu)
}
}
function resolveNavItemComponent(item: any): any {
if ('children' in item) return resolveComponent('LayoutSidebarNavGroup')
return resolveComponent('LayoutSidebarNavLink')
}
async function setMenu() {
const position_code = 'sys'
const res = await fetch(`/side-menu-items/${position_code}.json`)
const rawMenu = await res.text()
navMenu.value = JSON.parse(rawMenu)
}
watch(getActiveRole, async () => {
await setMenu()
// const activeRole = getActiveRole()
// const res = await fetch(`/side-menu-items/${activeRole}.json`)
// const rawMenu = await res.text()
// navMenu.value = JSON.parse(rawMenu)
})
</script>
<template>
+23 -18
View File
@@ -10,8 +10,8 @@ import { useSidebar } from '~/components/pub/ui/sidebar'
// }>()
const { isMobile } = useSidebar()
const { logout } = useUserStore()
const userStore = useUserStore().user
const { user, logout, setActiveRole, getActiveRole } = useUserStore()
// const userStore = useUserStore().user
function handleLogout() {
navigateTo('/auth/login')
@@ -32,19 +32,19 @@ const showModalTheme = ref(false)
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage src="" :alt="userStore?.user_name || 'system'" />
<AvatarImage src="" :alt="user.user_name || 'system'" />
<AvatarFallback class="rounded-lg">
{{
userStore?.user_name
user.user_name
?.split(' ')
.map((n) => n[0])
.map((n: string) => n[0])
.join('') || ''
}}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ userStore?.user_name || '' }}</span>
<span class="truncate text-xs">{{ userStore?.user_email || '' }}</span>
<span class="truncate font-semibold">{{ user.user_name || '' }}</span>
<span class="truncate text-xs">{{ user.user_email || '' }}</span>
</div>
<Icon name="i-lucide-chevrons-up-down" class="ml-auto size-4" />
</SidebarMenuButton>
@@ -52,35 +52,40 @@ const showModalTheme = ref(false)
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg bg-white"
:side="isMobile ? 'bottom' : 'right'"
align="end"
:align="'end'"
>
<DropdownMenuLabel class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar class="h-8 w-8 rounded-lg">
<AvatarImage src="" :alt="userStore?.user_name || 'system'" />
<AvatarImage src="" :alt="user.user_name || 'system'" />
<AvatarFallback class="rounded-lg">
{{
userStore?.user_name
user.user_name
?.split(' ')
.map((n) => n[0])
.map((n: string) => n[0])
.join('') || ''
}}
</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ userStore?.user_name || '' }}</span>
<span class="truncate text-xs">{{ userStore?.user_email || '' }}</span>
<span class="truncate font-semibold">{{ user.user_name || '' }}</span>
<span class="truncate text-xs">{{ user.user_email || '' }}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem class="hover:bg-gray-100" @click="showModalTheme = true">
<DropdownMenuItem class="hover:bg-gray-100" @click="showModalTheme = true">
<Icon name="i-lucide-user" />
Profile
</DropdownMenuItem>
<template v-if="user.roles.length > 1">
<DropdownMenuSeparator />
<DropdownMenuItem v-for="role in user.roles" class="hover:bg-gray-100" @click="setActiveRole(role)">
<Icon name="i-lucide-user" />
Profile
{{ role }}
<Icon name="i-lucide-check" class="ml-auto" v-if="getActiveRole() === role" />
</DropdownMenuItem>
</DropdownMenuGroup>
</template>
<DropdownMenuSeparator />
<DropdownMenuItem class="hover:bg-gray-100" @click="handleLogout">
<Icon name="i-lucide-log-out" />
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { cn } from '~/lib/utils';
const props = withDefaults(defineProps<{
text?: string
description?: string | string[]
class?: string
}>(), {
})
</script>
<template>
<div :class="cn('flex items-center gap-4 p-3 rounded-md text-orange-500 border border-orange-400 bg-orange-50',
props.class
)">
<Icon name="i-lucide-triangle-alert" class="h-12 w-12 align-middle transition-colors" />
<div class="">
<p class="font-medium text-base">{{text}}</p>
<ul class="list-disc list-inside">
<li v-for="(desc, index) in (Array.isArray(description) ? description : [description])" :key="index">
{{ desc }}
</li>
</ul>
</div>
</div>
</template>
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
import { activeStatusCodes } from '~/lib/constants';
const props = defineProps<{
rec: any
idx?: number
}>()
const statusText = computed(() => {
const code: keyof typeof activeStatusCodes = props.rec.status_code === 1 ? `active` : `inactive`
return activeStatusCodes[code]
})
const badgeVariant = computed(() => {
return props.rec.status_code === 1 ? 'default' : 'destructive'
})
</script>
<template>
<div class="flex justify-center">
<Badge :variant="badgeVariant" class="rounded-2xl text-[0.6rem]" >
{{ statusText }}
</Badge>
</div>
</template>
@@ -20,4 +20,17 @@ export function recStrToItem(input: Record<string, string>): Item[] {
return items
}
export function objectsToItems(input: object[], key = 'id', label = 'name'): Item[] {
const items: Item[] = []
for (const item of input) {
if (item.hasOwnProperty(key) && item.hasOwnProperty(label)) {
items.push({
value: item[key as keyof typeof item], // the hasOwnProperty check should be enough
label: item[label as keyof typeof item], // the hasOwnProperty check should be enough
})
}
}
return items
}
export { default as Combobox } from './combobox.vue'
@@ -2,5 +2,6 @@ export interface TabItem {
value: string
label: string
component?: any
groups?: string[]
props?: Record<string, any>
}
@@ -71,7 +71,7 @@ function handleCancel() {
<Dialog v-model:open="isOpen" :title="title" :size="size">
<div class="space-y-4">
<!-- Icon dan pesan -->
<div class="flex items-start gap-3">
<div class="flex items-center gap-3">
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-6 h-6 mt-1 flex-shrink-0" />
<div class="flex-1">
<p class="text-sm text-muted-foreground leading-relaxed">
@@ -22,7 +22,7 @@ const selected = ref<any[]>([])
function toggleSelection(row: any, event?: Event) {
if (event) event.stopPropagation() // cegah event bubble ke TableRow
const isMultiple = props.selectMode === 'multi' || props.selectMode === 'multiple'
const isMultiple = props.selectMode === 'multiple' // props.selectMode === 'multi' ||
// gunakan pembanding berdasarkan id atau stringify data
const findIndex = selected.value.findIndex((r) => JSON.stringify(r) === JSON.stringify(row))
@@ -128,7 +128,7 @@ function handleActionCellClick(event: Event, _cellRef: string) {
'bg-green-50':
props.selectMode === 'single' && selected.some((r) => JSON.stringify(r) === JSON.stringify(row)),
'bg-blue-50':
(props.selectMode === 'multi' || props.selectMode === 'multiple') &&
(props.selectMode === 'multiple') && // props.selectMode === 'multi' ||
selected.some((r) => JSON.stringify(r) === JSON.stringify(row)),
}"
@click="toggleSelection(row)"
+8 -1
View File
@@ -8,6 +8,7 @@ export interface ListItemDto {
}
export type ComponentType = Component
export type ComponentWithProps = { component: Component, props: Record<string, any> }
export interface ButtonNav {
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
@@ -41,17 +42,23 @@ export interface RefSearchNav {
onClear: () => void
}
export interface RefExportNav {
onExportPdf?: () => void
onExportCsv?: () => void
onExportExcel?: () => void
}
// prepared header for relatively common usage
export interface HeaderPrep {
title?: string
icon?: string
components?: ComponentWithProps[]
refSearchNav?: RefSearchNav
quickSearchNav?: QuickSearchNav
filterNav?: ButtonNav
addNav?: ButtonNav
printNav?: ButtonNav
}
export interface KeyLabel {
key: string
label: string
+5 -1
View File
@@ -19,6 +19,8 @@ const props = defineProps<{
maxLength?: number
isRequired?: boolean
isDisabled?: boolean
rightLabel?: string
bottomLabel?: string
}>()
function handleInput(event: Event) {
@@ -61,7 +63,7 @@ function handleInput(event: Event) {
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormItem :class="`relative`">
<FormControl>
<Input
:disabled="isDisabled"
@@ -76,10 +78,12 @@ function handleInput(event: Event) {
spellcheck="false"
@input="handleInput"
/>
<p v-show="rightLabel" class="text-gray-400 absolute top-0 right-3">{{ rightLabel }}</p>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
<p v-show="bottomLabel" class="text-gray-400">{{ bottomLabel }}</p>
</DE.Cell>
</template>
@@ -1,10 +1,12 @@
<script setup lang="ts">
const props = defineProps<{
enableDraft?: boolean
smallMode?: boolean
defaultClass?: string
class?: string
}>()
const enableDraft = props.enableDraft ?? true
const defaultClass = props.defaultClass ?? 'm-2 flex gap-2 px-2'
const additionalClass = props.class ?? ''
const btnClass = props.smallMode ? '[&_button]:w-7 [&_button]:h-7 [&_button]:2xl:w-8 [&_button]:2xl:h-9 [&_button]:!p-0' : ''
@@ -29,7 +31,7 @@ function onClick(type: ClickType) {
</Button>
</div>
<div>
<Button variant="secondary" type="button" @click="onClick('draft')">
<Button v-show="enableDraft" variant="secondary" type="button" @click="onClick('draft')">
<Icon name="i-lucide-file" />
Draft
</Button>
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { DateRange } from 'radix-vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { cn } from '~/lib/utils'
import type { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
const props = defineProps<{
prep: HeaderPrep
refSearchNav?: RefSearchNav
enableExport?: boolean
refExportNav?: RefExportNav
}>()
// function emitSearchNavClick() {
// props.refSearchNav?.onClick()
// }
//
// function onInput(event: Event) {
// props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
// }
//
// function btnClick() {
// props.prep?.addNav?.onClick?.()
// }
const searchQuery = ref('')
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: new Date(),
to: new Date(),
})
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
})
const value = ref({
start: new CalendarDate(2022, 1, 20),
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
}) as Ref<DateRange>
function onFilterClick() {
console.log('Search:', searchQuery.value)
console.log('Date Range:', dateRange.value)
props.refSearchNav?.onClick()
}
</script>
<template>
<header>
<div class="flex items-center gap-2 mb-4 2xl:mb-5">
<Button variant="outline" class="border-orange-500 text-orange-600 hover:bg-orange-50" @click="onFilterClick">
<FilterIcon class="mr-2 size-4" />
Filter
</Button>
<DropdownMenu v-show="props.enableExport">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50">
<Icon name="i-lucide-download" class="h-4 w-4" />
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-show="props.refExportNav?.onExportPdf"
@click="props.refExportNav?.onExportPdf">
Ekspor PDF
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportCsv"
@click="props.refExportNav?.onExportCsv">
Ekspor CSV
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportExcel"
@click="props.refExportNav?.onExportExcel">
Ekspor Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
</template>
+28 -2
View File
@@ -5,11 +5,13 @@ import type { Ref } from 'vue'
import type { DateRange } from 'radix-vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { cn } from '~/lib/utils'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import type { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
const props = defineProps<{
prep: HeaderPrep
refSearchNav?: RefSearchNav
enableExport?: boolean
refExportNav?: RefExportNav
}>()
// function emitSearchNavClick() {
@@ -57,7 +59,7 @@ function onFilterClick() {
<template>
<header>
<div class="flex items-center space-x-2 mb-4 2xl:mb-5">
<div class="flex items-center gap-2 mb-4 2xl:mb-5">
<div class="relative w-64">
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
@@ -97,6 +99,30 @@ function onFilterClick() {
<FilterIcon class="mr-2 size-4" />
Filter
</Button>
<DropdownMenu v-show="props.enableExport">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50">
<Icon name="i-lucide-download" class="h-4 w-4" />
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-show="props.refExportNav?.onExportPdf"
@click="props.refExportNav?.onExportPdf">
Ekspor PDF
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportCsv"
@click="props.refExportNav?.onExportCsv">
Ekspor CSV
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportExcel"
@click="props.refExportNav?.onExportExcel">
Ekspor Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
</template>
+12 -3
View File
@@ -30,15 +30,24 @@ function btnClick() {
<div class="flex items-center">
<div class="font-semibold text-gray-900 md:text-base 2xl:text-lg">
<Icon
:name="props.prep.icon!"
:name="prep.icon!"
class="mr-2 align-middle md:size-6"
/>
{{ props.prep.title }}
{{ prep.title }}
</div>
</div>
<div class="flex items-center">
<div v-if="prep.components">
<template v-for="cwp in prep.components">
<component
:is="cwp.component"
class="mr-2"
v-bind="cwp.props"
/>
</template>
</div>
<div
v-if="props.refSearchNav"
v-if="refSearchNav"
class="text-lg text-gray-900"
>
<Input
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { ref, inject } from "vue"
// import Toggle from '~/components/pub/ui/toggle/Toggle.vue'
const props = defineProps<{
label: string,
providedKey?: string,
variant?: 'default' | 'outline' | null | undefined
}>()
const model = defineModel<boolean>()
const provideKey = props.providedKey || 'toggle-provide'
const { updateProvidedVal } = inject(provideKey)
watch(model, (newVal) => {
updateProvidedVal(newVal)
})
</script>
<template>
<Button
@click="() => model = !model"
:variant="model ? 'default' : 'outline'"
>
{{ label }}
</Button>
<!-- <Toggle
v-model="xval"
:variant="variant ?? 'default'"
:aria-label="label"
>
</Toggle> -->
<!-- {{ xval }} -->
</template>
+1 -1
View File
@@ -19,7 +19,7 @@ export const buttonVariants = cva(
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'md:h8 2xl:h-9 px-4 py-2',
default: 'md:h-8 2xl:h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
+3 -3
View File
@@ -4,16 +4,16 @@ import { cva } from 'class-variance-authority'
export { default as Toggle } from './Toggle.vue'
export const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
'inline-flex items-center justify-center rounded-md text-xs 2xl:text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
{
variants: {
variant: {
default: 'bg-transparent',
outline:
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
'border border-slate-300 bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-9 px-3',
default: 'md:h-8 2xl:h-9 px-3',
sm: 'h-8 px-2',
lg: 'h-10 px-3',
},
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/control-letter.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+17
View File
@@ -0,0 +1,17 @@
import { createCrudHandler, genCrudHandler } from '~/handlers/_handler'
import { create, update, remove } from '~/services/prescription.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({ create, update, remove})
+17
View File
@@ -0,0 +1,17 @@
import { createCrudHandler, genCrudHandler } from '~/handlers/_handler'
import { create, update, remove } from '~/services/mcu-order.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({ create, update, remove})

Some files were not shown because too many files have changed in this diff Show More