fix(FE) : integrate api antrean table

This commit is contained in:
Yusron alamsyah
2026-02-03 13:53:54 +07:00
parent eeda789acd
commit 458e4c7158
20 changed files with 607 additions and 201 deletions

No files matched your search

@@ -12,6 +12,7 @@ import { Icon } from '@iconify/vue';
import Logo from '../logo/Logo.vue';
import ThemeToggler from './ThemeToggler.vue';
const customizer = useCustomizerStore();
const route = useRoute();
const showSearch = ref(false);
const priority = ref(customizer.setHorizontalLayout ? 0 : 0);
function searchbox() {
@@ -21,6 +22,39 @@ watch(priority, (newPriority) => {
priority.value = newPriority;
});
// Generate pendaftaran URL with query params based on current route
const pendaftaranUrl = computed(() => {
const baseUrl = '/antrean/pendaftaran';
const params = new URLSearchParams();
// If on category page, add idKategori
if (route.path.startsWith('/antrean/kategori/')) {
const idKategori = route.params.id as string;
if (idKategori) {
params.append('idKategori', idKategori);
}
}
// If on spesialis page, add kodeSpesialis
if (route.path.startsWith('/antrean/spesialis/')) {
const kodeSpesialis = route.params.kode as string;
if (kodeSpesialis) {
params.append('kodeSpesialis', kodeSpesialis);
}
}
// If on sub-spesialis page, add kodeSubSpesialis
if (route.path.startsWith('/antrean/subspesialis/')) {
const kodeSubSpesialis = route.params.kode as string;
if (kodeSubSpesialis) {
params.append('kodeSubSpesialis', kodeSubSpesialis);
}
}
const queryString = params.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
});
// count items
// const store = useEcomStore();
// const getCart = computed(() => {
@@ -43,8 +77,11 @@ watch(priority, (newPriority) => {
<!-- ---------------------------------------------- -->
<!-- Mega menu -->
<!-- ---------------------------------------------- -->
<div class="hidden-sm-and-down">
<!-- <Navigations /> -->
<div class="hidden-sm-and-down hidden-md">
<v-btn class="custom-hover-primary" size="small" variant="text" color="primary" icon
@click.stop="customizer.SET_SIDEBAR_DRAWER">
<Icon icon="solar:hamburger-menu-line-duotone" height="22" />
</v-btn>
</div>
<v-spacer class="hidden-sm-and-down" />
@@ -87,7 +124,7 @@ watch(priority, (newPriority) => {
<template v-slot:activator="{ props }">
<v-btn
v-bind="props" color="primary" variant="flat" class="text-none mr-2" size="default"
to="/antrean/pendaftaran">
:to="pendaftaranUrl">
<Icon icon="mdi:plus" height="20" />
Pendaftaran Antrean
</v-btn>
+81 -61
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import api from '~/services/api';
import TableAntrian from './TableAntrian.vue';
import type { Props } from '~/types/common';
import type { Dokter } from '~/types/pendaftaran';
import type { KategoriOperasi, Spesialis, SubSpesialis } from '~/types/antrean';
@@ -130,24 +131,25 @@ const isLoadingDokter = ref(false);
const dokterLimit = ref(10);
const dokterOffset = ref(0);
const dokterTotalCount = ref(0);
const dokterCurrentPage = ref(1);
const dokterItemsPerPage = ref(10);
// Fetch dokter from API
const fetchDokter = async () => {
try {
isLoadingDokter.value = true;
const offset = (dokterCurrentPage.value - 1) * dokterItemsPerPage.value;
const params = new URLSearchParams({
search: searchDokter.value,
limit: dokterLimit.value.toString(),
offset: dokterOffset.value.toString()
limit: dokterItemsPerPage.value.toString(),
offset: offset.toString()
});
const response = await api.get(`/dokter?${params}`);
if (response.data.success && response.data.data) {
dokterList.value = response.data.data;
// TODO: Update when backend implements pagination
// dokterTotalCount.value = response.data.total || response.data.data.length;
dokterTotalCount.value = response.data.data.length;
dokterTotalCount.value = response.data.Paginate.Total;
}
} catch (error) {
console.error('Error fetching dokter:', error);
@@ -158,7 +160,24 @@ const fetchDokter = async () => {
};
// Watch search with debounce
let dokterSearchTimeout: NodeJS.Timeout | null = null;
watch(searchDokter, () => {
if (dokterSearchTimeout) {
clearTimeout(dokterSearchTimeout);
}
dokterSearchTimeout = setTimeout(() => {
dokterCurrentPage.value = 1;
fetchDokter();
}, 500);
});
// Watch for page changes
watch(dokterCurrentPage, () => {
fetchDokter();
});
watch(dokterItemsPerPage, () => {
dokterCurrentPage.value = 1;
fetchDokter();
});
@@ -171,6 +190,7 @@ const openDokterModal = () => {
// Pre-select already added doctors
selectedDokters.value = dokterPelaksanaItems.value.map(d => d.id);
searchDokter.value = '';
dokterCurrentPage.value = 1;
showDokterModal.value = true;
fetchDokter();
};
@@ -208,6 +228,22 @@ const toggleDokterSelection = (dokterId: string) => {
selectedDokters.value.push(dokterId);
}
};
// Headers for dokter table
const dokterHeaders = [
{ title: 'Select', key: 'select', sortable: false, width: '80px' },
{ title: 'NIP', key: 'nip', sortable: false },
{ title: 'Nama Lengkap', key: 'nama_lengkap', sortable: false },
{ title: 'KSM', key: 'nama_ksm', sortable: false }
];
const handleDokterPageUpdate = (page: unknown) => {
dokterCurrentPage.value = page as number;
};
const handleDokterItemsPerPageUpdate = (items: unknown) => {
dokterItemsPerPage.value = items as number;
};
</script>
<template>
@@ -398,63 +434,47 @@ const toggleDokterSelection = (dokterId: string) => {
class="mb-4"
></v-text-field>
<!-- Info Badge -->
<div class="text-caption text-medium-emphasis mb-3">
1-{{ dokterList.length }} of {{ dokterTotalCount }} Items
</div>
<!-- Data Table -->
<v-table density="compact" class="border">
<thead>
<tr>
<th class="text-left">
<v-checkbox
:model-value="isAllSelected"
@click="toggleAllDokters"
hide-details
density="compact"
:disabled="isLoadingDokter"
></v-checkbox>
</th>
<th class="text-left">NIP</th>
<th class="text-left">Nama Lengkap</th>
<th class="text-left">KSM</th>
</tr>
</thead>
<tbody>
<tr
v-for="dokter in dokterList"
:key="dokter.id"
@click="toggleDokterSelection(dokter.id)"
class="cursor-pointer"
style="cursor: pointer;"
>
<td @click.stop>
<v-checkbox
v-model="selectedDokters"
:value="dokter.id"
hide-details
density="compact"
></v-checkbox>
</td>
<td>{{ dokter.nip }}</td>
<td>{{ dokter.nama_lengkap }}</td>
<td>{{ dokter.nama_ksm }}</td>
</tr>
</tbody>
</v-table>
<!-- Loading State -->
<div v-if="isLoadingDokter" class="text-center py-8">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<p class="text-medium-emphasis mt-2">Loading...</p>
</div>
<!-- Empty State -->
<div v-else-if="dokterList.length === 0" class="text-center py-8">
<v-icon size="large" class="mb-2 text-medium-emphasis">mdi-information-outline</v-icon>
<p class="text-medium-emphasis">No results found</p>
</div>
<TableAntrian
:headers="dokterHeaders"
:items="dokterList"
:server-side="true"
:total-items="dokterTotalCount"
:current-page="dokterCurrentPage"
:items-per-page="dokterItemsPerPage"
:loading="isLoadingDokter"
min-width="700px"
@update:page="handleDokterPageUpdate"
@update:itemsPerPage="handleDokterItemsPerPageUpdate"
>
<template #item.select="{ item }">
<v-checkbox
v-model="selectedDokters"
:value="item.id"
hide-details
density="compact"
@click.stop
></v-checkbox>
</template>
<template #item.nip="{ item }">
<div @click="toggleDokterSelection(item.id)" style="cursor: pointer;">
{{ item.nip }}
</div>
</template>
<template #item.nama_lengkap="{ item }">
<div @click="toggleDokterSelection(item.id)" style="cursor: pointer;">
{{ item.nama_lengkap }}
</div>
</template>
<template #item.nama_ksm="{ item }">
<div @click="toggleDokterSelection(item.id)" style="cursor: pointer;">
{{ item.nama_ksm }}
</div>
</template>
</TableAntrian>
</v-card-text>
<v-divider></v-divider>
+19 -18
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { Props } from '~/types/common';
import { STATUS } from '~/types/antrean';
interface StatusProps extends Props {
rencanaOperasiData?: {
@@ -25,7 +26,7 @@ const minTanggalSelesai = computed(() => {
// Validation rules
const rules = {
requiredIfSelesai: (value: any) => {
if (statusPasienData.value.statusOperasi === 'Selesai' && !value) {
if (statusPasienData.value.statusOperasi === STATUS.SELESAI && !value) {
return 'Tanggal selesai operasi wajib diisi jika status Selesai';
}
return true;
@@ -55,12 +56,12 @@ const rules = {
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Belum' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Belum' ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = 'Belum')"
:variant="statusPasienData.statusOperasi === STATUS.BELUM ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === STATUS.BELUM ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = STATUS.BELUM)"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Belum' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === STATUS.BELUM ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Belum
</v-btn>
</v-col>
@@ -68,12 +69,12 @@ const rules = {
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Selesai' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Selesai' ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = 'Selesai')"
:variant="statusPasienData.statusOperasi === STATUS.SELESAI ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === STATUS.SELESAI ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = STATUS.SELESAI)"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Selesai' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === STATUS.SELESAI ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Selesai
</v-btn>
</v-col>
@@ -81,12 +82,12 @@ const rules = {
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Tunda' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Tunda' ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = 'Tunda')"
:variant="statusPasienData.statusOperasi === STATUS.TUNDA ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === STATUS.TUNDA ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = STATUS.TUNDA)"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Tunda' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === STATUS.TUNDA ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Tunda
</v-btn>
</v-col>
@@ -94,12 +95,12 @@ const rules = {
<v-btn
block
size="large"
:variant="statusPasienData.statusOperasi === 'Batal' ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === 'Batal' ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = 'Batal')"
:variant="statusPasienData.statusOperasi === STATUS.BATAL ? 'flat' : 'outlined'"
:color="statusPasienData.statusOperasi === STATUS.BATAL ? 'primary' : 'default'"
@click="!readonly && (statusPasienData.statusOperasi = STATUS.BATAL)"
:disabled="readonly"
>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === 'Batal' ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
<v-icon left class="mr-2">{{ statusPasienData.statusOperasi === STATUS.BATAL ? 'mdi-radiobox-marked' : 'mdi-radiobox-blank' }}</v-icon>
Batal
</v-btn>
</v-col>
@@ -110,7 +111,7 @@ const rules = {
<v-col cols="6">
<v-label class="mb-2 font-weight-medium">
Tanggal Selesai Operasi
<span v-if="statusPasienData.statusOperasi === 'Selesai'" class="text-error">*</span>
<span v-if="statusPasienData.statusOperasi === STATUS.SELESAI" class="text-error">*</span>
</v-label>
<v-text-field
v-model="statusPasienData.tanggalSelesai"
+58 -15
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { Header, Action } from '~/types/common';
import { STATUS } from '~/types/antrean';
interface Props {
headers: Header[];
@@ -8,37 +9,79 @@ interface Props {
itemsPerPage?: number;
minWidth?: string;
actions?: Action[];
serverSide?: boolean;
totalItems?: number;
currentPage?: number;
}
const props = withDefaults(defineProps<Props>(), {
search: '',
itemsPerPage: 10,
minWidth: '1200px',
actions: () => []
actions: () => [],
serverSide: false,
totalItems: 0,
currentPage: 1
});
const emit = defineEmits<{
(e: string, item: any): void;
(e: 'update:page', page: number): void;
(e: 'update:itemsPerPage', itemsPerPage: number): void;
}>();
// Status helper
const getStatusText = (status: string) => {
switch (status) {
case STATUS.BELUM: return 'Belum';
case STATUS.SELESAI: return 'Selesai';
case STATUS.TUNDA: return 'Tunda';
case STATUS.BATAL: return 'Batal';
default: return 'Unknown';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case STATUS.BELUM: return 'info';
case STATUS.SELESAI: return 'success';
case STATUS.TUNDA: return 'warning';
case STATUS.BATAL: return 'error';
default: return 'default';
}
};
const handleAction = (event: string, item: any) => {
emit(event, item);
};
const currentPage = ref(1);
const page = ref(props.currentPage);
const itemsPerPageLocal = ref(props.itemsPerPage);
watch(() => props.currentPage, (newVal) => {
page.value = newVal;
});
watch(page, (newVal) => {
emit('update:page', newVal);
});
watch(itemsPerPageLocal, (newVal) => {
emit('update:itemsPerPage', newVal);
});
</script>
<template>
<div class="table-wrapper">
<v-data-table
<v-data-table-server
:headers="headers"
:items="items"
:search="search"
:items-length="serverSide ? totalItems : 0"
v-model:page="page"
v-model:items-per-page="itemsPerPageLocal"
:search="serverSide ? undefined : search"
class="elevation-1"
item-value="id"
v-model:page="currentPage"
v-model:items-per-page="itemsPerPageLocal"
>
<!-- Pass through all slots to parent -->
<template v-for="(_, name) in $slots" v-slot:[name]="slotData">
@@ -46,25 +89,25 @@ const itemsPerPageLocal = ref(props.itemsPerPage);
</template>
<!-- Default slot for Jenis Kelamin if not provided -->
<template #item.jenisKelamin="{ item }">
<slot name="item.jenisKelamin" :item="item">
<template #item.JenisKelamin="{ item }">
<slot name="item.JenisKelamin" :item="item">
<v-chip
:color="item.jenisKelamin === 'L' ? 'primary' : 'error'"
:color="item.JenisKelamin === 'L' ? 'primary' : 'error'"
size="small"
>
{{ item.jenisKelamin }}
{{ item.JenisKelamin }}
</v-chip>
</slot>
</template>
<!-- Default slot for Status if not provided -->
<template #item.status="{ item }">
<slot name="item.status" :item="item">
<template #item.StatusOperasi="{ item }">
<slot name="item.StatusOperasi" :item="item">
<v-chip
:color="item.status === 'selesai' ? 'success' : item.status === 'batal' ? 'error' : item.status === 'tunda' ? 'warning' : 'info'"
:color="getStatusColor(item.StatusOperasi)"
size="small"
>
{{ item.status }}
{{ getStatusText(item.StatusOperasi) }}
</v-chip>
</slot>
</template>
@@ -90,7 +133,7 @@ const itemsPerPageLocal = ref(props.itemsPerPage);
</div>
</slot>
</template>
</v-data-table>
</v-data-table-server>
</div>
</template>