update verif dkk
This commit is contained in:
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
/**
|
||||
* Reusable Infinite Scroll Composable
|
||||
* @param {import('vue').Ref<Array>} sourceData - The full source array of data
|
||||
* @param {number} pageSize - Number of items to load per "page"
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Object} { visibleItems, targetRef, loadMore, reset }
|
||||
*/
|
||||
export function useInfiniteScroll(sourceData, pageSize = 20, options = {}) {
|
||||
const visibleCount = ref(pageSize);
|
||||
const targetRef = ref(null);
|
||||
|
||||
// Compute visible items based on current count
|
||||
const visibleItems = computed(() => {
|
||||
const data = sourceData.value || [];
|
||||
return data.slice(0, visibleCount.value);
|
||||
});
|
||||
|
||||
// Check if there are more items to load
|
||||
const hasMore = computed(() => {
|
||||
const data = sourceData.value || [];
|
||||
return visibleCount.value < data.length;
|
||||
});
|
||||
|
||||
const loadMore = () => {
|
||||
if (hasMore.value) {
|
||||
visibleCount.value += pageSize;
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
visibleCount.value = pageSize;
|
||||
};
|
||||
|
||||
// Setup IntersectionObserver
|
||||
let observer = null;
|
||||
|
||||
onMounted(() => {
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry.isIntersecting && hasMore.value) {
|
||||
loadMore();
|
||||
}
|
||||
}, {
|
||||
root: null,
|
||||
rootMargin: '100px', // Preload before reaching bottom
|
||||
threshold: 0.1,
|
||||
...options
|
||||
});
|
||||
|
||||
if (targetRef.value) {
|
||||
observer.observe(targetRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer) observer.disconnect();
|
||||
});
|
||||
|
||||
// Watch for targetRef changes (e.g., if valid status changes)
|
||||
watch(targetRef, (el) => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
if (el) observer.observe(el);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset when source data changes significantly (optional, depending on use case)
|
||||
// watch(sourceData, () => {
|
||||
// reset(); // Uncomment if you want to reset scroll on data refresh
|
||||
// });
|
||||
|
||||
return {
|
||||
visibleItems,
|
||||
targetRef,
|
||||
loadMore,
|
||||
reset,
|
||||
hasMore
|
||||
};
|
||||
}
|
||||
@@ -104,7 +104,8 @@ const ruangStore = useRuangStore();
|
||||
const loading = ref(false);
|
||||
|
||||
const klinikRuangList = computed(() => {
|
||||
return masterStore.ruangData || [];
|
||||
const list = masterStore.ruangData || [];
|
||||
return [...list].sort((a, b) => a.namaKlinik.localeCompare(b.namaKlinik));
|
||||
});
|
||||
|
||||
const navigateToKlinik = (kodeKlinik, jenisLayanan) => {
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
</div>
|
||||
|
||||
<div class="klinik-selection-container">
|
||||
<div class="kliniks-grid" v-if="paginatedKliniks.length > 0">
|
||||
<div class="kliniks-grid" v-if="visibleKliniks.length > 0">
|
||||
<div
|
||||
v-for="klinik in paginatedKliniks"
|
||||
v-for="klinik in visibleKliniks"
|
||||
:key="`${klinik.kodeKlinik}-${klinik.jenisLayanan}`"
|
||||
class="klinik-card"
|
||||
@click="navigateToKlinik(klinik)"
|
||||
@@ -79,6 +79,9 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sentinel for Infinite Scroll -->
|
||||
<div ref="targetRef" class="sentinel" style="height: 20px; width: 100%; grid-column: 1 / -1;"></div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
@@ -96,11 +99,7 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="paginatedKliniks.length > 0 && totalPages > 1" class="pagination">
|
||||
<v-btn variant="outlined" @click="goPrev" :disabled="page <= 1">Prev</v-btn>
|
||||
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
|
||||
<v-btn variant="outlined" @click="goNext" :disabled="page >= totalPages">Next</v-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -110,6 +109,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useMasterStore } from '@/stores/masterStore';
|
||||
import { useClinicStore } from '@/stores/clinicStore';
|
||||
import { useRuangStore } from '@/stores/ruangStore';
|
||||
import { useInfiniteScroll } from '@/composables/useInfiniteScroll';
|
||||
import { useRoute } from '#app';
|
||||
|
||||
definePageMeta({
|
||||
@@ -123,30 +123,15 @@ const route = useRoute();
|
||||
|
||||
// Simplified computed to use masterStore.ruangData (which is already grouped/processed by ruangStore)
|
||||
const kliniksWithRuang = computed(() => {
|
||||
return masterStore.ruangData || [];
|
||||
const list = masterStore.ruangData || [];
|
||||
return [...list].sort((a, b) => a.namaKlinik.localeCompare(b.namaKlinik));
|
||||
});
|
||||
|
||||
// Pagination
|
||||
const itemsPerPage = 12; // Increased for better fill
|
||||
const page = computed({
|
||||
get: () => Number(route.query.page || 1),
|
||||
set: (val) => navigateTo({ query: { ...route.query, page: val } }),
|
||||
});
|
||||
// Infinite Scroll
|
||||
const { visibleItems: visibleKliniks, targetRef, hasMore } = useInfiniteScroll(kliniksWithRuang, 12);
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(kliniksWithRuang.value.length / itemsPerPage)));
|
||||
// Deprecated: Pagination logic removed
|
||||
|
||||
const paginatedKliniks = computed(() => {
|
||||
const start = (page.value - 1) * itemsPerPage;
|
||||
return kliniksWithRuang.value.slice(start, start + itemsPerPage);
|
||||
});
|
||||
|
||||
const goPrev = () => {
|
||||
if (page.value > 1) page.value = page.value - 1;
|
||||
};
|
||||
|
||||
const goNext = () => {
|
||||
if (page.value < totalPages.value) page.value = page.value + 1;
|
||||
};
|
||||
|
||||
const navigateToKlinik = (klinik) => {
|
||||
// Build URL with jenisLayanan query parameter to differentiate same clinic codes
|
||||
|
||||
@@ -18,80 +18,86 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loket-selection-container">
|
||||
<div v-if="paginatedLokets.length > 0" class="lokets-grid">
|
||||
<div
|
||||
v-for="loket in paginatedLokets"
|
||||
:key="loket.id"
|
||||
class="loket-card"
|
||||
@click="navigateToLoket(loket.id)"
|
||||
>
|
||||
<div class="loket-card-header">
|
||||
<div class="loket-info">
|
||||
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
|
||||
<ClientOnly>
|
||||
<div class="loket-selection-container">
|
||||
<div v-if="visibleLokets.length > 0" class="lokets-grid">
|
||||
<div
|
||||
v-for="loket in visibleLokets"
|
||||
:key="loket.id"
|
||||
class="loket-card"
|
||||
@click="navigateToLoket(loket.id)"
|
||||
>
|
||||
<div class="loket-card-header">
|
||||
<div class="loket-info">
|
||||
<h3 class="loket-name">{{ loket.namaLoket }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loket-preview">
|
||||
<div class="loket-service-count">
|
||||
<v-icon size="18" color="primary-600">mdi-hospital-building</v-icon>
|
||||
<span>{{ loket.pelayanan?.length || 0 }} Pelayanan</span>
|
||||
|
||||
<div class="loket-preview">
|
||||
<div class="loket-service-count">
|
||||
<v-icon size="18" color="primary-600">mdi-hospital-building</v-icon>
|
||||
<span>{{ loket.pelayanan?.length || 0 }} Pelayanan</span>
|
||||
</div>
|
||||
<div class="loket-tags">
|
||||
<v-chip
|
||||
v-for="(pelayananKode, idx) in (loket.pelayanan || []).slice(0, 3)"
|
||||
:key="idx"
|
||||
size="small"
|
||||
class="ma-1 chip-preview"
|
||||
>
|
||||
{{ getKlinikName(pelayananKode, loket) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="(loket.pelayanan || []).length > 3"
|
||||
size="small"
|
||||
class="ma-1 chip-more"
|
||||
>
|
||||
+{{ (loket.pelayanan || []).length - 3 }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loket-tags">
|
||||
<v-chip
|
||||
v-for="(pelayananKode, idx) in (loket.pelayanan || []).slice(0, 3)"
|
||||
:key="idx"
|
||||
size="small"
|
||||
class="ma-1 chip-preview"
|
||||
|
||||
<div class="loket-card-footer">
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
size="large"
|
||||
block
|
||||
class="btn-view text-white"
|
||||
>
|
||||
{{ getKlinikName(pelayananKode, loket) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="(loket.pelayanan || []).length > 3"
|
||||
size="small"
|
||||
class="ma-1 chip-more"
|
||||
>
|
||||
+{{ (loket.pelayanan || []).length - 3 }}
|
||||
</v-chip>
|
||||
<v-icon left>mdi-eye</v-icon>
|
||||
Tampilkan
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loket-card-footer">
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
size="large"
|
||||
block
|
||||
class="btn-view text-white"
|
||||
>
|
||||
<v-icon left>mdi-eye</v-icon>
|
||||
Tampilkan
|
||||
</v-btn>
|
||||
<!-- Sentinel for Infinite Scroll (placed inside grid or after it, depending on layout structure) -->
|
||||
<!-- Since grid is flex/grid, we might need it to be full width or just present -->
|
||||
<div ref="targetRef" style="height: 20px; width: 100%; grid-column: 1 / -1;"></div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-view-dashboard-off</v-icon>
|
||||
<h3>Tidak Ada Loket Tersedia</h3>
|
||||
<p>Silakan tambah loket terlebih dahulu di halaman master</p>
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
class="mt-4"
|
||||
@click="navigateToSettings"
|
||||
>
|
||||
<v-icon left>mdi-cog</v-icon>
|
||||
Ke Halaman Master
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<template #fallback>
|
||||
<div class="d-flex justify-center align-center" style="height: 400px;">
|
||||
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-view-dashboard-off</v-icon>
|
||||
<h3>Tidak Ada Loket Tersedia</h3>
|
||||
<p>Silakan tambah loket terlebih dahulu di halaman master</p>
|
||||
<v-btn
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
class="mt-4"
|
||||
@click="navigateToSettings"
|
||||
>
|
||||
<v-icon left>mdi-cog</v-icon>
|
||||
Ke Halaman Master
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="paginatedLokets.length > 0 && totalPages > 1" class="pagination">
|
||||
<v-btn variant="outlined" :disabled="page <= 1" @click="goPrev">Prev</v-btn>
|
||||
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
|
||||
<v-btn variant="outlined" :disabled="page >= totalPages" @click="goNext">Next</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -99,6 +105,7 @@
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useLoketStore } from '@/stores/loketStore';
|
||||
import { useMasterStore } from '@/stores/masterStore';
|
||||
import { useInfiniteScroll } from '@/composables/useInfiniteScroll';
|
||||
import { useRoute } from '#app';
|
||||
|
||||
definePageMeta({
|
||||
@@ -138,27 +145,12 @@ const allLokets = computed(() => {
|
||||
return Array.isArray(lokets) ? lokets : (lokets.value || [])
|
||||
});
|
||||
|
||||
// Pagination
|
||||
const itemsPerPage = 20;
|
||||
const page = computed({
|
||||
get: () => Number(route.query.page || 1),
|
||||
set: (val) => navigateTo({ query: { ...route.query, page: val } }),
|
||||
});
|
||||
// Infinite Scroll (Replaces Pagination)
|
||||
const pageSize = 20;
|
||||
const { visibleItems: visibleLokets, targetRef, hasMore } = useInfiniteScroll(allLokets, pageSize);
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(allLokets.value.length / itemsPerPage)));
|
||||
// Deprecated: Pagination logic removed
|
||||
|
||||
const paginatedLokets = computed(() => {
|
||||
const start = (page.value - 1) * itemsPerPage;
|
||||
return allLokets.value.slice(start, start + itemsPerPage);
|
||||
});
|
||||
|
||||
const goPrev = () => {
|
||||
if (page.value > 1) page.value = page.value - 1;
|
||||
};
|
||||
|
||||
const goNext = () => {
|
||||
if (page.value < totalPages.value) page.value = page.value + 1;
|
||||
};
|
||||
|
||||
const navigateToLoket = (loketId) => {
|
||||
navigateTo(`/anjungan/antrianloket/${loketId}`);
|
||||
|
||||
@@ -134,38 +134,39 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { useDisplay } from 'vuetify';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import QRVerificationDialog from '@/components/verification/QRVerificationDialog.vue';
|
||||
import { useVerificationStore } from '~/stores/verificationStore';
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
});
|
||||
|
||||
const display = useDisplay();
|
||||
const verificationStore = useVerificationStore();
|
||||
const { patients, loading, error } = storeToRefs(verificationStore);
|
||||
|
||||
// Load initial patients on mount
|
||||
onMounted(() => {
|
||||
verificationStore.fetchAllPatients();
|
||||
});
|
||||
|
||||
// Table Headers
|
||||
const headers = ref([
|
||||
{ title: 'No', value: 'no', sortable: false, width: '60px' },
|
||||
{ title: 'Nama Pasien', value: 'nama', sortable: true },
|
||||
{ title: 'No. RM', value: 'rm', sortable: true },
|
||||
{ title: 'Alamat', value: 'alamat', sortable: false },
|
||||
{ title: 'No. Telepon', value: 'telepon', sortable: false },
|
||||
{ title: 'Status', value: 'status', sortable: true, width: '180px' },
|
||||
{ title: 'Actions', value: 'actions', sortable: false, width: '200px' },
|
||||
]);
|
||||
|
||||
// Data
|
||||
const patients = ref([
|
||||
{ nama: 'Budi Santoso', rm: '00123456', alamat: 'Jl. Melati No. 10', telepon: '', status: 'Belum Terverifikasi' },
|
||||
{ nama: 'Siti Aminah', rm: '00123457', alamat: 'Perum. Indah Blok A', telepon: '081234567890', status: 'Terverifikasi' },
|
||||
{ nama: 'Joko Susilo', rm: '00123458', alamat: 'Jl. Pahlawan 5', telepon: '', status: 'Belum Terverifikasi' },
|
||||
{ nama: 'Dewi Lestari', rm: '00123459', alamat: 'Gang Mawar 12', telepon: '089876543210', status: 'Terverifikasi' },
|
||||
{ nama: 'Rina Wijaya', rm: '00123460', alamat: 'Jl. Sudirman 21', telepon: '', status: 'Belum Terverifikasi' },
|
||||
{ title: 'No', value: 'no', sortable: false, width: '60px', align: 'center' },
|
||||
{ title: 'Nama Pasien', value: 'nama', sortable: true, align: 'center' },
|
||||
{ title: 'No. RM', value: 'rm', sortable: true, align: 'center' },
|
||||
{ title: 'Alamat', value: 'alamat', sortable: false, align: 'center' },
|
||||
{ title: 'No. Telepon', value: 'telepon', sortable: false, align: 'center' },
|
||||
{ title: 'Status', value: 'status', sortable: true, width: '180px', align: 'center' },
|
||||
{ title: 'Actions', value: 'actions', sortable: false, width: '200px', align: 'center' },
|
||||
]);
|
||||
|
||||
// Local State
|
||||
const filterStatus = ref(0);
|
||||
const search = ref('');
|
||||
const isModalOpen = ref(false);
|
||||
@@ -173,6 +174,26 @@ const selectedPatient = ref({});
|
||||
const tempTelepon = ref('');
|
||||
const qrCodeGenerated = ref(false);
|
||||
|
||||
// Debounced Search
|
||||
let searchTimeout;
|
||||
watch(search, (newVal) => {
|
||||
clearTimeout(searchTimeout);
|
||||
if (!newVal || newVal.length < 2) {
|
||||
if (!newVal) verificationStore.fetchAllPatients();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set loading via store action if possible or utilize store's async nature
|
||||
// Ideally store handles loading state internally during fetch
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (/^\d+$/.test(newVal)) {
|
||||
verificationStore.fetchPatientByRm(newVal);
|
||||
} else {
|
||||
verificationStore.searchPatientsByName(newVal);
|
||||
}
|
||||
}, 600);
|
||||
});
|
||||
|
||||
// Computed
|
||||
const currentDate = computed(() => {
|
||||
const now = new Date();
|
||||
@@ -197,21 +218,15 @@ const filteredPatients = computed(() => {
|
||||
} else if (filterStatus.value === 2) {
|
||||
statusFiltered = verifiedPatients.value;
|
||||
}
|
||||
|
||||
if (!search.value) return statusFiltered;
|
||||
|
||||
const searchTerm = search.value.toLowerCase();
|
||||
return statusFiltered.filter(p =>
|
||||
p.nama.toLowerCase().includes(searchTerm) ||
|
||||
p.rm.includes(searchTerm)
|
||||
);
|
||||
return statusFiltered;
|
||||
});
|
||||
|
||||
const qrCodeData = computed(() => {
|
||||
return qrCodeGenerated.value
|
||||
? JSON.stringify({
|
||||
rm: selectedPatient.value.rm,
|
||||
phone: tempTelepon.value,
|
||||
phone: tempTelepon.value, // Using tempTelepon which is bound to input
|
||||
action: 'verify-account',
|
||||
timestamp: Date.now()
|
||||
})
|
||||
@@ -241,35 +256,31 @@ const closeModal = () => {
|
||||
|
||||
const generateQrCode = () => {
|
||||
if (tempTelepon.value.length >= 8) {
|
||||
console.log(`[API CALL] Generating QR for ${selectedPatient.value.nama}. QR Data: ${qrCodeData.value}`);
|
||||
setTimeout(() => {
|
||||
qrCodeGenerated.value = true;
|
||||
}, 200);
|
||||
qrCodeGenerated.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const reloadQr = () => {
|
||||
console.log('[API CALL] Reloading QR Code...');
|
||||
qrCodeGenerated.value = false;
|
||||
setTimeout(() => {
|
||||
qrCodeGenerated.value = true;
|
||||
console.log('QR Code reloaded.');
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const completeVerification = () => {
|
||||
const patientIndex = patients.value.findIndex(
|
||||
(p) => p.rm === selectedPatient.value.rm
|
||||
);
|
||||
// Use store action to update local state optimistically
|
||||
verificationStore.updatePatientVerification(selectedPatient.value.rm, tempTelepon.value);
|
||||
|
||||
// TODO: Add API call in store to save verification
|
||||
|
||||
if (patientIndex !== -1) {
|
||||
patients.value[patientIndex].status = 'Terverifikasi';
|
||||
patients.value[patientIndex].telepon = tempTelepon.value;
|
||||
}
|
||||
|
||||
filterStatus.value = 2;
|
||||
filterStatus.value = 2; // Switch tab to verified
|
||||
closeModal();
|
||||
};
|
||||
|
||||
// Cleanup
|
||||
onUnmounted(() => {
|
||||
verificationStore.clearPatients();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -385,12 +396,26 @@ $font-weight-semibold: 600;
|
||||
color: $neutral-800;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center !important;
|
||||
|
||||
.v-data-table-header__content {
|
||||
justify-content: center !important;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.v-data-table__td) {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: $neutral-900;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
// Ensure the cell content itself is centered if it contains flex/divs
|
||||
:deep(.v-data-table__td > *) {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const config = useRuntimeConfig();
|
||||
// Define session duration (default to 1 hour if not specified in config)
|
||||
const SESSION_DURATION = (config.sessionDurationHours || 1) * 60 * 60;
|
||||
const SESSION_DURATION = (config.sessionDurationHours || 1) * 60 * 60 * 24;
|
||||
|
||||
// This is the MAIN SESSION duration. It controls how long a user stays logged in.
|
||||
// Current configuration: 1 hour (3600 seconds).
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useVerificationStore = defineStore('verification', () => {
|
||||
// State
|
||||
const patients = ref([]);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// API Base URL
|
||||
const API_BASE_URL = 'http://10.10.150.131:8089/api/v1';
|
||||
|
||||
// Helpers
|
||||
const formatAddress = (p) => {
|
||||
return [p.alamat, p.namakelurahan, p.namakecamatan, p.nama_kota, p.namaprovinsi]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
const mapPatientData = (apiData) => {
|
||||
return {
|
||||
nama: apiData.nama,
|
||||
rm: apiData.nomr,
|
||||
alamat: formatAddress(apiData),
|
||||
telepon: apiData.notlp || '',
|
||||
status: apiData.notlp ? 'Terverifikasi' : 'Belum Terverifikasi',
|
||||
originalData: apiData // Keep original data if needed
|
||||
};
|
||||
};
|
||||
|
||||
// Actions
|
||||
const fetchAllPatients = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const result = await $fetch(`${API_BASE_URL}/pasien/search`, {
|
||||
params: { nama: 'AA' }
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const data = result.data || result;
|
||||
const results = Array.isArray(data) ? data : [];
|
||||
patients.value = results.map(mapPatientData);
|
||||
} else {
|
||||
patients.value = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Exception fetching all patients:', e);
|
||||
error.value = e;
|
||||
patients.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPatientByRm = async (rm) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const result = await $fetch(`${API_BASE_URL}/pasien/${rm}`);
|
||||
|
||||
if (result) {
|
||||
const data = result.data || result;
|
||||
if (data && data.nomr) {
|
||||
patients.value = [mapPatientData(data)];
|
||||
} else {
|
||||
patients.value = [];
|
||||
}
|
||||
} else {
|
||||
patients.value = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Exception fetching patient by RM:', e);
|
||||
error.value = e;
|
||||
patients.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const searchPatientsByName = async (name) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const result = await $fetch(`${API_BASE_URL}/pasien/search`, {
|
||||
params: { nama: name }
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const data = result.data || result;
|
||||
const results = Array.isArray(data) ? data : [];
|
||||
patients.value = results.map(mapPatientData);
|
||||
} else {
|
||||
patients.value = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Exception searching patients:', e);
|
||||
error.value = e;
|
||||
patients.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearPatients = () => {
|
||||
patients.value = [];
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
// Optimistic update for verification status
|
||||
const updatePatientVerification = (rm, phoneNumber) => {
|
||||
const patientIndex = patients.value.findIndex(p => p.rm === rm);
|
||||
if (patientIndex !== -1) {
|
||||
patients.value[patientIndex].status = 'Terverifikasi';
|
||||
patients.value[patientIndex].telepon = phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
patients,
|
||||
loading,
|
||||
error,
|
||||
fetchAllPatients,
|
||||
fetchPatientByRm,
|
||||
searchPatientsByName,
|
||||
clearPatients,
|
||||
updatePatientVerification
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user