Squashed commit of the following:

commit 1a5cf194cd0d6eec85bbb8bf1c199df82aa79d4f
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Tue Dec 2 10:30:51 2025 +0700

    Squashed commit of the following:

    commit ec24dd8383e3a9c7cb7190b7ed9864ae7225805f
    Merge: 17cd42e 7319cbc
    Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
    Date:   Tue Dec 2 10:18:33 2025 +0700

        Merge branch 'dev' into feat/data-vaksin-192

    commit 17cd42ef03d1cda15a8f85831e747723fbb2c0a5
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Mon Dec 1 16:41:06 2025 +0700

        Feat: UI Data Vaksin

    commit 1ced91229792420daca732256d535d100570d5bc
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Mon Dec 1 14:10:27 2025 +0700

        Squashed commit of the following:

        commit 8e6a6b3fd1a8ed6c19099b52f5d7fc38f6a1a39a
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Fri Nov 28 14:32:47 2025 +0700

            Feat: UI PRB

        commit 4f2da6cd1e077598fb7f3cdede8d771e9b39b2d7
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Thu Nov 27 19:41:18 2025 +0700

            Squashed commit of the following:

            commit 4a465f3992
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Mon Nov 24 13:40:22 2025 +0700

                progress

            commit 7811f051a5
            Merge: f060ed3 8aac6c4
            Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
            Date:   Mon Nov 24 10:25:15 2025 +0700

                Merge branch 'dev' into feat/kfr-kemoterapi-174

            commit f060ed33d2
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Mon Nov 24 10:21:20 2025 +0700

                Feat: UI KFR

            commit 399c3cbaee
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Thu Nov 20 11:19:03 2025 +0700

                Squashed commit of the following:

                commit 72ce2260c50597f782f07d29db3985607ecc2f34
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 17 15:19:36 2025 +0700

                    Feat: add doc preview in Therpay protocol List

                commit 7032cd2409a660d40899ffd421137e4158cfde4a
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Wed Nov 12 15:34:30 2025 +0700

                    Fix: prepare API integration protokol terapi verification

                commit dbf6f78d00afc818baf2b34d128ee2153814cc88
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Wed Nov 12 14:09:28 2025 +0700

                    Feat: add basic CRUD therapy protocol

                commit 46a44e90fe4d4097b5460d2d4e5689b2a5389467
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 11 15:57:54 2025 +0700

                    Fix: Prepare protokol terapi API Integration

                commit 4674090566727cebd947e50e2c06c44e8c7afa7e
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 10 15:33:22 2025 +0700

                    Fix: hotfix style add protokol terapi

                commit 919c91abd8ef8b4cd193012eed7f5e8cf635cda2
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 10 15:17:14 2025 +0700

                    Fix: Typo drpodown-action-p in protokol-terapi

                commit e21d30eacf1a08118e289d4bb64889e708d5023a
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 10 15:14:33 2025 +0700

                    Fix: add diagnose & procedure dialog picker in add protokol terapi

                commit 9a3d73b72b0dceea778d83e7630c5ead110a6a4c
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 4 10:21:24 2025 +0700

                    Fix: Add Schema therapy protocol rehab medik

                commit 4d8d2d633bbbd78038b1cc607558c1ceb31c5986
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 4 09:30:55 2025 +0700

                    Fix: refactor Actions Btn ba-dr-su

                commit 5f290a6e4bd1559c0e5864a508c5ab650cfae6e8
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 3 17:08:20 2025 +0700

                    Feat: UI protokol terapi in Rehab medik

                commit 63010f69ff30532bb8ac91443112f31d4942b221
                Author: Khafid Prayoga <khafidp@pm.me>
                Date:   Tue Oct 21 09:54:13 2025 +0700

                    wip: list protokol terapi

                commit 44eedc298680a5255fee0ee8feee3e24beda93dd
                Author: Khafid Prayoga <khafidp@pm.me>
                Date:   Mon Oct 20 12:54:01 2025 +0700

                    feat(therapy-protocol): init form entry

                    feat(therapy-protocol): init page routes

                    wip: init entry form

                    wip: form entry protokol terapi

                    todo: table procedure, and diagnose  picker

                    wip: schema form new entry

                    todo: picker/modal yang relateds ke form entry

        commit b2a6cdee0b7beb775830c4dceb69ff12c01d3ca4
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Wed Nov 26 14:44:57 2025 +0700

            Squashed commit of the following:

            commit 39b778ab78
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Tue Nov 25 14:39:41 2025 +0700

                Feat: UI Laporan Operasi

            commit f6ae61849d
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Tue Nov 25 14:09:25 2025 +0700

                Squashed commit of the following:

                commit 8e3ea9e8d1d7e3b06bc6e53e0b97f62222276171
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Thu Nov 20 16:14:03 2025 +0700

                    Feat: UI control letter history

                commit f11f97f936447bdb225918abb43313f8db540d67
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Thu Nov 20 15:18:25 2025 +0700

                    Squashed commit of the following:

                    commit dab6adc4a9
                    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                    Date:   Tue Nov 18 11:19:48 2025 +0700

                        Fix: add role authorization in Resume

                    commit c28fc8f7aa
                    Merge: 7ed1cc8 bcfb4c1
                    Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
                    Date:   Tue Nov 18 09:02:16 2025 +0700

                        Merge branch 'dev' into feat/resume-81

                    commit 7ed1cc83bf
                    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                    Date:   Mon Nov 17 15:04:01 2025 +0700

                        Feat: add doc preview in Resume List

                    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 15ab43c1b1
                    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                    Date:   Mon Nov 17 10:38:21 2025 +0700

                        Feat: add verification capthca and form adjustment

                    commit 53bd8e7f6e
                    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                    Date:   Fri Nov 7 08:55:23 2025 +0700

                        Fix: refactor rehab medik - Resume UI

                    commit fc308809b8
                    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                    Date:   Wed Oct 29 14:57:19 2025 +0700

                        Feat: add UI Rehab Medik > Proses > Resume

                    commit 9b383a5437
                    Merge: a4dc7d7 831749a
                    Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
                    Date:   Wed Oct 29 13:32:47 2025 +0700

                        Merge pull request #139 from dikstub-rssa/dev

                        Update branch feat/resume-81

                commit 2b7bea70d66e8472220a2a2406889fc489cc1ebd
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 18 11:20:51 2025 +0700

                    Fix: Typo in Control Letter

                commit 808e91527cf95de2a47387bb792a3af2e16d907b
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 18 10:59:50 2025 +0700

                    Fix: add role authorization in Control Letter

            commit 1dd8e8e7b3
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Tue Nov 25 09:23:57 2025 +0700

                Squashed commit of the following:

                commit 72ce2260c50597f782f07d29db3985607ecc2f34
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 17 15:19:36 2025 +0700

                    Feat: add doc preview in Therpay protocol List

                commit 7032cd2409a660d40899ffd421137e4158cfde4a
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Wed Nov 12 15:34:30 2025 +0700

                    Fix: prepare API integration protokol terapi verification

                commit dbf6f78d00afc818baf2b34d128ee2153814cc88
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Wed Nov 12 14:09:28 2025 +0700

                    Feat: add basic CRUD therapy protocol

                commit 46a44e90fe4d4097b5460d2d4e5689b2a5389467
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 11 15:57:54 2025 +0700

                    Fix: Prepare protokol terapi API Integration

                commit 4674090566727cebd947e50e2c06c44e8c7afa7e
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 10 15:33:22 2025 +0700

                    Fix: hotfix style add protokol terapi

                commit 919c91abd8ef8b4cd193012eed7f5e8cf635cda2
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 10 15:17:14 2025 +0700

                    Fix: Typo drpodown-action-p in protokol-terapi

                commit e21d30eacf1a08118e289d4bb64889e708d5023a
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 10 15:14:33 2025 +0700

                    Fix: add diagnose & procedure dialog picker in add protokol terapi

                commit 9a3d73b72b0dceea778d83e7630c5ead110a6a4c
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 4 10:21:24 2025 +0700

                    Fix: Add Schema therapy protocol rehab medik

                commit 4d8d2d633bbbd78038b1cc607558c1ceb31c5986
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Tue Nov 4 09:30:55 2025 +0700

                    Fix: refactor Actions Btn ba-dr-su

                commit 5f290a6e4bd1559c0e5864a508c5ab650cfae6e8
                Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
                Date:   Mon Nov 3 17:08:20 2025 +0700

                    Feat: UI protokol terapi in Rehab medik

                commit 63010f69ff30532bb8ac91443112f31d4942b221
                Author: Khafid Prayoga <khafidp@pm.me>
                Date:   Tue Oct 21 09:54:13 2025 +0700

                    wip: list protokol terapi

                commit 44eedc298680a5255fee0ee8feee3e24beda93dd
                Author: Khafid Prayoga <khafidp@pm.me>
                Date:   Mon Oct 20 12:54:01 2025 +0700

                    feat(therapy-protocol): init form entry

                    feat(therapy-protocol): init page routes

                    wip: init entry form

                    wip: form entry protokol terapi

                    todo: table procedure, and diagnose  picker

                    wip: schema form new entry

                    todo: picker/modal yang relateds ke form entry

            commit 3e5c03148b
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Tue Nov 25 09:17:11 2025 +0700

                progress

commit a89c480474c025fb683383693e6a6808baa1d0d6
Merge: 8e6a6b3 7319cbc
Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
Date:   Tue Dec 2 10:27:07 2025 +0700

    Merge branch 'dev' into feat/prb-189

commit 8e6a6b3fd1a8ed6c19099b52f5d7fc38f6a1a39a
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Fri Nov 28 14:32:47 2025 +0700

    Feat: UI PRB

