558 lines
21 KiB
JavaScript
558 lines
21 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'], // Changed to array for consistency
|
|
tipeLoket: 'EKSEKUTIF', // Add tipeLoket field for consistency
|
|
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: loketPayload.pembayaran || ['EKSEKUTIF'], // Ensure array format
|
|
tipeLoket: loketPayload.tipeLoket || 'EKSEKUTIF', // Add tipeLoket support
|
|
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: loketPayload.pembayaran && Array.isArray(loketPayload.pembayaran)
|
|
? loketPayload.pembayaran
|
|
: ['EKSEKUTIF'], // Ensure array format
|
|
tipeLoket: loketPayload.tipeLoket || 'EKSEKUTIF', // Add tipeLoket support
|
|
};
|
|
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
|
|
* MENGGUNAKAN endpoint /api/v1/klinik/loket yang sudah terstruktur per Loket
|
|
*/
|
|
const fetchLoketFromAPI = async (force = false, retryCount = 0) => {
|
|
if (activeFetchPromise && retryCount === 0) {
|
|
console.log('⏳ Loket fetch in progress, reusing existing request...');
|
|
return activeFetchPromise;
|
|
}
|
|
|
|
const hasData = apiLoketData.value && apiLoketData.value.length > 0;
|
|
const hasCachedData = hasData && lastSyncTimestamp.value;
|
|
|
|
// If we have cached data and not forcing refresh, use cache
|
|
if (hasCachedData && !force && retryCount === 0) {
|
|
const cacheAge = Date.now() - new Date(lastSyncTimestamp.value).getTime();
|
|
const cacheAgeMinutes = Math.floor(cacheAge / 60000);
|
|
console.log(`📦 Using cached API data (${cacheAgeMinutes} minutes old, ${apiLoketData.value.length} items)`);
|
|
return {
|
|
success: true,
|
|
message: `Data tersedia dari cache (${cacheAgeMinutes} menit yang lalu)`,
|
|
cached: true
|
|
};
|
|
}
|
|
|
|
const performFetch = async () => {
|
|
isLoadingAPI.value = true;
|
|
apiError.value = null;
|
|
|
|
try {
|
|
console.log(`🔄 [loketStore] Fetching loket configuration (Try ${retryCount + 1})...`);
|
|
const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/loket');
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 429 && retryCount < 3) {
|
|
const delay = (retryCount + 1) * 1500;
|
|
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();
|
|
const loketsRaw = rawData.data || [];
|
|
|
|
// MAPPING: Convert API Structure to Store Format
|
|
const mappedLokets = loketsRaw.map(l => {
|
|
const id = parseInt(l.idloket);
|
|
|
|
// Map pelayanan (clinic codes) dan detail spesialis
|
|
const spesialisDetail = (l.spesialis || []).map(s => {
|
|
// Coba cari kode klinik dari clinicStore untuk konsistensi
|
|
const clinic = clinicStore.clinics.find(c =>
|
|
String(c.id) === String(s.idklinik) ||
|
|
c.name === s.namaklinik
|
|
);
|
|
|
|
return {
|
|
idklinik: s.idklinik,
|
|
namaklinik: s.namaklinik,
|
|
code: clinic ? clinic.kode : (s.kode || s.idklinik)
|
|
};
|
|
});
|
|
|
|
// Extract unique codes for 'pelayanan' field
|
|
const pelayananCodes = [...new Set(spesialisDetail.map(s => s.code))];
|
|
|
|
// Map payment types as array for multiple chips display
|
|
const pembayaranArray = (l.pembayaran || [])
|
|
.map(p => p.pembayaran)
|
|
.filter(Boolean);
|
|
|
|
// If no payment types, default to ['JKN']
|
|
const pembayaran = pembayaranArray.length > 0 ? pembayaranArray : ['JKN'];
|
|
|
|
return {
|
|
id: id,
|
|
namaLoket: l.namaloket,
|
|
kodeLoket: l.kodeloket,
|
|
kuota: parseInt(l.kuotaloket) || 100,
|
|
pelayanan: pelayananCodes,
|
|
_spesialisDetail: spesialisDetail,
|
|
pembayaran: pembayaran, // Now an array instead of joined string
|
|
tipeLoket: l.tipeloket || 'REGULER', // Map tipeloket to tipeLoket (capital L)
|
|
source: 'api',
|
|
loketAktif: l.loketaktif ?? true,
|
|
jenisloket: l.jenisloket,
|
|
tipeloket: l.tipeloket // Keep original for backward compatibility
|
|
};
|
|
});
|
|
|
|
// Sort by id for stability and set sequential No
|
|
mappedLokets.sort((a, b) => a.id - b.id);
|
|
mappedLokets.forEach((loket, idx) => {
|
|
loket.no = idx + 1;
|
|
});
|
|
|
|
console.log(`✅ [loketStore] Successfully mapped ${mappedLokets.length} lokets from API`);
|
|
|
|
apiLoketData.value = mappedLokets;
|
|
lastSyncTimestamp.value = new Date().toISOString();
|
|
|
|
return {
|
|
success: true,
|
|
message: `${mappedLokets.length} loket berhasil dikonfigurasi`,
|
|
data: mappedLokets
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('❌ [loketStore] Error fetching loket config:', error);
|
|
apiError.value = error.message;
|
|
|
|
// FALLBACK 1: Use cached data if available
|
|
if (apiLoketData.value.length > 0) {
|
|
const cacheAge = lastSyncTimestamp.value
|
|
? Math.floor((Date.now() - new Date(lastSyncTimestamp.value).getTime()) / 60000)
|
|
: 'unknown';
|
|
console.warn(`⚠️ [loketStore] API failed, using cached data (${cacheAge} minutes old)`);
|
|
return {
|
|
success: true,
|
|
message: `API gagal, menggunakan data cache (${cacheAge} menit yang lalu)`,
|
|
warning: true,
|
|
cached: true
|
|
};
|
|
}
|
|
|
|
// FALLBACK 2: If no cache, populate with dummy Reguler data for Dev/Offline mode
|
|
console.warn('⚠️ [loketStore] No cache available, using FALLBACK dummy data...');
|
|
const dummyLokets = Array.from({length: 6}, (_, i) => ({
|
|
id: i + 1,
|
|
namaLoket: `LOKET ${i + 1} REG (Mock)`,
|
|
kodeLoket: `L${i+1}`,
|
|
kuota: 100,
|
|
pelayanan: ['UM', 'BP', 'OB', 'AN', 'IP', 'SR', 'TH', 'MT', 'KK', 'PR'], // Mock all services
|
|
_spesialisDetail: [], // Empty detail
|
|
pembayaran: ['BPJS', 'UMUM'], // Changed to array for consistency
|
|
tipeLoket: 'REGULER', // Add tipeLoket for fallback data
|
|
source: 'api', // Mimic API
|
|
loketAktif: true,
|
|
jenisloket: 'REGULER',
|
|
tipeloket: 'REGULER',
|
|
no: i + 1
|
|
}));
|
|
apiLoketData.value = dummyLokets;
|
|
return { success: true, message: 'Menggunakan data fallback (API Offline)', warning: true };
|
|
|
|
return { success: false, message: `Gagal memuat: ${error.message}` };
|
|
} finally {
|
|
isLoadingAPI.value = false;
|
|
activeFetchPromise = null;
|
|
}
|
|
};
|
|
|
|
activeFetchPromise = performFetch();
|
|
return activeFetchPromise;
|
|
};
|
|
|
|
/**
|
|
* Fetch detail data for a SPECIFIC loket
|
|
* Endpoint: /api/v1/loket/:loketid
|
|
*/
|
|
const fetchSingleLoketFromAPI = async (loketId) => {
|
|
if (!loketId) return { success: false, message: 'ID Loket diperlukan' };
|
|
|
|
isLoadingAPI.value = true;
|
|
apiError.value = null;
|
|
|
|
try {
|
|
console.log(`🔄 [loketStore] Fetching detail for loket ${loketId}...`);
|
|
const response = await fetch(`http://10.10.150.131:8089/api/v1/loket/${loketId}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const rawData = await response.json();
|
|
|
|
if (rawData.metadata && rawData.metadata.code !== 200) {
|
|
throw new Error(rawData.message || 'API returned error status');
|
|
}
|
|
|
|
// Check if data is array or single object
|
|
const data = rawData.data;
|
|
if (!data) throw new Error('No data returned from API');
|
|
|
|
// The API /api/v1/loket/:id seems to return structure similar to /api/v1/klinik/loket
|
|
// but filtered for one loket. Let's handle both array and object.
|
|
const l = Array.isArray(data) ? data[0] : data;
|
|
|
|
if (!l) throw new Error('Loket data empty');
|
|
|
|
// Map to store format
|
|
const id = parseInt(l.idloket || l.id);
|
|
|
|
const spesialisDetail = (l.spesialis || []).map(s => {
|
|
const clinic = clinicStore.clinics.find(c =>
|
|
String(c.id) === String(s.idklinik) ||
|
|
c.name === s.namaklinik
|
|
);
|
|
return {
|
|
idklinik: s.idklinik,
|
|
namaklinik: s.namaklinik,
|
|
code: clinic ? clinic.kode : (s.kode || s.idklinik)
|
|
};
|
|
});
|
|
|
|
const pelayananCodes = [...new Set(spesialisDetail.map(s => s.code))];
|
|
const pembayaranArray = (l.pembayaran || []).map(p => p.pembayaran).filter(Boolean);
|
|
const pembayaran = pembayaranArray.length > 0 ? pembayaranArray : ['JKN'];
|
|
|
|
const mapped = {
|
|
id: id,
|
|
namaLoket: l.namaloket,
|
|
kodeLoket: l.kodeloket,
|
|
kuota: parseInt(l.kuotaloket) || 100, // Kuota is part of the detail but we keep it
|
|
pelayanan: pelayananCodes,
|
|
_spesialisDetail: spesialisDetail,
|
|
pembayaran: pembayaran,
|
|
tipeLoket: l.tipeloket || 'REGULER',
|
|
source: 'api',
|
|
loketAktif: l.loketaktif ?? true,
|
|
jenisloket: l.jenisloket,
|
|
tipeloket: l.tipeloket
|
|
};
|
|
|
|
// Sync to apiLoketData
|
|
const index = apiLoketData.value.findIndex(item => item.id === id);
|
|
if (index !== -1) {
|
|
// PRESERVE sequential 'no' if exists
|
|
mapped.no = apiLoketData.value[index].no;
|
|
apiLoketData.value[index] = mapped;
|
|
} else {
|
|
mapped.no = apiLoketData.value.length + 1;
|
|
apiLoketData.value.push(mapped);
|
|
apiLoketData.value.sort((a, b) => a.id - b.id);
|
|
}
|
|
|
|
return { success: true, message: `Detail loket ${mapped.namaLoket} diperbarui`, data: mapped };
|
|
} catch (error) {
|
|
console.error(`❌ [loketStore] Error fetching detail for loket ${loketId}:`, error);
|
|
apiError.value = error.message;
|
|
return { success: false, message: `Gagal memuat detail loket: ${error.message}` };
|
|
} finally {
|
|
isLoadingAPI.value = false;
|
|
}
|
|
};
|
|
|
|
return {
|
|
// State - Base refs (for persist)
|
|
apiLoketData, // Raw API data array (persisted)
|
|
localLoketData, // Raw local data array (persisted)
|
|
|
|
// State - Computed
|
|
loketData,
|
|
lokets: loketData, // Alias for backward compatibility/clarity
|
|
availableServices,
|
|
|
|
// API State
|
|
isLoadingAPI,
|
|
apiError,
|
|
lastSyncTimestamp,
|
|
|
|
// Actions
|
|
addLoket,
|
|
updateLoket,
|
|
deleteLoket,
|
|
getLoketById,
|
|
getLoketLetter,
|
|
|
|
// API Actions
|
|
fetchLoketFromAPI,
|
|
fetchSingleLoketFromAPI,
|
|
};
|
|
}, {
|
|
persist: {
|
|
key: 'loket-store-state',
|
|
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
|
paths: [
|
|
'localLoketData', // Persist EKSEKUTIF data (ID 1000+)
|
|
'apiLoketData', // Persist API data (REGULER) to minimize data loss
|
|
'lastSyncTimestamp' // Track when API data was last fetched
|
|
],
|
|
serializer: {
|
|
deserialize: JSON.parse,
|
|
serialize: JSON.stringify,
|
|
},
|
|
},
|
|
});
|
|
|