// stores/loketStore.js import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { useClinicStore } from './clinicStore'; import { useAnjunganStore } from './anjunganStore'; // Helper function: Convert nomor loket ke huruf (1 -> A, 2 -> B, dst) const numberToLetter = (num) => { if (num < 1) return 'A'; // 1 -> A (65), 2 -> B (66), ..., 26 -> Z (90) const charCode = 64 + num; // 64 = '@', 65 = 'A' return String.fromCharCode(Math.min(charCode, 90)); // Maksimal Z (90) }; // Helper function: Get pelayanan contoh dari master anjungan // Mengambil data klinik dari anjungan pertama (Reguler) sebagai contoh const getPelayananContohDariAnjungan = (anjunganItems) => { // Ambil data klinik dari anjungan pertama (Reguler) jika ada let klinikAnjunganReguler = []; if (anjunganItems && anjunganItems.length > 0) { // Cari anjungan dengan jenisPasien 'Reguler' atau ambil yang pertama const anjunganReguler = anjunganItems.find(a => a.jenisPasien === 'Reguler') || anjunganItems[0]; klinikAnjunganReguler = anjunganReguler.klinik || []; } // Jika tidak ada data dari anjungan, gunakan data default dari master anjungan if (klinikAnjunganReguler.length === 0) { klinikAnjunganReguler = ['AN', 'AS', 'BD', 'GR', 'HO', 'GI', 'GZ', 'IP', 'JT', 'JW', 'KK', 'MT', 'OB', 'PR', 'RT', 'RM', 'SR']; } // Distribusi pelayanan ke beberapa loket sebagai contoh // Menggunakan data klinik yang tersedia dari master anjungan return { loket1: klinikAnjunganReguler.filter(k => ['AN','RT', 'RM', 'TD'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['RT', 'RM', 'TD'].includes(k)) : klinikAnjunganReguler.slice(0, 3), // Ambil 3 pertama jika tidak ada match loket2: klinikAnjunganReguler.filter(k => ['JW', 'SR'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['JW', 'SR'].includes(k)) : klinikAnjunganReguler.slice(3, 5), // Ambil 2 berikutnya loket3: klinikAnjunganReguler.filter(k => ['AS', 'JT'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['AS', 'JT'].includes(k)) : klinikAnjunganReguler.slice(5, 7), // Ambil 2 berikutnya loket4: klinikAnjunganReguler.filter(k => ['KK', 'PR'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['KK', 'PR'].includes(k)) : klinikAnjunganReguler.slice(7, 9), // Ambil 2 berikutnya loket5: klinikAnjunganReguler.filter(k => ['BD', 'GI'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['BD', 'GI'].includes(k)) : klinikAnjunganReguler.slice(9, 11), // Ambil 2 berikutnya loket6: klinikAnjunganReguler.filter(k => ['GR', 'GZ'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['GR', 'GZ'].includes(k)) : klinikAnjunganReguler.slice(11, 13), // Ambil 2 berikutnya loket7: klinikAnjunganReguler.filter(k => ['IP', 'MT'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['IP', 'MT'].includes(k)) : klinikAnjunganReguler.slice(13, 15), // Ambil 2 berikutnya loket8: klinikAnjunganReguler.filter(k => ['OB', 'HO'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['OB', 'HO'].includes(k)) : klinikAnjunganReguler.slice(15, 17), // Ambil 2 berikutnya loket9: klinikAnjunganReguler.filter(k => ['AN', 'BD'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['AN', 'BD'].includes(k)) : klinikAnjunganReguler.slice(0, 2), // Ambil 2 pertama jika tidak ada match loket10: klinikAnjunganReguler.filter(k => ['GI', 'GZ'].includes(k)).length > 0 ? klinikAnjunganReguler.filter(k => ['GI', 'GZ'].includes(k)) : klinikAnjunganReguler.slice(2, 4), // Ambil 2 berikutnya }; }; export const useLoketStore = defineStore('loket', () => { const clinicStore = useClinicStore(); const anjunganStore = useAnjunganStore(); // ============================================ // STATE - SEPARATED DATA SOURCES // ============================================ // 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 // Menggunakan getAllClinics computed untuk memastikan reactivity const availableServices = computed(() => { try { // Gunakan getAllClinics computed dari clinicStore // Ini akan otomatis reactive ketika clinics berubah const clinicsComputed = clinicStore.getAllClinics; if (!clinicsComputed) { console.warn('clinicStore.getAllClinics is not available'); return []; } // getAllClinics adalah computed, akses .value untuk mendapatkan array // Vue akan otomatis track dependency ini const clinicsArray = clinicsComputed.value || []; // Pastikan clinicsArray adalah array if (!Array.isArray(clinicsArray)) { console.warn('getAllClinics.value is not an array:', typeof clinicsArray, clinicsArray); return []; } if (clinicsArray.length === 0) { console.warn('No clinics available from clinicStore'); return []; } // Map ke format yang dibutuhkan form: { id, nama, kode } // id menggunakan kode untuk kompatibilitas dengan form yang menggunakan item-value="id" return clinicsArray.map(c => { if (!c || !c.kode || !c.name) { console.warn('Invalid clinic data:', c); return null; } return { id: c.kode, // Menggunakan kode sebagai id (sesuai dengan item-value="id" di form) nama: c.name, kode: c.kode, }; }).filter(Boolean); // Filter out null values } catch (error) { console.error('Error getting available services from clinicStore:', error); return []; } }); // Actions - CRUD Operations // ADD: Hanya untuk EKSEKUTIF (ID 1000+) const addLoket = (loketPayload) => { // 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; const newLoket = { id: newId, no: newNo, ...loketPayload, pembayaran: 'EKSEKUTIF', // Force EKSEKUTIF for local data source: 'local', // Mark as local data loketAktif: loketPayload.loketAktif ?? true, // Default aktif }; localLoketData.value.push(newLoket); return { success: true, message: `Loket ${newLoket.namaLoket} berhasil ditambahkan`, data: newLoket }; }; // UPDATE: Hanya untuk EKSEKUTIF (source='local') const updateLoket = (loketPayload) => { // 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) { 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) => { // 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 = localLoketData.value[index].namaLoket; localLoketData.value.splice(index, 1); return { success: true, message: `Loket ${loketName} berhasil dihapus` }; } return { success: false, message: 'Loket tidak ditemukan' }; }; const getLoketById = (id) => { return loketData.value.find(l => l.id === id); }; // 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, updateLoket, deleteLoket, getLoketById, getLoketLetter, // API Actions fetchLoketFromAPI, }; }, { persist: { key: 'loket-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, paths: ['localLoketData'], // ONLY persist EKSEKUTIF data serializer: { deserialize: JSON.parse, serialize: JSON.stringify, }, }, });