Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
537
pages/notification/queue/batch.vue
Normal file
537
pages/notification/queue/batch.vue
Normal file
@@ -0,0 +1,537 @@
|
||||
<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");
|
||||
if (response.success) {
|
||||
stats.value = response.data;
|
||||
} else {
|
||||
throw new Error(response.statusMessage || "Failed to fetch statistics");
|
||||
}
|
||||
} 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,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
batches.value = response.data.batches;
|
||||
pagination.value = {
|
||||
...pagination.value,
|
||||
total: response.data.pagination.total,
|
||||
totalPages: response.data.pagination.totalPages,
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.statusMessage || "Failed to fetch batches");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching batches:", error);
|
||||
useToast().error("Failed to fetch batches");
|
||||
batches.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTemplates() {
|
||||
try {
|
||||
const response = await $fetch("/api/notifications/templates");
|
||||
if (Array.isArray(response)) {
|
||||
templates.value = response;
|
||||
} else if (response && response.success && Array.isArray(response.data)) {
|
||||
templates.value = response.data;
|
||||
} else {
|
||||
templates.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
templates.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSegments() {
|
||||
try {
|
||||
const response = await $fetch("/api/notifications/segments");
|
||||
if (Array.isArray(response)) {
|
||||
segments.value = response;
|
||||
} else if (response && response.success && Array.isArray(response.data)) {
|
||||
segments.value = response.data;
|
||||
} else {
|
||||
segments.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching segments:", error);
|
||||
segments.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
if (!response || !response.success) {
|
||||
throw new Error(response?.statusMessage || "Failed to create batch");
|
||||
}
|
||||
|
||||
// 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.message || "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>
|
||||
Reference in New Issue
Block a user