push perbaikan

This commit is contained in:
Fanrouver
2026-05-25 08:43:30 +07:00
parent ec9dac0789
commit ffdb88d13c
43 changed files with 945 additions and 566 deletions
+65
View File
@@ -1,5 +1,70 @@
<script setup>
import { onMounted, ref } from 'vue';
const nuxtApp = useNuxtApp();
const loading = ref(false);
nuxtApp.hook('page:start', () => {
loading.value = true;
});
nuxtApp.hook('page:finish', () => {
loading.value = false;
});
// Naikkan versi ini setiap kali ada perubahan struktur state (schema migration).
// Saat versi tidak cocok, semua cache Pinia akan dibersihkan otomatis.
const STORE_SCHEMA_VERSION = 3;
const VERSION_KEY = 'app-store-version';
// Daftar semua localStorage key yang dikelola oleh Pinia stores
const PINIA_STORE_KEYS = [
'queue-store-state',
'clinic-store-state',
'doctor-store',
'loket-store-state',
'ruang-store-state',
'klinikruang-store-state',
'antrean-masuk-screen-store',
'anjungan-store',
'screen-store',
'penunjang-store',
];
onMounted(() => {
try {
if (typeof window === 'undefined' || !window.localStorage) return;
const savedVersion = parseInt(localStorage.getItem(VERSION_KEY) || '0', 10);
if (savedVersion < STORE_SCHEMA_VERSION) {
// Versi lama terdeteksi → hapus semua cache Pinia sekaligus
console.log(`[Migration] Store schema v${savedVersion} → v${STORE_SCHEMA_VERSION}. Clearing all caches...`);
PINIA_STORE_KEYS.forEach(key => localStorage.removeItem(key));
localStorage.setItem(VERSION_KEY, String(STORE_SCHEMA_VERSION));
console.log('[Migration] Done. App will fetch fresh data from API.');
}
} catch (e) {
// Ignore storage errors (e.g., private browsing mode)
}
});
</script>
<template>
<v-app>
<v-overlay
:model-value="loading"
class="align-center justify-center"
persistent
z-index="9999"
>
<v-progress-circular
color="primary"
indeterminate
size="64"
width="6"
></v-progress-circular>
</v-overlay>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
+3
View File
@@ -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,
+9 -9
View File
@@ -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(() => {
+13 -4
View File
@@ -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.
+20
View File
@@ -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;
+5 -1
View File
@@ -98,7 +98,7 @@
</template>
<script setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { useAnjunganStore } from '@/stores/anjunganStore';
import { useMasterStore } from '@/stores/masterStore';
import { useRoute } from '#app';
@@ -111,6 +111,10 @@ const anjunganStore = useAnjunganStore();
const masterStore = useMasterStore();
const route = useRoute();
onMounted(() => {
anjunganStore.fetchAnjungan();
});
// Safeguard supaya tidak undefined saat store belum terisi
const anjunganList = computed(() => {
const fromGetter = anjunganStore.getAllAnjungan?.value;
+19
View File
@@ -1383,6 +1383,25 @@ 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}".
if (result.patient.loketId && queueStore.sendViaPost) {
const targetLoketId = result.patient.loketId;
console.log(`📡 [AnjunganCopy] 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('⚠️ [AnjunganCopy] Could not notify AdminLoket via WS:', err?.message);
});
}
// Tampilkan dialog print - gunakan nextTick untuk memastikan reactive update
await nextTick();
showPrintDialog.value = true;
+5 -1
View File
@@ -98,7 +98,7 @@
</template>
<script setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { useAnjunganStore } from '@/stores/anjunganStore';
import { useMasterStore } from '@/stores/masterStore';
import { useRoute } from '#app';
@@ -111,6 +111,10 @@ const anjunganStore = useAnjunganStore();
const masterStore = useMasterStore();
const route = useRoute();
onMounted(() => {
anjunganStore.fetchAnjungan();
});
// Safeguard supaya tidak undefined saat store belum terisi
const anjunganList = computed(() => {
const fromGetter = anjunganStore.getAllAnjungan?.value;
+6 -2
View File
@@ -98,7 +98,7 @@
</template>
<script setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { useAntreanMasukScreenStore } from '@/stores/antreanMasukScreenStore';
import { useLoketStore } from '@/stores/loketStore';
import { useRoute } from '#app';
@@ -111,6 +111,10 @@ const antreanMasukScreenStore = useAntreanMasukScreenStore();
const loketStore = useLoketStore();
const route = useRoute();
onMounted(() => {
antreanMasukScreenStore.fetchAntreanMasukScreens();
});
// Helper to get loket name by ID
const getLoketNameById = (loketId) => {
const loket = loketStore.getLoketById(loketId);
@@ -145,7 +149,7 @@ const goNext = () => {
};
const navigateToScreen = (screenId) => {
navigateTo(`/anjungan/antreanmasuk/${screenId}`);
navigateTo(`/Anjungan/AntreanMasuk/${screenId}`);
};
const navigateToSettings = () => {
+5 -1
View File
@@ -91,7 +91,7 @@
</template>
<script setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { useScreenStore } from '@/stores/screenStore';
import { useMasterStore } from '@/stores/masterStore';
import { useRoute } from '#app';
@@ -104,6 +104,10 @@ const screenStore = useScreenStore();
const masterStore = useMasterStore();
const route = useRoute();
onMounted(() => {
screenStore.fetchScreens();
});
// Safeguard supaya tidak undefined saat store belum terisi
const screens = computed(() => {
const fromGetter = screenStore.getAllScreens?.value;
+6 -4
View File
@@ -681,7 +681,7 @@ const areaChartOptions = ref({
backgroundColor: 'rgba(255, 255, 255, 0.95)',
titleColor: colors.neutral[900],
bodyColor: colors.neutral[700],
borderColor: colors.neutral[400],
borderColor: colors.neutral[500],
borderWidth: 1,
padding: 12,
boxPadding: 6,
@@ -778,7 +778,7 @@ const doughnutOptions = ref({
backgroundColor: 'rgba(255, 255, 255, 0.95)',
titleColor: colors.neutral[900],
bodyColor: colors.neutral[700],
borderColor: colors.neutral[400],
borderColor: colors.neutral[500],
borderWidth: 1,
padding: 12,
boxPadding: 6,
@@ -814,7 +814,7 @@ const horizontalBarOptions = ref({
backgroundColor: 'rgba(255, 255, 255, 0.95)',
titleColor: colors.neutral[900],
bodyColor: colors.neutral[700],
borderColor: colors.neutral[400],
borderColor: colors.neutral[500],
borderWidth: 1,
padding: 12,
boxPadding: 6,
@@ -889,7 +889,7 @@ const polarAreaOptions = ref({
backgroundColor: 'rgba(255, 255, 255, 0.95)',
titleColor: colors.neutral[900],
bodyColor: colors.neutral[700],
borderColor: colors.neutral[400],
borderColor: colors.neutral[500],
borderWidth: 1,
padding: 12,
boxPadding: 6,
@@ -1841,6 +1841,7 @@ const downloadBlob = (blob, filename) => {
background: linear-gradient(90deg, var(--color-primary-500) 0%, var(--color-primary-600) 50%, var(--color-secondary-500) 100%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
&::after {
@@ -1853,6 +1854,7 @@ const downloadBlob = (blob, filename) => {
background: radial-gradient(circle, rgba(58, 97, 201, 0.05) 0%, transparent 70%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
&:hover {
+6 -9
View File
@@ -348,9 +348,9 @@ const submitForm = async () => {
let result;
if (isEdit.value) {
result = anjunganStore.updateAnjungan(formData.value);
result = await anjunganStore.updateAnjungan(formData.value);
} else {
result = anjunganStore.addAnjungan(formData.value);
result = await anjunganStore.addAnjungan(formData.value);
}
snackbar.value = {
@@ -364,14 +364,10 @@ const submitForm = async () => {
}
};
const handleDelete = (item) => {
const handleDelete = async (item) => {
if (confirm(`Hapus anjungan ${item.namaAnjungan}?`)) {
const result = anjunganStore.deleteAnjungan(item.id);
snackbar.value = {
show: true,
message: result.message,
color: result.success ? 'success' : 'error',
};
const result = await anjunganStore.deleteAnjungan(item.id);
snackbar.value = { show: true, message: result.message, color: result.success ? 'success' : 'error' };
}
};
@@ -391,6 +387,7 @@ const closePreviewDialog = () => {
// Fetch Reguler clinics from API on mount
onMounted(async () => {
await clinicStore.fetchRegulerClinics();
await anjunganStore.fetchAnjungan();
});
</script>
+16 -39
View File
@@ -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' };
}
});
+13 -23
View File
@@ -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' };
}
};
+11 -25
View File
@@ -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' };
}
};
+8
View File
@@ -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) };
});
+19
View File
@@ -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` };
});
+14
View File
@@ -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` };
});
+31
View File
@@ -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' };
});
@@ -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) };
});
@@ -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 });
}
});
@@ -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` };
});
@@ -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' };
});
+8
View File
@@ -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) };
});
+19
View File
@@ -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` };
});
@@ -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` };
});
@@ -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' };
});
+8
View File
@@ -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) };
});
+26
View File
@@ -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 });
}
});
+14
View File
@@ -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` };
});
+31
View File
@@ -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' };
});
+4 -3
View File
@@ -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}`)
+4 -3
View File
@@ -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}`)
+4 -3
View File
@@ -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}`)
+124
View File
@@ -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<typeof Database> | 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<typeof Database>, table: string, rows: Record<string, any>[]) {
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;
};
+48 -60
View File
@@ -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,
});
+50 -58
View File
@@ -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,
});
+1 -1
View File
@@ -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'],
},
});
+1 -1
View File
@@ -184,6 +184,6 @@ export const useDoctorStore = defineStore('doctor', () => {
}, {
persist: {
key: 'doctor-store',
pick: ['doctorsByKlinikId', 'lastSyncTimestamp']
pick: ['lastSyncTimestamp']
}
});
+95 -247
View File
@@ -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,
});
+62 -2
View File
@@ -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,
+1 -5
View File
@@ -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,
});
+51 -65
View File
@@ -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,
});