commit 4f2da6cd1e077598fb7f3cdede8d771e9b39b2d7
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Thu Nov 27 19:41:18 2025 +0700

    Squashed commit of the following:

    commit 4a465f3992
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Mon Nov 24 13:40:22 2025 +0700

        progress

    commit 7811f051a5
    Merge: f060ed3 8aac6c4
    Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
    Date:   Mon Nov 24 10:25:15 2025 +0700

        Merge branch 'dev' into feat/kfr-kemoterapi-174

    commit f060ed33d2
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Mon Nov 24 10:21:20 2025 +0700

        Feat: UI KFR

    commit 399c3cbaee
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Thu Nov 20 11:19:03 2025 +0700

        Squashed commit of the following:

        commit 72ce2260c50597f782f07d29db3985607ecc2f34
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 17 15:19:36 2025 +0700

            Feat: add doc preview in Therpay protocol List

        commit 7032cd2409a660d40899ffd421137e4158cfde4a
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Wed Nov 12 15:34:30 2025 +0700

            Fix: prepare API integration protokol terapi verification

        commit dbf6f78d00afc818baf2b34d128ee2153814cc88
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Wed Nov 12 14:09:28 2025 +0700

            Feat: add basic CRUD therapy protocol

        commit 46a44e90fe4d4097b5460d2d4e5689b2a5389467
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 11 15:57:54 2025 +0700

            Fix: Prepare protokol terapi API Integration

        commit 4674090566727cebd947e50e2c06c44e8c7afa7e
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 10 15:33:22 2025 +0700

            Fix: hotfix style add protokol terapi

        commit 919c91abd8ef8b4cd193012eed7f5e8cf635cda2
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 10 15:17:14 2025 +0700

            Fix: Typo drpodown-action-p in protokol-terapi

        commit e21d30eacf1a08118e289d4bb64889e708d5023a
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 10 15:14:33 2025 +0700

            Fix: add diagnose & procedure dialog picker in add protokol terapi

        commit 9a3d73b72b0dceea778d83e7630c5ead110a6a4c
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 4 10:21:24 2025 +0700

            Fix: Add Schema therapy protocol rehab medik

        commit 4d8d2d633bbbd78038b1cc607558c1ceb31c5986
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 4 09:30:55 2025 +0700

            Fix: refactor Actions Btn ba-dr-su

        commit 5f290a6e4bd1559c0e5864a508c5ab650cfae6e8
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 3 17:08:20 2025 +0700

            Feat: UI protokol terapi in Rehab medik

        commit 63010f69ff30532bb8ac91443112f31d4942b221
        Author: Khafid Prayoga <khafidp@pm.me>
        Date:   Tue Oct 21 09:54:13 2025 +0700

            wip: list protokol terapi

        commit 44eedc298680a5255fee0ee8feee3e24beda93dd
        Author: Khafid Prayoga <khafidp@pm.me>
        Date:   Mon Oct 20 12:54:01 2025 +0700

            feat(therapy-protocol): init form entry

            feat(therapy-protocol): init page routes

            wip: init entry form

            wip: form entry protokol terapi

            todo: table procedure, and diagnose  picker

            wip: schema form new entry

            todo: picker/modal yang relateds ke form entry

commit b2a6cdee0b7beb775830c4dceb69ff12c01d3ca4
Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
Date:   Wed Nov 26 14:44:57 2025 +0700

    Squashed commit of the following:

    commit 39b778ab78
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Tue Nov 25 14:39:41 2025 +0700

        Feat: UI Laporan Operasi

    commit f6ae61849d
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Tue Nov 25 14:09:25 2025 +0700

        Squashed commit of the following:

        commit 8e3ea9e8d1d7e3b06bc6e53e0b97f62222276171
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Thu Nov 20 16:14:03 2025 +0700

            Feat: UI control letter history

        commit f11f97f936447bdb225918abb43313f8db540d67
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Thu Nov 20 15:18:25 2025 +0700

            Squashed commit of the following:

            commit dab6adc4a9
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Tue Nov 18 11:19:48 2025 +0700

                Fix: add role authorization in Resume

            commit c28fc8f7aa
            Merge: 7ed1cc8 bcfb4c1
            Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
            Date:   Tue Nov 18 09:02:16 2025 +0700

                Merge branch 'dev' into feat/resume-81

            commit 7ed1cc83bf
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Mon Nov 17 15:04:01 2025 +0700

                Feat: add doc preview in Resume List

            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 15ab43c1b1
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Mon Nov 17 10:38:21 2025 +0700

                Feat: add verification capthca and form adjustment

            commit 53bd8e7f6e
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Fri Nov 7 08:55:23 2025 +0700

                Fix: refactor rehab medik - Resume UI

            commit fc308809b8
            Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
            Date:   Wed Oct 29 14:57:19 2025 +0700

                Feat: add UI Rehab Medik > Proses > Resume

            commit 9b383a5437
            Merge: a4dc7d7 831749a
            Author: Muhammad Hasyim Chaidir Ali <68959522+Hasyim-Kai@users.noreply.github.com>
            Date:   Wed Oct 29 13:32:47 2025 +0700

                Merge pull request #139 from dikstub-rssa/dev

                Update branch feat/resume-81

        commit 2b7bea70d66e8472220a2a2406889fc489cc1ebd
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 18 11:20:51 2025 +0700

            Fix: Typo in Control Letter

        commit 808e91527cf95de2a47387bb792a3af2e16d907b
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 18 10:59:50 2025 +0700

            Fix: add role authorization in Control Letter

    commit 1dd8e8e7b3
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Tue Nov 25 09:23:57 2025 +0700

        Squashed commit of the following:

        commit 72ce2260c50597f782f07d29db3985607ecc2f34
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 17 15:19:36 2025 +0700

            Feat: add doc preview in Therpay protocol List

        commit 7032cd2409a660d40899ffd421137e4158cfde4a
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Wed Nov 12 15:34:30 2025 +0700

            Fix: prepare API integration protokol terapi verification

        commit dbf6f78d00afc818baf2b34d128ee2153814cc88
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Wed Nov 12 14:09:28 2025 +0700

            Feat: add basic CRUD therapy protocol

        commit 46a44e90fe4d4097b5460d2d4e5689b2a5389467
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 11 15:57:54 2025 +0700

            Fix: Prepare protokol terapi API Integration

        commit 4674090566727cebd947e50e2c06c44e8c7afa7e
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 10 15:33:22 2025 +0700

            Fix: hotfix style add protokol terapi

        commit 919c91abd8ef8b4cd193012eed7f5e8cf635cda2
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 10 15:17:14 2025 +0700

            Fix: Typo drpodown-action-p in protokol-terapi

        commit e21d30eacf1a08118e289d4bb64889e708d5023a
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 10 15:14:33 2025 +0700

            Fix: add diagnose & procedure dialog picker in add protokol terapi

        commit 9a3d73b72b0dceea778d83e7630c5ead110a6a4c
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 4 10:21:24 2025 +0700

            Fix: Add Schema therapy protocol rehab medik

        commit 4d8d2d633bbbd78038b1cc607558c1ceb31c5986
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Tue Nov 4 09:30:55 2025 +0700

            Fix: refactor Actions Btn ba-dr-su

        commit 5f290a6e4bd1559c0e5864a508c5ab650cfae6e8
        Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
        Date:   Mon Nov 3 17:08:20 2025 +0700

            Feat: UI protokol terapi in Rehab medik

        commit 63010f69ff30532bb8ac91443112f31d4942b221
        Author: Khafid Prayoga <khafidp@pm.me>
        Date:   Tue Oct 21 09:54:13 2025 +0700

            wip: list protokol terapi

        commit 44eedc298680a5255fee0ee8feee3e24beda93dd
        Author: Khafid Prayoga <khafidp@pm.me>
        Date:   Mon Oct 20 12:54:01 2025 +0700

            feat(therapy-protocol): init form entry

            feat(therapy-protocol): init page routes

            wip: init entry form

            wip: form entry protokol terapi

            todo: table procedure, and diagnose  picker

            wip: schema form new entry

            todo: picker/modal yang relateds ke form entry

    commit 3e5c03148b
    Author: hasyim_kai <muhammad.hasyim.c.a@gmail.com>
    Date:   Tue Nov 25 09:17:11 2025 +0700

        progress
