fix(FE) : integrate api antrean table
This commit is contained in:
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user