diff --git a/app.vue b/app.vue index 587c79d..be705dd 100644 --- a/app.vue +++ b/app.vue @@ -1,5 +1,70 @@ + + diff --git a/pages/Setting/MasterKlinikRuang.vue b/pages/Setting/MasterKlinikRuang.vue index 6195b23..e761a55 100644 --- a/pages/Setting/MasterKlinikRuang.vue +++ b/pages/Setting/MasterKlinikRuang.vue @@ -376,20 +376,21 @@ definePageMeta({ middleware: ['auth', 'check-page-access'] }) -import { ref, onMounted } from 'vue'; +import { ref, onMounted, computed, watch } from 'vue'; import { useMasterStore } from '@/stores/masterStore'; import { useClinicStore } from '@/stores/clinicStore'; import { useRuangStore } from '@/stores/ruangStore'; +import { useKlinikRuangStore } from '@/stores/klinikruangstore'; const masterStore = useMasterStore(); const clinicStore = useClinicStore(); const ruangStore = useRuangStore(); +const klinikRuangStore = useKlinikRuangStore(); const page = ref(1); const itemsPerPage = ref(10); const search = ref(''); const filteredTotal = ref(masterStore.ruangData.length); -import { watch } from 'vue'; watch(() => masterStore.ruangData.length, (newLen) => { if (!search.value) filteredTotal.value = newLen; }, { immediate: true }); @@ -511,9 +512,9 @@ const submitForm = async () => { let result; if (isEdit.value) { - result = masterStore.updateRuang(formData.value); + result = await klinikRuangStore.updateKlinikRuang(formData.value.id, formData.value); } else { - result = masterStore.addRuang(formData.value); + result = await klinikRuangStore.createKlinikRuang(formData.value); } if (result.success) { @@ -524,14 +525,10 @@ const submitForm = async () => { } }; -const handleDelete = (item) => { - if (confirm(`Hapus ruangan ${item.namaRuang} dari klinik ${item.namaKlinik}?`)) { - const result = masterStore.deleteRuang(item.id); - snackbar.value = { - show: true, - message: result.message, - color: result.success ? 'success' : 'error' - }; +const handleDelete = async (item) => { + if (confirm(`Hapus ruangan dari klinik ${item.namaKlinik}?`)) { + const result = await klinikRuangStore.deleteKlinikRuang(item.id); + snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' }; } }; @@ -552,36 +549,16 @@ const closePreviewDialog = () => { // Fetch clinics and sync rooms on mount onMounted(async () => { console.log('🚀 MasterKlinikRuang mounted, syncing data...'); - try { - // 1. Fetch clinics from API (uses cache if available) - const fetchClinicResult = await clinicStore.fetchRegulerClinics(); - console.log('đŸ“Ĩ Clinic fetch result:', fetchClinicResult); - - if (!fetchClinicResult.success) { - snackbar.value = { - show: true, - message: `Klinik: ${fetchClinicResult.message}`, - color: 'warning' - }; - } - - // 2. Fetch and merge rooms from API - const syncRuangResult = await ruangStore.fetchRuangFromAPI(); - console.log('🔄 Room sync result:', syncRuangResult); - - snackbar.value = { - show: true, - message: syncRuangResult.message, - color: syncRuangResult.success ? 'success' : 'error' - }; + await Promise.all([ + clinicStore.fetchRegulerClinics(), + klinikRuangStore.fetchKlinikRuang(), + ruangStore.fetchRuangFromAPI(), + ]); + console.log('✅ MasterKlinikRuang data loaded'); } catch (error) { console.error('❌ Error in onMounted:', error); - snackbar.value = { - show: true, - message: `Error: ${error.message}`, - color: 'error' - }; + snackbar.value = { show: true, message: `Error: ${error.message}`, color: 'error' }; } }); diff --git a/pages/Setting/Screen.vue b/pages/Setting/Screen.vue index cabfd74..59f92bc 100644 --- a/pages/Setting/Screen.vue +++ b/pages/Setting/Screen.vue @@ -271,12 +271,16 @@ definePageMeta({ middleware: ['auth', 'check-page-access'] }) -import { ref, computed } from 'vue'; +import { ref, computed, onMounted } from 'vue'; import { useMasterStore } from '@/stores/masterStore'; import { useScreenStore } from '@/stores/screenStore'; const masterStore = useMasterStore(); const screenStore = useScreenStore(); + +onMounted(() => { + screenStore.fetchScreens(); +}); const dialog = ref(false); const isEdit = ref(false); const formRef = ref(null); @@ -348,34 +352,20 @@ const submitForm = async () => { if (!valid) return; if (isEdit.value) { - // Update screen - const result = screenStore.updateScreen(formData.value); - snackbar.value = { - show: true, - message: result.message, - color: result.success ? 'success' : 'error' - }; + const result = await screenStore.updateScreen(formData.value); + snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' }; } else { - // Add new screen - const result = screenStore.addScreen(formData.value); - snackbar.value = { - show: true, - message: result.message, - color: result.success ? 'success' : 'error' - }; + const result = await screenStore.addScreen(formData.value); + snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' }; } - closeDialog(); + if (snackbar.value.color === 'success') closeDialog(); }; -const handleDelete = (item) => { +const handleDelete = async (item) => { if (confirm(`Hapus screen ${item.namaScreen}?`)) { - const result = screenStore.deleteScreen(item.id); - snackbar.value = { - show: true, - message: result.message, - color: result.success ? 'success' : 'error' - }; + const result = await screenStore.deleteScreen(item.id); + snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' }; } }; diff --git a/pages/Setting/ScreenAntreanMasuk.vue b/pages/Setting/ScreenAntreanMasuk.vue index 115f48c..ae911b2 100644 --- a/pages/Setting/ScreenAntreanMasuk.vue +++ b/pages/Setting/ScreenAntreanMasuk.vue @@ -316,9 +316,9 @@ const initData = async () => { // Call immediately (non-blocking) initData(); -// Also try onMounted for good measure -onMounted(() => { - initData(); +onMounted(async () => { + await antreanMasukScreenStore.fetchAntreanMasukScreens(); + initData(); }); const refreshData = async () => { @@ -429,34 +429,20 @@ const submitForm = async () => { if (!valid) return; if (isEdit.value) { - // Update screen - const result = antreanMasukScreenStore.updateAntreanMasukScreen(formData.value); - snackbar.value = { - show: true, - message: result.message, - color: result.success ? 'success' : 'error' - }; + const result = await antreanMasukScreenStore.updateAntreanMasukScreen(formData.value); + snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' }; } else { - // Add new screen - const result = antreanMasukScreenStore.addAntreanMasukScreen(formData.value); - snackbar.value = { - show: true, - message: result.message, - color: result.success ? 'success' : 'error' - }; + const result = await antreanMasukScreenStore.addAntreanMasukScreen(formData.value); + snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' }; } - closeDialog(); + if (snackbar.value.color === 'success') closeDialog(); }; -const handleDelete = (item) => { +const handleDelete = async (item) => { if (confirm(`Hapus screen ${item.namaScreen}?`)) { - const result = antreanMasukScreenStore.deleteAntreanMasukScreen(item.id); - snackbar.value = { - show: true, - message: result.message, - color: result.success ? 'success' : 'error' - }; + const result = await antreanMasukScreenStore.deleteAntreanMasukScreen(item.id); + snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' }; } }; diff --git a/server/api/config/anjungan.get.ts b/server/api/config/anjungan.get.ts new file mode 100644 index 0000000..cefc264 --- /dev/null +++ b/server/api/config/anjungan.get.ts @@ -0,0 +1,8 @@ +// server/api/config/anjungan.get.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(() => { + const db = getConfigDb(); + const rows = db.prepare('SELECT * FROM config_anjungan ORDER BY id ASC').all(); + return { success: true, data: rows.map(parseConfigRow) }; +}); diff --git a/server/api/config/anjungan.post.ts b/server/api/config/anjungan.post.ts new file mode 100644 index 0000000..1ca282c --- /dev/null +++ b/server/api/config/anjungan.post.ts @@ -0,0 +1,19 @@ +// server/api/config/anjungan.post.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const { namaAnjungan, jenisPasien, klinik = [] } = body; + + if (!namaAnjungan || !jenisPasien) { + throw createError({ statusCode: 400, message: 'namaAnjungan dan jenisPasien wajib diisi' }); + } + + const db = getConfigDb(); + const result = db.prepare( + `INSERT INTO config_anjungan (namaAnjungan, jenisPasien, klinik) VALUES (?, ?, ?)` + ).run(namaAnjungan, jenisPasien, JSON.stringify(klinik)); + + const newRow = db.prepare('SELECT * FROM config_anjungan WHERE id = ?').get(result.lastInsertRowid); + return { success: true, data: parseConfigRow(newRow), message: `Anjungan ${namaAnjungan} berhasil ditambahkan` }; +}); diff --git a/server/api/config/anjungan/[id].delete.ts b/server/api/config/anjungan/[id].delete.ts new file mode 100644 index 0000000..738dc57 --- /dev/null +++ b/server/api/config/anjungan/[id].delete.ts @@ -0,0 +1,14 @@ +// server/api/config/anjungan/[id].delete.ts +import { getConfigDb } from '~/server/utils/configDb'; + +export default defineEventHandler((event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_anjungan WHERE id = ?').get(id) as any; + if (!existing) throw createError({ statusCode: 404, message: 'Anjungan tidak ditemukan' }); + + db.prepare('DELETE FROM config_anjungan WHERE id = ?').run(id); + return { success: true, message: `Anjungan ${existing.namaAnjungan} berhasil dihapus` }; +}); diff --git a/server/api/config/anjungan/[id].put.ts b/server/api/config/anjungan/[id].put.ts new file mode 100644 index 0000000..a58bcad --- /dev/null +++ b/server/api/config/anjungan/[id].put.ts @@ -0,0 +1,31 @@ +// server/api/config/anjungan/[id].put.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const body = await readBody(event); + const { namaAnjungan, jenisPasien, klinik } = body; + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_anjungan WHERE id = ?').get(id) as any; + if (!existing) throw createError({ statusCode: 404, message: 'Anjungan tidak ditemukan' }); + + db.prepare( + `UPDATE config_anjungan SET + namaAnjungan = ?, + jenisPasien = ?, + klinik = ?, + updatedAt = strftime('%s', 'now') + WHERE id = ?` + ).run( + namaAnjungan ?? existing.namaAnjungan, + jenisPasien ?? existing.jenisPasien, + klinik !== undefined ? JSON.stringify(klinik) : existing.klinik, + id + ); + + const updated = db.prepare('SELECT * FROM config_anjungan WHERE id = ?').get(id); + return { success: true, data: parseConfigRow(updated), message: 'Anjungan berhasil diperbarui' }; +}); diff --git a/server/api/config/antrean-masuk-screen.get.ts b/server/api/config/antrean-masuk-screen.get.ts new file mode 100644 index 0000000..a0da0a6 --- /dev/null +++ b/server/api/config/antrean-masuk-screen.get.ts @@ -0,0 +1,8 @@ +// server/api/config/antrean-masuk-screen.get.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(() => { + const db = getConfigDb(); + const rows = db.prepare('SELECT * FROM config_antrean_masuk_screen ORDER BY id ASC').all(); + return { success: true, data: rows.map(parseConfigRow) }; +}); diff --git a/server/api/config/antrean-masuk-screen.post.ts b/server/api/config/antrean-masuk-screen.post.ts new file mode 100644 index 0000000..a680811 --- /dev/null +++ b/server/api/config/antrean-masuk-screen.post.ts @@ -0,0 +1,26 @@ +// server/api/config/antrean-masuk-screen.post.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const { namaScreen, nomorScreen, loket = [] } = body; + + if (!namaScreen || !nomorScreen) { + throw createError({ statusCode: 400, message: 'namaScreen dan nomorScreen wajib diisi' }); + } + + const db = getConfigDb(); + try { + const result = db.prepare( + `INSERT INTO config_antrean_masuk_screen (namaScreen, nomorScreen, loket) VALUES (?, ?, ?)` + ).run(namaScreen, nomorScreen, JSON.stringify(loket)); + + const newRow = db.prepare('SELECT * FROM config_antrean_masuk_screen WHERE id = ?').get(result.lastInsertRowid); + return { success: true, data: parseConfigRow(newRow), message: `Screen ${namaScreen} berhasil ditambahkan` }; + } catch (e: any) { + if (e.message?.includes('UNIQUE')) { + throw createError({ statusCode: 409, message: `nomorScreen "${nomorScreen}" sudah digunakan` }); + } + throw createError({ statusCode: 500, message: e.message }); + } +}); diff --git a/server/api/config/antrean-masuk-screen/[id].delete.ts b/server/api/config/antrean-masuk-screen/[id].delete.ts new file mode 100644 index 0000000..a63a2bc --- /dev/null +++ b/server/api/config/antrean-masuk-screen/[id].delete.ts @@ -0,0 +1,14 @@ +// server/api/config/antrean-masuk-screen/[id].delete.ts +import { getConfigDb } from '~/server/utils/configDb'; + +export default defineEventHandler((event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_antrean_masuk_screen WHERE id = ?').get(id) as any; + if (!existing) throw createError({ statusCode: 404, message: 'Screen tidak ditemukan' }); + + db.prepare('DELETE FROM config_antrean_masuk_screen WHERE id = ?').run(id); + return { success: true, message: `Screen ${existing.namaScreen} berhasil dihapus` }; +}); diff --git a/server/api/config/antrean-masuk-screen/[id].put.ts b/server/api/config/antrean-masuk-screen/[id].put.ts new file mode 100644 index 0000000..abaa8ff --- /dev/null +++ b/server/api/config/antrean-masuk-screen/[id].put.ts @@ -0,0 +1,29 @@ +// server/api/config/antrean-masuk-screen/[id].put.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const body = await readBody(event); + const { namaScreen, nomorScreen, loket } = body; + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_antrean_masuk_screen WHERE id = ?').get(id) as any; + if (!existing) throw createError({ statusCode: 404, message: 'Screen tidak ditemukan' }); + + db.prepare( + `UPDATE config_antrean_masuk_screen SET + namaScreen = ?, nomorScreen = ?, loket = ?, + updatedAt = strftime('%s', 'now') + WHERE id = ?` + ).run( + namaScreen ?? existing.namaScreen, + nomorScreen ?? existing.nomorScreen, + loket !== undefined ? JSON.stringify(loket) : existing.loket, + id + ); + + const updated = db.prepare('SELECT * FROM config_antrean_masuk_screen WHERE id = ?').get(id); + return { success: true, data: parseConfigRow(updated), message: 'Screen berhasil diperbarui' }; +}); diff --git a/server/api/config/klinik-ruang.get.ts b/server/api/config/klinik-ruang.get.ts new file mode 100644 index 0000000..c477728 --- /dev/null +++ b/server/api/config/klinik-ruang.get.ts @@ -0,0 +1,8 @@ +// server/api/config/klinik-ruang.get.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(() => { + const db = getConfigDb(); + const rows = db.prepare('SELECT * FROM config_klinik_ruang ORDER BY id ASC').all(); + return { success: true, data: rows.map(parseConfigRow) }; +}); diff --git a/server/api/config/klinik-ruang.post.ts b/server/api/config/klinik-ruang.post.ts new file mode 100644 index 0000000..822165a --- /dev/null +++ b/server/api/config/klinik-ruang.post.ts @@ -0,0 +1,19 @@ +// server/api/config/klinik-ruang.post.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const { kodeKlinik, namaKlinik, ruangList = [] } = body; + + if (!kodeKlinik || !namaKlinik) { + throw createError({ statusCode: 400, message: 'kodeKlinik dan namaKlinik wajib diisi' }); + } + + const db = getConfigDb(); + const result = db.prepare( + `INSERT INTO config_klinik_ruang (kodeKlinik, namaKlinik, ruangList) VALUES (?, ?, ?)` + ).run(kodeKlinik, namaKlinik, JSON.stringify(ruangList)); + + const newRow = db.prepare('SELECT * FROM config_klinik_ruang WHERE id = ?').get(result.lastInsertRowid); + return { success: true, data: parseConfigRow(newRow), message: `Klinik Ruang ${namaKlinik} berhasil ditambahkan` }; +}); diff --git a/server/api/config/klinik-ruang/[id].delete.ts b/server/api/config/klinik-ruang/[id].delete.ts new file mode 100644 index 0000000..075a57a --- /dev/null +++ b/server/api/config/klinik-ruang/[id].delete.ts @@ -0,0 +1,14 @@ +// server/api/config/klinik-ruang/[id].delete.ts +import { getConfigDb } from '~/server/utils/configDb'; + +export default defineEventHandler((event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_klinik_ruang WHERE id = ?').get(id) as any; + if (!existing) throw createError({ statusCode: 404, message: 'Klinik Ruang tidak ditemukan' }); + + db.prepare('DELETE FROM config_klinik_ruang WHERE id = ?').run(id); + return { success: true, message: `Klinik Ruang ${existing.namaKlinik} berhasil dihapus` }; +}); diff --git a/server/api/config/klinik-ruang/[id].put.ts b/server/api/config/klinik-ruang/[id].put.ts new file mode 100644 index 0000000..e23884d --- /dev/null +++ b/server/api/config/klinik-ruang/[id].put.ts @@ -0,0 +1,29 @@ +// server/api/config/klinik-ruang/[id].put.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const body = await readBody(event); + const { kodeKlinik, namaKlinik, ruangList } = body; + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_klinik_ruang WHERE id = ?').get(id) as any; + if (!existing) throw createError({ statusCode: 404, message: 'Klinik Ruang tidak ditemukan' }); + + db.prepare( + `UPDATE config_klinik_ruang SET + kodeKlinik = ?, namaKlinik = ?, ruangList = ?, + updatedAt = strftime('%s', 'now') + WHERE id = ?` + ).run( + kodeKlinik ?? existing.kodeKlinik, + namaKlinik ?? existing.namaKlinik, + ruangList !== undefined ? JSON.stringify(ruangList) : existing.ruangList, + id + ); + + const updated = db.prepare('SELECT * FROM config_klinik_ruang WHERE id = ?').get(id); + return { success: true, data: parseConfigRow(updated), message: 'Klinik Ruang berhasil diperbarui' }; +}); diff --git a/server/api/config/screen.get.ts b/server/api/config/screen.get.ts new file mode 100644 index 0000000..4b8f82a --- /dev/null +++ b/server/api/config/screen.get.ts @@ -0,0 +1,8 @@ +// server/api/config/screen.get.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(() => { + const db = getConfigDb(); + const rows = db.prepare('SELECT * FROM config_screen ORDER BY id ASC').all(); + return { success: true, data: rows.map(parseConfigRow) }; +}); diff --git a/server/api/config/screen.post.ts b/server/api/config/screen.post.ts new file mode 100644 index 0000000..7030d16 --- /dev/null +++ b/server/api/config/screen.post.ts @@ -0,0 +1,26 @@ +// server/api/config/screen.post.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const body = await readBody(event); + const { namaScreen, nomorScreen, klinik = [] } = body; + + if (!namaScreen || !nomorScreen) { + throw createError({ statusCode: 400, message: 'namaScreen dan nomorScreen wajib diisi' }); + } + + const db = getConfigDb(); + try { + const result = db.prepare( + `INSERT INTO config_screen (namaScreen, nomorScreen, klinik) VALUES (?, ?, ?)` + ).run(namaScreen, nomorScreen, JSON.stringify(klinik)); + + const newRow = db.prepare('SELECT * FROM config_screen WHERE id = ?').get(result.lastInsertRowid); + return { success: true, data: parseConfigRow(newRow), message: `Screen ${namaScreen} berhasil ditambahkan` }; + } catch (e: any) { + if (e.message?.includes('UNIQUE')) { + throw createError({ statusCode: 409, message: `nomorScreen "${nomorScreen}" sudah digunakan` }); + } + throw createError({ statusCode: 500, message: e.message }); + } +}); diff --git a/server/api/config/screen/[id].delete.ts b/server/api/config/screen/[id].delete.ts new file mode 100644 index 0000000..39d0d34 --- /dev/null +++ b/server/api/config/screen/[id].delete.ts @@ -0,0 +1,14 @@ +// server/api/config/screen/[id].delete.ts +import { getConfigDb } from '~/server/utils/configDb'; + +export default defineEventHandler((event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_screen WHERE id = ?').get(id) as any; + if (!existing) throw createError({ statusCode: 404, message: 'Screen tidak ditemukan' }); + + db.prepare('DELETE FROM config_screen WHERE id = ?').run(id); + return { success: true, message: `Screen ${existing.namaScreen} berhasil dihapus` }; +}); diff --git a/server/api/config/screen/[id].put.ts b/server/api/config/screen/[id].put.ts new file mode 100644 index 0000000..30c809b --- /dev/null +++ b/server/api/config/screen/[id].put.ts @@ -0,0 +1,31 @@ +// server/api/config/screen/[id].put.ts +import { getConfigDb, parseConfigRow } from '~/server/utils/configDb'; + +export default defineEventHandler(async (event) => { + const id = Number(getRouterParam(event, 'id')); + if (!id) throw createError({ statusCode: 400, message: 'ID tidak valid' }); + + const body = await readBody(event); + const { namaScreen, nomorScreen, klinik } = body; + + const db = getConfigDb(); + const existing = db.prepare('SELECT * FROM config_screen WHERE id = ?').get(id); + if (!existing) throw createError({ statusCode: 404, message: 'Screen tidak ditemukan' }); + + db.prepare( + `UPDATE config_screen SET + namaScreen = ?, + nomorScreen = ?, + klinik = ?, + updatedAt = strftime('%s', 'now') + WHERE id = ?` + ).run( + namaScreen ?? (existing as any).namaScreen, + nomorScreen ?? (existing as any).nomorScreen, + klinik !== undefined ? JSON.stringify(klinik) : (existing as any).klinik, + id + ); + + const updated = db.prepare('SELECT * FROM config_screen WHERE id = ?').get(id); + return { success: true, data: parseConfigRow(updated), message: 'Screen berhasil diperbarui' }; +}); diff --git a/server/routes/klinik-api/[...].ts b/server/routes/klinik-api/[...].ts index 1f50aa5..4a7d3c0 100644 --- a/server/routes/klinik-api/[...].ts +++ b/server/routes/klinik-api/[...].ts @@ -16,12 +16,13 @@ export default defineEventHandler(async (event) => { const urlObj = new URL(targetBase) headers.host = urlObj.host } catch (e) { - headers.host = '10.10.123.140:8089' + headers.host = config.proxyTargetHostKlinikFallback || '10.10.123.140:8089' } // Spoof the origin and referer to match the server's trusted/whitelisted client origin - headers.origin = 'http://10.10.150.175:3000' - headers.referer = 'http://10.10.150.175:3000/' + const proxyOrigin = config.proxyClientOrigin || 'http://10.10.150.175:3000' + headers.origin = proxyOrigin + headers.referer = proxyOrigin.endsWith('/') ? proxyOrigin : `${proxyOrigin}/` console.log(`[Proxy Klinik-API] Forwarding ${event.node.req.method} to: ${targetUrl}`) console.log(`[Proxy Klinik-API] Headers - Host: ${headers.host}, Origin: ${headers.origin}`) diff --git a/server/routes/stats-api/[...].ts b/server/routes/stats-api/[...].ts index 4ef8d3e..2462c9e 100644 --- a/server/routes/stats-api/[...].ts +++ b/server/routes/stats-api/[...].ts @@ -16,12 +16,13 @@ export default defineEventHandler(async (event) => { const urlObj = new URL(targetBase) headers.host = urlObj.host } catch (e) { - headers.host = '10.10.123.135:8084' + headers.host = config.proxyTargetHostFallback || '10.10.123.135:8084' } // Spoof the origin and referer to match the server's trusted/whitelisted client origin - headers.origin = 'http://10.10.150.175:3000' - headers.referer = 'http://10.10.150.175:3000/' + const proxyOrigin = config.proxyClientOrigin || 'http://10.10.150.175:3000' + headers.origin = proxyOrigin + headers.referer = proxyOrigin.endsWith('/') ? proxyOrigin : `${proxyOrigin}/` console.log(`[Proxy Stats-API] Forwarding ${event.node.req.method} to: ${targetUrl}`) console.log(`[Proxy Stats-API] Headers - Host: ${headers.host}, Origin: ${headers.origin}`) diff --git a/server/routes/visit-api/[...].ts b/server/routes/visit-api/[...].ts index b91b65d..4129664 100644 --- a/server/routes/visit-api/[...].ts +++ b/server/routes/visit-api/[...].ts @@ -16,12 +16,13 @@ export default defineEventHandler(async (event) => { const urlObj = new URL(targetBase) headers.host = urlObj.host } catch (e) { - headers.host = '10.10.123.135:8084' + headers.host = config.proxyTargetHostFallback || '10.10.123.135:8084' } // Spoof the origin and referer to match the server's trusted/whitelisted client origin - headers.origin = 'http://10.10.150.175:3000' - headers.referer = 'http://10.10.150.175:3000/' + const proxyOrigin = config.proxyClientOrigin || 'http://10.10.150.175:3000' + headers.origin = proxyOrigin + headers.referer = proxyOrigin.endsWith('/') ? proxyOrigin : `${proxyOrigin}/` console.log(`[Proxy Visit-API] Forwarding ${event.node.req.method} to: ${targetUrl}`) console.log(`[Proxy Visit-API] Headers - Host: ${headers.host}, Origin: ${headers.origin}`) diff --git a/server/utils/configDb.ts b/server/utils/configDb.ts new file mode 100644 index 0000000..4feabe3 --- /dev/null +++ b/server/utils/configDb.ts @@ -0,0 +1,124 @@ +// server/utils/configDb.ts +// Shared SQLite utility for device configuration (screens, anjungan, klinik-ruang, etc.) +// Designed to be a temporary layer - can be swapped to an external API with minimal changes. + +import Database from 'better-sqlite3'; +import { join } from 'path'; +import { existsSync, mkdirSync } from 'fs'; + +const getDbPath = () => { + const dbDir = join(process.cwd(), 'data'); + if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true }); + return join(dbDir, 'users.db'); // Reuse same DB file +}; + +// ─── Default Seed Data ─────────────────────────────────────────────────────── +// Used when tables are empty on first run. Mirrors the hardcoded defaults in Pinia stores. + +const SEED_SCREENS = [ + { namaScreen: 'Layar Screen 1', nomorScreen: 'SCR-001', klinik: JSON.stringify(['AN', 'AS', 'BD', 'GI', 'GR', 'GZ', 'IP', 'JT']) }, + { namaScreen: 'Layar Screen 2', nomorScreen: 'SCR-002', klinik: JSON.stringify(['JW', 'KK', 'MT', 'SR', 'OB', 'PR']) }, + { namaScreen: 'Layar Screen 3', nomorScreen: 'SCR-003', klinik: JSON.stringify(['RT', 'RM', 'HO']) }, +]; + +const SEED_ANJUNGAN = [ + { namaAnjungan: 'Anjungan Reguler', jenisPasien: 'Reguler', klinik: JSON.stringify(['AK', 'AN', 'BD', 'GR', 'GM', 'GZ', 'HO', 'IP', 'JT', 'JW', 'KD', 'KK', 'KM', 'KO', 'MC', 'MT', 'ON', 'PR', 'RD', 'RM', 'RT', 'SR', 'TH']) }, + { namaAnjungan: 'Anjungan Eksekutif', jenisPasien: 'Eksekutif', klinik: JSON.stringify(['AK', 'AN', 'BD', 'GM', 'GR', 'GZ', 'HO', 'IP', 'JT', 'JW', 'KD', 'KK', 'KM', 'KO', 'MC', 'MT', 'ON', 'PR', 'RD', 'RM', 'RT', 'SR', 'TH']) }, +]; + +const SEED_ANTREAN_MASUK_SCREENS = [ + { namaScreen: 'Layar Antrean Masuk 1', nomorScreen: 'AM-001', loket: JSON.stringify([1, 2, 12, 14]) }, + { namaScreen: 'Layar Antrean Masuk 2', nomorScreen: 'AM-002', loket: JSON.stringify([3, 4]) }, +]; + +const SEED_KLINIK_RUANG = [ + { kodeKlinik: 'AN', namaKlinik: 'ANAK', ruangList: JSON.stringify([{ nomorRuang: '1', namaRuang: 'R. TINDAKAN', nomorScreen: '101' }]) }, + { kodeKlinik: 'AS', namaKlinik: 'ANESTESI', ruangList: JSON.stringify([{ nomorRuang: '1', namaRuang: 'Ruang 1', nomorScreen: '201' }, { nomorRuang: '2', namaRuang: 'Ruang 2', nomorScreen: '202' }, { nomorRuang: '3', namaRuang: 'Ruang 3', nomorScreen: '203' }]) }, + { kodeKlinik: 'BD', namaKlinik: 'BEDAH', ruangList: JSON.stringify([{ nomorRuang: '1', namaRuang: 'Ruang Konsultasi', nomorScreen: '301' }]) }, + { kodeKlinik: 'GR', namaKlinik: 'GERIATRI', ruangList: JSON.stringify([{ nomorRuang: '1', namaRuang: 'Ruang Pemeriksaan', nomorScreen: '401' }]) }, + { kodeKlinik: 'GI', namaKlinik: 'GIGI DAN MULUT', ruangList: JSON.stringify([{ nomorRuang: '1', namaRuang: 'Ruang 1', nomorScreen: '501' }, { nomorRuang: '2', namaRuang: 'Ruang 2', nomorScreen: '502' }, { nomorRuang: '3', namaRuang: 'Ruang 3', nomorScreen: '503' }]) }, +]; + +// ─── DB Initialization ──────────────────────────────────────────────────────── + +let _db: InstanceType | null = null; + +export const getConfigDb = () => { + if (_db) return _db; + + const dbPath = getDbPath(); + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); // Better concurrent performance + + db.exec(` + CREATE TABLE IF NOT EXISTS config_screen ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + namaScreen TEXT NOT NULL, + nomorScreen TEXT UNIQUE NOT NULL, + klinik TEXT DEFAULT '[]', + createdAt INTEGER DEFAULT (strftime('%s', 'now')), + updatedAt INTEGER DEFAULT (strftime('%s', 'now')) + ); + + CREATE TABLE IF NOT EXISTS config_anjungan ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + namaAnjungan TEXT NOT NULL, + jenisPasien TEXT NOT NULL, + klinik TEXT DEFAULT '[]', + createdAt INTEGER DEFAULT (strftime('%s', 'now')), + updatedAt INTEGER DEFAULT (strftime('%s', 'now')) + ); + + CREATE TABLE IF NOT EXISTS config_antrean_masuk_screen ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + namaScreen TEXT NOT NULL, + nomorScreen TEXT UNIQUE NOT NULL, + loket TEXT DEFAULT '[]', + createdAt INTEGER DEFAULT (strftime('%s', 'now')), + updatedAt INTEGER DEFAULT (strftime('%s', 'now')) + ); + + CREATE TABLE IF NOT EXISTS config_klinik_ruang ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kodeKlinik TEXT NOT NULL, + namaKlinik TEXT NOT NULL, + ruangList TEXT DEFAULT '[]', + createdAt INTEGER DEFAULT (strftime('%s', 'now')), + updatedAt INTEGER DEFAULT (strftime('%s', 'now')) + ); + `); + + // Seed defaults if tables are empty + _seedIfEmpty(db, 'config_screen', SEED_SCREENS); + _seedIfEmpty(db, 'config_anjungan', SEED_ANJUNGAN); + _seedIfEmpty(db, 'config_antrean_masuk_screen', SEED_ANTREAN_MASUK_SCREENS); + _seedIfEmpty(db, 'config_klinik_ruang', SEED_KLINIK_RUANG); + + console.log('✅ [configDb] Config database initialized'); + _db = db; + return db; +}; + +function _seedIfEmpty(db: InstanceType, table: string, rows: Record[]) { + const count = (db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get() as any).c; + if (count === 0) { + console.log(`🌱 [configDb] Seeding ${table} with ${rows.length} default rows`); + const keys = Object.keys(rows[0]); + const placeholders = keys.map(() => '?').join(', '); + const stmt = db.prepare(`INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`); + rows.forEach(row => stmt.run(...keys.map(k => row[k]))); + } +} + +// ─── Helper: Parse JSON columns ─────────────────────────────────────────────── +export const parseConfigRow = (row: any) => { + if (!row) return null; + const parsed = { ...row }; + // Auto-parse any column that looks like a JSON array/object + for (const key of Object.keys(parsed)) { + if (typeof parsed[key] === 'string' && (parsed[key].startsWith('[') || parsed[key].startsWith('{'))) { + try { parsed[key] = JSON.parse(parsed[key]); } catch {} + } + } + return parsed; +}; diff --git a/stores/anjunganStore.js b/stores/anjunganStore.js index 2bb41ff..77a5fb2 100644 --- a/stores/anjunganStore.js +++ b/stores/anjunganStore.js @@ -1,23 +1,12 @@ // stores/anjunganStore.js +// Konfigurasi anjungan — data disimpan di server SQLite via /api/config/anjungan import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; export const useAnjunganStore = defineStore('anjungan', () => { - // Seed: existing static Anjungan page sebagai id 1 - const anjunganItems = ref([ - { - id: 1, - namaAnjungan: 'Anjungan Reguler', - jenisPasien: 'Reguler', - klinik: ['AK', 'AN', 'BD', 'GR', 'GM', 'GZ', 'HO', 'IP', 'JT', 'JW', 'KD', 'KK', 'KM', 'KO', 'MC', 'MT', 'ON', 'PR', 'RD', 'RM', 'RT', 'SR', 'TH'], - }, - { - id: 2, - namaAnjungan: 'Anjungan Eksekutif', - jenisPasien: 'Eksekutif', - klinik: ['AK', 'AN', 'BD', 'GM', 'GR', 'GZ', 'HO', 'IP', 'JT', 'JW', 'KD', 'KK', 'KM', 'KO', 'MC', 'MT', 'ON', 'PR', 'RD', 'RM', 'RT', 'SR', 'TH'], - }, - ]); + const anjunganItems = ref([]); + const isLoading = ref(false); + const error = ref(null); const getAllAnjungan = computed(() => anjunganItems.value); @@ -28,66 +17,65 @@ export const useAnjunganStore = defineStore('anjungan', () => { }); }; - const addAnjungan = (payload) => { - const maxId = - anjunganItems.value.length > 0 - ? Math.max(...anjunganItems.value.map((a) => Number(a.id) || 0)) - : 0; - const newId = maxId + 1; - const newItem = { - ...payload, - id: newId, - }; - anjunganItems.value.push(newItem); - return { - success: true, - message: `Anjungan ${newItem.namaAnjungan} berhasil ditambahkan`, - data: newItem, - }; + // ── Fetch from API ───────────────────────────────────────────────────────── + const fetchAnjungan = async () => { + if (isLoading.value) return; + isLoading.value = true; + error.value = null; + try { + const res = await $fetch('/api/config/anjungan'); + anjunganItems.value = res.data || []; + } catch (e) { + error.value = e.message; + console.error('❌ [anjunganStore] Gagal fetch anjungan config:', e); + } finally { + isLoading.value = false; + } }; - const updateAnjungan = (payload) => { - const idx = anjunganItems.value.findIndex( - (a) => Number(a.id) === Number(payload.id) - ); - if (idx === -1) { - return { success: false, message: 'Anjungan tidak ditemukan' }; + // ── CRUD via API ─────────────────────────────────────────────────────────── + const addAnjungan = async (payload) => { + try { + const res = await $fetch('/api/config/anjungan', { method: 'POST', body: payload }); + anjunganItems.value.push(res.data); + return { success: true, message: res.message, data: res.data }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; } - anjunganItems.value[idx] = { - ...anjunganItems.value[idx], - ...payload, - }; - return { - success: true, - message: `Anjungan ${payload.namaAnjungan} berhasil diperbarui`, - }; }; - const deleteAnjungan = (id) => { - const idx = anjunganItems.value.findIndex( - (a) => Number(a.id) === Number(id) - ); - if (idx === -1) { - return { success: false, message: 'Anjungan tidak ditemukan' }; + const updateAnjungan = async (payload) => { + try { + const res = await $fetch(`/api/config/anjungan/${payload.id}`, { method: 'PUT', body: payload }); + const idx = anjunganItems.value.findIndex((a) => Number(a.id) === Number(payload.id)); + if (idx !== -1) anjunganItems.value[idx] = res.data; + return { success: true, message: res.message }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; + } + }; + + const deleteAnjungan = async (id) => { + try { + const res = await $fetch(`/api/config/anjungan/${id}`, { method: 'DELETE' }); + anjunganItems.value = anjunganItems.value.filter((a) => Number(a.id) !== Number(id)); + return { success: true, message: res.message }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; } - const name = anjunganItems.value[idx].namaAnjungan; - anjunganItems.value.splice(idx, 1); - return { success: true, message: `Anjungan ${name} berhasil dihapus` }; }; return { anjunganItems, + isLoading, + error, getAllAnjungan, getAnjunganById, + fetchAnjungan, addAnjungan, updateAnjungan, deleteAnjungan, }; }, { - persist: { - key: 'anjungan-store-state', - storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['anjunganItems'], - }, + persist: false, }); - diff --git a/stores/antreanMasukScreenStore.js b/stores/antreanMasukScreenStore.js index d942f54..c211bae 100644 --- a/stores/antreanMasukScreenStore.js +++ b/stores/antreanMasukScreenStore.js @@ -1,25 +1,15 @@ // stores/antreanMasukScreenStore.js +// Konfigurasi layar antrean masuk — data disimpan di server SQLite via /api/config/antrean-masuk-screen import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; export const useAntreanMasukScreenStore = defineStore('antreanMasukScreen', () => { - // Initial antrean masuk screen items data - const antreanMasukScreenItems = ref([ - { - id: 1, - namaScreen: "Layar Antrean Masuk 1", - nomorScreen: "AM-001", - loket: [1, 2, 12, 14], // Array of loket IDs - }, - { - id: 2, - namaScreen: "Layar Antrean Masuk 2", - nomorScreen: "AM-002", - loket: [3, 4], - }, - ]); + const antreanMasukScreenItems = ref([]); + const isLoading = ref(false); + const error = ref(null); + + const getAllAntreanMasukScreens = computed(() => antreanMasukScreenItems.value); - // Computed const getAntreanMasukScreenById = (id) => { return computed(() => { const targetId = Number(id); @@ -27,63 +17,65 @@ export const useAntreanMasukScreenStore = defineStore('antreanMasukScreen', () = }); }; - const getAllAntreanMasukScreens = computed(() => antreanMasukScreenItems.value); - - // Actions - const addAntreanMasukScreen = (screenPayload) => { - // Ensure we get a valid ID even if antreanMasukScreenItems is empty - const maxId = antreanMasukScreenItems.value.length > 0 - ? Math.max(...antreanMasukScreenItems.value.map(s => s.id), 0) - : 0; - const newId = maxId + 1; - // Pastikan id baru tidak tertimpa payload (payload.id bisa null) - const newScreen = { - ...screenPayload, - id: newId, - }; - antreanMasukScreenItems.value.push(newScreen); - return { success: true, message: `Screen ${newScreen.namaScreen} berhasil ditambahkan`, data: newScreen }; + // ── Fetch from API ───────────────────────────────────────────────────────── + const fetchAntreanMasukScreens = async () => { + if (isLoading.value) return; + isLoading.value = true; + error.value = null; + try { + const res = await $fetch('/api/config/antrean-masuk-screen'); + antreanMasukScreenItems.value = res.data || []; + } catch (e) { + error.value = e.message; + console.error('❌ [antreanMasukScreenStore] Gagal fetch config:', e); + } finally { + isLoading.value = false; + } }; - const updateAntreanMasukScreen = (screenPayload) => { - const index = antreanMasukScreenItems.value.findIndex(s => s.id === screenPayload.id); - if (index !== -1) { - antreanMasukScreenItems.value[index] = { - ...antreanMasukScreenItems.value[index], - ...screenPayload, - }; - return { success: true, message: `Konfigurasi ${screenPayload.namaScreen} berhasil disimpan` }; + // ── CRUD via API ─────────────────────────────────────────────────────────── + const addAntreanMasukScreen = async (screenPayload) => { + try { + const res = await $fetch('/api/config/antrean-masuk-screen', { method: 'POST', body: screenPayload }); + antreanMasukScreenItems.value.push(res.data); + return { success: true, message: res.message, data: res.data }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; } - return { success: false, message: 'Screen tidak ditemukan' }; }; - const deleteAntreanMasukScreen = (screenId) => { - const index = antreanMasukScreenItems.value.findIndex(s => s.id === screenId); - if (index !== -1) { - const screenName = antreanMasukScreenItems.value[index].namaScreen; - antreanMasukScreenItems.value.splice(index, 1); - return { success: true, message: `Screen ${screenName} berhasil dihapus` }; + const updateAntreanMasukScreen = async (screenPayload) => { + try { + const res = await $fetch(`/api/config/antrean-masuk-screen/${screenPayload.id}`, { method: 'PUT', body: screenPayload }); + const index = antreanMasukScreenItems.value.findIndex(s => s.id === screenPayload.id); + if (index !== -1) antreanMasukScreenItems.value[index] = res.data; + return { success: true, message: res.message }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; + } + }; + + const deleteAntreanMasukScreen = async (screenId) => { + try { + const res = await $fetch(`/api/config/antrean-masuk-screen/${screenId}`, { method: 'DELETE' }); + antreanMasukScreenItems.value = antreanMasukScreenItems.value.filter(s => s.id !== screenId); + return { success: true, message: res.message }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; } - return { success: false, message: 'Screen tidak ditemukan' }; }; return { - // State antreanMasukScreenItems, - - // Computed + isLoading, + error, getAllAntreanMasukScreens, getAntreanMasukScreenById, - - // Actions + fetchAntreanMasukScreens, addAntreanMasukScreen, updateAntreanMasukScreen, deleteAntreanMasukScreen, }; }, { - persist: { - key: 'antrean-masuk-screen-store-state', - storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['antreanMasukScreenItems'], - }, + persist: false, }); diff --git a/stores/clinicStore.js b/stores/clinicStore.js index ef18c84..077538b 100644 --- a/stores/clinicStore.js +++ b/stores/clinicStore.js @@ -1000,6 +1000,6 @@ export const useClinicStore = defineStore('clinic', () => { persist: { key: 'clinic-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['clinics', 'lastSyncTimestamp'], + paths: ['lastSyncTimestamp'], }, }); diff --git a/stores/doctorStore.js b/stores/doctorStore.js index 1d39317..c120544 100644 --- a/stores/doctorStore.js +++ b/stores/doctorStore.js @@ -184,6 +184,6 @@ export const useDoctorStore = defineStore('doctor', () => { }, { persist: { key: 'doctor-store', - pick: ['doctorsByKlinikId', 'lastSyncTimestamp'] + pick: ['lastSyncTimestamp'] } }); diff --git a/stores/klinikruangstore.js b/stores/klinikruangstore.js index 6aecbdb..cfd0336 100644 --- a/stores/klinikruangstore.js +++ b/stores/klinikruangstore.js @@ -1,4 +1,5 @@ -// stores/klinikRuangStore.js +// stores/klinikruangstore.js +// Konfigurasi mapping klinik ke ruangan — data disimpan di server SQLite via /api/config/klinik-ruang import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { useClinicStore } from './clinicStore'; @@ -6,284 +7,131 @@ import { useClinicStore } from './clinicStore'; export const useKlinikRuangStore = defineStore('klinikRuang', () => { const clinicStore = useClinicStore(); - // State/Computed - List Master Klinik (disinkronkan dengan clinicStore) + // Master klinik list (from clinicStore — in-memory, from API) const masterKlinikList = computed(() => { const baseList = typeof clinicStore.getClinicsForDropdown === 'function' ? clinicStore.getClinicsForDropdown() : []; - - return baseList.map((c) => ({ - kode: c.kode, - nama: c.name, - })); + return baseList.map((c) => ({ kode: c.kode, nama: c.name })); }); - // State - Klinik Ruang Data - const klinikRuangList = ref([ - { - id: 1, - no: 1, - kodeKlinik: 'AN', - namaKlinik: 'ANAK', - namaRuang: 'R. TINDAKAN', - ruangList: [ - { nomorRuang: '1', namaRuang: 'R. TINDAKAN', nomorScreen: '101' } - ] - }, - { - id: 2, - no: 2, - kodeKlinik: 'AS', - namaKlinik: 'ANESTESI', - namaRuang: 'Ruang 1, Ruang 2, Ruang 3, Ruang 4, Ruang 5, Ruang 6', - ruangList: [ - { nomorRuang: '1', namaRuang: 'Ruang 1', nomorScreen: '201' }, - { nomorRuang: '2', namaRuang: 'Ruang 2', nomorScreen: '202' }, - { nomorRuang: '3', namaRuang: 'Ruang 3', nomorScreen: '203' }, - { nomorRuang: '4', namaRuang: 'Ruang 4', nomorScreen: '204' }, - { nomorRuang: '5', namaRuang: 'Ruang 5', nomorScreen: '205' }, - { nomorRuang: '6', namaRuang: 'Ruang 6', nomorScreen: '206' } - ] - }, - { - id: 3, - no: 3, - kodeKlinik: 'BD', - namaKlinik: 'BEDAH', - namaRuang: 'Ruang Konsultasi', - ruangList: [ - { nomorRuang: '1', namaRuang: 'Ruang Konsultasi', nomorScreen: '301' } - ] - }, - { - id: 4, - no: 4, - kodeKlinik: 'GR', - namaKlinik: 'GERIATRI', - namaRuang: 'Ruang Pemeriksaan', - ruangList: [ - { nomorRuang: '1', namaRuang: 'Ruang Pemeriksaan', nomorScreen: '401' } - ] - }, - { - id: 5, - no: 5, - kodeKlinik: 'GI', - namaKlinik: 'GIGI DAN MULUT', - namaRuang: 'Ruang 1, Ruang 2, Ruang 3', - ruangList: [ - { nomorRuang: '1', namaRuang: 'Ruang 1', nomorScreen: '501' }, - { nomorRuang: '2', namaRuang: 'Ruang 2', nomorScreen: '502' }, - { nomorRuang: '3', namaRuang: 'Ruang 3', nomorScreen: '503' } - ] - }, - ]); + const klinikRuangList = ref([]); + const isLoading = ref(false); + const error = ref(null); - // Computed const totalKlinikRuang = computed(() => klinikRuangList.value.length); + const totalRuangan = computed(() => + klinikRuangList.value.reduce((total, k) => total + (k.ruangList?.length || 0), 0) + ); - const totalRuangan = computed(() => { - return klinikRuangList.value.reduce((total, klinik) => { - return total + klinik.ruangList.length; - }, 0); - }); - - // Get klinik by code - const getKlinikByCode = (kode) => { - return klinikRuangList.value.find(k => k.kodeKlinik === kode); - }; - - // Get all ruang for a specific klinik + const getKlinikByCode = (kode) => klinikRuangList.value.find(k => k.kodeKlinik === kode); const getRuangByKlinik = (kodeKlinik) => { const klinik = klinikRuangList.value.find(k => k.kodeKlinik === kodeKlinik); return klinik ? klinik.ruangList : []; }; - // Actions - CRUD Operations - - // Create new Klinik Ruang - const createKlinikRuang = (data) => { - const newId = Math.max(...klinikRuangList.value.map(r => r.id), 0) + 1; - const newNo = klinikRuangList.value.length + 1; - - // Generate nama ruang untuk display - const namaRuangDisplay = data.ruangList - .map(r => r.namaRuang) - .filter(n => n) - .join(', '); - - const newKlinikRuang = { - id: newId, - no: newNo, - kodeKlinik: data.kodeKlinik, - namaKlinik: data.namaKlinik, - namaRuang: namaRuangDisplay, - ruangList: data.ruangList - }; - - klinikRuangList.value.push(newKlinikRuang); - - return { - success: true, - message: `Klinik Ruang ${data.namaKlinik} berhasil ditambahkan`, - data: newKlinikRuang - }; - }; - - // Update existing Klinik Ruang - const updateKlinikRuang = (id, data) => { - const index = klinikRuangList.value.findIndex(r => r.id === id); - - if (index === -1) { - return { - success: false, - message: 'Klinik Ruang tidak ditemukan' - }; - } - - // Generate nama ruang untuk display - const namaRuangDisplay = data.ruangList - .map(r => r.namaRuang) - .filter(n => n) - .join(', '); - - klinikRuangList.value[index] = { - ...klinikRuangList.value[index], - kodeKlinik: data.kodeKlinik, - namaKlinik: data.namaKlinik, - namaRuang: namaRuangDisplay, - ruangList: data.ruangList - }; - - return { - success: true, - message: `Klinik Ruang ${data.namaKlinik} berhasil diupdate`, - data: klinikRuangList.value[index] - }; - }; - - // Delete Klinik Ruang - const deleteKlinikRuang = (id) => { - const index = klinikRuangList.value.findIndex(r => r.id === id); - - if (index === -1) { - return { - success: false, - message: 'Klinik Ruang tidak ditemukan' - }; - } - - const deletedKlinik = klinikRuangList.value[index]; - klinikRuangList.value.splice(index, 1); - - // Reorder numbers - klinikRuangList.value.forEach((r, idx) => { - r.no = idx + 1; - }); - - return { - success: true, - message: `Klinik Ruang ${deletedKlinik.namaKlinik} berhasil dihapus` - }; - }; - - // Add ruang to existing klinik - const addRuangToKlinik = (klinikId, ruangData) => { - const klinik = klinikRuangList.value.find(k => k.id === klinikId); - - if (!klinik) { - return { - success: false, - message: 'Klinik tidak ditemukan' - }; - } - - klinik.ruangList.push(ruangData); - - // Update display name - klinik.namaRuang = klinik.ruangList - .map(r => r.namaRuang) - .filter(n => n) - .join(', '); - - return { - success: true, - message: `Ruang ${ruangData.namaRuang} berhasil ditambahkan`, - data: klinik - }; - }; - - // Remove ruang from klinik - const removeRuangFromKlinik = (klinikId, ruangIndex) => { - const klinik = klinikRuangList.value.find(k => k.id === klinikId); - - if (!klinik) { - return { - success: false, - message: 'Klinik tidak ditemukan' - }; - } - - if (klinik.ruangList.length <= 1) { - return { - success: false, - message: 'Minimal harus ada 1 ruangan' - }; - } - - const removedRuang = klinik.ruangList[ruangIndex]; - klinik.ruangList.splice(ruangIndex, 1); - - // Update display name - klinik.namaRuang = klinik.ruangList - .map(r => r.namaRuang) - .filter(n => n) - .join(', '); - - return { - success: true, - message: `Ruang ${removedRuang.namaRuang} berhasil dihapus`, - data: klinik - }; - }; - - // Search klinik ruang const searchKlinikRuang = (searchTerm) => { if (!searchTerm) return klinikRuangList.value; - const term = searchTerm.toLowerCase(); - return klinikRuangList.value.filter(k => - k.kodeKlinik.toLowerCase().includes(term) || - k.namaKlinik.toLowerCase().includes(term) || - k.namaRuang.toLowerCase().includes(term) + return klinikRuangList.value.filter(k => + k.kodeKlinik?.toLowerCase().includes(term) || + k.namaKlinik?.toLowerCase().includes(term) ); }; + // ── Fetch from API ───────────────────────────────────────────────────────── + const fetchKlinikRuang = async () => { + if (isLoading.value) return; + isLoading.value = true; + error.value = null; + try { + const res = await $fetch('/api/config/klinik-ruang'); + klinikRuangList.value = (res.data || []).map((item, idx) => ({ + ...item, + no: idx + 1, + namaRuang: (item.ruangList || []).map(r => r.namaRuang).filter(Boolean).join(', '), + })); + } catch (e) { + error.value = e.message; + console.error('❌ [klinikRuangStore] Gagal fetch config:', e); + } finally { + isLoading.value = false; + } + }; + + // ── CRUD via API ─────────────────────────────────────────────────────────── + const createKlinikRuang = async (data) => { + try { + const res = await $fetch('/api/config/klinik-ruang', { method: 'POST', body: data }); + const newItem = { ...res.data, no: klinikRuangList.value.length + 1, namaRuang: (res.data.ruangList || []).map(r => r.namaRuang).join(', ') }; + klinikRuangList.value.push(newItem); + return { success: true, message: res.message, data: newItem }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; + } + }; + + const updateKlinikRuang = async (id, data) => { + try { + const res = await $fetch(`/api/config/klinik-ruang/${id}`, { method: 'PUT', body: data }); + const index = klinikRuangList.value.findIndex(r => r.id === id); + if (index !== -1) { + klinikRuangList.value[index] = { + ...res.data, + no: klinikRuangList.value[index].no, + namaRuang: (res.data.ruangList || []).map(r => r.namaRuang).join(', '), + }; + } + return { success: true, message: res.message, data: res.data }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; + } + }; + + const deleteKlinikRuang = async (id) => { + try { + const res = await $fetch(`/api/config/klinik-ruang/${id}`, { method: 'DELETE' }); + klinikRuangList.value = klinikRuangList.value.filter(r => r.id !== id); + // Reorder 'no' + klinikRuangList.value.forEach((r, idx) => { r.no = idx + 1; }); + return { success: true, message: res.message }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; + } + }; + + const addRuangToKlinik = async (klinikId, ruangData) => { + const klinik = klinikRuangList.value.find(k => k.id === klinikId); + if (!klinik) return { success: false, message: 'Klinik tidak ditemukan' }; + const newRuangList = [...(klinik.ruangList || []), ruangData]; + return updateKlinikRuang(klinikId, { ...klinik, ruangList: newRuangList }); + }; + + const removeRuangFromKlinik = async (klinikId, ruangIndex) => { + const klinik = klinikRuangList.value.find(k => k.id === klinikId); + if (!klinik) return { success: false, message: 'Klinik tidak ditemukan' }; + if ((klinik.ruangList || []).length <= 1) return { success: false, message: 'Minimal harus ada 1 ruangan' }; + const newRuangList = klinik.ruangList.filter((_, i) => i !== ruangIndex); + return updateKlinikRuang(klinikId, { ...klinik, ruangList: newRuangList }); + }; + return { - // State masterKlinikList, klinikRuangList, - - // Computed + isLoading, + error, totalKlinikRuang, totalRuangan, - - // Getters getKlinikByCode, getRuangByKlinik, - - // Actions + searchKlinikRuang, + fetchKlinikRuang, createKlinikRuang, updateKlinikRuang, deleteKlinikRuang, addRuangToKlinik, removeRuangFromKlinik, - searchKlinikRuang, }; }, { - persist: { - key: 'klinikruang-store-state', - storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['klinikRuangList'], - }, + persist: false, }); \ No newline at end of file diff --git a/stores/queueStore.js b/stores/queueStore.js index eb0be2d..1c08f82 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -266,6 +266,8 @@ export const useQueueStore = defineStore('queue', () => { allPatients.value.push(...newPatientMap.values()); // console.log(`✅ [queueStore] Successfully fetched ${mappedClinicPatients.length} patients for clinic ${kodeKlinik}`); + // SYNC COUNTERS WITH API DATA SO NEW TICKETS DONT RESET + syncCountersWithState(); return { success: true, message: `${mappedClinicPatients.length} pasien dimuat` }; } catch (error) { @@ -420,6 +422,8 @@ export const useQueueStore = defineStore('queue', () => { url: wsBaseUrl, clientId: wsClientId, fallbackPostUrl: '/stats-api/ws', + reconnectInterval: 5000, // 5 seconds between reconnect attempts + maxReconnectAttempts: 9999, // Effectively infinite — never give up on remote machines onOpen: () => { console.log('✅ [queueStore] WebSocket connected'); isWsConnected.value = true; @@ -435,12 +439,60 @@ export const useQueueStore = defineStore('queue', () => { onMessage: onWsMessage }); + // ============================================ + // STORE-LEVEL AUTO-POLLING (cross-device sync fallback) + // ============================================ + // Runs on every browser instance every 30 seconds. + // Fetches data based on whatever active interests are registered + // (lokets, clinics, or global). This ensures any device always + // has fresh data regardless of WS delivery reliability. + let _autoSyncInterval = null; + + const startAutoSync = () => { + // Guard: only run on client, and only start once + if (typeof window === 'undefined') return; + if (_autoSyncInterval) return; // Already running + + console.log('🔄 [queueStore] Starting store-level auto-sync (30s interval)'); + + _autoSyncInterval = setInterval(async () => { + const hasLoketInterest = Object.keys(activeLoketInterest.value).length > 0; + const hasClinicInterest = Object.keys(activeClinicInterest.value).length > 0; + const hasGlobalInterest = globalInterestCount.value > 0; + + if (hasGlobalInterest) { + fetchAllPatients(); + } else { + if (hasLoketInterest) { + Object.keys(activeLoketInterest.value).forEach(loketId => { + fetchPatientsForLoket(loketId, true); + }); + } + if (hasClinicInterest) { + Object.keys(activeClinicInterest.value).forEach(kodeKlinik => { + fetchPatientsForClinic(kodeKlinik, true); + }); + } + } + }, 30000); // 30 seconds + }; + + const stopAutoSync = () => { + if (_autoSyncInterval) { + clearInterval(_autoSyncInterval); + _autoSyncInterval = null; + console.log('âšī¸ [queueStore] Store-level auto-sync stopped'); + } + }; + /** * Initialize Global WebSocket */ const initWebSocket = (customClientId = null) => { if (isConnected.value && customClientId === wsClientId.value) { console.log('🔌 [queueStore] WebSocket already connected with same ID.'); + // Auto-sync should still start even if WS is already connected + startAutoSync(); return; } @@ -448,11 +500,14 @@ export const useQueueStore = defineStore('queue', () => { wsClientId.value = customClientId; // Re-connect with new ID if changed disconnect(); - // useWebSocket will use the new wsClientId.value if it's reactive } console.log(`🔌 [queueStore] Connecting to WebSocket: ${wsBaseUrl} as ${wsClientId.value}`); connect(); + + // Start store-level auto-polling if not already running (client-side only). + // This guarantees cross-device sync even when WS messages are missed. + startAutoSync(); }; /** @@ -925,6 +980,9 @@ const fetchPatientsForLoket = async (loketId, force = false) => { // console.log(`✅ [queueStore] Successfully fetched ${finalPatients.length} unique patients for loket ${loketId} (Original: ${mappedPatients.length})`); // console.log(`📊 [queueStore] Total patients in allPatients: ${allPatients.value.length}`); + // SYNC COUNTERS WITH API DATA SO NEW TICKETS DONT RESET + syncCountersWithState(); + return { success: true, message: `${mappedPatients.length} pasien berhasil dimuat`, @@ -3366,13 +3424,15 @@ const fetchPatientsForLoket = async (loketId, force = false) => { activeClinicInterest, registerGlobalInterest, unregisterGlobalInterest, + startAutoSync, + stopAutoSync, }; }, { persist: { key: 'queue-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['allPatients', 'quotaUsed', 'currentProcessingPatient', 'apiPatientsPerLoket', 'lastUpdated'], + paths: ['quotaUsed', 'lastUpdated'], serializer: { deserialize: JSON.parse, serialize: JSON.stringify, diff --git a/stores/ruangStore.js b/stores/ruangStore.js index 2b4d75a..4c911ca 100644 --- a/stores/ruangStore.js +++ b/stores/ruangStore.js @@ -552,10 +552,6 @@ export const useRuangStore = defineStore('ruang', () => { fetchRuangFromAPI, }; }, { - persist: { - key: 'ruang-store-state', - storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['ruangData'], - }, + persist: false, }); diff --git a/stores/screenStore.js b/stores/screenStore.js index 0db7c27..f6c7bd3 100644 --- a/stores/screenStore.js +++ b/stores/screenStore.js @@ -1,31 +1,15 @@ // stores/screenStore.js +// Konfigurasi layar TV klinik — data disimpan di server SQLite via /api/config/screen import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; export const useScreenStore = defineStore('screen', () => { - // Initial screen items data - const screenItems = ref([ - { - id: 1, - namaScreen: "Layar Screen 1", - nomorScreen: "SCR-001", - klinik: ["AN", "AS", "BD", "GI", "GR", "GZ", "IP", "JT"], - }, - { - id: 2, - namaScreen: "Layar Screen 2", - nomorScreen: "SCR-002", - klinik: ["JW", "KK", "MT", "SR", "OB", "PR"], - }, - { - id: 3, - namaScreen: "Layar Screen 3", - nomorScreen: "SCR-003", - klinik: ["RT", "RM", "HO"], - }, - ]); + const screenItems = ref([]); + const isLoading = ref(false); + const error = ref(null); + + const getAllScreens = computed(() => screenItems.value); - // Computed const getScreenById = (id) => { return computed(() => { const targetId = Number(id); @@ -33,64 +17,66 @@ export const useScreenStore = defineStore('screen', () => { }); }; - const getAllScreens = computed(() => screenItems.value); - - // Actions - const addScreen = (screenPayload) => { - // Ensure we get a valid ID even if screenItems is empty - const maxId = screenItems.value.length > 0 - ? Math.max(...screenItems.value.map(s => s.id), 0) - : 0; - const newId = maxId + 1; - // Pastikan id baru tidak tertimpa payload (payload.id bisa null) - const newScreen = { - ...screenPayload, - id: newId, - }; - screenItems.value.push(newScreen); - return { success: true, message: `Screen ${newScreen.namaScreen} berhasil ditambahkan`, data: newScreen }; + // ── Fetch from API ───────────────────────────────────────────────────────── + const fetchScreens = async () => { + if (isLoading.value) return; + isLoading.value = true; + error.value = null; + try { + const res = await $fetch('/api/config/screen'); + screenItems.value = res.data || []; + } catch (e) { + error.value = e.message; + console.error('❌ [screenStore] Gagal fetch screen config:', e); + } finally { + isLoading.value = false; + } }; - const updateScreen = (screenPayload) => { - const index = screenItems.value.findIndex(s => s.id === screenPayload.id); - if (index !== -1) { - screenItems.value[index] = { - ...screenItems.value[index], - ...screenPayload, - }; - return { success: true, message: `Konfigurasi ${screenPayload.namaScreen} berhasil disimpan` }; + // ── CRUD via API ─────────────────────────────────────────────────────────── + const addScreen = async (screenPayload) => { + try { + const res = await $fetch('/api/config/screen', { method: 'POST', body: screenPayload }); + screenItems.value.push(res.data); + return { success: true, message: res.message, data: res.data }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; } - return { success: false, message: 'Screen tidak ditemukan' }; }; - const deleteScreen = (screenId) => { - const index = screenItems.value.findIndex(s => s.id === screenId); - if (index !== -1) { - const screenName = screenItems.value[index].namaScreen; - screenItems.value.splice(index, 1); - return { success: true, message: `Screen ${screenName} berhasil dihapus` }; + const updateScreen = async (screenPayload) => { + try { + const res = await $fetch(`/api/config/screen/${screenPayload.id}`, { method: 'PUT', body: screenPayload }); + const index = screenItems.value.findIndex(s => s.id === screenPayload.id); + if (index !== -1) screenItems.value[index] = res.data; + return { success: true, message: res.message }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; + } + }; + + const deleteScreen = async (screenId) => { + try { + const res = await $fetch(`/api/config/screen/${screenId}`, { method: 'DELETE' }); + screenItems.value = screenItems.value.filter(s => s.id !== screenId); + return { success: true, message: res.message }; + } catch (e) { + return { success: false, message: e.data?.message || e.message }; } - return { success: false, message: 'Screen tidak ditemukan' }; }; return { - // State screenItems, - - // Computed + isLoading, + error, getAllScreens, getScreenById, - - // Actions + fetchScreens, addScreen, updateScreen, deleteScreen, }; }, { - persist: { - key: 'screen-store-state', - storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['screenItems'], - }, + // No persist — data lives in the server DB + persist: false, }); -