diff --git a/components/preview/LoketPreview.vue b/components/preview/LoketPreview.vue new file mode 100644 index 0000000..778b5c7 --- /dev/null +++ b/components/preview/LoketPreview.vue @@ -0,0 +1,541 @@ + + + + + + Memuat data loket... + + + + + + + mdi-hospital-box + + + ANTRIAN LOKET + RSUD dr. Saiful Anwar Provinsi Jawa Timur + {{ loketData.namaLoket }} + + + + + {{ currentTime }} + {{ currentDate }} + + + + + + + + + + {{ klinik.name }} + + mdi-account-multiple + {{ klinik.totalQueues }} + + + + + + + + SEDANG DILAYANI + + {{ klinik.currentQueue.noAntrian.split(' |')[0] }} + + + MENUNGGU PANGGILAN + + + + + + + {{ queue.noAntrian.split(' |')[0] }} + + + + + + mdi-clock-outline + Tidak Ada Antrian + + + + + + + + + + diff --git a/pages/Anjungan/AntrianLoket/[id].vue b/pages/Anjungan/AntrianLoket/[id].vue index cedbfd4..ba7e6f7 100644 --- a/pages/Anjungan/AntrianLoket/[id].vue +++ b/pages/Anjungan/AntrianLoket/[id].vue @@ -256,12 +256,23 @@ const getKlinikNameFromPatient = (patient) => { // Prioritas 1: Gunakan kodeKlinik dari data pasien (dari nomor antrian yang dibuat sebelumnya) // Ini adalah sumber kebenaran karena kodeKlinik disimpan saat nomor antrian dibuat if (patient.kodeKlinik) { - // Cari di clinicStore terlebih dahulu + // 1a. Coba cari di loketData._spesialisDetail (data dari API loket) terlebih dahulu + if (loketData.value && loketData.value._spesialisDetail) { + const spesialis = loketData.value._spesialisDetail.find(s => + String(s.idklinik) === String(patient.kodeKlinik) + ) + if (spesialis && spesialis.namaklinik) { + return spesialis.namaklinik.trim() + } + } + + // 1b. Cari di clinicStore const clinic = clinicStore.getClinicByKode ? clinicStore.getClinicByKode(patient.kodeKlinik) : null if (clinic && clinic.name) { return clinic.name.trim() } - // Fallback ke masterStore + + // 1c. Fallback ke masterStore const klinikData = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(patient.kodeKlinik) : null if (klinikData && klinikData.nama) { return klinikData.nama.trim() @@ -399,13 +410,51 @@ const displayedClinics = computed(() => { clinicsMap.get(klinikName).push(patient) }) - // Jika ada filter pelayanan, pastikan semua pelayanan loket ditampilkan (meskipun belum ada antrian) - if (shouldFilterByPelayanan) { + // PENTING: Selalu tampilkan card untuk semua pelayanan loket, + // bahkan jika tidak ada antrian + if (targetLoketId && loketData.value) { allowedPelayananCodes.forEach(kode => { - const klinikData = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(kode) : null - if (klinikData && !clinicsMap.has(klinikData.nama)) { - // Tambahkan klinik kosong jika belum ada antrian - clinicsMap.set(klinikData.nama, []) + // Prioritas 1: Cari nama klinik dari _spesialisDetail (data API) + let klinikName = null + + if (loketData.value._spesialisDetail) { + const spesialis = loketData.value._spesialisDetail.find(s => + String(s.idklinik) === String(kode) + ) + if (spesialis && spesialis.namaklinik) { + klinikName = spesialis.namaklinik + } + } + + // Prioritas 2: Coba getKlinikById untuk local data (numeric ID seperti 1000, 1001) + // Local data EKSEKUTIF menggunakan ID numeric, bukan kode + if (!klinikName) { + const klinikData = masterStore.getKlinikById ? masterStore.getKlinikById(kode) : null + if (klinikData && klinikData.nama) { + klinikName = klinikData.nama + } + } + + // Prioritas 3: Fallback ke masterStore.getKlinikByKode untuk data yang pakai kode + if (!klinikName) { + const klinikData = masterStore.getKlinikByKode ? masterStore.getKlinikByKode(kode) : null + if (klinikData && klinikData.nama) { + klinikName = klinikData.nama + } + } + + // Prioritas 4: Fallback ke clinicStore + if (!klinikName) { + const clinic = clinicStore.getClinicByKode ? clinicStore.getClinicByKode(kode) : null + if (clinic && clinic.name) { + klinikName = clinic.name + } + } + + // Jika berhasil mendapatkan nama klinik, tambahkan ke map + if (klinikName && !clinicsMap.has(klinikName)) { + // Tambahkan klinik kosong (akan ditampilkan sebagai "Tidak Ada Antrian") + clinicsMap.set(klinikName, []) } }) } @@ -864,12 +913,18 @@ const updateTime = () => { } onMounted(() => { - // Redirect to index if loket not found - if (!loketData.value) { - navigateTo('/anjungan/antrianloket') - return - } + // Fetch loket data dari API di background + loketStore.fetchLoketFromAPI(true).then(result => { + if (result.success) { + console.log('✅ [AntrianLoket] Loket API data loaded:', result.message); + } else { + console.warn('⚠️ [AntrianLoket] Failed to fetch loket from API:', result.message); + } + }).catch(err => { + console.error('❌ [AntrianLoket] Error fetching loket from API:', err); + }); + // Langsung start timer tanpa menunggu fetch updateTime() timeInterval = setInterval(updateTime, 1000) }) diff --git a/pages/Anjungan/AntrianLoket/index.vue b/pages/Anjungan/AntrianLoket/index.vue index b84bc43..36b11c6 100644 --- a/pages/Anjungan/AntrianLoket/index.vue +++ b/pages/Anjungan/AntrianLoket/index.vue @@ -41,7 +41,7 @@ size="small" class="ma-1 chip-preview" > - {{ getKlinikName(pelayananKode) }} + {{ getKlinikName(pelayananKode, loket) }} diff --git a/pages/Setting/MasterKlinik.vue b/pages/Setting/MasterKlinik.vue index 9937df0..716d46c 100644 --- a/pages/Setting/MasterKlinik.vue +++ b/pages/Setting/MasterKlinik.vue @@ -43,7 +43,7 @@ {{ item.jenisLayanan }} @@ -808,15 +808,15 @@ $font-weight-bold: 700; } .chip-reguler { - background-color: $success-600 !important; - color: $neutral-100 !important; - font-weight: $font-weight-medium; + background-color: #009262 !important; + color: #FFFFFF !important; + font-weight: 500; } .chip-eksekutif { - background-color: $secondary-600 !important; - color: $neutral-100 !important; - font-weight: $font-weight-medium; + background-color: #E67E22 !important; + color: #FFFFFF !important; + font-weight: 500; } .btn-edit { diff --git a/pages/Setting/MasterLoket.vue b/pages/Setting/MasterLoket.vue index abc69a2..951e7e8 100644 --- a/pages/Setting/MasterLoket.vue +++ b/pages/Setting/MasterLoket.vue @@ -36,7 +36,7 @@ @@ -47,7 +47,7 @@ size="small" class="mr-1 mb-1 chip-primary-outline" > - {{ masterStore.getKlinikNameByKode(serviceKode) }} + {{ getServiceName(item, serviceKode) }} + + + {{ item.pembayaran }} + + + + + + {{ item.loketAktif ? 'Aktif' : 'Tidak Aktif' }} + + + + + mdi-eye + Preview + + - @@ -246,6 +274,34 @@ + + + + + + mdi-view-dashboard + Preview Loket: {{ previewItem?.namaLoket || '' }} + + + mdi-close + + + + + + + + Memuat preview... + + + + + {{ snackbar.message }} @@ -258,11 +314,15 @@ \ No newline at end of file diff --git a/stores/loketStore.js b/stores/loketStore.js index 67d4ec7..17ada66 100644 --- a/stores/loketStore.js +++ b/stores/loketStore.js @@ -69,145 +69,71 @@ export const useLoketStore = defineStore('loket', () => { const clinicStore = useClinicStore(); const anjunganStore = useAnjunganStore(); - // Get pelayanan contoh dari master anjungan secara dinamis - // Menggunakan computed untuk mendapatkan data terbaru dari anjunganStore - const getPelayananContoh = () => { - // getAllAnjungan adalah computed yang mengembalikan anjunganItems.value - // Jadi langsung akses tanpa .value karena computed sudah handle itu - try { - const anjunganItems = anjunganStore.getAllAnjungan || []; - return getPelayananContohDariAnjungan(anjunganItems); - } catch { - // Fallback jika store belum terinisialisasi - console.warn('AnjunganStore belum terinisialisasi, menggunakan data default'); - return getPelayananContohDariAnjungan([]); - } - }; - - // Inisialisasi data loket dengan pelayanan dari master anjungan - // Data akan diupdate saat store diinisialisasi - const initLoketData = () => { - const pelayananContoh = getPelayananContoh(); - - // Base loket (A-J) dengan pelayanan dari contoh - const baseLoket = [ - { - id: 1, - no: 1, - namaLoket: "Loket A", - kuota: 500, - pelayanan: pelayananContoh.loket1, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 2, - no: 2, - namaLoket: "Loket B", - kuota: 666, - pelayanan: pelayananContoh.loket2, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 3, - no: 3, - namaLoket: "Loket C", - kuota: 666, - pelayanan: pelayananContoh.loket3, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 4, - no: 4, - namaLoket: "Loket D", - kuota: 3676, - pelayanan: pelayananContoh.loket4, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 5, - no: 5, - namaLoket: "Loket E", - kuota: 500, - pelayanan: pelayananContoh.loket5, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 6, - no: 6, - namaLoket: "Loket F", - kuota: 500, - pelayanan: pelayananContoh.loket6, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 7, - no: 7, - namaLoket: "Loket G", - kuota: 500, - pelayanan: pelayananContoh.loket7, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 8, - no: 8, - namaLoket: "Loket H", - kuota: 500, - pelayanan: pelayananContoh.loket8, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 9, - no: 9, - namaLoket: "Loket I", - kuota: 500, - pelayanan: pelayananContoh.loket9, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - { - id: 10, - no: 10, - namaLoket: "Loket J", - kuota: 500, - pelayanan: pelayananContoh.loket10, // Dari master anjungan - pembayaran: "JKN", - keterangan: "ONLINE", - statusPelayanan: "RAWAT JALAN", - }, - ]; - - return baseLoket; - }; - - // State - Loket Data - // Data pelayanan diambil dari contoh master anjungan - const initialData = initLoketData(); - // Initialize with default data, will be overridden by persisted data if exists - const loketData = ref(initialData); + // ============================================ + // STATE - SEPARATED DATA SOURCES + // ============================================ - // After store is created, check if data needs migration - // This runs after Pinia restores from localStorage - // If loketData has less than 10 items, reinitialize with default data (A-J) - if (loketData.value && loketData.value.length < 10) { - loketData.value = initialData; - } + // API Data (JKN/REGULER) - ID 1-14 dari backend + // TIDAK di-persist ke localStorage, selalu fetch fresh dari API + const apiLoketData = ref([]) + + // Local Data (EKSEKUTIF) - ID 1000+ manual input + // DI-persist ke localStorage. Default 14 loket hardcoded. + const localLoketData = ref(Array.from({ length: 14 }, (_, i) => { + // Distribute services securely + // Loket 1-5: Single service + // Loket 6-10: Dual services + // Loket 11+: Single service + let services = []; + if (i < 5) { + services = [1000 + i]; + } else if (i < 10) { + services = [1000 + i, 1005 + i]; + } else { + services = [1000 + i]; + } + + return { + id: 1000 + i, + no: i + 1, + namaLoket: `LOKET ${i + 1} EKS`, + kuota: 500, + pelayanan: services, + pembayaran: 'EKSEKUTIF', + keterangan: 'ONLINE', + statusPelayanan: 'RAWAT JALAN', + source: 'local', + loketAktif: true + }; + })) + + // Merged data (computed) - Gabungan API + Local + const loketData = computed(() => { + // Debug log + if (apiLoketData.value.length > 0) { + console.log('📊 [loketData computed] API data found:', apiLoketData.value.length, 'items'); + } + if (localLoketData.value.length > 0) { + console.log('📊 [loketData computed] Local data found:', localLoketData.value.length, 'items'); + } + + // Create new objects to prevent state mutation in computed + const merged = [ + ...apiLoketData.value.map(i => ({...i})), + ...localLoketData.value.map(i => ({...i})) + ] + + console.log('📊 [loketData computed] Merged total:', merged.length, 'items'); + + // Sort by ID + merged.sort((a, b) => a.id - b.id) + + // Recalculate sequential No + return merged.map((item, index) => ({ + ...item, + no: index + 1 + })) + }) // Computed - Available services (reference dari clinicStore) // Mengambil data dari clinicStore untuk dropdown pelayanan @@ -258,48 +184,60 @@ export const useLoketStore = defineStore('loket', () => { }); // Actions - CRUD Operations + // ADD: Hanya untuk EKSEKUTIF (ID 1000+) const addLoket = (loketPayload) => { - const newId = Math.max(...loketData.value.map(l => l.id), 0) + 1; + // Generate ID starting from 1000 for EKSEKUTIF + const maxLocalId = localLoketData.value.length > 0 + ? Math.max(...localLoketData.value.map(l => l.id)) + : 999; + + const newId = maxLocalId + 1; const newNo = loketData.value.length + 1; - // Jika namaLoket tidak disediakan atau masih menggunakan angka, ubah ke huruf - let namaLoket = loketPayload.namaLoket; - if (!namaLoket || namaLoket.match(/Loket \d+/)) { - const letter = numberToLetter(newNo); - namaLoket = `Loket ${letter}`; - } - const newLoket = { id: newId, no: newNo, ...loketPayload, - namaLoket: namaLoket, + pembayaran: 'EKSEKUTIF', // Force EKSEKUTIF for local data + source: 'local', // Mark as local data + loketAktif: loketPayload.loketAktif ?? true, // Default aktif }; - loketData.value.push(newLoket); + localLoketData.value.push(newLoket); return { success: true, message: `Loket ${newLoket.namaLoket} berhasil ditambahkan`, data: newLoket }; }; + // UPDATE: Hanya untuk EKSEKUTIF (source='local') const updateLoket = (loketPayload) => { - const index = loketData.value.findIndex(l => l.id === loketPayload.id); + // Check if trying to edit API data + if (loketPayload.source === 'api' || (loketPayload.id >= 1 && loketPayload.id <= 100)) { + return { success: false, message: 'Data JKN dari API tidak bisa diedit. Hanya data EKSEKUTIF yang bisa diubah.' }; + } + + const index = localLoketData.value.findIndex(l => l.id === loketPayload.id); if (index !== -1) { - loketData.value[index] = { - ...loketData.value[index], + localLoketData.value[index] = { + ...localLoketData.value[index], ...loketPayload, + source: 'local', // Ensure source stays local + pembayaran: 'EKSEKUTIF', // Ensure pembayaran stays EKSEKUTIF }; return { success: true, message: `Loket ${loketPayload.namaLoket} berhasil diupdate` }; } return { success: false, message: 'Loket tidak ditemukan' }; }; + // DELETE: Hanya untuk EKSEKUTIF (source='local') const deleteLoket = (loketId) => { - const index = loketData.value.findIndex(l => l.id === loketId); + // Check if trying to delete API data + if (loketId >= 1 && loketId <= 100) { + return { success: false, message: 'Data JKN dari API tidak bisa dihapus. Hanya data EKSEKUTIF yang bisa dihapus.' }; + } + + const index = localLoketData.value.findIndex(l => l.id === loketId); if (index !== -1) { - const loketName = loketData.value[index].namaLoket; - loketData.value.splice(index, 1); - loketData.value.forEach((l, idx) => { - l.no = idx + 1; - }); + const loketName = localLoketData.value[index].namaLoket; + localLoketData.value.splice(index, 1); return { success: true, message: `Loket ${loketName} berhasil dihapus` }; } return { success: false, message: 'Loket tidak ditemukan' }; @@ -312,10 +250,148 @@ export const useLoketStore = defineStore('loket', () => { // Helper function untuk convert nomor ke huruf (export untuk digunakan di komponen) const getLoketLetter = (num) => numberToLetter(num); + // ============================================ + // API INTEGRATION + // ============================================ + + // State untuk API management + const isLoadingAPI = ref(false); + const apiError = ref(null); + const lastSyncTimestamp = ref(null); + let activeFetchPromise = null; + + /** + * Fetch loket data dari API backend + * Mengikuti pattern dari clinicStore.fetchRegulerClinics() + * Filter hanya loket dengan pembayaran JKN (REGULER) + * Sort berdasarkan nama loket (urut dari 1) + */ + const fetchLoketFromAPI = async (force = false, retryCount = 0) => { + // Guard: singleton pattern - prevent duplicate fetch + if (activeFetchPromise && retryCount === 0) { + console.log('⏳ Loket fetch in progress, reusing existing request...'); + return activeFetchPromise; + } + + // Guard: Skip if data already exists and not forced + const hasData = loketData.value && loketData.value.length > 0; + if (hasData && !force && retryCount === 0) { + return { success: true, message: 'Data sudah tersedia' }; + } + + // Define the actual fetch operation + const performFetch = async () => { + isLoadingAPI.value = true; + apiError.value = null; + + try { + console.log(`🔄 Fetching loket from API (Try ${retryCount + 1})...`); + const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/loket'); + + if (!response.ok) { + // Handle Rate Limiting with exponential backoff + if (response.status === 429 && retryCount < 3) { + const delay = (retryCount + 1) * 1500; + console.warn(`⚠️ Rate limit hit (429). Retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + activeFetchPromise = null; + return fetchLoketFromAPI(force, retryCount + 1); + } + throw new Error(`HTTP error! status: ${response.status}`); + } + + const rawData = await response.json(); + console.log('📦 Raw API Response received'); + console.log('📦 Raw data:', rawData); + + // Extract data array + const data = rawData.data || rawData; + if (!Array.isArray(data)) { + throw new Error('Data format tidak valid. Expected array.'); + } + + console.log('📊 Total items from API:', data.length); + + // Filter hanya JKN (REGULER) dan transform ke format internal + const lokets = data + .filter(item => { + const hasJKN = item.pembayaran?.some(p => p.pembayaran === 'JKN'); + if (!hasJKN) { + console.warn('⚠️ Loket skipped (no JKN):', item.namaloket, 'pembayaran:', item.pembayaran); + } + return hasJKN; + }) + .map((item) => ({ + id: parseInt(item.idloket), + no: 0, // Will be set after sort + namaLoket: item.namaloket, + kodeLoket: item.kodeloket, + kuota: parseInt(item.kuotaloket), + pelayanan: item.spesialis?.map(s => s.idklinik) || [], + pembayaran: 'JKN', + keterangan: item.tipevisit?.map(t => t.tipevisit).join(', ') || 'ONLINE', + statusPelayanan: item.tipeloket || 'REGULER', + loketAktif: item.loketaktif, + jenisLoket: item.jenisloket, + + // Simpan detail lengkap spesialis untuk display nama klinik + _spesialisDetail: item.spesialis || [], + })); + + // Sort by nama loket (extract number and sort: LOKET 1, 2, 3,... 14) + lokets.sort((a, b) => { + const numA = parseInt(a.namaLoket.match(/\d+/)?.[0] || 0); + const numB = parseInt(b.namaLoket.match(/\d+/)?.[0] || 0); + return numA - numB; + }); + + // Update `no` field to sequential after sort + lokets.forEach((loket, idx) => { + loket.no = idx + 1; + loket.source = 'api'; // Mark as API data + }); + + console.log(`✅ ${lokets.length} loket JKN berhasil di-filter dan dimuat dari API`); + console.log('✅ Loket data:', lokets.map(l => ({ id: l.id, namaLoket: l.namaLoket, pembayaran: l.pembayaran }))); + + // Update API store - TIDAK di-persist + apiLoketData.value = lokets; + lastSyncTimestamp.value = new Date().toISOString(); + + console.log(`✅ ${lokets.length} loket JKN berhasil dimuat dari API`); + return { + success: true, + message: `${lokets.length} loket berhasil dimuat`, + data: lokets + }; + + } catch (error) { + console.error('❌ Error fetching loket:', error); + apiError.value = error.message; + return { + success: false, + message: `Gagal memuat data: ${error.message}` + }; + } finally { + isLoadingAPI.value = false; + activeFetchPromise = null; + console.log('✅ API fetch flow completed'); + } + }; + + activeFetchPromise = performFetch(); + return activeFetchPromise; + }; + return { // State loketData, availableServices, + + // API State + isLoadingAPI, + apiError, + lastSyncTimestamp, // Actions addLoket, @@ -323,29 +399,19 @@ export const useLoketStore = defineStore('loket', () => { deleteLoket, getLoketById, getLoketLetter, + + // API Actions + fetchLoketFromAPI, }; }, { persist: { key: 'loket-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, - paths: ['loketData'], + paths: ['localLoketData'], // ONLY persist EKSEKUTIF data serializer: { deserialize: JSON.parse, serialize: JSON.stringify, }, - restore: (value) => { - // Ensure loketData is always an array - if (value && value.loketData && !Array.isArray(value.loketData)) { - value.loketData = []; - } - // If restored data has less than 10 lokets, reinitialize with default data (A-J) - if (value && value.loketData && Array.isArray(value.loketData) && value.loketData.length < 10) { - // Reinitialize with default data (A-J) - this will happen on next store initialization - // For now, we'll let the store initialization handle it - value.loketData = null; // Force reinitialization - } - return value; - }, }, });
Memuat data loket...
RSUD dr. Saiful Anwar Provinsi Jawa Timur
{{ loketData.namaLoket }}
Tidak Ada Antrian
Memuat preview...