update api klinik ruang

This commit is contained in:
Fanrouver
2026-02-03 15:11:55 +07:00
parent efeb42e0fe
commit b327997021
20 changed files with 2959 additions and 332 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
// Basic test to verify Vitest is working
describe('Vitest Setup', () => {
it('works correctly', () => {
expect(1 + 1).toBe(2)
})
})
// Basic test for Vue component mounting (optional, depends on having a simple component)
/*
import MyComponent from './MyComponent.vue'
describe('Component Test', () => {
it('mounts properly', () => {
const wrapper = mount(MyComponent)
expect(wrapper.exists()).toBe(true)
})
})
*/
+2 -2
View File
@@ -144,8 +144,8 @@ const {
}
.qr-code {
width: 30mm;
height: 30mm;
width: 45mm;
height: 45mm;
margin: 0 auto;
border: none;
padding: 0.5mm;
+3 -3
View File
@@ -32,7 +32,7 @@ export const useThermalPrint = () => {
type: 'image/png',
quality: 1,
margin: 2, // Added margin (quiet zone) for better scanner detection
width: 150, // Slightly larger width for better clarity
width: 400, // High quality width for clear printing
version: 3,
color: {
dark: '#000000',
@@ -230,8 +230,8 @@ export const useThermalPrint = () => {
}
.qr-code {
width: 40mm;
height: 40mm;
width: 45mm;
height: 45mm;
margin: 0 auto;
border: none;
padding: 0.5mm;
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://10.10.150.175:3000",
supportFile: false,
specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
},
component: {
devServer: {
framework: "vue",
bundler: "vite",
},
},
});
+7
View File
@@ -0,0 +1,7 @@
describe('Login Page', () => {
it('should load the login page', () => {
cy.visit('/')
// Adjust this to match a selector on your home or login page
cy.contains('h1', /login/i).should('be.visible')
})
})
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "[email protected]",
"body": "Fixtures are a great way to mock data for responses to routes"
}
+37
View File
@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import { mount } from 'cypress/vue'
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)
// Example use:
// cy.mount(MyComponent)
+2486 -7
View File
File diff suppressed because it is too large Load diff
+10 -1
View File
@@ -10,7 +10,11 @@
"dev:https": "nuxt dev -o --host 0.0.0.0 --port 3000",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
"postinstall": "nuxt prepare",
"test": "vitest",
"test:ui": "vitest --ui",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
},
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^7.1.0",
@@ -49,9 +53,14 @@
"@nuxtjs/google-fonts": "^3.2.0",
"@types/node": "^24.7.2",
"@vitejs/plugin-basic-ssl": "^2.1.4",
"@vitest/ui": "^3.2.4",
"@vue/test-utils": "^2.4.6",
"cypress": "^15.9.0",
"happy-dom": "^20.5.0",
"sass": "^1.93.3",
"sass-embedded": "^1.89.2",
"vite-plugin-vuetify": "^2.1.2",
"vitest": "^3.2.4",
"vuetify": "^3.9.3"
}
}
+32 -10
View File
@@ -795,6 +795,8 @@ import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useQueueStore } from '@/stores/queueStore';
import { useMasterStore } from '@/stores/masterStore';
import { useClinicStore } from '@/stores/clinicStore';
import { useRuangStore } from '@/stores/ruangStore';
import PageHeader from '@/components/common/PageHeader.vue';
import PatientCard from '@/components/features/queue/PatientCard.vue';
import AppSnackbar from '@/components/common/AppSnackbar.vue';
@@ -805,6 +807,8 @@ const route = useRoute();
const router = useRouter();
const queueStore = useQueueStore();
const masterStore = useMasterStore();
const clinicStore = useClinicStore();
const ruangStore = useRuangStore();
const kodeKlinik = computed(() => {
const kode = route.params.kodeKlinik;
@@ -1769,18 +1773,36 @@ const applyFilters = (ruang) => {
pendingPage.value[ruang.nomorRuang] = 1;
};
onMounted(() => {
if (!klinikData.value) {
router.push('/admin-klinik-ruang');
// Watch for ruangList changes to re-initialize filters
watch(ruangList, (newList) => {
if (newList && newList.length > 0) {
newList.forEach(ruang => {
if (!pendingPage.value[ruang.nomorRuang]) {
pendingPage.value[ruang.nomorRuang] = 1;
}
initializeFilterOptions(ruang);
});
}
}, { immediate: true });
onMounted(async () => {
console.log('🚀 AdminKlinikRuang mounted, проверяем data...');
// Initialize pagination, sort options, and filter options for each room
ruangList.value.forEach(ruang => {
if (!pendingPage.value[ruang.nomorRuang]) {
pendingPage.value[ruang.nomorRuang] = 1;
}
initializeFilterOptions(ruang);
});
// 1. Sync data if needed
try {
// Fetch clinics first
await clinicStore.fetchRegulerClinics();
// Then sync rooms
await ruangStore.fetchRuangFromAPI();
} catch (error) {
console.error('❌ Error syncing data in AdminKlinikRuang:', error);
}
if (!klinikData.value) {
console.warn('⚠️ Klinik data not found after sync');
// We don't automatically redirect anymore to allow sync to complete
// but we can check again after a short delay or just let the computed handle it
}
});
</script>
+8 -68
View File
@@ -119,80 +119,20 @@ const navigateToSettings = () => {
router.push('/setting/masterklinikruang');
};
// Generate rooms from clinics based on spesialis field (same logic as MasterKlinikRuang)
const generateRoomsFromClinics = () => {
console.log('🔄 AdminKlinikRuang/index - Generating rooms from clinics...');
const allRooms = [];
let roomIdCounter = 1;
const allClinics = clinicStore.clinics;
console.log('📊 Total clinics:', allClinics.length);
allClinics.forEach(clinic => {
if (clinic.jenisLayanan === 'Reguler') {
if (clinic.spesialis && Array.isArray(clinic.spesialis) && clinic.spesialis.length > 0) {
// Create room ONLY for each specialist (no duplicate clinic-name room)
clinic.spesialis.forEach((spec, index) => {
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
namaRuang: spec.Spesialis,
nomorRuang: (index + 1).toString(),
nomorScreen: `${clinic.kode}${index + 1}`,
jenisLayanan: 'Reguler'
});
});
} else {
// Only create 1 room with clinic name when NO spesialis
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: clinic.kode,
namaRuang: clinic.name,
nomorRuang: '1',
nomorScreen: `${clinic.kode}1`,
jenisLayanan: 'Reguler'
});
}
} else if (clinic.jenisLayanan === 'Eksekutif') {
// For Eksekutif: always 1 room (no spesialis in seed data)
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: clinic.kode,
namaRuang: clinic.name,
nomorRuang: '1',
nomorScreen: `${clinic.kode}1`,
jenisLayanan: 'Eksekutif'
});
}
});
console.log(`✅ Generated ${allRooms.length} rooms from ${allClinics.length} clinics`);
// Update ruangStore with generated rooms
const result = ruangStore.replaceAllRooms(allRooms);
console.log('💾 Rooms saved to ruangStore:', result.message);
return result;
};
// Load data on mount (same as MasterKlinikRuang)
// Load data on mount
onMounted(async () => {
console.log('🚀 AdminKlinikRuang/index mounted, loading data...');
loading.value = true;
try {
// Fetch clinics from API (uses cache if available)
const fetchResult = await clinicStore.fetchRegulerClinics();
console.log('📥 Fetch result:', fetchResult);
// 1. Fetch clinics from API (uses cache if available)
await clinicStore.fetchRegulerClinics();
// Generate rooms based on fetched data
generateRoomsFromClinics();
// 2. Sync room definitions from API (centralized logic)
// This will update ruangStore.ruangData which klinikRuangList depends on
await ruangStore.fetchRuangFromAPI();
console.log('✅ Data loading and sync completed in AdminKlinikRuang/index');
} catch (error) {
console.error('❌ Error loading data:', error);
} finally {
+14 -27
View File
@@ -385,31 +385,15 @@ const displayedLokets = computed(() => {
// Calculate grid columns based on number of displayed lokets
// User wants all lokets in a single row (columns only)
const gridColumns = computed(() => {
return displayedLokets.value.length || 1
})
const gridStyle = computed(() => {
return {
gridTemplateColumns: `repeat(${gridColumns.value}, 1fr)`
gridTemplateColumns: `repeat(${displayedLokets.value.length || 1}, 1fr)`
}
})
// Calculate ticket grid columns based on loket grid columns
const ticketGridColumns = computed(() => {
const loketCols = gridColumns.value
// If few lokets, show tickets in multiple columns
if (loketCols <= 2) return 5
if (loketCols === 3) return 4
if (loketCols === 4) return 3
if (loketCols === 5) return 2
// If many lokets, show tickets in 1 column (vertical)
return 1
})
const ticketGridStyle = computed(() => {
return {
gridTemplateColumns: `repeat(${ticketGridColumns.value}, 1fr)`
gridTemplateColumns: `repeat(auto-fill, minmax(min(115px, 100%), 1fr))`
}
})
@@ -906,25 +890,24 @@ onUnmounted(() => {
.ticket-cards-grid {
display: grid;
gap: clamp(4px, 0.5vw, 10px);
gap: clamp(4px, 0.4vw, 8px);
width: 100%;
padding: clamp(2px, 0.3vw, 5px);
/* grid-template-columns is set dynamically via :style binding */
padding: 4px;
}
.ticket-card {
background: var(--color-neutral-400, #E1E5EA);
border: 1px solid var(--color-neutral-500, #CDD4DC);
border-radius: 8px;
padding: 12px 16px;
padding: clamp(6px, 0.8vh, 10px) 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
gap: 2px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
min-height: 80px;
min-height: clamp(55px, 7vh, 80px);
position: relative;
cursor: default;
}
@@ -948,12 +931,16 @@ onUnmounted(() => {
}
.ticket-number {
font-size: clamp(20px, 2.5vw, 32px);
font-size: clamp(14px, 3.5vw, 32px);
font-weight: 800;
color: var(--color-primary-600, #3A61C9);
letter-spacing: 0.5px;
line-height: 1.1;
letter-spacing: -0.5px;
line-height: 1;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
text-align: center;
white-space: nowrap;
width: 100%;
overflow: visible; /* Don't hide/ellipsis, let it fit via clamp font-size */
}
.ticket-klinik {
@@ -139,6 +139,8 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useQueueStore } from '@/stores/queueStore'
import { useMasterStore } from '@/stores/masterStore'
import { useClinicStore } from '@/stores/clinicStore'
import { useRuangStore } from '@/stores/ruangStore'
import { useRoute } from '#app'
import { useWebSocket } from '@/composables/useWebSocket'
@@ -159,6 +161,8 @@ useHead({
const route = useRoute()
const queueStore = useQueueStore()
const masterStore = useMasterStore()
const clinicStore = useClinicStore()
const ruangStore = useRuangStore()
const kodeKlinik = computed(() => {
const kode = route.params.kodeKlinik
@@ -566,9 +570,20 @@ const initWebSocket = () => {
return wsInstance
}
onMounted(() => {
// Redirect to index if klinik not found
onMounted(async () => {
console.log('🚀 AntrianKlinikRuang (Anjungan) mounted, syncing data...')
// 1. Sync data if needed
try {
await clinicStore.fetchRegulerClinics()
await ruangStore.fetchRuangFromAPI()
} catch (error) {
console.error('❌ Error syncing data in AntrianKlinikRuang:', error)
}
// Redirect to index if klinik not found after sync
if (!klinikData.value) {
console.warn('⚠️ Klinik data not found after sync, redirecting...')
navigateTo('/anjungan/antrianklinikruang')
return
}
+9 -110
View File
@@ -121,53 +121,13 @@ const clinicStore = useClinicStore();
const ruangStore = useRuangStore();
const route = useRoute();
// Group klinik berdasarkan data flatten getAllRuangList agar nama ruang baru langsung muncul
// Simplified computed to use masterStore.ruangData (which is already grouped/processed by ruangStore)
const kliniksWithRuang = computed(() => {
const fromGetter = masterStore.getAllRuangList?.value;
const fromState = masterStore.ruangData?.value || masterStore.ruangData || [];
let allRuang = Array.isArray(fromGetter) ? fromGetter : [];
// Jika getter kosong, flatten dari ruangData (grouped)
if (allRuang.length === 0 && Array.isArray(fromState)) {
fromState.forEach((r) => {
(r?.ruangList || []).forEach((ru) => {
allRuang.push({
kodeKlinik: r.kodeKlinik,
namaKlinik: r.namaKlinik,
nomorRuang: ru.nomorRuang,
namaRuang: ru.namaRuang,
nomorScreen: ru.nomorScreen,
jenisLayanan: r.jenisLayanan
});
});
});
}
const klinikMap = {};
allRuang.forEach((ruang) => {
// Use kodeKlinik + jenisLayanan as key to differentiate same clinic with different service types
const key = `${ruang.kodeKlinik}-${ruang.jenisLayanan || 'default'}`;
if (!klinikMap[key]) {
klinikMap[key] = {
kodeKlinik: ruang.kodeKlinik,
namaKlinik: ruang.namaKlinik,
jenisLayanan: ruang.jenisLayanan,
ruangList: [],
};
}
klinikMap[key].ruangList.push({
namaRuang: ruang.namaRuang,
nomorRuang: ruang.nomorRuang,
nomorScreen: ruang.nomorScreen,
});
});
return Object.values(klinikMap);
return masterStore.ruangData || [];
});
// Pagination
const itemsPerPage = 10;
const itemsPerPage = 12; // Increased for better fill
const page = computed({
get: () => Number(route.query.page || 1),
set: (val) => navigateTo({ query: { ...route.query, page: val } }),
@@ -199,79 +159,18 @@ const navigateToSettings = () => {
navigateTo('/setting/masterklinikruang');
};
// Generate rooms from clinics (same logic as AdminKlinikRuang)
const generateRoomsFromClinics = () => {
console.log('🔄 Anjungan/index - Generating rooms from clinics...');
const allRooms = [];
let roomIdCounter = 1;
const allClinics = clinicStore.clinics;
console.log('📊 Total clinics:', allClinics.length);
allClinics.forEach(clinic => {
if (clinic.jenisLayanan === 'Reguler') {
if (clinic.spesialis && Array.isArray(clinic.spesialis) && clinic.spesialis.length > 0) {
// Create room ONLY for each specialist (no duplicate clinic-name room)
clinic.spesialis.forEach((spec, index) => {
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
namaRuang: spec.Spesialis,
nomorRuang: (index + 1).toString(),
nomorScreen: `${clinic.kode}${index + 1}`,
jenisLayanan: 'Reguler'
});
});
} else {
// Only create 1 room with clinic name when NO spesialis
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: clinic.kode,
namaRuang: clinic.name,
nomorRuang: '1',
nomorScreen: `${clinic.kode}1`,
jenisLayanan: 'Reguler'
});
}
} else if (clinic.jenisLayanan === 'Eksekutif') {
// For Eksekutif: always 1 room (no spesialis in seed data)
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: clinic.kode,
namaRuang: clinic.name,
nomorRuang: '1',
nomorScreen: `${clinic.kode}1`,
jenisLayanan: 'Eksekutif'
});
}
});
console.log(`✅ Generated ${allRooms.length} rooms from ${allClinics.length} clinics`);
// Update ruangStore with generated rooms
const result = ruangStore.replaceAllRooms(allRooms);
console.log('💾 Rooms saved to ruangStore:', result.message);
return result;
};
// Load data on mount
onMounted(async () => {
console.log('🚀 Anjungan/AntrianKlinikRuang/index mounted, loading data...');
try {
// Fetch clinics from API (uses cache if available)
const fetchResult = await clinicStore.fetchRegulerClinics();
console.log('📥 Fetch result:', fetchResult);
// 1. Fetch clinics from API (uses cache if available)
await clinicStore.fetchRegulerClinics();
// Generate rooms based on fetched data
generateRoomsFromClinics();
// 2. Sync room definitions from API (centralized logic)
await ruangStore.fetchRuangFromAPI();
console.log('✅ Anjungan/index data sync completed');
} catch (error) {
console.error('❌ Error loading data:', error);
}
+42 -11
View File
@@ -3576,8 +3576,7 @@ onUnmounted(() => {
background: $neutral-100;
min-height: 100vh;
max-height: 100vh;
padding: 0 4px 0 4px;
width: 100%;
padding: 0 24px;
max-width: 100vw;
box-sizing: border-box;
overflow: hidden;
@@ -4769,7 +4768,7 @@ onUnmounted(() => {
/* Content Grid - Same pattern as AdminLoket.vue */
.content-grid {
margin: 0 8px;
margin: 0;
flex: 1;
min-height: 0;
overflow: visible;
@@ -5981,7 +5980,7 @@ onUnmounted(() => {
/* Mobile Optimization */
@media (max-width: 600px) {
.checkin-page-container {
padding: 0 8px 8px 8px !important;
padding: 0 12px 12px 12px !important;
}
.checkin-page-container :deep(.page-header) {
@@ -6224,10 +6223,18 @@ onUnmounted(() => {
/* Tablet - Medium screens (961px to 1263px) */
@media (min-width: 961px) and (max-width: 1263px) {
.checkin-page-container {
max-height: none !important;
overflow-y: auto !important;
height: auto !important;
padding-bottom: 20px !important;
}
.qr-scan-card,
.manual-input-card {
max-height: calc(100vh - 220px) !important;
overflow-y: auto;
max-height: none !important;
overflow-y: visible !important;
height: auto !important;
}
.content-grid {
@@ -6237,31 +6244,48 @@ onUnmounted(() => {
/* Small tablets and large phones (721px to 960px) */
@media (min-width: 721px) and (max-width: 960px) {
.checkin-page-container {
max-height: none !important;
overflow-y: auto !important;
height: auto !important;
padding-bottom: 20px !important;
}
.qr-scan-card,
.manual-input-card {
max-height: calc(50vh - 100px) !important;
overflow-y: auto;
max-height: none !important;
overflow-y: visible !important;
height: auto !important;
}
}
/* Very small screens (up to 720px) */
@media (max-width: 720px) {
.checkin-page-container {
max-height: none !important;
overflow-y: auto !important;
height: auto !important;
padding-bottom: 16px !important;
}
.qr-scan-card,
.manual-input-card {
max-height: calc(45vh - 70px) !important;
overflow-y: auto;
max-height: none !important;
overflow-y: visible !important;
height: auto !important;
}
.stats-footer-horizontal {
padding: 6px 8px;
gap: 4px;
margin-bottom: 8px !important;
}
}
/* Extra small screens (phones in portrait, up to 480px) */
@media (max-width: 480px) {
.checkin-page-container {
padding: 0 4px 4px 4px !important;
padding: 0 8px 8px 8px !important;
}
.content-grid {
@@ -6516,4 +6540,11 @@ onUnmounted(() => {
display: block;
margin: 0 auto;
}
/* Padding Consistency Overrides for Tablet/Intermediate */
@media (min-width: 601px) and (max-width: 1263px) {
.checkin-page-container {
padding: 0 16px !important;
}
}
</style>
+23 -91
View File
@@ -90,6 +90,13 @@
</v-chip>
<span class="ml-2 body-3 text-medium">{{ item.namaKlinik }}</span>
</template>
<template v-slot:item.jumlahRuang="{ item }">
<v-chip size="small" color="primary" variant="flat" class="text-white">
{{ item.ruangList?.length || 0 }} Ruang
</v-chip>
</template>
<template v-slot:item.namaRuang="{ item }">
<div class="ruang-tags">
@@ -400,12 +407,14 @@ const headers = ref([
{ title: 'No', value: 'no', sortable: true },
{ title: 'Nama Klinik', value: 'namaKlinik', sortable: true },
{ title: 'Kode', value: 'kodeKlinik', sortable: true },
{ title: 'Nama Ruang', value: 'namaRuang', sortable: true },
{ title: 'Jumlah Ruang', value: 'jumlahRuang', sortable: true },
{ title: 'Daftar Ruang', value: 'namaRuang', sortable: true },
{ title: 'Jenis Layanan', value: 'jenisLayanan', sortable: true },
{ title: 'Layar Informasi', value: 'layarInformasi', sortable: false, width: '150px' },
{ title: 'Aksi', value: 'aksi', sortable: false },
]);
const pageCount = computed(() => {
return Math.ceil(filteredTotal.value / itemsPerPage.value) || 1;
});
@@ -536,109 +545,31 @@ const closePreviewDialog = () => {
previewUrl.value = '';
};
// Generate rooms from clinics based on spesialis field
const generateRoomsFromClinics = () => {
console.log('🔄 Generating rooms from clinics...');
const allRooms = [];
let roomIdCounter = 1;
// Get all clinics (Reguler from API + Eksekutif from seed)
const allClinics = clinicStore.clinics;
console.log('📊 Total clinics:', allClinics.length);
console.log('📊 Clinics by type:', {
Reguler: allClinics.filter(c => c.jenisLayanan === 'Reguler').length,
Eksekutif: allClinics.filter(c => c.jenisLayanan === 'Eksekutif').length
});
allClinics.forEach(clinic => {
console.log(`\n Processing clinic: ${clinic.name} (${clinic.kode}) - ${clinic.jenisLayanan}`);
if (clinic.jenisLayanan === 'Reguler') {
// For Reguler: check spesialis from API
if (clinic.spesialis && Array.isArray(clinic.spesialis) && clinic.spesialis.length > 0) {
// Create room ONLY for each specialist (no duplicate clinic-name room)
console.log(` ✅ Has ${clinic.spesialis.length} specialists - creating rooms ONLY from specialists`);
clinic.spesialis.forEach((spec, index) => {
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
namaRuang: spec.Spesialis,
nomorRuang: (index + 1).toString(),
nomorScreen: `${clinic.kode}${index + 1}`,
jenisLayanan: 'Reguler'
});
console.log(` → Room: ${spec.Spesialis}`);
});
} else {
// Only create 1 room with clinic name when NO spesialis
console.log(' ⚠️ No specialists, creating 1 room with clinic name');
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: clinic.kode,
namaRuang: clinic.name,
nomorRuang: '1',
nomorScreen: `${clinic.kode}1`,
jenisLayanan: 'Reguler'
});
}
} else if (clinic.jenisLayanan === 'Eksekutif') {
// For Eksekutif: always 1 room (no spesialis in seed data)
console.log(' 📦 Eksekutif clinic, creating 1 room');
allRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: clinic.kode,
namaRuang: clinic.name,
nomorRuang: '1',
nomorScreen: `${clinic.kode}1`,
jenisLayanan: 'Eksekutif'
});
} else {
console.log(` ⚠️ Unknown jenisLayanan: ${clinic.jenisLayanan}`);
}
});
console.log(`\n✅ Generated ${allRooms.length} rooms from ${allClinics.length} clinics`);
console.log('📋 Rooms by Jenis Layanan:');
console.log(' - Reguler:', allRooms.filter(r => r.jenisLayanan === 'Reguler').length);
console.log(' - Eksekutif:', allRooms.filter(r => r.jenisLayanan === 'Eksekutif').length);
// Update ruangStore with generated rooms
const result = ruangStore.replaceAllRooms(allRooms);
console.log('💾 Rooms saved to ruangStore:', result.message);
return result;
};
// Fetch clinics and generate rooms on mount
// Fetch clinics and sync rooms on mount
onMounted(async () => {
console.log('🚀 MasterKlinikRuang mounted, fetching clinics...');
console.log('🚀 MasterKlinikRuang mounted, syncing data...');
try {
// Fetch clinics from API (uses cache if available)
const fetchResult = await clinicStore.fetchRegulerClinics();
console.log('📥 Fetch result:', fetchResult);
// 1. Fetch clinics from API (uses cache if available)
const fetchClinicResult = await clinicStore.fetchRegulerClinics();
console.log('📥 Clinic fetch result:', fetchClinicResult);
if (!fetchResult.success) {
if (!fetchClinicResult.success) {
snackbar.value = {
show: true,
message: fetchResult.message,
message: `Klinik: ${fetchClinicResult.message}`,
color: 'warning'
};
}
// Generate rooms based on fetched data
const generateResult = generateRoomsFromClinics();
// 2. Fetch and merge rooms from API
const syncRuangResult = await ruangStore.fetchRuangFromAPI();
console.log('🔄 Room sync result:', syncRuangResult);
snackbar.value = {
show: true,
message: generateResult.message,
color: 'success'
message: syncRuangResult.message,
color: syncRuangResult.success ? 'success' : 'error'
};
} catch (error) {
console.error('❌ Error in onMounted:', error);
@@ -649,6 +580,7 @@ onMounted(async () => {
};
}
});
</script>
<style scoped lang="scss">
+172
View File
@@ -322,6 +322,177 @@ export const useRuangStore = defineStore('ruang', () => {
};
};
/**
* Fetch room data from API and merge with clinic metadata
* preserving seed/local data (Eksekutif)
*/
const fetchRuangFromAPI = async (force = false) => {
try {
console.log('🔄 Fetching rooms from API...');
// 1. Ensure clinics are loaded (from clinicStore)
// clinicStore.fetchRegulerClinics is already called in MasterKlinikRuang.vue
// but we use the existing clinics in the store
const allClinics = clinicStore.clinics;
// 2. Fetch room data from new API
const response = await fetch('http://10.10.150.131:8089/api/v1/loket/ruang');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const rawData = await response.json();
const apiRoomsRaw = rawData.data || [];
console.log('📥 Received API rooms:', apiRoomsRaw.length);
// 3. Prepare rooms list (API + Seed)
const mergedRooms = [];
let roomIdCounter = 1;
// Track which clinics are handled by API to avoid duplicates for Reguler
const clinicCodesInAPI = new Set();
// Process API Clinics and their nested Rooms
apiRoomsRaw.forEach(apiClinic => {
const clinicId = apiClinic.idklinik;
const clinicCode = apiClinic.kodeloket || apiClinic.kodeklinik;
const apiRuangan = apiClinic.ruangan || [];
// Find clinic metadata in store
// Find clinic metadata in store - Specifically look for Reguler clinic first
// as API data (idklinik, kodeloket) is for Reguler clinics.
const clinic = allClinics.find(c =>
c.jenisLayanan === 'Reguler' && (
(clinicId && String(c.id) === String(clinicId)) ||
(clinicCode && String(c.kode) === String(clinicCode)) ||
(apiClinic.namaklinik && c.name.toUpperCase() === apiClinic.namaklinik.toUpperCase())
)
) || allClinics.find(c =>
(clinicId && String(c.id) === String(clinicId)) ||
(clinicCode && String(c.kode) === String(clinicCode)) ||
(apiClinic.namaklinik && c.name.toUpperCase() === apiClinic.namaklinik.toUpperCase())
);
if (clinic) {
clinicCodesInAPI.add(clinic.kode);
// Check if API room data is "generic" (only 1 room with same name as clinic)
// to decide whether to prefer Specialist fallback
const isGenericSingleRoom = apiRuangan.length === 1 &&
(apiRuangan[0].namaruangan?.trim().toUpperCase() === clinic.name.toUpperCase() ||
!apiRuangan[0].namaruangan ||
apiRuangan[0].namaruangan === '-');
const hasManySpecialists = clinic.spesialis && clinic.spesialis.length > 1;
// Priority 1: Use nested "ruangan" array from new API
// Skip this if it's just 1 generic room and we have better data in Specialists
if (apiRuangan.length > 0 && !(isGenericSingleRoom && hasManySpecialists)) {
apiRuangan.forEach((apiRoom, index) => {
const specialistName = (clinic.spesialis && clinic.spesialis[index])
? clinic.spesialis[index].Spesialis
: null;
const fallbackName = `Ruang ${apiRoom.idruangan || apiRoom.nomor_ruang || index + 1}`;
mergedRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: apiRoom.idruangan || apiRoom.idruang || `${clinic.kode}-${roomIdCounter}`,
namaRuang: apiRoom.namaruangan || apiRoom.nama_ruang || apiRoom.nama || specialistName || fallbackName,
nomorRuang: apiRoom.idruangan || apiRoom.nomor_ruang || (index + 1).toString(),
nomorScreen: apiRoom.nomor_screen || `${clinic.kode}${apiRoom.idruangan || index + 1}`,
jenisLayanan: clinic.jenisLayanan || 'Reguler'
});
});
}
// Priority 2: Fallback to specialist metadata (Old API) if new API is empty OR generic
else if (clinic.spesialis && clinic.spesialis.length > 0) {
console.log(`️ Using specialists fallback for ${clinic.name} (${apiRuangan.length === 1 ? 'API generic' : 'API empty'})`);
clinic.spesialis.forEach((spec, index) => {
mergedRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
namaRuang: spec.Spesialis,
nomorRuang: (index + 1).toString(),
nomorScreen: `${clinic.kode}${index + 1}`,
jenisLayanan: 'Reguler'
});
});
}
// Priority 3: Single room fallback
else {
const fallbackName = apiClinic.nama_ruang || apiClinic.nama || clinic.name || 'Ruang 1';
mergedRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: apiClinic.idruang || clinic.kode,
namaRuang: fallbackName,
nomorRuang: apiClinic.nomor_ruang || '1',
nomorScreen: apiClinic.nomor_screen || `${clinic.kode}1`,
jenisLayanan: clinic.jenisLayanan || 'Reguler'
});
}
}
});
// 4. Add Seed Data (Eksekutif) + Reguler clinics not in API
allClinics.forEach(clinic => {
if (clinic.jenisLayanan === 'Eksekutif' || !clinicCodesInAPI.has(clinic.kode)) {
console.log(` Adding clinic from seed/metadata: ${clinic.name} (${clinic.jenisLayanan})`);
// Fallback logic similar to original generateRoomsFromClinics
if (clinic.jenisLayanan === 'Reguler' && clinic.spesialis?.length > 0) {
clinic.spesialis.forEach((spec, index) => {
mergedRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
namaRuang: spec.Spesialis,
nomorRuang: (index + 1).toString(),
nomorScreen: `${clinic.kode}${index + 1}`,
jenisLayanan: 'Reguler'
});
});
} else {
mergedRooms.push({
id: roomIdCounter++,
kodeKlinik: clinic.kode,
namaKlinik: clinic.name,
kodeRuang: clinic.kode,
namaRuang: clinic.name,
nomorRuang: '1',
nomorScreen: `${clinic.kode}1`,
jenisLayanan: clinic.jenisLayanan || 'Reguler'
});
}
}
});
// 5. Update store
const result = replaceAllRooms(mergedRooms);
return {
success: true,
message: `Berhasil sinkronisasi ${apiRoomsRaw.length} ruang dari API dan ${allClinics.length} data klinik`
};
} catch (error) {
console.error('❌ Error in fetchRuangFromAPI:', error);
return {
success: false,
message: `Gagal memuat data ruang: ${error.message}`
};
}
};
// Helper to generate unique room ID
const generateRoomId = () => {
return Date.now() + Math.floor(Math.random() * 1000);
@@ -356,6 +527,7 @@ export const useRuangStore = defineStore('ruang', () => {
// New method for API-based room generation
replaceAllRooms,
fetchRuangFromAPI,
};
}, {
persist: {
+8
View File
@@ -0,0 +1,8 @@
import { defineVitestConfig } from '@nuxt/test-utils/config'
export default defineVitestConfig({
test: {
environment: 'happy-dom',
globals: true,
}
})