510 lines
14 KiB
Vue

<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-batch-prediction"></Icon>
<h1 class="text-xl font-bold text-primary">Batch Processing</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Process notifications in batches for better efficiency.
</p>
</template>
</rs-card>
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<rs-card
class="cursor-pointer hover:shadow-md transition-shadow"
@click="showCreateModal = true"
>
<div class="p-4 flex items-center gap-4">
<div class="p-3 bg-blue-100 rounded-lg">
<Icon class="text-blue-600 text-xl" name="ic:outline-add"></Icon>
</div>
<div>
<h3 class="font-semibold text-blue-600">Create New Batch</h3>
<p class="text-sm text-gray-600">Start a new batch processing job</p>
</div>
</div>
</rs-card>
<rs-card class="cursor-pointer hover:shadow-md transition-shadow">
<div class="p-4 flex items-center gap-4">
<div class="p-3 bg-green-100 rounded-lg">
<Icon class="text-green-600 text-xl" name="ic:outline-schedule"></Icon>
</div>
<div>
<h3 class="font-semibold text-green-600">Schedule Batch</h3>
<p class="text-sm text-gray-600">Schedule for later execution</p>
</div>
</div>
</rs-card>
</div>
<!-- Batch Statistics -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-blue-600">{{ stats.pending }}</h3>
<p class="text-sm text-gray-600">Pending</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-yellow-600">
{{ stats.processing }}
</h3>
<p class="text-sm text-gray-600">Processing</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-green-600">
{{ stats.completed }}
</h3>
<p class="text-sm text-gray-600">Completed</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-red-600">{{ stats.failed }}</h3>
<p class="text-sm text-gray-600">Failed</p>
</div>
</rs-card>
</div>
<!-- Active Batches -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Active Batches</h3>
<rs-button
variant="outline"
size="sm"
@click="refreshBatches"
:loading="isLoading"
>
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8">
<rs-spinner size="lg" />
</div>
<!-- Empty State -->
<div v-else-if="batches.length === 0" class="text-center py-8">
<Icon name="ic:outline-inbox" class="text-4xl text-gray-400 mb-2" />
<p class="text-gray-600">No batches found</p>
</div>
<!-- Batches List -->
<div v-else class="space-y-4">
<div
v-for="batch in batches"
:key="batch.id"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div
class="w-3 h-3 rounded-full mr-3"
:class="getStatusColor(batch.status)"
></div>
<div>
<h4 class="font-semibold">{{ batch.name }}</h4>
<p class="text-sm text-gray-600">{{ batch.description }}</p>
</div>
</div>
<div class="text-right">
<span class="text-sm font-medium capitalize">{{ batch.status }}</span>
<p class="text-xs text-gray-500">
{{ formatDate(batch.time) }}
</p>
</div>
</div>
<!-- Progress Info -->
<div class="space-y-2">
<div class="text-sm text-gray-600">
Progress: {{ batch.processed }}/{{ batch.total }} ({{
Math.round((batch.processed / batch.total) * 100)
}}%)
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full"
:style="{
width: `${Math.round((batch.processed / batch.total) * 100)}%`,
}"
></div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div
v-if="pagination.totalPages > 1"
class="flex justify-center items-center space-x-2 mt-4"
>
<rs-button
variant="outline"
size="sm"
:disabled="pagination.page === 1"
@click="pagination.page--"
>
Previous
</rs-button>
<span class="text-sm text-gray-600">
Page {{ pagination.page }} of {{ pagination.totalPages }}
</span>
<rs-button
variant="outline"
size="sm"
:disabled="pagination.page === pagination.totalPages"
@click="pagination.page++"
>
Next
</rs-button>
</div>
</div>
</template>
</rs-card>
<!-- Create Batch Modal -->
<rs-modal v-model="showCreateModal" title="Create New Batch">
<div class="space-y-4">
<!-- Basic Information -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Batch Name</label>
<input
v-model="newBatch.name"
type="text"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="Enter batch name"
/>
</div>
<!-- Message Type -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Message Type</label>
<select
v-model="newBatch.type"
class="w-full p-2 border border-gray-300 rounded-md"
>
<option value="">Select type</option>
<option value="email">Email</option>
<option value="push">Push Notification</option>
</select>
</div>
<!-- Priority -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Priority</label>
<select
v-model="newBatch.priority"
class="w-full p-2 border border-gray-300 rounded-md"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<!-- Template Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
>Template (Optional)</label
>
<select
v-model="newBatch.template"
class="w-full p-2 border border-gray-300 rounded-md"
>
<option value="">No template</option>
<option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.name }}
</option>
</select>
</div>
<!-- User Segment -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
>User Segment (Optional)</label
>
<select
v-model="newBatch.segment"
class="w-full p-2 border border-gray-300 rounded-md"
>
<option value="">All Users</option>
<option v-for="segment in segments" :key="segment.id" :value="segment.value">
{{ segment.name }}
</option>
</select>
</div>
<!-- Scheduled Time -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"
>Schedule For (Optional)</label
>
<input
v-model="newBatch.scheduledAt"
type="datetime-local"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Description</label>
<textarea
v-model="newBatch.description"
class="w-full p-2 border border-gray-300 rounded-md"
rows="3"
placeholder="Describe this batch"
></textarea>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button
variant="outline"
@click="showCreateModal = false"
:disabled="isCreating"
>Cancel</rs-button
>
<rs-button @click="createBatch" :loading="isCreating" :disabled="isCreating">
{{ isCreating ? "Creating..." : "Create Batch" }}
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
import { onMounted, watch } from "vue";
import { useToast } from "vue-toastification";
definePageMeta({
title: "Batch Processing",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Batch Processing",
path: "/notification/queue-scheduler/batch",
},
],
});
// Basic stats
const stats = ref({
pending: 0,
processing: 0,
completed: 0,
failed: 0,
});
// Pagination
const pagination = ref({
page: 1,
limit: 10,
total: 0,
totalPages: 0,
});
// Batches list
const batches = ref([]);
// Loading states
const isLoading = ref(false);
const isCreating = ref(false);
// Modal state
const showCreateModal = ref(false);
// Form data
const newBatch = ref({
name: "",
type: "",
description: "",
scheduledAt: "",
template: "",
segment: "",
priority: "medium",
});
// Templates and segments (will be fetched)
const templates = ref([]);
const segments = ref([]);
// Fetch data on mount
onMounted(async () => {
await Promise.all([fetchStats(), fetchBatches(), fetchTemplates(), fetchSegments()]);
});
// Watch for page changes
watch(() => pagination.value.page, fetchBatches);
// Fetch functions
async function fetchStats() {
try {
const response = await $fetch("/api/notifications/batch/stats");
stats.value = response;
} catch (error) {
console.error("Error fetching stats:", error);
// Show error notification
useToast().error("Failed to fetch statistics");
}
}
async function fetchBatches() {
try {
isLoading.value = true;
const response = await $fetch("/api/notifications/batch", {
params: {
page: pagination.value.page,
limit: pagination.value.limit,
},
});
batches.value = response.batches;
pagination.value = {
...pagination.value,
total: response.pagination.total,
totalPages: response.pagination.totalPages,
};
} catch (error) {
console.error("Error fetching batches:", error);
useToast().error("Failed to fetch batches");
} finally {
isLoading.value = false;
}
}
async function fetchTemplates() {
try {
const response = await $fetch("/api/notifications/templates");
templates.value = response;
} catch (error) {
console.error("Error fetching templates:", error);
}
}
async function fetchSegments() {
try {
const response = await $fetch("/api/notifications/segments");
segments.value = response;
} catch (error) {
console.error("Error fetching segments:", error);
}
}
// Create batch
async function createBatch() {
try {
isCreating.value = true;
// Validate form
if (!newBatch.value.name || !newBatch.value.type) {
useToast().error("Please fill in all required fields");
return;
}
// Create a copy of the batch data for submission
const batchData = { ...newBatch.value };
// Format scheduledAt if it exists
if (batchData.scheduledAt) {
try {
const date = new Date(batchData.scheduledAt);
if (isNaN(date.getTime())) {
useToast().error("Invalid scheduled date");
return;
}
batchData.scheduledAt = date.toISOString();
} catch (error) {
useToast().error("Invalid scheduled date");
return;
}
}
const response = await $fetch("/api/notifications/batch", {
method: "POST",
body: batchData,
});
// Show success message
useToast().success("Batch created successfully");
// Reset form
newBatch.value = {
name: "",
type: "",
description: "",
scheduledAt: "",
template: "",
segment: "",
priority: "medium",
};
// Close modal
showCreateModal.value = false;
// Refresh data
await Promise.all([fetchStats(), fetchBatches()]);
} catch (error) {
console.error("Error creating batch:", error);
useToast().error(error.data?.statusMessage || "Failed to create batch");
} finally {
isCreating.value = false;
}
}
// Utility function for status color
const getStatusColor = (status) => {
const colors = {
draft: "bg-gray-500",
scheduled: "bg-blue-500",
sending: "bg-yellow-500",
sent: "bg-green-500",
failed: "bg-red-500",
};
return colors[status] || "bg-gray-500";
};
// Format date
const formatDate = (date) => {
return new Date(date).toLocaleString();
};
// Refresh data
const refreshBatches = async () => {
await Promise.all([fetchStats(), fetchBatches()]);
};
</script>
<style lang="scss" scoped></style>