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/nuxt.config.ts b/nuxt.config.ts
index 6b373ab..63caa25 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -77,6 +77,9 @@ export default defineNuxtConfig({
// External API
externalApiBaseUrl: process.env.EXTERNAL_API_BASE_URL || 'http://10.10.123.135:8084',
externalApiTimeout: parseInt(process.env.EXTERNAL_API_TIMEOUT || '10000', 10),
+ proxyClientOrigin: process.env.PROXY_CLIENT_ORIGIN || 'http://10.10.150.175:3000',
+ proxyTargetHostFallback: process.env.PROXY_TARGET_HOST_FALLBACK || '10.10.123.135:8084',
+ proxyTargetHostKlinikFallback: process.env.PROXY_TARGET_HOST_KLINIK_FALLBACK || '10.10.123.140:8089',
public: {
authUrl: process.env.AUTH_ORIGIN,
diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue
index 768c551..bea0693 100644
--- a/pages/AdminKlinikRuang/[kodeKlinik].vue
+++ b/pages/AdminKlinikRuang/[kodeKlinik].vue
@@ -933,12 +933,10 @@ const filterOptionsList = {
// WebSocket configuration
const config = useRuntimeConfig();
-// Generate a unique session suffix (random ID)
-const uniqueSessionSuffix = ref(process.client ? Math.random().toString(36).substring(2, 8) : '')
-
-// WebSocket client ID for admin
+// Use a DETERMINISTIC client ID so that other pages can broadcast directly to this admin.
+// Format: admin-klinik-ruang-{kodeKlinik} â stable and targetable from any device.
const adminClientId = computed(() => {
- return `admin-klinik-ruang-${kodeKlinik.value}-${uniqueSessionSuffix.value}`
+ return `admin-klinik-ruang-${kodeKlinik.value}`
})
const isConnected = computed(() => queueStore.isWsConnected);
@@ -1941,23 +1939,25 @@ onMounted(async () => {
await fetchAllData();
// 3. Centralized WebSocket & interest registration
- // initWebSocket is already called via the watcher on adminClientId
+ // Explicitly init WS with deterministic ID so other pages can target this admin directly.
if (kodeKlinik.value) {
- queueStore.registerClinicInterest(kodeKlinik.value);
+ queueStore.initWebSocket(adminClientId.value);
+ queueStore.registerClinicInterest(kodeKlinik.value);
}
});
// 4. Lifecycle cleanup (Keep outside async block to preserve context)
let pollInterval;
onMounted(() => {
- // Check for daily reset and poll data every minute
+ // Safety-net polling: re-fetch every 30 seconds to catch any missed WS events.
+ // This guarantees data is never more than 30 seconds stale.
pollInterval = setInterval(() => {
const didReset = queueStore.checkAndResetDaily();
if (didReset) {
console.log("đ [AdminKlinikRuang] 2 AM threshold reached. Data reset performed.");
}
fetchAllData();
- }, 60000);
+ }, 30000); // 30 seconds
});
onUnmounted(() => {
diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue
index 08edc6d..25ae9b7 100644
--- a/pages/AdminLoket/[id].vue
+++ b/pages/AdminLoket/[id].vue
@@ -392,12 +392,11 @@ const changeKlinik = async (klinik) => {
};
-// Generate a unique session suffix (random ID)
-const uniqueSessionSuffix = ref(process.client ? Math.random().toString(36).substring(2, 8) : '')
-
+// Use a DETERMINISTIC client ID so that Anjungan can broadcast directly to this admin.
+// Format: admin-loket-{loketId} â stable and targetable from any device.
const anjunganClientId = computed(() => {
if (!loketId.value) return ''
- return `admin-loket-${loketId.value}-${uniqueSessionSuffix.value}`
+ return `admin-loket-${loketId.value}`
})
const fetchAllData = async () => {
@@ -450,6 +449,9 @@ watch(anjunganClientId, (newClientId, oldClientId) => {
})
// PERSISTENCE FIX: Ensure data exists on mount
+// Periodic polling interval ref (safety-net fallback)
+let periodicRefreshInterval = null;
+
onMounted(async () => {
console.log("đ AdminLoket Component mounted");
@@ -470,6 +472,12 @@ onMounted(async () => {
}
}, 60000); // Check every minute
+ // Safety-net: Poll every 30 seconds to catch patients registered from Anjungan
+ // even if a WebSocket notification was missed or not received.
+ periodicRefreshInterval = setInterval(() => {
+ fetchPatientsForCurrentLoket();
+ }, 30000);
+
// Initialize and connect WebSocket (Centralized)
queueStore.initWebSocket(anjunganClientId.value);
@@ -484,6 +492,7 @@ onMounted(async () => {
onUnmounted(() => {
if (resetCheckInterval) clearInterval(resetCheckInterval);
+ if (periodicRefreshInterval) clearInterval(periodicRefreshInterval);
// WebSocket is now global, we might not want to disconnect on every unmount
// if other pages are still open, but usually for Admin/Anjungan it's okay.
// We keep it connected unless specified otherwise.
diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue
index 1039536..f8f2199 100644
--- a/pages/Anjungan/Anjungan/[id].vue
+++ b/pages/Anjungan/Anjungan/[id].vue
@@ -1280,6 +1280,26 @@ const registerPatient = async (
lastRegisteredPatient.value,
);
+ // REAL-TIME SYNC: Notify AdminLoket to refresh its patient list immediately.
+ // AdminLoket listens on the deterministic client ID "admin-loket-{loketId}".
+ // Without this, AdminLoket only updates on manual refresh or 30s polling.
+ if (result.patient.loketId && queueStore.sendViaPost) {
+ const targetLoketId = result.patient.loketId;
+ console.log(`đĄ [Anjungan] Notifying AdminLoket ${targetLoketId} of new patient registration...`);
+ queueStore.sendViaPost({
+ to_client: `admin-loket-${targetLoketId}`,
+ data: {
+ loketId: targetLoketId,
+ triggerRefresh: true,
+ source: 'anjungan-registration',
+ ticket: result.patient.noAntrian?.split(' |')[0] || '',
+ }
+ }).catch(err => {
+ // Non-critical: polling will catch this within 30s anyway
+ console.warn('â ī¸ [Anjungan] Could not notify AdminLoket via WS:', err?.message);
+ });
+ }
+
// Tampilkan dialog print - gunakan nextTick untuk memastikan reactive update
await nextTick();
showPrintDialog.value = true;
diff --git a/pages/Anjungan/Anjungan/index.vue b/pages/Anjungan/Anjungan/index.vue
index c9e2516..1a3fc2c 100644
--- a/pages/Anjungan/Anjungan/index.vue
+++ b/pages/Anjungan/Anjungan/index.vue
@@ -98,7 +98,7 @@
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,
});
-