This commit is contained in:
Hasyim Kai
2025-12-04 09:46:11 +07:00
parent b80ee5a55e
commit 44863c200a
62 changed files with 3918 additions and 2 deletions
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ActionEvents, type ListItemDto } from '~/components/pub/my-ui/data/types';
import Button from '~/components/pub/ui/button/Button.vue';
const props = defineProps<{
}>()
const isModalOpen = inject<Ref<boolean>>('isHistoryDialogOpen')!
function openDialog() {
isModalOpen.value = true
}
</script>
<template>
<Button type="button" variant="outline" class="text-orange-500 border border-orange-400 bg-orange-50"
@click="openDialog">
<Icon name="i-lucide-history" class="h-4 w-4 align-middle transition-colors" />
History
</Button>
</template>
+73
View File
@@ -0,0 +1,73 @@
<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 { cn } from '~/lib/utils'
interface InstallationFormData {
name: string
code: string
encounterClassCode: string
}
const props = defineProps<{
schema: any
initialValues?: Partial<InstallationFormData>
}>()
const emit = defineEmits<{
submit: [values: InstallationFormData, resetForm: () => void]
reset: [resetForm: () => void]
}>()
// Form submission handler
function onSubmitForm(values: any, { resetForm }: { resetForm: () => void }) {
emit('submit', values, resetForm)
}
// Form cancel handler
function onResetForm({ resetForm }: { resetForm: () => void }) {
emit('reset', resetForm)
}
</script>
<template>
<Form
v-slot="{ handleSubmit, resetForm }"
as=""
keep-values
: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">
<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"
/>
</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,119 @@
<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
isDisabled?: boolean
isWithTime?: 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 && !isDisabled"
>
{{ 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="props.isWithTime ? 'datetime-local' : 'date'"
min="1900-01-01"
v-bind="componentField"
:placeholder="placeholder"
:disabled="isDisabled"
@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,86 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn } from '~/lib/utils'
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() {
doctors.value = await getDoctorLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
includes: 'employee-person',
}, true)
}
onMounted(() => {
fetchDpjp()
})
// function handleDpjpChange(selected: string) {
// selectedDpjpId.value = selected ?? null
// }
</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"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,86 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn } from '~/lib/utils'
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() {
doctors.value = await getDoctorLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
includes: 'employee-person',
}, true)
}
onMounted(() => {
fetchDpjp()
})
// function handleDpjpChange(selected: string) {
// selectedDpjpId.value = selected ?? null
// }
</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"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,86 @@
<script setup lang="ts">
import type { FormErrors } from '~/types/error'
import Combobox from '~/components/pub/my-ui/combobox/combobox.vue'
import { cn } from '~/lib/utils'
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() {
doctors.value = await getDoctorLabelList({
serviceType: 1,
serviceDate: new Date().toISOString().substring(0, 10),
includes: 'employee-person',
}, true)
}
onMounted(() => {
fetchDpjp()
})
// function handleDpjpChange(selected: string) {
// selectedDpjpId.value = selected ?? null
// }
</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"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,77 @@
<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 { PrbProgramTypeOptList } 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
onMounted(() => {
})
// function handleDpjpChange(selected: string) {
// selectedDpjpId.value = selected ?? null
// }
</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="PrbProgramTypeOptList"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
+60
View File
@@ -0,0 +1,60 @@
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-upd.vue'))
export const config: Config = {
cols: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {width: 50}, ],
headers: [
[
{ label: 'No. PRB' },
{ label: 'No. RM' },
{ label: 'Nama Pasien' },
{ label: 'Jenis Kelamin' },
{ label: 'Alamat' },
{ label: 'Klinik' },
{ label: 'Nama Dokter' },
{ label: 'Tanggal' },
{ label: 'Program PRB' },
{ label: 'No. SEP' },
{ label: 'Action' },
],
],
keys: ['date', 'name1', 'name2', 'name3', 'name4', 'name5', 'name6', 'name7', 'name8', 'name9', '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
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {
},
}
+62
View File
@@ -0,0 +1,62 @@
<script setup lang="ts">
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
import { type Variants, Badge } from '~/components/pub/ui/badge'
import { cn, } from '~/lib/utils'
import type { Prb } from '~/models/prb'
import * as DE from '~/components/pub/my-ui/doc-entry'
// #region Props & Emits
const props = defineProps<{
instance: Prb | null
}>()
// const emit = defineEmits<{
// (e: 'click'): void
// }>()
const dummy = [
{
id: 1,
number: 1,
name: 'Operasi',
code: 'OP-001'
}
]
// #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() {
// emit('click')
// }
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<article :class="cn('mb-5 space-y-1',)">
<DetailRow label="No. PRB">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Waktu dan Tanggal">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Program PRB">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="DPJP">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Keterangan">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Saran">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
</article>
</template>
<style scoped></style>
+102
View File
@@ -0,0 +1,102 @@
<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 ObatPicker from './obat-picker/picker-dialog.vue'
import SepPicker from './sep-picker/picker-dialog.vue'
import SelectDpjp from './_common/select-dpjp.vue'
import SelectProgram from './_common/select-program.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
const props = withDefaults(defineProps<{
schema: any
initialValues?: any
errors?: FormErrors
isBpjs?: boolean
}>(), {
isBpjs: false,
})
const formSchema = toTypedSchema(props.schema)
const formRef = ref()
const isSepDialogOpen = ref(false)
provide("isSepDialogOpen", isSepDialogOpen);
const handleToggleSepDialog = () => isSepDialogOpen.value = !isSepDialogOpen.value
const setNoSep = (inputValue: string) => formRef.value?.setValues({ a0: inputValue }, false)
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">
<DE.Cell :col-span="2" v-if="isBpjs">
<div class="w-1/2 flex items-end gap-3">
<InputBase :errors="errors"
field-name="a0"
label="No. RM/Kartu BPJS" placeholder="Masukkan No. RM/Kartu BPJS"
/>
<Button @click="handleToggleSepDialog" size="icon" variant="outline"
class="text-orange-400 border-orange-400 bg-transparent">
<Icon name="i-lucide-search" class="h-4 w-4 align-middle transition-colors" />
</Button>
</div>
</DE.Cell>
<InputBase :errors="errors"
field-name="a1"
label="No. PRB" placeholder="Akan terisi otomatis"
is-disabled
/>
<SelectDate
field-name="a2"
label="Waktu & Tanggal"
:errors="errors"
is-disabled
is-with-time
/>
<SelectProgram :errors="errors"
field-name="a3"
label="Program PRB" placeholder="Pilih Program PRB"
is-required
/>
<SelectDpjp :errors="errors"
field-name="a4"
label="DPJP" placeholder="Pilih DPJP"
is-required
/>
<TextAreaInput :errors="errors"
field-name="a5"
label="Keterangan" placeholder="Isi Keterangan"
/>
<TextAreaInput :errors="errors"
field-name="a6"
label="Saran" placeholder="Isi Saran"
/>
</DE.Block>
<!-- PICKER -->
<DE.Block :col-count="1" :cell-flex="false">
<ObatPicker field-name="a7" title="Obat" />
</DE.Block>
<SepPicker title="SEP" :choose-fn="setNoSep" />
<!-- -->
</Form>
</template>
@@ -0,0 +1,62 @@
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-print.vue'))
const statusBadge = defineAsyncComponent(() => import('~/components/pub/my-ui/badge/status-badge.vue'))
export const config: Config = {
cols: [{}, {}, {}, {}, {}, {}, {width: 100}, {width: 50}, ],
headers: [
[
{ label: 'Tgl Diterbitkan' },
{ label: 'Kode Obat' },
{ label: 'Nama Obat' },
{ label: 'Signa' },
{ label: 'Jumlah' },
{ label: 'Durasi Pemberian' },
{ label: 'status' },
{ label: 'Action' },
],
],
keys: ['date', 'name1', 'name2', 'name3', 'name4', 'name5', '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
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
status(rec, idx) {
return {
idx,
rec: rec as object,
component: statusBadge,
}
},
},
htmls: {
},
}
+35
View File
@@ -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 { config } from './history-list.cfg'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import type { DateRange } from 'radix-vue'
import { cn } from '~/lib/utils'
interface Props {
data: any[]
paginationMeta: PaginationMeta
}
const props = defineProps<Props>()
const emit = defineEmits<{
pageChange: [page: number]
'update:dateValue': [value: DateRange]
}>()
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>
+56
View File
@@ -0,0 +1,56 @@
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-upd.vue'))
export const config: Config = {
cols: [{}, {}, {}, {}, {}, {}, {width: 50}, ],
headers: [
[
{ label: 'Tgl Diterbitkan' },
{ label: 'Kode Obat' },
{ label: 'Nama Obat' },
{ label: 'Signa' },
{ label: 'Jumlah' },
{ label: 'Durasi Pemberian' },
{ label: 'Action' },
],
],
keys: ['date', 'name1', 'name2', 'name3', 'name4', 'name5', '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
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {
},
}
+34
View File
@@ -0,0 +1,34 @@
<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'
import { config as bpjsConfig } from './bpjs-list.cfg'
const props = withDefaults(defineProps<{
data: any[]
isBpjs?: boolean
}>(), {
isBpjs: false,
})
const tableConfig = computed(() => {
return props.isBpjs ? bpjsConfig : config
})
// const emit = defineEmits<{
// pageChange: [page: number]
// }>()
// function handlePageChange(page: number) {
// emit('pageChange', page)
// }
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="tableConfig"
:rows="data"
/>
<!-- <PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" /> -->
</div>
</template>
+114
View File
@@ -0,0 +1,114 @@
<script setup lang="ts">
// Components
import type { FormErrors } from '~/types/error'
import { Form } from '~/components/pub/ui/form'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
// Helpers
// Types
import { toTypedSchema } from '@vee-validate/zod'
// Handlers
import {
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/procedure-src.handler'
// Services
import { getList, getDetail } from '~/services/procedure-src.service'
import SelectMedicine from '../_common/select-medicine.vue'
import SelectMedicineForm from '../_common/select-medicine-form.vue'
interface Props {
schema: any
initialValues?: any
errors?: FormErrors
medicineData: any[]
medicineFormsData: any[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
toggleDialog: []
}>()
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="6" :cell-flex="false">
<DE.Cell :col-span="6">
<SelectMedicine :errors="errors"
field-name="a1"
label="Obat" placeholder="Pilih Obat"
/>
</DE.Cell>
<DE.Cell :col-span="4" class="mt-1.5">
<h1 class="mb-1">Signa</h1>
<div class="flex items-start gap-3">
<InputBase :errors="errors"
field-name="a2"
label="" placeholder="0"
right-label="Frekuensi"
numeric-only
/>
<p class="text-3xl text-gray-400">X</p>
<InputBase :errors="errors"
field-name="a3"
label="" placeholder="0"
right-label="Dosis"
numeric-only
/>
</div>
</DE.Cell>
<DE.Cell :col-span="2">
<SelectMedicineForm :errors="errors"
field-name="a4"
label="Satuan" placeholder="Pilih Satuan"
/>
</DE.Cell>
<DE.Cell :col-span="3">
<InputBase :errors="errors"
field-name="a5"
label="Jumlah" placeholder="Masukkan Jumlah"
/>
</DE.Cell>
<DE.Cell :col-span="3">
<InputBase :errors="errors"
field-name="a6"
label="Durasi Pemberian" placeholder="Masukkan Durasi Pemberian"
right-label="Hari"
numeric-only
/>
</DE.Cell>
</DE.Block>
</Form>
</template>
@@ -0,0 +1,140 @@
<script setup lang="ts">
import ProcedureListDialog from './form.vue'
import type { FormErrors } from '~/types/error'
import { toTypedSchema } from '@vee-validate/zod'
import { FieldArray } from 'vee-validate'
import InputBase from '~/components/pub/my-ui/form/input-base.vue'
import TextAreaInput from '~/components/pub/my-ui/form/text-area-input.vue'
import { cn } from '~/lib/utils'
import TableHeader from '~/components/pub/ui/table/TableHeader.vue'
import { is } from 'date-fns/locale'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import List from './form.vue'
import { getValueLabelList as getMedicineFormList } from '~/services/medicine-form.service'
import { getValueLabelList as getMedicineList } from '~/services/medicine.service'
import Action from '~/components/pub/my-ui/nav-footer/ba-dr-su.vue'
import * as DE from '~/components/pub/my-ui/doc-entry'
import FormEntry from './form.vue'
import { PrbSchema } from '~/schemas/prb.schema'
import type { ExposedForm } from '~/types/form'
import type { Prb } from '~/models/prb'
import { PrbMedicineSchema } from '~/schemas/prb-medicine.schema'
interface Props {
fieldName: string
title: string
}
const props = defineProps<Props>()
const isOperativeActionDialogOpen = ref(false)
provide("isOperativeActionDialogOpen", isOperativeActionDialogOpen);
const inputForm = ref<ExposedForm<any> | null>(null)
const medicine = ref<{ value: string; label: string }[]>([])
const medicineForms = ref<{ value: string; label: string }[]>([])
const handleToggleOperativeActionDialog = () => {
isOperativeActionDialogOpen.value = !isOperativeActionDialogOpen.value
}
async function init(){
const [medicineRes, medicineFormRes] = await Promise.all([
getMedicineList({ sort: 'createdAt:asc', }),
getMedicineFormList({ sort: 'createdAt:asc', })
])
medicine.value = medicineRes
medicineForms.value = medicineFormRes
}
onMounted(()=>{
init()
})
async function handleAdd(psuhFn: (input: unknown) => void) {
const validated = await composeFormData()
psuhFn({
code: validated.a1,
name: validated.a2,
signa: validated.a3,
total: validated.a5,
duration: validated.a6,
})
handleToggleOperativeActionDialog()
}
async function composeFormData(): Promise<any> {
const [input,] = await Promise.all([
inputForm.value?.validate(),
])
const results = [input]
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
if (!allValid) return Promise.reject('Form validation failed')
const formData = input?.values
return new Promise((resolve) => resolve(formData))
}
</script>
<template>
<div class="">
<div class="mb-2 flex items-center gap-3">
<h1 class="font-medium text-base">{{ title }}</h1>
<Button @click="isOperativeActionDialogOpen = true" size="xs" variant="outline"
class="rounded-lg text-orange-400 border-orange-400 bg-transparent">
<Icon name="i-lucide-search" class="h-4 w-4 align-middle transition-colors" />
Pilih {{ title }}
</Button>
</div>
<FieldArray v-slot="{ fields, push, remove }" :name="props.fieldName">
<Dialog
v-model:open="isOperativeActionDialogOpen"
title="" size="xl">
<FormEntry
ref="inputForm"
:schema="PrbMedicineSchema"
:medicine-data="medicine"
:medicine-forms-data="medicineForms"
@toggle-dialog="handleToggleOperativeActionDialog"
/>
<div class="mt-2 flex justify-end">
<Button @click="handleAdd(push)" class="">
<Icon name="i-lucide-save" class="h-4 w-4 align-middle transition-colors" />
Simpan
</Button>
</div>
</Dialog>
<div class="border border-gray-200 rounded-lg overflow-hidden">
<Table>
<TableHeader class="bg-gray-100">
<TableRow>
<TableHead class="">Code</TableHead>
<TableHead class="">Name</TableHead>
<TableHead class="">Signa</TableHead>
<TableHead class="">Jumlah</TableHead>
<TableHead class="">Durasi Pemberian</TableHead>
<TableHead class="w-24">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(field, idx) in fields" :key="idx">
<TableCell class="2xl:pl-4">{{ field.value?.code }}</TableCell>
<TableCell class="2xl:pl-4">{{ field.value?.name }}</TableCell>
<TableCell class="2xl:pl-4">{{ field.value?.signa }}</TableCell>
<TableCell class="2xl:pl-4">{{ field.value?.total }}</TableCell>
<TableCell class="2xl:pl-4">{{ field.value?.duration }}</TableCell>
<TableCell class="2xl:pl-4">
<Button type="button" variant="destructive" size="sm" @click="remove(idx)">
<Icon name="i-lucide-trash-2" class="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</FieldArray>
</div>
</template>
@@ -0,0 +1,83 @@
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-choose.vue'))
const statusBadge = defineAsyncComponent(() => import('~/components/pub/my-ui/badge/status-badge.vue'))
export const config: Config = {
cols: [
{ width: 120 }, // TGL. SEP
{ width: 150 }, // NO. SEP
{ width: 120 }, // PELAYANAN
{ width: 100 }, // JALUR
{ width: 150 }, // NO. RM
{ width: 200 }, // NAMA PASIEN
{ width: 150 }, // NO. KARTU BPJS
{ width: 150 }, // NO. SURAT KONTROL
{ width: 150 }, // TGL SURAT KONTROL
{ width: 150 }, // KLINIK TUJUAN
{ width: 200 }, // DPJP
{ width: 200 }, // DIAGNOSIS AWAL
{ width: 100 }, // AKSI
],
headers: [
[
{ label: 'TGL. SEP' },
{ label: 'NO. SEP' },
{ label: 'PELAYANAN' },
{ label: 'JALUR' },
{ label: 'NO. RM' },
{ label: 'NAMA PASIEN' },
{ label: 'NO. KARTU BPJS' },
{ label: 'NO. SURAT KONTROL' },
{ label: 'TGL SURAT KONTROL' },
{ label: 'KLINIK TUJUAN' },
{ label: 'DPJP' },
{ label: 'DIAGNOSIS AWAL' },
{ label: 'AKSI' },
],
],
keys: [
'letterDate',
'letterNumber',
'serviceType',
'flow',
'medicalRecordNumber',
'patientName',
'cardNumber',
'controlLetterNumber',
'controlLetterDate',
'clinicDestination',
'attendingDoctor',
'diagnosis',
'action',
],
delKeyNames: [
{ key: 'letterNumber', label: 'NO. SEP' },
{ key: 'patientName', label: 'Nama Pasien' },
],
parses: {},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
status(rec, idx) {
return {
idx,
rec: rec as object,
component: statusBadge,
}
},
},
htmls: {},
}
@@ -0,0 +1,53 @@
<script setup lang="ts">
// Components
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import RecordConfirmation from '~/components/pub/my-ui/confirmation/record-confirmation.vue'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import PaginationView from '~/components/pub/my-ui/pagination/pagination-view.vue'
// Helpers
import { usePaginatedList } from '~/composables/usePaginatedList'
import { toast } from '~/components/pub/ui/toast'
import { config } from './list.cfg'
// Types
import { ActionEvents, type HeaderPrep } from '~/components/pub/my-ui/data/types'
import { ProcedureSrcSchema, type ProcedureSrcFormData } from '~/schemas/procedure-src.schema'
// Handlers
import {
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} from '~/handlers/procedure-src.handler'
// Services
import { getList, getDetail } from '~/services/procedure-src.service'
const title = ref('')
interface Props {
data: any[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
toggleDialog: []
}>()
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="config"
:rows="data"
/>
</div>
</template>
@@ -0,0 +1,141 @@
<script setup lang="ts">
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import List from './list.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { ActionEvents, type HeaderPrep, type RefSearchNav } from '~/components/pub/my-ui/data/types'
import { getList } from '~/services/vclaim-monitoring-visit.service'
import type { VclaimSepData } from '~/models/vclaim'
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
import { defaultQuerySchema, defaultQueryParams } from '~/composables/usePaginatedList'
interface Props {
title: string
chooseFn: (inputValue: string) => void
}
const props = defineProps<Props>()
const isSepDialogOpen = inject('isSepDialogOpen') as Ref<boolean>;
const handleToggleSepDialog = () => isSepDialogOpen.value = !isSepDialogOpen.value
const data = ref<VclaimSepData[]>([])// Search model with debounce
const searchInput = ref('')
const debouncedSearch = refDebounced(searchInput, 500) // 500ms debounce
// const paginationMeta = reactive<PaginationMeta>({
// recordCount: 0,
// page: 1,
// pageSize: 10,
// totalPage: 5,
// hasNext: false,
// hasPrev: false,
// })
// // URL state management
// const queryParams = useUrlSearchParams('history', {
// initialValue: defaultQueryParams,
// removeFalsyValues: true,
// })
// const params = computed(() => {
// const result = defaultQuerySchema.safeParse(queryParams)
// return result.data || defaultQueryParams
// })
const recId = ref<string>(``)
const recAction = ref<string>(``)
const recItem = ref<any>({})
const timestamp = ref<any>({})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('timestamp', timestamp)
const headerPrep: HeaderPrep = {
title: props.title,
icon: 'i-lucide-clipboard-list',
}
const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (val: string) => {
searchInput.value = val
},
onClear: () => {
searchInput.value = ''
},
}
async function getMonitoringVisitMappers() {
data.value = []
const tempArr: VclaimSepData[] = []
const result = await getList({
serviceType: 1,
search: debouncedSearch.value,
})
if (result && result.success && result.body) {
const visitsRaw = result.body?.response?.sep || []
visitsRaw.forEach((result: any) => {
// Format pelayanan: "R.Inap" -> "Rawat Inap", "1" -> "Rawat Jalan", dll
let serviceType = result.jnsPelayanan || '-'
if (serviceType === 'R.Inap') {
serviceType = 'Rawat Inap'
} else if (serviceType === '1' || serviceType === 'R.Jalan') {
serviceType = 'Rawat Jalan'
}
tempArr.push({
letterDate: result.tglSep || '-',
letterNumber: result.noSep || '-',
serviceType: serviceType,
flow: '-',
medicalRecordNumber: '-',
patientName: result.nama || '-',
cardNumber: result.noKartu || '-',
controlLetterNumber: result.noRujukan || '-',
controlLetterDate: result.tglPlgSep || '-',
clinicDestination: result.poli || '-',
attendingDoctor: '-',
diagnosis: result.diagnosa || '-',
careClass: result.kelasRawat || '-',
})
})
data.value = tempArr
}
}
// Handle pagination page change
// function handlePageChange(page: number) {
// // Update URL params - this will trigger watcher
// queryParams['page-number'] = page
// }
onMounted(async () => {
await getMonitoringVisitMappers()
})
watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showProcess:
props.chooseFn(recItem.value.cardNumber)
handleToggleSepDialog()
break
}
})
watch(debouncedSearch, () => {
getMonitoringVisitMappers()
})
</script>
<template>
<div class="">
<Dialog v-model:open="isSepDialogOpen" title="" size="full">
<Header
v-model="searchInput"
:prep="headerPrep"
:ref-search-nav="refSearchNav" />
<List :data="data" />
</Dialog>
</div>
</template>
@@ -0,0 +1,119 @@
<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
isDisabled?: boolean
isWithTime?: 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 && !isDisabled"
>
{{ 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="props.isWithTime ? 'datetime-local' : 'date'"
min="1900-01-01"
v-bind="componentField"
:placeholder="placeholder"
:disabled="isDisabled"
@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,77 @@
<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 { PrbProgramTypeOptList } 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
onMounted(() => {
})
// function handleDpjpChange(selected: string) {
// selectedDpjpId.value = selected ?? null
// }
</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="PrbProgramTypeOptList"
:placeholder="placeholder"
search-placeholder="Cari..."
empty-message="Data tidak ditemukan"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
</DE.Cell>
</template>
@@ -0,0 +1,68 @@
<script setup lang="ts">
import DetailRow from '~/components/pub/my-ui/form/view/detail-row.vue'
import { type Variants, Badge } from '~/components/pub/ui/badge'
import { cn, } from '~/lib/utils'
import * as DE from '~/components/pub/my-ui/doc-entry'
import type { VaccineData } from '~/models/vaccine-data';
// #region Props & Emits
const props = defineProps<{
instance: VaccineData | null
}>()
const emit = defineEmits<{
(e: 'click'): void
}>()
const dummy = [
{
id: 1,
number: 1,
name: 'Operasi',
code: 'OP-001'
}
]
// #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() {
emit('click')
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<article :class="cn('mb-5 space-y-1',)">
<DetailRow label="Jenis Vaksin">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Tanggal Pemberian Vaksin">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Tanggal Kedaluwarsa Vaksin">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Nomor Batch Vaksin">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Dosis Vaksin">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Dosis Ke">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Lokasi Injeksi">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
<DetailRow label="Tanggal Terlaksana">{{ props.instance?.date ? props.instance?.date : '-' }}</DetailRow>
</article>
<div class="border-t-1 py-2 flex justify-end border-t-slate-300">
<PubMyUiNavFooterBa @click="onClick" />
</div>
</template>
<style scoped></style>
+82
View File
@@ -0,0 +1,82 @@
<script setup lang="ts">
import * as DE from '~/components/pub/my-ui/doc-entry'
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 SelectVaccineType from './_common/select-vaccine-type.vue'
const props = withDefaults(defineProps<{
schema: any
initialValues?: any
errors?: FormErrors
}>(), {
})
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="3" :cell-flex="false">
<SelectVaccineType :errors="errors"
field-name="a1"
label="Jenis Vaksin" placeholder="Pilih Jenis Vaksin"
is-required
/>
<SelectDate
field-name="a2"
label="Tanggal Pemberian Vaksin"
:errors="errors"
is-with-time
/>
<SelectDate
field-name="a3"
label="Tanggal Kedaluwarsa Vaksin"
:errors="errors"
is-with-time
/>
</DE.Block>
<DE.Block :col-count="2" :cell-flex="false">
<InputBase :errors="errors"
field-name="a4"
label="Nomor Batch Vaksin" placeholder="Isi Nomor Batch Vaksin"
/>
<InputBase :errors="errors"
field-name="a5"
label="Dosis Vaksin" placeholder="Isi Dosis Vaksin"
right-label="ml"
numeric-only
/>
<InputBase :errors="errors"
field-name="a6"
label="Dosis Ke" placeholder="Isi Dosis Ke"
numeric-only
/>
<InputBase :errors="errors"
field-name="a7"
label="Lokasi Injeksi" placeholder="Isi Lokasi Injeksi"
/>
</DE.Block>
</Form>
</template>
@@ -0,0 +1,53 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import actionDoctor from '~/components/pub/my-ui/data/dropdown-action-dd.vue'
import actionNursePhysio from '~/components/pub/my-ui/data/dropdown-action-detail.vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-dd.vue'))
export const config: Config = {
cols: [{}, {}, {}, {}, {width: 50}, ],
headers: [
[
{ label: 'Jenis Vaksin' },
{ label: 'Tanggal Pemberian Vaksin' },
{ label: 'Tanggal Kedaluwarsa Vaksin' },
{ label: 'Dosis Ke' },
{ label: 'Action' },
],
],
keys: ['name1', 'date', 'date', 'name2', '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
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {
},
}
@@ -0,0 +1,53 @@
import type { Config } from '~/components/pub/my-ui/data-table'
import { defineAsyncComponent } from 'vue'
import actionDoctor from '~/components/pub/my-ui/data/dropdown-action-dd.vue'
import actionNursePhysio from '~/components/pub/my-ui/data/dropdown-action-detail.vue'
const action = defineAsyncComponent(() => import('~/components/pub/my-ui/data/dropdown-action-detail.vue'))
export const config: Config = {
cols: [{}, {}, {}, {}, {width: 50}, ],
headers: [
[
{ label: 'Jenis Vaksin' },
{ label: 'Tanggal Pemberian Vaksin' },
{ label: 'Tanggal Kedaluwarsa Vaksin' },
{ label: 'Dosis Ke' },
{ label: 'Action' },
],
],
keys: ['name1', 'date', 'date', 'name2', '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
},
},
components: {
action(rec, idx) {
return {
idx,
rec: rec as object,
component: action,
}
},
},
htmls: {
},
}
+41
View File
@@ -0,0 +1,41 @@
<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 as configDoctor } from './list-doctor.cfg'
import { config as configNursePhysio } from './list-nurse-physio.cfg'
const props = withDefaults(defineProps<{
data: any[]
paginationMeta: PaginationMeta
}>(), {
})
const { user, } = useUserStore()
const tableConfig = computed(() => {
if(user.activeRole === 'emp|doc' || user.activeRole === 'system') {
return configDoctor
} else {
return configNursePhysio
}
})
const emit = defineEmits<{
pageChange: [page: number]
}>()
function handlePageChange(page: number) {
emit('pageChange', page)
}
</script>
<template>
<div class="space-y-4">
<PubMyUiDataTable
v-bind="tableConfig"
:rows="data"
:skeleton-size="paginationMeta?.pageSize"
/>
<PaginationView :pagination-meta="paginationMeta" @page-change="handlePageChange" />
</div>
</template>
@@ -26,6 +26,7 @@ import DocUploadList from '~/components/content/document-upload/list.vue'
import GeneralConsentList from '~/components/content/general-consent/entry.vue' import GeneralConsentList from '~/components/content/general-consent/entry.vue'
import ResumeList from '~/components/content/resume/list.vue' import ResumeList from '~/components/content/resume/list.vue'
import ControlLetterList from '~/components/content/control-letter/list.vue' import ControlLetterList from '~/components/content/control-letter/list.vue'
import VaccineDataList from '~/components/content/vaccine-data/list.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -96,6 +97,7 @@ const tabs: TabItem[] = [
component: DocUploadList, component: DocUploadList,
props: { encounter: data }, props: { encounter: data },
}, },
{ value: 'vaccine-data', label: 'Data Vaksin', component: VaccineDataList, props: { encounter: data } },
] ]
</script> </script>
+1 -1
View File
@@ -139,4 +139,4 @@ async function getData() {
</div> </div>
<SubMenu :data="menus" :initial-active-menu="activeMenu" @change-menu="activeMenu = $event" /> <SubMenu :data="menus" :initial-active-menu="activeMenu" @change-menu="activeMenu = $event" />
</div> </div>
</template> </template>
+225
View File
@@ -0,0 +1,225 @@
<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 Filter from '~/components/pub/my-ui/nav-header/filter.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { getList, remove } from '~/services/prb.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'
import type { Prb } from '~/models/prb'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
// #endregion
// #region State
const props = withDefaults(defineProps<{
encounter?: Encounter
isBpjs?: boolean
}>(), {
isBpjs: false,
})
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: '', }),
entityName: 'prb',
})
const prbHistory = usePaginatedList({
fetchFn: (params) => getList({ ...params }),
entityName: 'prb-history',
})
const dummy = [
{
"id": 1,
"date": new Date().toISOString(),
"name1": "Dr. Smith",
"name2": "Maria S.",
"name3": "Project Alpha",
"name4": "Completed",
"name5": 95.5
},
]
const isHistoryDialogOpen = ref(false)
const isDocPreviewDialogOpen = ref(false)
const isFilterDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
const summaryLoading = ref(false)
const isRequirementsMet = ref(true)
const Prb = ref<Prb | null>(null)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const timestamp = ref<any>(null)
const headerPrep: HeaderPrep = {
title: "Program Rujuk Balik",
icon: 'i-lucide-history',
}
if(true){
headerPrep.addNav = {
label: "Program Rujuk Balik",
onClick: () => navigateTo({
name: 'integration-bpjs-prb-add',
}),
};
}
headerPrep.components = [
{
component: defineAsyncComponent(() => import('~/components/app/prb/_common/btn-history.vue')),
props: { }
},
];
const refSearchNav: RefSearchNav = {
onClick: () => {
isFilterDialogOpen.value = true
},
onInput: (val: string) => {
searchInput.value = val
},
onClear: () => {
searchInput.value = ''
},
}
// #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
}
}
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
}
function handleFiltering() {
isFilterDialogOpen.value = false
}
// #endregion
// #region Provide
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('timestamp', timestamp)
provide('table_data_loader', isLoading)
provide('isHistoryDialogOpen', isHistoryDialogOpen)
// #endregion
// #region Watchers
watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showEdit:
navigateTo({
name: 'integration-bpjs-prb-prb_id-edit',
params: { id: encounterId, "prb_id": recId.value },
})
break
case ActionEvents.showPrint:
isDocPreviewDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<WarningAlert v-if="!isRequirementsMet"
class="mb-5"
text="Syarat pembuatan PRB belum terpenuhi"
:description="[
'Lanjutan Penatalaksanaan Pasien harus terisi Dirujuk Eksternal',
'Jenis Pembayaran pasien harus JKN'
]" />
<div v-else>
<Header :prep="headerPrep" />
<Filter
:prep="headerPrep"
:ref-search-nav="refSearchNav"
:enable-export="false"
/>
<AppPrbList :is-bpjs="true" :data="dummy" />
<Dialog v-model:open="isHistoryDialogOpen" title="History" size="full">
<AppPrbHistoryList
:data="dummy"
:pagination-meta="prbHistory.paginationMeta"
@page-change="prbHistory.handlePageChange" />
</Dialog>
<Dialog v-model:open="isFilterDialogOpen" title="Filter" size="lg">
<AppPrbCommonFilter @submit="handleFiltering" />
</Dialog>
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
</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>
</div>
</template>
+65
View File
@@ -0,0 +1,65 @@
<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/prb.service'
// Components
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import type { Prb } from '~/models/prb'
// #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 PrbId = typeof route.params.surgery_report_id == 'string' ? parseInt(route.params.surgery_report_id) : 0
const Prb = ref<Prb | null>(null)
const headerPrep: HeaderPrep = {
title: 'Detail Program Rujuk Balik',
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() {
goBack()
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<Header :prep="headerPrep" :ref-search-nav="headerPrep.refSearchNav" />
<AppPrbDetail :instance="Prb" @click="handleAction" />
</template>
+160
View File
@@ -0,0 +1,160 @@
<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 Header from '~/components/pub/my-ui/nav-header/prep.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/prb.service'
import type { Prb } from '~/models/prb'
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
import { toast } from '~/components/pub/ui/toast'
import { withBase } from '~/models/_base'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import { PrbSchema } from '~/schemas/prb.schema'
import { formatDateYyyyMmDd } from '~/lib/date'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { getList, remove } from '~/services/prb.service'
import { handleActionEdit } from '~/handlers/prb.handler'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import { formatDateToDatetimeLocal } from '~/lib/utils'
// #region Props & Emits
const props = withDefaults(defineProps<{
callbackUrl?: string
isBpjs?: boolean
}>(), {
isBpjs: false,
})
// form related state
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
entityName: 'prb',
})
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const PrbId = typeof route.params.surgery_report_id == 'string' ? parseInt(route.params.surgery_report_id) : 0
const inputForm = ref<ExposedForm<any> | null>(null)
const Prb = ref({})
const isConfirmationOpen = ref(false)
const selectedOperativeAction = ref<any>(null)
const isSepDialogOpen = ref(false)
provide("isSepDialogOpen", isSepDialogOpen);
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
const result = await getDetail(PrbId)
if (result.success) {
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
Prb.value = responseData
inputForm.value?.setValues(responseData)
}
})
// #endregion
// #region Functions
function goBack() {
router.go(-1)
}
async function handleConfirmAdd() {
const response = await handleActionEdit(
PrbId,
await composeFormData(),
() => { },
() => { },
toast,
)
goBack()
}
async function composeFormData(): Promise<Prb> {
const [input,] = await Promise.all([
inputForm.value?.validate(),
])
const results = [input]
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
if (!allValid) return Promise.reject('Form validation failed')
const formData = input?.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
}
// #endregion
// #region Watchers
// #endregion
const initialValues = {
a1: "",
a2: formatDateToDatetimeLocal(new Date()),
}
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Update Program Rujuk Balik</div>
<AppPrbEntry
ref="inputForm"
:schema="PrbSchema"
:initial-values="initialValues"
:is-bpjs="isBpjs"
/>
<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>
+202
View File
@@ -0,0 +1,202 @@
<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 Filter from '~/components/pub/my-ui/nav-header/filter.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { getList, remove } from '~/services/prb.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'
import type { Prb } from '~/models/prb'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
// #endregion
// #region State
const props = withDefaults(defineProps<{
encounter?: Encounter
isBpjs?: boolean
}>(), {
isBpjs: false,
})
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: '', }),
entityName: 'prb',
})
const prbHistory = usePaginatedList({
fetchFn: (params) => getList({ ...params }),
entityName: 'prb-history',
})
const dummy = [
{
"id": 1,
"date": new Date().toISOString(),
"name1": "Dr. Smith",
"name2": "Maria S.",
"name3": "Project Alpha",
"name4": "Completed",
"name5": 95.5
},
]
const isHistoryDialogOpen = ref(false)
const isDocPreviewDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
const summaryLoading = ref(false)
const isRequirementsMet = ref(true)
const Prb = ref<Prb | null>(null)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const timestamp = ref<any>(null)
const headerPrep: HeaderPrep = {
title: "Program Rujuk Balik",
icon: 'i-lucide-history',
}
if(true){
headerPrep.addNav = {
label: "Program Rujuk Balik",
onClick: () => navigateTo({
name: 'rehab-encounter-id-prb-add',
params: { id: encounterId },
}),
};
}
headerPrep.components = [
{
component: defineAsyncComponent(() => import('~/components/app/prb/_common/btn-history.vue')),
props: { }
},
];
// #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
}
}
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('timestamp', timestamp)
provide('table_data_loader', isLoading)
provide('isHistoryDialogOpen', isHistoryDialogOpen)
// #endregion
// #region Watchers
watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showEdit:
navigateTo({
name: 'rehab-encounter-id-prb-prb_id-edit',
params: { id: encounterId, "prb_id": recId.value },
})
break
case ActionEvents.showPrint:
isDocPreviewDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<WarningAlert v-if="!isRequirementsMet"
class="mb-5"
text="Syarat pembuatan PRB belum terpenuhi"
:description="[
'Lanjutan Penatalaksanaan Pasien harus terisi Dirujuk Eksternal',
'Jenis Pembayaran pasien harus JKN'
]" />
<div v-else>
<Header :prep="headerPrep" />
<AppPrbDetail :instance="Prb" />
<h1 class="font-semibold text-lg mb-2">Obat</h1>
<AppPrbList :is-bpjs="isBpjs" :data="dummy" />
<Dialog v-model:open="isHistoryDialogOpen" title="History" size="full">
<AppPrbHistoryList
:data="dummy"
:pagination-meta="prbHistory.paginationMeta"
@page-change="prbHistory.handlePageChange" />
</Dialog>
<Dialog v-model:open="isDocPreviewDialogOpen" title="Preview Dokumen" size="2xl">
<DocPreviewDialog :link="`https://www.antennahouse.com/hubfs/xsl-fo-sample/pdf/basic-link-1.pdf`" />
</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>
</div>
</template>
@@ -0,0 +1,65 @@
<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/vaccine-data.service'
// Components
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import type { VaccineData } from '~/models/vaccine-data'
// #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 VaccineDataId = typeof route.params.surgery_report_id == 'string' ? parseInt(route.params.surgery_report_id) : 0
const VaccineData = ref<VaccineData | null>(null)
const headerPrep: HeaderPrep = {
title: 'Detail Data Vaksin',
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() {
goBack()
}
// #endregion
// #region Watchers
// #endregion
</script>
<template>
<Header :prep="headerPrep" :ref-search-nav="headerPrep.refSearchNav" />
<AppVaccineDataDetail :instance="VaccineData" @click="handleAction" />
</template>
@@ -0,0 +1,159 @@
<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 Header from '~/components/pub/my-ui/nav-header/prep.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/vaccine-data.service'
import type { VaccineData } from '~/models/vaccine-data'
import ActionDialog from '~/components/pub/my-ui/nav-footer/ba-su.vue'
import { toast } from '~/components/pub/ui/toast'
import { withBase } from '~/models/_base'
import Confirmation from '~/components/pub/my-ui/confirmation/confirmation.vue'
import { VaccineDataSchema } from '~/schemas/vaccine-data.schema'
import { formatDateYyyyMmDd } from '~/lib/date'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import { getList, remove } from '~/services/vaccine-data.service'
import { handleActionEdit, handleActionSave } from '~/handlers/vaccine-data.handler'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import { formatDateToDatetimeLocal } from '~/lib/utils'
// #region Props & Emits
const props = withDefaults(defineProps<{
callbackUrl?: string
isBpjs?: boolean
}>(), {
isBpjs: false,
})
// form related state
const { data, isLoading, paginationMeta, searchInput, handlePageChange, handleSearch, fetchData } = usePaginatedList({
fetchFn: (params) => getList({ ...params, includes: 'specialist,subspecialist,doctor-employee-person', }),
entityName: 'vaccine-data',
})
// #endregion
// #region State & Computed
const route = useRoute()
const router = useRouter()
const encounterId = typeof route.params.id == 'string' ? parseInt(route.params.id) : 0
const VaccineDataId = typeof route.params.surgery_report_id == 'string' ? parseInt(route.params.surgery_report_id) : 0
const inputForm = ref<ExposedForm<any> | null>(null)
const VaccineData = ref({})
const isConfirmationOpen = ref(false)
const selectedOperativeAction = ref<any>(null)
const isSepDialogOpen = ref(false)
provide("isSepDialogOpen", isSepDialogOpen);
// #endregion
// #region Lifecycle Hooks
onMounted(async () => {
const result = await getDetail(VaccineDataId)
if (result.success) {
const responseData = {...result.body.data, date: formatDateYyyyMmDd(result.body.data.date)}
VaccineData.value = responseData
inputForm.value?.setValues(responseData)
}
})
// #endregion
// #region Functions
function goBack() {
router.go(-1)
}
async function handleConfirmAdd() {
const response = await handleActionSave(
await composeFormData(),
() => { },
() => { },
toast,
)
goBack()
}
async function composeFormData(): Promise<VaccineData> {
const [input,] = await Promise.all([
inputForm.value?.validate(),
])
const results = [input]
const allValid = results.every((r) => r?.valid)
// exit, if form errors happend during validation
if (!allValid) return Promise.reject('Form validation failed')
const formData = input?.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
}
// #endregion
// #region Watchers
// #endregion
const initialValues = {
a1: "",
a2: formatDateToDatetimeLocal(new Date()),
}
</script>
<template>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg font-semibold xl:text-xl">Update Data Vaksin</div>
<AppVaccineDataEntry
ref="inputForm"
:schema="VaccineDataSchema"
:initial-values="initialValues"
:is-bpjs="isBpjs"
/>
<div class="my-2 flex justify-end py-2">
<Action @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,165 @@
<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 Filter from '~/components/pub/my-ui/nav-header/filter.vue'
import Header from '~/components/pub/my-ui/nav-header/prep.vue'
import { usePaginatedList } from '~/composables/usePaginatedList'
import { getList, remove } from '~/services/vaccine-data.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'
import DocPreviewDialog from '~/components/pub/my-ui/modal/doc-preview-dialog.vue'
import Dialog from '~/components/pub/my-ui/modal/dialog.vue'
import type { VaccineData } from '~/models/vaccine-data'
import { medicalRoles } from '~/const/common/role'
// #endregion
// #region State
const props = withDefaults(defineProps<{
encounter?: Encounter
isBpjs?: boolean
}>(), {
isBpjs: false,
})
const route = useRoute()
const {user} = useUserStore()
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: '', }),
entityName: 'vaccine-data',
})
const dummy = [
{
"id": 1,
"date": new Date().toISOString(),
"name1": "Dr. Smith",
"name2": 1,
},
]
const isHistoryDialogOpen = ref(false)
const isDocPreviewDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
const summaryLoading = ref(false)
const isRequirementsMet = ref(true)
const vaccineData = ref<VaccineData | null>(null)
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const timestamp = ref<any>(null)
const headerPrep: HeaderPrep = {
title: "Data Vaksin",
icon: 'i-lucide-syringe',
}
if(user.activeRole === 'emp|doc' || user.activeRole === 'system') {
headerPrep.addNav = {
label: "Data Vaksin",
onClick: () => navigateTo({
name: 'rehab-encounter-id-vaccine-data-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
}
}
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('timestamp', timestamp)
provide('table_data_loader', isLoading)
provide('isHistoryDialogOpen', isHistoryDialogOpen)
// #endregion
// #region Watchers
watch([recId, recAction, timestamp], () => {
switch (recAction.value) {
case ActionEvents.showDetail:
navigateTo({
name: 'rehab-encounter-id-vaccine-data-vaccine_data_id',
params: { id: encounterId, "vaccine_data_id": recId.value },
})
break
case ActionEvents.showConfirmDelete:
isRecordConfirmationOpen.value = true
break
}
})
// #endregion
</script>
<template>
<div>
<Header :prep="headerPrep" />
<AppVaccineDataList
:data="dummy"
: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?.name">
<strong>Nama:</strong>
{{ record.name }}
</p>
</div>
</template>
</RecordConfirmation>
</div>
</template>
@@ -0,0 +1,30 @@
<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 timestamp = inject<Ref<any>>('timestamp')!
const activeKey = ref<string | null>(null)
function process() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showProcess
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
</script>
<template>
<Button @click="process" variant="outline"
class="text-orange-400 border-orange-400 bg-transparent">
Pilih
<Icon name="i-lucide-arrow-right" class="h-4 w-4 align-middle transition-colors" />
</Button>
</template>
@@ -0,0 +1,30 @@
<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 timestamp = inject<Ref<any>>('timestamp')!
const activeKey = ref<string | null>(null)
function process() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showProcess
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
</script>
<template>
<Button @click="process" variant="outline"
class="text-orange-400 border-orange-400 bg-transparent">
Detail
<Icon name="i-lucide-search" class="h-4 w-4 align-middle transition-colors" />
</Button>
</template>
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './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 timestamp = inject<Ref<any>>('timestamp')!
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showPrint
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
</script>
<template>
<div>
<Button type="button" variant="outline"
class="text-orange-500 border border-orange-400"
@click="print">
<Icon name="i-lucide-printer" class="h-4 w-4 align-middle transition-colors" />
Preview
</Button>
</div>
</template>
@@ -0,0 +1,94 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './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 timestamp = inject<Ref<any>>('timestamp')!
const activeKey = ref<string | null>(null)
const linkItems: LinkItem[] = [
{
label: 'Edit',
onClick: () => {
edit()
},
icon: 'i-lucide-pencil',
},
{
label: 'Print',
onClick: () => {
print()
},
icon: 'i-lucide-printer',
},
{
label: 'Delete',
onClick: () => {
del()
},
icon: 'i-lucide-trash',
},
]
function edit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
function print() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showPrint
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
timestamp.value = new Date().getTime()
}
</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>
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/prb.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+24
View File
@@ -0,0 +1,24 @@
// Handlers
import { genCrudHandler } from '~/handlers/_handler'
// Services
import { create, update, remove } from '~/services/vaccine-data.service'
export const {
recId,
recAction,
recItem,
isReadonly,
isProcessing,
isFormEntryDialogOpen,
isRecordConfirmationOpen,
onResetState,
handleActionSave,
handleActionEdit,
handleActionRemove,
handleCancelForm,
} = genCrudHandler({
create,
update,
remove,
})
+12
View File
@@ -486,4 +486,16 @@ export const SpecimenTypeOptList: { label: string; value: SpecimenType }[] = [
{ label: 'Mikrobiologi', value: 'mikrobiologi' }, { label: 'Mikrobiologi', value: 'mikrobiologi' },
{ label: 'Laborat', value: 'laborat' }, { label: 'Laborat', value: 'laborat' },
{ label: 'Tidak perlu', value: 'tidak perlu' }, { label: 'Tidak perlu', value: 'tidak perlu' },
]
export type PrbProgramType = "ashma" | "diabetes mellitus" | "hipertensi" | "penyakit jantung" | "ppok" | "schizopherenia" | "stroke" | "systemic lupus erythematosus"
export const PrbProgramTypeOptList: { label: string; value: PrbProgramType }[] = [
{ label: 'ASHMA', value: 'ashma' },
{ label: 'Diabetes Mellitus', value: 'diabetes mellitus' },
{ label: 'Hipertensi', value: 'hipertensi' },
{ label: 'Penyakit Jantung', value: 'penyakit jantung' },
{ label: 'PPOK (Penyakit Paru Obstruktif Kronik)', value: 'ppok' },
{ label: 'Schizopherenia', value: 'schizopherenia' },
{ label: 'Stroke', value: 'stroke' },
{ label: 'Systemic Lupus Erythematosus', value: 'systemic lupus erythematosus' },
] ]
+6
View File
@@ -1,5 +1,6 @@
import type { ClassValue } from 'clsx' import type { ClassValue } from 'clsx'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { format } from 'date-fns'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { toast } from '~/components/pub/ui/toast' import { toast } from '~/components/pub/ui/toast'
@@ -106,6 +107,11 @@ export function calculateAge(birthDate: Date | string | null | undefined): strin
} }
} }
export function formatDateToDatetimeLocal(inputDate: Date) {
const formattedString = format(inputDate, "yyyy-MM-dd'T'HH:mm")
return formattedString
}
/** /**
* Converts a plain JavaScript object (including File objects) into a FormData instance. * Converts a plain JavaScript object (including File objects) into a FormData instance.
+37
View File
@@ -0,0 +1,37 @@
import { type Base, genBase } from "./_base"
import { genDoctor, type Doctor } from "./doctor"
import { genEncounter, type Encounter } from "./encounter"
import { genSpecialist, type Specialist } from "./specialist"
import { genSubspecialist, type Subspecialist } from "./subspecialist"
import { genUnit, type Unit } from "./unit"
export interface Prb extends Base {
encounter_id: number
encounter: Encounter
unit_id: number
unit: Unit
specialist_id: number
specialist: Specialist
subspecialist_id: number
subspecialist: Subspecialist
doctor_id: number
doctor: Doctor
date: ''
}
export function genPrb(): Prb {
return {
...genBase(),
encounter_id: 0,
encounter: genEncounter(),
unit_id: 0,
unit: genUnit(),
specialist_id: 0,
specialist: genSpecialist(),
subspecialist_id: 0,
subspecialist: genSubspecialist(),
doctor_id: 0,
doctor: genDoctor(),
date: ''
}
}
+37
View File
@@ -0,0 +1,37 @@
import { type Base, genBase } from "./_base"
import { genDoctor, type Doctor } from "./doctor"
import { genEncounter, type Encounter } from "./encounter"
import { genSpecialist, type Specialist } from "./specialist"
import { genSubspecialist, type Subspecialist } from "./subspecialist"
import { genUnit, type Unit } from "./unit"
export interface VaccineData extends Base {
encounter_id: number
encounter: Encounter
unit_id: number
unit: Unit
specialist_id: number
specialist: Specialist
subspecialist_id: number
subspecialist: Subspecialist
doctor_id: number
doctor: Doctor
date: ''
}
export function genVaccineData(): VaccineData {
return {
...genBase(),
encounter_id: 0,
encounter: genEncounter(),
unit_id: 0,
unit: genUnit(),
specialist_id: 0,
specialist: genSpecialist(),
subspecialist_id: 0,
subspecialist: genSubspecialist(),
doctor_id: 0,
doctor: genDoctor(),
date: ''
}
}
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Update PRB',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentPrbEntry :is-bpjs="true" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Detail PRB',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentPrbDetail :patient-id="Number(route.params.id)" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah PRB',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, getPagePermissions } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
const pagePermission = getPagePermissions(roleAccess)
const callbackUrl = route.query['return-path'] as string | undefined
</script>
<template>
<div>
<div v-if="pagePermission.canRead">
<ContentPrbEntry :is-bpjs="true" :callback-url="callbackUrl" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Daftar PRB',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, getPagePermissions } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
const pagePermission = getPagePermissions(roleAccess)
const callbackUrl = route.query['return-path'] as string | undefined
</script>
<template>
<div>
<div v-if="pagePermission.canRead">
<ContentPrbBpjsList :callback-url="callbackUrl" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Update PRB',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentPrbEntry />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Detail PRB',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentPrbDetail :patient-id="Number(route.params.id)" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah PRB',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, getPagePermissions } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
const pagePermission = getPagePermissions(roleAccess)
const callbackUrl = route.query['return-path'] as string | undefined
</script>
<template>
<div>
<div v-if="pagePermission.canRead">
<ContentPrbEntry :callback-url="callbackUrl" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -2,7 +2,7 @@
import type { PagePermission } from '~/models/role' import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue' import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission' import { PAGE_PERMISSIONS } from '~/lib/page-permission'
import EncounterProcess from '~/components/content/encounter/process.vue' import EncounterProcess from '~/components/content/encounter/process-bu.vue'
definePageMeta({ definePageMeta({
middleware: ['rbac'], middleware: ['rbac'],
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Detail Data Vaksin',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/patient']
const { checkRole, hasReadAccess } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
// const canRead = hasReadAccess(roleAccess)
const canRead = true
</script>
<template>
<div>
<div v-if="canRead">
<ContentVaccineDataDetail :patient-id="Number(route.params.id)" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type { PagePermission } from '~/models/role'
import Error from '~/components/pub/my-ui/error/error.vue'
import { PAGE_PERMISSIONS } from '~/lib/page-permission'
definePageMeta({
middleware: ['rbac'],
roles: ['doctor', 'nurse', 'admisi', 'pharmacy', 'billing', 'management'],
title: 'Tambah Data Vaksin',
contentFrame: 'cf-full-width',
})
const route = useRoute()
useHead({
title: () => route.meta.title as string,
})
const roleAccess: PagePermission = PAGE_PERMISSIONS['/rehab/encounter']
const { checkRole, getPagePermissions } = useRBAC()
// Check if user has access to this page
const hasAccess = checkRole(roleAccess)
// if (!hasAccess) {
// navigateTo('/403')
// }
// Define permission-based computed properties
const pagePermission = getPagePermissions(roleAccess)
const callbackUrl = route.query['return-path'] as string | undefined
</script>
<template>
<div>
<div v-if="pagePermission.canRead">
<ContentVaccineDataEntry :callback-url="callbackUrl" />
</div>
<Error v-else :status-code="403" />
</div>
</template>
+27
View File
@@ -0,0 +1,27 @@
import { z } from 'zod'
const PrbMedicineSchema = z.object({
a1: z.string({
required_error: 'Mohon isi',
}).optional(),
a2: z.string({
required_error: 'Mohon isi',
}),
a3: z.string({
required_error: 'Mohon isi',
}),
a4: z.string({
required_error: 'Mohon isi',
}).optional(),
a5: z.string({
required_error: 'Mohon isi',
}),
a6: z.string({
required_error: 'Mohon isi',
}),
})
type PrbMedicineFormData = z.infer<typeof PrbMedicineSchema>
export { PrbMedicineSchema }
export type { PrbMedicineFormData }
+47
View File
@@ -0,0 +1,47 @@
import { z } from 'zod'
const PrbSchema = z.object({
sepStatus: z.string({
required_error: 'Mohon isi status SEP',
}).default('SEP Internal'),
unit_code: z.string({
required_error: 'Mohon isi Unit',
}),
specialist_code: z.string({
required_error: 'Mohon isi Spesialis',
}),
subspecialist_code: z.string({
required_error: 'Mohon isi Sub Spesialis',
}),
doctor_code: z.string({
required_error: 'Mohon isi DPJP',
}),
encounter_code: z.string().optional(),
date: z.string({
required_error: 'Mohon lengkapi Tanggal Kontrol',
})
.refine(
(date) => {
// Jika kosong, return false untuk required validation
if (!date || date.trim() === '') return false
// Jika ada isi, validasi format tanggal
try {
const dateObj = new Date(date)
// Cek apakah tanggal valid dan tahun >= 1900
return !isNaN(dateObj.getTime()) && dateObj.getFullYear() >= 1900
} catch {
return false
}
},
{
message: 'Mohon lengkapi Tanggal Kontrol dengan format yang valid',
},
)
.transform((dateStr) => new Date(dateStr).toISOString()),
})
type PrbFormData = z.infer<typeof PrbSchema>
export { PrbSchema }
export type { PrbFormData }
+47
View File
@@ -0,0 +1,47 @@
import { z } from 'zod'
const VaccineDataSchema = z.object({
sepStatus: z.string({
required_error: 'Mohon isi status SEP',
}).default('SEP Internal'),
unit_code: z.string({
required_error: 'Mohon isi Unit',
}),
specialist_code: z.string({
required_error: 'Mohon isi Spesialis',
}),
subspecialist_code: z.string({
required_error: 'Mohon isi Sub Spesialis',
}),
doctor_code: z.string({
required_error: 'Mohon isi DPJP',
}),
encounter_code: z.string().optional(),
date: z.string({
required_error: 'Mohon lengkapi Tanggal Kontrol',
})
.refine(
(date) => {
// Jika kosong, return false untuk required validation
if (!date || date.trim() === '') return false
// Jika ada isi, validasi format tanggal
try {
const dateObj = new Date(date)
// Cek apakah tanggal valid dan tahun >= 1900
return !isNaN(dateObj.getTime()) && dateObj.getFullYear() >= 1900
} catch {
return false
}
},
{
message: 'Mohon lengkapi Tanggal Kontrol dengan format yang valid',
},
)
.transform((dateStr) => new Date(dateStr).toISOString()),
})
type VaccineDataFormData = z.infer<typeof VaccineDataSchema>
export { VaccineDataSchema }
export type { VaccineDataFormData }
+14
View File
@@ -1,4 +1,5 @@
// Base // Base
import type { Medicine } from '~/models/medicine'
import * as base from './_crud-base' import * as base from './_crud-base'
const path = '/api/v1/medicine' const path = '/api/v1/medicine'
@@ -23,3 +24,16 @@ export function update(id: number | string, data: any) {
export function remove(id: number | string) { export function remove(id: number | string) {
return base.remove(path, id, name) return base.remove(path, id, name)
} }
export async function getValueLabelList(params: any = null): Promise<{ value: string; label: string }[]> {
let data: { value: string; label: string }[] = []
const result = await getList(params)
if (result.success) {
const resultData = result.body?.data || []
data = resultData.map((item: Medicine) => ({
value: item.code,
label: item.name,
}))
}
return data
}
+28
View File
@@ -0,0 +1,28 @@
// Base
import * as base from './_crud-base'
// Constants
import { encounterClassCodes } from '~/lib/constants'
const path = '/api/v1/prb'
const name = 'prb'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string, params?: any) {
return base.getDetail(path, id, name, params)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
+28
View File
@@ -0,0 +1,28 @@
// Base
import * as base from './_crud-base'
// Constants
import { encounterClassCodes } from '~/lib/constants'
const path = '/api/v1/vaccine-data'
const name = 'vaccine-data'
export function create(data: any) {
return base.create(path, data, name)
}
export function getList(params: any = null) {
return base.getList(path, params, name)
}
export function getDetail(id: number | string, params?: any) {
return base.getDetail(path, id, name, params)
}
export function update(id: number | string, data: any) {
return base.update(path, id, data, name)
}
export function remove(id: number | string) {
return base.remove(path, id, name)
}
+5
View File
@@ -200,6 +200,11 @@
"icon": "i-lucide-circuit-board", "icon": "i-lucide-circuit-board",
"link": "/integration/bpjs-vclaim/member" "link": "/integration/bpjs-vclaim/member"
}, },
{
"title": "PRB",
"icon": "i-lucide-circuit-board",
"link": "/integration/bpjs/prb"
},
{ {
"title": "Surat Kontrol", "title": "Surat Kontrol",
"icon": "i-lucide-circuit-board", "icon": "i-lucide-circuit-board",