Files
web-antrean/stores/loketStore.js
T
2026-01-23 08:24:14 +07:00

418 lines
16 KiB
JavaScript

// 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,
},
},
});