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 @@ + + 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 @@ + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + 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 @@ + + + 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 @@ 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', + }, + } +})