diff --git a/app/assets/css/main.css b/app/assets/css/main.css
index 4c488385..7e7f0b70 100644
--- a/app/assets/css/main.css
+++ b/app/assets/css/main.css
@@ -67,41 +67,43 @@
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
- /* .dark { */
- /* --background: 210 25% 8%; */
- /* --foreground: 210 20% 95%; */
- /* --card: 210 25% 10%; */
- /* --card-foreground: 210 20% 95%; */
- /* --popover: 210 25% 10%; */
- /* --popover-foreground: 210 20% 95%; */
- /* --primary: 150 75% 45%; */
- /* --primary-foreground: 0 0% 100%; */
- /* --primary-hover: 150 75% 50%; */
- /* --secondary: 210 25% 15%; */
- /* --secondary-foreground: 210 20% 90%; */
- /* --muted: 210 25% 15%; */
- /* --muted-foreground: 210 15% 65%; */
- /* --accent: 210 100% 55%; */
- /* --accent-foreground: 0 0% 100%; */
- /* --destructive: 0 75% 60%; */
- /* --destructive-foreground: 0 0% 100%; */
- /* --border: 210 25% 20%; */
- /* --input: 210 25% 15%; */
- /* --ring: 150 75% 45%; */
- /* --success: 150 75% 50%; */
- /* --warning: 45 95% 65%; */
- /* --info: 210 100% 60%; */
- /* --gradient-primary: linear-gradient(135deg, hsl(150 75% 45%), hsl(150 75% 55%)); */
- /* --gradient-medical: linear-gradient(135deg, hsl(150 75% 45%), hsl(210 100% 55%)); */
- /* --gradient-subtle: linear-gradient(180deg, hsl(210 25% 8%), hsl(210 25% 12%)); */
- /* --sidebar-background: 240 5.9% 10%; */
- /* --sidebar-foreground: 240 4.8% 95.9%; */
- /* --sidebar-primary: 224.3 76.3% 48%; */
- /* --sidebar-primary-foreground: 0 0% 100%; */
- /* --sidebar-accent: 240 3.7% 15.9%; */
- /* --sidebar-accent-foreground: 240 4.8% 95.9%; */
- /* --sidebar-border: 240 3.7% 15.9%; */
- /* --sidebar-ring: 217.2 91.2% 59.8%; */
+}
+
+.dark {
+ --background: 210 25% 8%;
+ --foreground: 210 20% 95%;
+ --card: 210 25% 10%;
+ --card-foreground: 210 20% 95%;
+ --popover: 210 25% 10%;
+ --popover-foreground: 210 20% 95%;
+ --primary: 150 75% 45%;
+ --primary-foreground: 0 0% 100%;
+ --primary-hover: 150 75% 50%;
+ --secondary: 210 25% 15%;
+ --secondary-foreground: 210 20% 90%;
+ --muted: 210 25% 15%;
+ --muted-foreground: 210 15% 65%;
+ --accent: 210 100% 55%;
+ --accent-foreground: 0 0% 100%;
+ --destructive: 0 75% 60%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 210 25% 20%;
+ --input: 210 25% 15%;
+ --ring: 150 75% 45%;
+ --success: 150 75% 50%;
+ --warning: 45 95% 65%;
+ --info: 210 100% 60%;
+ --gradient-primary: linear-gradient(135deg, hsl(150 75% 45%), hsl(150 75% 55%));
+ --gradient-medical: linear-gradient(135deg, hsl(150 75% 45%), hsl(210 100% 55%));
+ --gradient-subtle: linear-gradient(180deg, hsl(210 25% 8%), hsl(210 25% 12%));
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
/* Keyframes for Animations */
@@ -330,7 +332,8 @@ body {
/* Form Error Styling */
.field-error-info {
- @apply text-xs ml-1;
+ font-size: 0.75rem;
+ margin-left: 0.25rem;
color: hsl(var(--destructive));
/* font-size: 0.875rem; */
margin-top: 0.25rem;
@@ -338,3 +341,28 @@ body {
}
/* .rounded-md { border-radius: var */
+
+/* Dashboard grid utility */
+.dashboard-grid {
+ display: grid;
+ gap: 1rem;
+}
+
+@media (min-width: 768px) {
+ .dashboard-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (min-width: 1024px) {
+ .dashboard-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
+
+@media (min-width: 1280px) {
+ .dashboard-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 1.25rem;
+ }
+}
\ No newline at end of file
diff --git a/app/components/app/divison/entry-form.vue b/app/components/app/divison/entry-form.vue
index 3008d3c8..7f21d90a 100644
--- a/app/components/app/divison/entry-form.vue
+++ b/app/components/app/divison/entry-form.vue
@@ -1,6 +1,8 @@
+
+
+
+
+
Demo: Tree Select dengan Lazy Loading
+
+ Contoh penggunaan komponen TreeSelect dengan data teknologi.
+ Pilih item untuk melihat sub-kategori yang dimuat secara lazy.
+
+
+
+
+
💡 Catatan untuk Developer:
+
+ Untuk implementasi nyata dengan data divisi rumah sakit,
+ lihat komponen content/division/entry.vue
+
+
+
+
+
+
+
+
+
+
+
Value yang terpilih:
+
+ {{ selectedValue || 'Belum ada yang dipilih' }}
+
+
+
+
+
+
🔄 Data dimuat secara lazy saat node parent dibuka
+
⏱️ Simulasi delay 600ms untuk menampilkan loading state
+
+
+
diff --git a/app/components/content/dashboard/index.vue b/app/components/content/dashboard/index.vue
index 2195b428..71167dac 100644
--- a/app/components/content/dashboard/index.vue
+++ b/app/components/content/dashboard/index.vue
@@ -113,7 +113,7 @@ onMounted(() => {
Dashboard SIMRS
-
Status: Aktif
+
Status: Aktif
-
+
-
+
Recent Sales
@@ -143,7 +143,7 @@ onMounted(() => {
{{ recentSales.name }}
-
+
{{ recentSales.email }}
@@ -156,19 +156,19 @@ onMounted(() => {
-
+
Aksi Cepat
-
+
+ v-for="item in linkItems"
+ :key="item.title"
+ class="my-2 h-32 border border-primary transition-colors duration-200 hover:bg-gray-200 hover:bg-primary"
+ >
-
+
{{ item.title }}
diff --git a/app/components/content/division/entry.ts b/app/components/content/division/entry.ts
index 60617c45..aef024e7 100644
--- a/app/components/content/division/entry.ts
+++ b/app/components/content/division/entry.ts
@@ -1,58 +1,145 @@
+import type { TreeItem } from '~/components/pub/base/select-tree/type'
import * as z from 'zod'
-export const division = {
+export const divisionConf = {
msg: {
placeholder: '---pilih divisi utama',
search: 'kode, nama divisi',
empty: 'divisi tidak ditemukan',
},
items: [
- { value: '1', label: 'Medical', code: 'MED' },
- { value: '2', label: 'Nursing', code: 'NUR' },
- { value: '3', label: 'Admin', code: 'ADM' },
- { value: '4', label: 'Support', code: 'SUP' },
- { value: '5', label: 'Education', code: 'EDU' },
- { value: '6', label: 'Pharmacy', code: 'PHA' },
- { value: '7', label: 'Radiology', code: 'RAD' },
- { value: '8', label: 'Laboratory', code: 'LAB' },
- { value: '9', label: 'Finance', code: 'FIN' },
- { value: '10', label: 'Human Resources', code: 'HR' },
- { value: '11', label: 'IT Services', code: 'ITS' },
- { value: '12', label: 'Maintenance', code: 'MNT' },
- { value: '13', label: 'Catering', code: 'CAT' },
- { value: '14', label: 'Security', code: 'SEC' },
- { value: '15', label: 'Emergency', code: 'EMR' },
- { value: '16', label: 'Surgery', code: 'SUR' },
- { value: '17', label: 'Outpatient', code: 'OUT' },
- { value: '18', label: 'Inpatient', code: 'INP' },
- { value: '19', label: 'Rehabilitation', code: 'REB' },
- { value: '20', label: 'Research', code: 'RSH' },
+ { value: '1', label: 'Medical' },
+ { value: '2', label: 'Nursing' },
+ { value: '3', label: 'Admin' },
+ { value: '4', label: 'Support' },
+ { value: '5', label: 'Education' },
+ { value: '6', label: 'Pharmacy' },
+ { value: '7', label: 'Radiology' },
+ { value: '8', label: 'Laboratory' },
+ { value: '9', label: 'Finance' },
+ { value: '10', label: 'Human Resources' },
+ { value: '11', label: 'IT Services' },
+ { value: '12', label: 'Maintenance' },
+ { value: '13', label: 'Catering' },
+ { value: '14', label: 'Security' },
+ { value: '15', label: 'Emergency' },
+ { value: '16', label: 'Surgery' },
+ { value: '17', label: 'Outpatient' },
+ { value: '18', label: 'Inpatient' },
+ { value: '19', label: 'Rehabilitation' },
+ { value: '20', label: 'Research' },
],
}
export const schema = z.object({
- name: z.string({
- required_error: 'Nama wajib diisi',
- }).min(1, 'Nama divisi wajib diisi'),
+ name: z
+ .string({
+ required_error: 'Nama wajib diisi',
+ })
+ .min(1, 'Nama divisi wajib diisi'),
- code: z.string({
- required_error: 'Kode wajib diisi',
- }).min(1, 'Kode divisi wajib diisi'),
+ code: z
+ .string({
+ required_error: 'Kode wajib diisi',
+ })
+ .min(1, 'Kode divisi wajib diisi'),
- parentId: z.preprocess(
- (input: unknown) => {
- if (typeof input === 'string') {
- // Handle empty string case
- if (input.trim() === '') {
- return undefined
- }
- return Number(input)
- }
-
- return input
- },
- z.number({
- required_error: 'Kelompok wajib dipilih',
- }).min(1, 'Kelompok wajib dipilih'),
- ),
+ parentId: z.string().optional(),
})
+
+// State untuk tree data divisi - dimulai dengan data level atas
+const divisionTreeData = ref([
+ { value: '1', label: 'Medical', hasChildren: true },
+ { value: '2', label: 'Nursing', hasChildren: true },
+ { value: '3', label: 'Admin', hasChildren: false },
+ { value: '4', label: 'Support', hasChildren: true },
+ { value: '5', label: 'Education', hasChildren: false },
+ { value: '6', label: 'Pharmacy', hasChildren: true },
+ { value: '7', label: 'Radiology', hasChildren: false },
+ { value: '8', label: 'Laboratory', hasChildren: true },
+])
+
+// Helper function untuk mencari dan menyisipkan data anak ke dalam tree
+function findAndInsertChildren(nodes: TreeItem[], parentId: string, newChildren: TreeItem[]): boolean {
+ for (const node of nodes) {
+ if (node.value === parentId) {
+ node.children = newChildren
+ return true
+ }
+ if (node.children && findAndInsertChildren(node.children as TreeItem[], parentId, newChildren)) {
+ return true
+ }
+ }
+ return false
+}
+
+// Fungsi untuk fetch data anak divisi (lazy loading)
+async function handleFetchDivisionChildren(parentId: string): Promise {
+ console.log(`Mengambil data sub-divisi untuk parent: ${parentId}`)
+
+ // Simulasi delay API call
+ await new Promise((resolve) => setTimeout(resolve, 800))
+
+ let childrenData: TreeItem[] = []
+
+ // Sample data berdasarkan parent ID
+ switch (parentId) {
+ case '1': // Medical
+ childrenData = [
+ { value: '1-1', label: 'Cardiology', hasChildren: true },
+ { value: '1-2', label: 'Neurology', hasChildren: false },
+ { value: '1-3', label: 'Oncology', hasChildren: false },
+ ]
+ break
+ case '2': // Nursing
+ childrenData = [
+ { value: '2-1', label: 'ICU Nursing', hasChildren: false },
+ { value: '2-2', label: 'ER Nursing', hasChildren: false },
+ { value: '2-3', label: 'Ward Nursing', hasChildren: true },
+ ]
+ break
+ case '4': // Support
+ childrenData = [
+ { value: '4-1', label: 'IT Support', hasChildren: false },
+ { value: '4-2', label: 'Maintenance', hasChildren: false },
+ ]
+ break
+ case '6': // Pharmacy
+ childrenData = [
+ { value: '6-1', label: 'Inpatient Pharmacy', hasChildren: false },
+ { value: '6-2', label: 'Outpatient Pharmacy', hasChildren: false },
+ ]
+ break
+ case '8': // Laboratory
+ childrenData = [
+ { value: '8-1', label: 'Clinical Lab', hasChildren: false },
+ { value: '8-2', label: 'Pathology Lab', hasChildren: false },
+ ]
+ break
+ case '1-1': // Cardiology sub-divisions
+ childrenData = [
+ { value: '1-1-1', label: 'Cardiac Surgery', hasChildren: false },
+ { value: '1-1-2', label: 'Cardiac Cathlab', hasChildren: false },
+ ]
+ break
+ case '2-3': // Ward Nursing sub-divisions
+ childrenData = [
+ { value: '2-3-1', label: 'Pediatric Ward', hasChildren: false },
+ { value: '2-3-2', label: 'Surgical Ward', hasChildren: false },
+ ]
+ break
+ }
+
+ // Insert data ke dalam tree state
+ findAndInsertChildren(divisionTreeData.value, parentId, childrenData)
+}
+
+export const divisionTreeConfig = computed(() => ({
+ msg: {
+ placeholder: '--- Pilih divisi induk',
+ search: 'Cari divisi...',
+ empty: 'Divisi tidak ditemukan',
+ },
+ data: divisionTreeData.value,
+ onFetchChildren: handleFetchDivisionChildren,
+}))
diff --git a/app/components/content/division/list.vue b/app/components/content/division/list.vue
index f6a3efc7..d77b67d3 100644
--- a/app/components/content/division/list.vue
+++ b/app/components/content/division/list.vue
@@ -1,13 +1,12 @@
+
+
+
+
diff --git a/app/components/pub/base/breadcrumb/index.vue b/app/components/pub/base/breadcrumb/index.vue
new file mode 100644
index 00000000..306ffe5b
--- /dev/null
+++ b/app/components/pub/base/breadcrumb/index.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ {{ link.title }}
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/base/select-tree/command-item.vue b/app/components/pub/base/select-tree/command-item.vue
new file mode 100644
index 00000000..e94bed4d
--- /dev/null
+++ b/app/components/pub/base/select-tree/command-item.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/app/components/pub/base/select-tree/leaf.vue b/app/components/pub/base/select-tree/leaf.vue
new file mode 100644
index 00000000..81dabf2b
--- /dev/null
+++ b/app/components/pub/base/select-tree/leaf.vue
@@ -0,0 +1,40 @@
+
+
+
+
+ handleSelect(item.value)"
+ >
+ {{ item.label }}
+
+
+
+
+
+
diff --git a/app/components/pub/base/select-tree/tree-node.vue b/app/components/pub/base/select-tree/tree-node.vue
new file mode 100644
index 00000000..e390b7bb
--- /dev/null
+++ b/app/components/pub/base/select-tree/tree-node.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+ {{ isLoading ? 'Memuat...' : 'Tidak ada data' }}
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/base/select-tree/tree-select.vue b/app/components/pub/base/select-tree/tree-select.vue
new file mode 100644
index 00000000..d0525336
--- /dev/null
+++ b/app/components/pub/base/select-tree/tree-select.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+ Item tidak ditemukan.
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/base/select-tree/tree-view.vue b/app/components/pub/base/select-tree/tree-view.vue
new file mode 100644
index 00000000..dfc51d5c
--- /dev/null
+++ b/app/components/pub/base/select-tree/tree-view.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/base/select-tree/type.ts b/app/components/pub/base/select-tree/type.ts
new file mode 100644
index 00000000..37d16761
--- /dev/null
+++ b/app/components/pub/base/select-tree/type.ts
@@ -0,0 +1,6 @@
+export interface TreeItem {
+ value: string
+ label: string
+ hasChildren: boolean
+ children?: TreeItem[]
+}
diff --git a/app/components/pub/custom-ui/doc-entry/block.vue b/app/components/pub/custom-ui/doc-entry/block.vue
new file mode 100644
index 00000000..9a1403b4
--- /dev/null
+++ b/app/components/pub/custom-ui/doc-entry/block.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/components/pub/custom-ui/doc-entry/cell.vue b/app/components/pub/custom-ui/doc-entry/cell.vue
new file mode 100644
index 00000000..f37259ce
--- /dev/null
+++ b/app/components/pub/custom-ui/doc-entry/cell.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
diff --git a/app/components/pub/custom-ui/doc-entry/field.vue b/app/components/pub/custom-ui/doc-entry/field.vue
new file mode 100644
index 00000000..f3b52106
--- /dev/null
+++ b/app/components/pub/custom-ui/doc-entry/field.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
{{ props.errMessage }}
+
+
diff --git a/app/components/pub/custom-ui/doc-entry/label.vue b/app/components/pub/custom-ui/doc-entry/label.vue
new file mode 100644
index 00000000..c3c5f338
--- /dev/null
+++ b/app/components/pub/custom-ui/doc-entry/label.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
diff --git a/app/composables/useTheme.ts b/app/composables/useTheme.ts
new file mode 100644
index 00000000..f028d4c6
--- /dev/null
+++ b/app/composables/useTheme.ts
@@ -0,0 +1,31 @@
+import { ref, watchEffect } from 'vue'
+
+const THEME_KEY = 'theme-mode'
+
+export function useTheme() {
+ const theme = ref<'light' | 'dark'>(getInitialTheme())
+
+ function getInitialTheme() {
+ if (typeof window === 'undefined') return 'light'
+ const persisted = localStorage.getItem(THEME_KEY)
+ if (persisted === 'dark' || persisted === 'light') return persisted
+ // fallback: system preference
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+ }
+
+ function setTheme(newTheme: 'light' | 'dark') {
+ theme.value = newTheme
+ localStorage.setItem(THEME_KEY, newTheme)
+ document.documentElement.classList.toggle('dark', newTheme === 'dark')
+ }
+
+ function toggleTheme() {
+ setTheme(theme.value === 'dark' ? 'light' : 'dark')
+ }
+
+ watchEffect(() => {
+ setTheme(theme.value)
+ })
+
+ return { theme, toggleTheme }
+}
diff --git a/app/layouts/default.vue b/app/layouts/default.vue
index 7f5959b7..1f62236f 100644
--- a/app/layouts/default.vue
+++ b/app/layouts/default.vue
@@ -45,6 +45,8 @@ const contentContent = computed(() => {
margin-right: auto;
border-radius: 0.375rem;
padding-bottom: 5rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
}
.cf-container > *,
@@ -56,9 +58,9 @@ const contentContent = computed(() => {
margin-right: auto;
padding: 0.75rem; /* p-3 */
padding-bottom: 5rem; /* pb-20 */
- border-width: 1px;
- background-color: white !important;
+ background-color: hsl(var(--background));
border-radius: 0.375rem;
+ border: 1px solid hsl(var(--border));
border-color: rgb(226 232 240); /* slate-200 */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
@@ -78,18 +80,58 @@ const contentContent = computed(() => {
.cf-frame-width {
margin-left: auto;
margin-right: auto;
- background-color: white;
- border: 1px solid rgb(226 232 240);
+ background-color: hsl(var(--background));
border-radius: 0.375rem;
+ border: 1px solid hsl(var(--border));
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ max-width: 100%;
+ padding: 1rem;
}
+
.cf-frame {
margin-left: auto;
margin-right: auto;
- padding: 0.75rem;
- background-color: white;
+ padding: 1rem;
+ background-color: hsl(var(--background));
border-radius: 0.375rem;
- border: 1px solid rgb(226 232 240);
+ border: 1px solid hsl(var(--border));
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ max-width: 100%;
+}
+
+@media (min-width: 640px) {
+ .cf-container,
+ .cf-container-lg,
+ .cf-container-md,
+ .cf-container-sm {
+ padding-left: 2rem;
+ padding-right: 2rem;
+ }
+
+ .cf-frame {
+ padding: 2rem;
+ }
+
+ .cf-frame-width {
+ padding: 2rem;
+ }
+}
+
+@media (min-width: 1024px) {
+ .cf-container,
+ .cf-container-lg,
+ .cf-container-md,
+ .cf-container-sm {
+ padding-left: 3rem;
+ padding-right: 3rem;
+ }
+
+ .cf-frame {
+ padding: 3rem;
+ }
+
+ .cf-frame-width {
+ padding: 3rem;
+ }
}
diff --git a/app/pages/_dev/user/list.vue b/app/pages/_dev/user/list.vue
index 4c5edc33..ae207f41 100644
--- a/app/pages/_dev/user/list.vue
+++ b/app/pages/_dev/user/list.vue
@@ -1,9 +1,81 @@
- test list user
+
+
Tree Select (Lazy Loading)
+
+
+ Value yang terpilih:
+ {{ selectedValue || 'Belum ada' }}
+
+
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 3894b099..9aa570ee 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -15,6 +15,28 @@ export default defineNuxtConfig({
},
ssr: false,
+ // SPA optimizations
+ router: {
+ options: {
+ hashMode: false, // Use history mode for cleaner URLs
+ },
+ },
+
+ // Enable client-side rendering optimizations
+ nitro: {
+ prerender: {
+ crawlLinks: false, // Disable crawling for SPA
+ },
+ },
+
+ // Optimize app loading
+ app: {
+ head: {
+ viewport: 'width=device-width,initial-scale=1',
+ charset: 'utf-8',
+ },
+ },
+
modules: [
'@unocss/nuxt',
'@vueuse/nuxt',
diff --git a/public/side-menu-items/sys.json b/public/side-menu-items/sys.json
index 488d9fa2..d89634e4 100644
--- a/public/side-menu-items/sys.json
+++ b/public/side-menu-items/sys.json
@@ -81,7 +81,7 @@
"link": "/rehabilitasi",
"children": [
{
- "title": "Antrian Pendaftaran",
+ "title": "Antrian Poliklinik",
"icon": "i-lucide-stethoscope",
"link": "/rehab/examination-queue"
},
@@ -171,14 +171,33 @@
{
"title": "BPJS",
"icon": "i-lucide-circuit-board",
- "link": "/integration/bpjs",
- "badge": "Live"
+ "children": [
+ {
+ "title": "SEP",
+ "icon": "i-lucide-circuit-board",
+ "link": "/bpjs-integration/sep"
+ },
+ {
+ "title": "Peserta",
+ "icon": "i-lucide-circuit-board",
+ "link": "/bpjs-integration/member"
+ }
+ ]
},
{
"title": "SATUSEHAT",
"icon": "i-lucide-database",
- "link": "/integration/satusehat",
- "badge": "FHIR"
+ "link": "/satusehat-integration"
+ },
+ {
+ "heading": "Keuangan",
+ "items": [
+ {
+ "title": "Daftar harga",
+ "icon": "i-lucide-list",
+ "link": "/item"
+ }
+ ]
}
]
},
@@ -306,4 +325,4 @@
}
]
}
-]
+]
\ No newline at end of file
diff --git a/server/api/v1/_dev/division/list.get.ts b/server/api/v1/_dev/division/list.get.ts
new file mode 100644
index 00000000..377e1592
--- /dev/null
+++ b/server/api/v1/_dev/division/list.get.ts
@@ -0,0 +1,163 @@
+export default defineEventHandler(async (event) => {
+ // Ambil query parameters
+ const payload = { ...getQuery(event) }
+ const isTreeFormat = payload.tree === 'true' || payload.tree === '1'
+
+ // Mock data division dengan struktur nested meta parent
+ const baseDivisions = [
+ {
+ id: 1,
+ name: 'Direktorat Medis',
+ code: 'DIR-MED',
+ parentId: null,
+ },
+ {
+ id: 2,
+ name: 'Bidang Medik',
+ code: 'BDG-MED',
+ parentId: 1,
+ },
+ {
+ id: 3,
+ name: 'Tim Kerja Ranap, ICU, Bedah',
+ code: 'TIM-RAN',
+ parentId: 2,
+ },
+ {
+ id: 4,
+ name: 'Direktorat Keperawatan',
+ code: 'DIR-KEP',
+ parentId: null,
+ },
+ {
+ id: 5,
+ name: 'Bidang Keperawatan',
+ code: 'BDG-KEP',
+ parentId: 4,
+ },
+ {
+ id: 6,
+ name: 'Tim Kerja Keperawatan Ranap, ICU, Bedah',
+ code: 'TIM-KEP',
+ parentId: 5,
+ },
+ {
+ id: 7,
+ name: 'Direktorat Penunjang',
+ code: 'DIR-PNJ',
+ parentId: null,
+ },
+ {
+ id: 8,
+ name: 'Bidang Penunjang Medik',
+ code: 'BDG-PNJ',
+ parentId: 7,
+ },
+ {
+ id: 9,
+ name: 'Tim Kerja Radiologi',
+ code: 'TIM-RAD',
+ parentId: 8,
+ },
+ {
+ id: 10,
+ name: 'Direktorat Produksi',
+ code: 'DIR-PRD',
+ parentId: null,
+ },
+ {
+ id: 11,
+ name: 'Bidang Teknologi',
+ code: 'BDG-TEK',
+ parentId: 10,
+ },
+ {
+ id: 12,
+ name: 'Tim Kerja Software Engineering',
+ code: 'TIM-SWE',
+ parentId: 11,
+ },
+ {
+ id: 13,
+ name: 'Direktorat Operasional',
+ code: 'DIR-OPS',
+ parentId: null,
+ },
+ {
+ id: 14,
+ name: 'Bidang HR & GA',
+ code: 'BDG-HRG',
+ parentId: 13,
+ },
+ {
+ id: 15,
+ name: 'Tim Kerja Rekrutmen',
+ code: 'TIM-REC',
+ parentId: 14,
+ },
+ ]
+
+ // Menambahkan meta parent pada setiap division
+ const divisions = baseDivisions
+ .map((division) => {
+ const parent = baseDivisions.find((d) => d.id === division.parentId)
+
+ const mapped = {
+ ...division,
+ meta: {
+ parentId: parent?.id || null,
+ name: parent?.name || null,
+ code: parent?.code || null,
+ },
+ }
+
+ if (mapped.meta.parentId === null) {
+ mapped.meta = null
+ }
+
+ return mapped
+ })
+ .sort((a, b) => {
+ if (a.parentId === null && b.parentId !== null) return -1
+ if (a.parentId !== null && b.parentId === null) return 1
+
+ return a.id - b.id
+ })
+
+ // Jika tree format diminta, konversi ke struktur hierarki
+ if (isTreeFormat) {
+ const buildTree = (parentId = null) => {
+ return baseDivisions
+ .filter(division => division.parentId === parentId)
+ .map(division => ({
+ id: division.id,
+ name: division.name,
+ code: division.code,
+ children: buildTree(division.id),
+ }))
+ .sort((a, b) => a.id - b.id)
+ }
+
+ const treeData = buildTree()
+
+ return {
+ success: true,
+ data: treeData,
+ message: 'Data division dalam format tree berhasil diambil',
+ meta: {
+ record_totalCount: baseDivisions.length,
+ format: 'tree',
+ },
+ }
+ }
+
+ return {
+ success: true,
+ data: divisions,
+ message: 'Data division berhasil diambil',
+ meta: {
+ record_totalCount: divisions.length,
+ format: 'flat',
+ },
+ }
+})