183 lines
6.0 KiB
Vue
183 lines
6.0 KiB
Vue
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
import { useToast } from 'vue-toastification';
|
|
|
|
const toast = useToast();
|
|
const loading = ref(true);
|
|
const refreshInterval = ref(null);
|
|
|
|
// Use useFetch for queue data
|
|
const { data: queueData, refresh: refreshQueueData } = await useFetch('/api/queue/asnaf-analysis', {
|
|
onResponse({ response }) {
|
|
loading.value = false;
|
|
},
|
|
onResponseError({ error }) {
|
|
console.error('Error fetching queue data:', error);
|
|
toast.error('Gagal memuat data queue');
|
|
loading.value = false;
|
|
}
|
|
});
|
|
|
|
// Start auto-refresh
|
|
onMounted(() => {
|
|
// Refresh every 30 seconds
|
|
refreshInterval.value = setInterval(() => {
|
|
refreshQueueData();
|
|
}, 30000);
|
|
});
|
|
|
|
// Clean up interval on component unmount
|
|
onUnmounted(() => {
|
|
if (refreshInterval.value) {
|
|
clearInterval(refreshInterval.value);
|
|
}
|
|
});
|
|
|
|
// Retry failed job
|
|
async function retryJob(jobId) {
|
|
try {
|
|
await useFetch(`/api/queue/asnaf-analysis/${jobId}/retry`, {
|
|
method: 'POST',
|
|
onResponse() {
|
|
toast.success('Job akan diproses semula');
|
|
refreshQueueData();
|
|
},
|
|
onResponseError({ error }) {
|
|
console.error('Error retrying job:', error);
|
|
toast.error('Gagal memproses semula job');
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error retrying job:', error);
|
|
toast.error('Gagal memproses semula job');
|
|
}
|
|
}
|
|
|
|
// Remove job
|
|
async function removeJob(jobId) {
|
|
try {
|
|
await useFetch(`/api/queue/asnaf-analysis/${jobId}`, {
|
|
method: 'DELETE',
|
|
onResponse() {
|
|
toast.success('Job telah dipadam');
|
|
refreshQueueData();
|
|
},
|
|
onResponseError({ error }) {
|
|
console.error('Error removing job:', error);
|
|
toast.error('Gagal memadam job');
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error removing job:', error);
|
|
toast.error('Gagal memadam job');
|
|
}
|
|
}
|
|
|
|
// Page metadata
|
|
definePageMeta({
|
|
title: "Queue Analisis Asnaf",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{
|
|
name: "Dashboard",
|
|
path: "/",
|
|
},
|
|
{
|
|
name: "BF-PRF",
|
|
path: "/BF-PRF",
|
|
},
|
|
{
|
|
name: "Asnaf",
|
|
path: "/BF-PRF/AS",
|
|
},
|
|
{
|
|
name: "Queue",
|
|
path: "/BF-PRF/AS/QUEUE",
|
|
},
|
|
],
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-6">
|
|
<LayoutsBreadcrumb />
|
|
|
|
<div class="flex justify-between items-center">
|
|
<h1 class="text-2xl font-bold text-primary">Queue Analisis Asnaf</h1>
|
|
<rs-button variant="primary" @click="refreshQueueData">
|
|
<Icon name="mdi:refresh" size="18" class="mr-1" />
|
|
Refresh
|
|
</rs-button>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<div v-if="loading" class="flex justify-center items-center py-20">
|
|
<div class="text-center">
|
|
<Loading />
|
|
<p class="mt-4 text-gray-600">Memuat data queue...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<rs-card>
|
|
<template #body>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead>
|
|
<tr>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Asnaf ID</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Updated</th>
|
|
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr v-for="job in queueData" :key="job.id" class="hover:bg-gray-50">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ job.id }}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<rs-badge
|
|
:variant="job.status === 'completed' ? 'success' :
|
|
job.status === 'failed' ? 'danger' :
|
|
job.status === 'active' ? 'primary' :
|
|
'warning'">
|
|
{{ job.status }}
|
|
</rs-badge>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ job.data.asnafId }}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{{ new Date(job.timestamp).toLocaleString('ms-MY') }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{{ new Date(job.processedOn || job.finishedOn).toLocaleString('ms-MY') }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<div class="flex space-x-2">
|
|
<rs-button
|
|
v-if="job.status === 'failed'"
|
|
variant="warning"
|
|
size="sm"
|
|
@click="retryJob(job.id)">
|
|
<Icon name="mdi:refresh" size="16" class="mr-1" />
|
|
Retry
|
|
</rs-button>
|
|
<rs-button
|
|
variant="danger"
|
|
size="sm"
|
|
@click="removeJob(job.id)">
|
|
<Icon name="mdi:delete" size="16" class="mr-1" />
|
|
Remove
|
|
</rs-button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
</div>
|
|
</template> |