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:
1357
pages/notification/templates/create_template/index.vue
Normal file
1357
pages/notification/templates/create_template/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
764
pages/notification/templates/edit/[id].vue
Normal file
764
pages/notification/templates/edit/[id].vue
Normal file
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<!-- Info Card -->
|
||||
<rs-card class="mb-5">
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon
|
||||
class="mr-2 flex justify-center"
|
||||
name="material-symbols:edit-outline"
|
||||
></Icon>
|
||||
Edit Template
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Edit and update your notification template. Modify content, settings, and
|
||||
channel configurations to improve effectiveness.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="text-center py-12">
|
||||
<div class="flex justify-center mb-4">
|
||||
<Icon name="ic:outline-refresh" size="2rem" class="text-primary animate-spin" />
|
||||
</div>
|
||||
<p class="text-gray-600">Loading template details...</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Form Card -->
|
||||
<rs-card v-else>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">Edit Notification Template</h2>
|
||||
<div class="flex gap-3">
|
||||
<rs-button @click="$router.push('/notification/templates')" variant="outline">
|
||||
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
||||
Back to Templates
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="previewTemplate"
|
||||
variant="info-outline"
|
||||
:disabled="!templateForm.content || isSubmitting"
|
||||
>
|
||||
<Icon name="material-symbols:preview-outline" class="mr-1"></Icon>
|
||||
Preview
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="pt-2">
|
||||
<FormKit
|
||||
type="form"
|
||||
@submit="updateTemplate"
|
||||
:actions="false"
|
||||
class="w-full max-w-6xl mx-auto"
|
||||
>
|
||||
<!-- Basic Information Section -->
|
||||
<div class="space-y-6 mb-8">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
||||
Basic Information
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Configure the basic template settings and metadata
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="title"
|
||||
label="Template Name"
|
||||
placeholder="e.g., Welcome Email Template"
|
||||
validation="required"
|
||||
v-model="templateForm.title"
|
||||
help="Internal name for identifying this template"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="description"
|
||||
label="Description"
|
||||
placeholder="Brief description of template purpose and usage"
|
||||
v-model="templateForm.description"
|
||||
rows="3"
|
||||
help="Optional description to help team members understand the template's use"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
name="category"
|
||||
label="Category"
|
||||
placeholder="Select category"
|
||||
:options="categoryOptions"
|
||||
validation="required"
|
||||
v-model="templateForm.category"
|
||||
help="Organize templates by category for better management"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="channels"
|
||||
label="Supported Channels"
|
||||
:options="channelOptions"
|
||||
validation="required|min:1"
|
||||
v-model="templateForm.channels"
|
||||
decorator-icon="material-symbols:check"
|
||||
options-class="grid grid-cols-2 gap-x-3 gap-y-2 pt-2"
|
||||
help="Select which channels this template supports"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="select"
|
||||
name="status"
|
||||
label="Status"
|
||||
placeholder="Select status"
|
||||
:options="statusOptions"
|
||||
validation="required"
|
||||
v-model="templateForm.status"
|
||||
help="Template status - only active templates can be used"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
name="version"
|
||||
label="Version"
|
||||
placeholder="1.0"
|
||||
v-model="templateForm.version"
|
||||
help="Version number for tracking template changes"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Configuration Section -->
|
||||
<div class="space-y-6 mb-8">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
||||
Content Configuration
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Define subject lines and content for different channels
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Subject/Title Section -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="subject"
|
||||
label="Subject Line / Notification Title"
|
||||
placeholder="e.g., Welcome to {{company_name}}, {{first_name}}!"
|
||||
validation="required"
|
||||
v-model="templateForm.subject"
|
||||
help="Subject for emails or title for push notifications. Use {{variable}} for dynamic content"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
name="preheader"
|
||||
label="Email Preheader Text (Optional)"
|
||||
placeholder="Additional preview text for emails"
|
||||
v-model="templateForm.preheader"
|
||||
help="Preview text shown in email clients alongside the subject line"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content Editor Section -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="content"
|
||||
label="Template Content"
|
||||
validation="required"
|
||||
v-model="templateForm.content"
|
||||
help="Main content body. Use HTML for rich formatting and {{variable}} for dynamic content"
|
||||
rows="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel-Specific Settings -->
|
||||
<div class="space-y-6 mb-8">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
||||
Channel-Specific Settings
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Configure settings specific to each channel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<rs-tab fill>
|
||||
<!-- Email Settings Tab -->
|
||||
<rs-tab-item
|
||||
title="Email Settings"
|
||||
v-if="templateForm.channels.includes('email')"
|
||||
>
|
||||
<div class="space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="fromName"
|
||||
label="From Name"
|
||||
placeholder="Your Company Name"
|
||||
v-model="templateForm.fromName"
|
||||
help="Name shown as sender in email clients"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="email"
|
||||
name="replyTo"
|
||||
label="Reply-To Email"
|
||||
placeholder="noreply@yourcompany.com"
|
||||
v-model="templateForm.replyTo"
|
||||
help="Email address for replies"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="trackOpens"
|
||||
label="Track Email Opens"
|
||||
v-model="templateForm.trackOpens"
|
||||
help="Enable tracking of email opens for analytics"
|
||||
/>
|
||||
</div>
|
||||
</rs-tab-item>
|
||||
|
||||
<!-- Push Notification Settings Tab -->
|
||||
<rs-tab-item
|
||||
title="Push Settings"
|
||||
v-if="templateForm.channels.includes('push')"
|
||||
>
|
||||
<div class="space-y-4 pt-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="pushTitle"
|
||||
label="Push Notification Title"
|
||||
placeholder="Custom push title (optional)"
|
||||
v-model="templateForm.pushTitle"
|
||||
help="Leave empty to use the main subject line"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="url"
|
||||
name="pushIcon"
|
||||
label="Push Notification Icon URL"
|
||||
placeholder="https://yoursite.com/icon.png"
|
||||
v-model="templateForm.pushIcon"
|
||||
help="URL to icon for push notifications"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="url"
|
||||
name="pushUrl"
|
||||
label="Push Notification Action URL"
|
||||
placeholder="https://yoursite.com/action"
|
||||
v-model="templateForm.pushUrl"
|
||||
help="URL to open when push notification is clicked"
|
||||
/>
|
||||
</div>
|
||||
</rs-tab-item>
|
||||
|
||||
<!-- SMS Settings Tab -->
|
||||
<rs-tab-item
|
||||
title="SMS Settings"
|
||||
v-if="templateForm.channels.includes('sms')"
|
||||
>
|
||||
<div class="space-y-4 pt-4">
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="smsContent"
|
||||
label="SMS Content"
|
||||
placeholder="SMS version of your message (160 characters max)"
|
||||
v-model="templateForm.smsContent"
|
||||
help="Text-only version for SMS. Variables like {{name}} are supported"
|
||||
rows="4"
|
||||
maxlength="160"
|
||||
/>
|
||||
<p class="text-sm text-gray-500">
|
||||
Characters: {{ templateForm.smsContent?.length || 0 }}/160
|
||||
</p>
|
||||
</div>
|
||||
</rs-tab-item>
|
||||
</rs-tab>
|
||||
</div>
|
||||
|
||||
<!-- Additional Settings -->
|
||||
<div class="space-y-6 mb-8">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
||||
Additional Settings
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Configure additional template options
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="tags"
|
||||
label="Tags (comma-separated)"
|
||||
placeholder="welcome, onboarding, email"
|
||||
v-model="templateForm.tags"
|
||||
help="Tags for organizing and filtering templates"
|
||||
/>
|
||||
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="isPersonal"
|
||||
label="Personal Template"
|
||||
v-model="templateForm.isPersonal"
|
||||
help="Mark as personal template (visible only to you)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div
|
||||
class="flex gap-3 justify-end pt-6 border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<rs-button
|
||||
@click="$router.push('/notification/templates')"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="testTemplate"
|
||||
variant="info-outline"
|
||||
:disabled="!isFormValid || isSubmitting"
|
||||
>
|
||||
<Icon name="material-symbols:send" class="mr-1"></Icon>
|
||||
Send Test
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="updateTemplate"
|
||||
:disabled="!isFormValid || isSubmitting"
|
||||
:loading="isSubmitting"
|
||||
>
|
||||
<Icon name="material-symbols:save" class="mr-1"></Icon>
|
||||
{{ isSubmitting ? "Updating..." : "Update Template" }}
|
||||
</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<rs-modal v-model="showPreview" title="Template Preview" size="lg">
|
||||
<div class="space-y-4 p-1">
|
||||
<div class="flex gap-4 mb-4">
|
||||
<FormKit
|
||||
type="select"
|
||||
name="previewChannel"
|
||||
label="Preview Channel"
|
||||
:options="
|
||||
channelOptions.filter(
|
||||
(c) => c.value !== '' && templateForm.channels.includes(c.value)
|
||||
)
|
||||
"
|
||||
v-model="previewChannel"
|
||||
outer-class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-lg p-4 bg-gray-50 dark:bg-gray-800">
|
||||
<h3 class="font-semibold mb-2 text-gray-800 dark:text-gray-200">
|
||||
{{ templateForm.subject }}
|
||||
</h3>
|
||||
<div
|
||||
class="prose prose-sm max-w-none dark:prose-invert"
|
||||
v-html="templateForm.content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<rs-button @click="showPreview = false">Close</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Edit Template",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Get template ID from route params
|
||||
const templateId = computed(() => route.params.id);
|
||||
|
||||
// Reactive data
|
||||
const isLoading = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
const showPreview = ref(false);
|
||||
const previewChannel = ref("email");
|
||||
|
||||
// Form data
|
||||
const templateForm = ref({
|
||||
title: "",
|
||||
description: "",
|
||||
subject: "",
|
||||
preheader: "",
|
||||
category: "",
|
||||
channels: [],
|
||||
status: "Draft",
|
||||
version: "1.0",
|
||||
content: "",
|
||||
tags: "",
|
||||
isPersonal: false,
|
||||
// Email settings
|
||||
fromName: "",
|
||||
replyTo: "",
|
||||
trackOpens: true,
|
||||
// Push settings
|
||||
pushTitle: "",
|
||||
pushIcon: "",
|
||||
pushUrl: "",
|
||||
// SMS settings
|
||||
smsContent: "",
|
||||
});
|
||||
|
||||
// Form options
|
||||
const categoryOptions = [
|
||||
{ label: "User Management", value: "user_management" },
|
||||
{ label: "Orders & Transactions", value: "orders" },
|
||||
{ label: "Security & Authentication", value: "security" },
|
||||
{ label: "Marketing & Promotions", value: "marketing" },
|
||||
{ label: "System Updates", value: "system" },
|
||||
{ label: "General Information", value: "general" },
|
||||
];
|
||||
|
||||
const channelOptions = [
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "SMS", value: "sms" },
|
||||
{ label: "Push Notification", value: "push" },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "Draft", value: "Draft" },
|
||||
{ label: "Active", value: "Active" },
|
||||
{ label: "Archived", value: "Archived" },
|
||||
];
|
||||
|
||||
// Computed properties
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
templateForm.value.title &&
|
||||
templateForm.value.subject &&
|
||||
templateForm.value.content &&
|
||||
templateForm.value.category &&
|
||||
templateForm.value.channels.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
// Load template data
|
||||
const loadTemplate = async () => {
|
||||
if (!templateId.value) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
console.log("Loading template for editing:", templateId.value);
|
||||
|
||||
const response = await $fetch(`/api/notifications/templates/${templateId.value}`);
|
||||
|
||||
if (response.success) {
|
||||
const template = response.data.template;
|
||||
console.log("Template loaded:", template);
|
||||
|
||||
// Map API response to form data
|
||||
Object.assign(templateForm.value, {
|
||||
title: template.title || "",
|
||||
description: template.description || "",
|
||||
subject: template.subject || "",
|
||||
preheader: template.preheader || "",
|
||||
category: template.category || "",
|
||||
channels: template.channels || [],
|
||||
status: template.status || "Draft",
|
||||
version: template.version || "1.0",
|
||||
content: template.content || "",
|
||||
tags: template.tags || "",
|
||||
isPersonal: template.isPersonal || false,
|
||||
fromName: template.fromName || "",
|
||||
replyTo: template.replyTo || "",
|
||||
trackOpens: template.trackOpens !== false,
|
||||
pushTitle: template.pushTitle || "",
|
||||
pushIcon: template.pushIcon || "",
|
||||
pushUrl: template.pushUrl || "",
|
||||
smsContent: template.smsContent || "",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading template:", error);
|
||||
|
||||
let errorMessage = "Failed to load template data";
|
||||
if (error.data?.error) {
|
||||
errorMessage = error.data.error;
|
||||
}
|
||||
|
||||
await $swal.fire({
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
});
|
||||
|
||||
// Navigate back to templates list
|
||||
router.push("/notification/templates");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Preview template
|
||||
const previewTemplate = () => {
|
||||
showPreview.value = true;
|
||||
};
|
||||
|
||||
// Test template
|
||||
const testTemplate = async () => {
|
||||
if (!isFormValid.value) {
|
||||
$swal.fire("Error", "Please fill in all required fields", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const { value: email } = await $swal.fire({
|
||||
title: "Send Test Notification",
|
||||
text: "Enter email address to send test notification:",
|
||||
input: "email",
|
||||
inputPlaceholder: "your-email@example.com",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Send Test",
|
||||
cancelButtonText: "Cancel",
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return "Please enter a valid email address";
|
||||
}
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
||||
return "Please enter a valid email address";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (email) {
|
||||
// Show loading state
|
||||
const loadingSwal = $swal.fire({
|
||||
title: "Sending Test...",
|
||||
text: "Please wait while we send your test notification",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
showConfirmButton: false,
|
||||
didOpen: () => {
|
||||
$swal.showLoading();
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const testData = {
|
||||
subject: templateForm.value.subject,
|
||||
emailContent: templateForm.value.content,
|
||||
pushTitle: templateForm.value.pushTitle || templateForm.value.subject,
|
||||
pushBody: templateForm.value.content
|
||||
? templateForm.value.content.replace(/<[^>]*>/g, "").substring(0, 300)
|
||||
: "",
|
||||
callToActionText: "",
|
||||
callToActionUrl: "",
|
||||
};
|
||||
|
||||
console.log("Sending test notification with data:", testData);
|
||||
|
||||
const response = await $fetch("/api/notifications/test-send", {
|
||||
method: "POST",
|
||||
body: {
|
||||
email: email,
|
||||
testData: testData,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Test API Response:", response);
|
||||
|
||||
loadingSwal.close();
|
||||
|
||||
let successMessage = `Test notification sent to ${email}`;
|
||||
if (response.data?.results) {
|
||||
const results = response.data.results;
|
||||
const successfulChannels = results
|
||||
.filter((r) => r.status === "sent")
|
||||
.map((r) => r.channel);
|
||||
const failedChannels = results.filter((r) => r.status === "failed");
|
||||
|
||||
if (successfulChannels.length > 0) {
|
||||
successMessage += `\n\nSuccessfully sent via: ${successfulChannels.join(", ")}`;
|
||||
}
|
||||
|
||||
if (failedChannels.length > 0) {
|
||||
successMessage += `\n\nFailed channels: ${failedChannels
|
||||
.map((f) => `${f.channel} (${f.message})`)
|
||||
.join(", ")}`;
|
||||
}
|
||||
}
|
||||
|
||||
await $swal.fire({
|
||||
title: "Test Sent!",
|
||||
text: successMessage,
|
||||
icon: "success",
|
||||
timer: 4000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending test notification:", error);
|
||||
|
||||
loadingSwal.close();
|
||||
|
||||
let errorMessage = "Failed to send test notification. Please try again.";
|
||||
if (error.data?.error) {
|
||||
errorMessage = error.data.error;
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
await $swal.fire({
|
||||
title: "Test Failed",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update template
|
||||
const updateTemplate = async () => {
|
||||
if (!isFormValid.value) {
|
||||
$swal.fire("Error", "Please fill in all required fields", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubmitting.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
// Show loading state
|
||||
const loadingSwal = $swal.fire({
|
||||
title: "Updating Template...",
|
||||
text: "Please wait while we update your template",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
showConfirmButton: false,
|
||||
didOpen: () => {
|
||||
$swal.showLoading();
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Prepare the data for API submission
|
||||
const apiData = {
|
||||
title: templateForm.value.title,
|
||||
description: templateForm.value.description || "",
|
||||
subject: templateForm.value.subject,
|
||||
preheader: templateForm.value.preheader || "",
|
||||
category: templateForm.value.category,
|
||||
channels: templateForm.value.channels,
|
||||
status: templateForm.value.status,
|
||||
version: templateForm.value.version,
|
||||
content: templateForm.value.content,
|
||||
tags: templateForm.value.tags || "",
|
||||
isPersonal: templateForm.value.isPersonal,
|
||||
// Email specific settings
|
||||
fromName: templateForm.value.fromName || "",
|
||||
replyTo: templateForm.value.replyTo || "",
|
||||
trackOpens: templateForm.value.trackOpens,
|
||||
// Push notification specific settings
|
||||
pushTitle: templateForm.value.pushTitle || "",
|
||||
pushIcon: templateForm.value.pushIcon || "",
|
||||
pushUrl: templateForm.value.pushUrl || "",
|
||||
// SMS specific settings
|
||||
smsContent: templateForm.value.smsContent || "",
|
||||
};
|
||||
|
||||
console.log("Updating Template Data:", apiData);
|
||||
|
||||
const response = await $fetch(`/api/notifications/templates/${templateId.value}`, {
|
||||
method: "PUT",
|
||||
body: apiData,
|
||||
});
|
||||
|
||||
console.log("API Response:", response);
|
||||
|
||||
loadingSwal.close();
|
||||
isSubmitting.value = false;
|
||||
|
||||
await $swal.fire({
|
||||
title: "Template Updated!",
|
||||
text: `Template "${templateForm.value.title}" has been successfully updated.`,
|
||||
icon: "success",
|
||||
timer: 3000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// Navigate back to templates list
|
||||
router.push("/notification/templates");
|
||||
} catch (error) {
|
||||
console.error("Error updating template:", error);
|
||||
|
||||
loadingSwal.close();
|
||||
isSubmitting.value = false;
|
||||
|
||||
let errorMessage = "Failed to update template. Please try again.";
|
||||
let errorDetails = "";
|
||||
|
||||
if (error.data) {
|
||||
if (error.data.errors && Array.isArray(error.data.errors)) {
|
||||
errorMessage = "Please fix the following errors:";
|
||||
errorDetails = error.data.errors.join("\n• ");
|
||||
} else if (error.data.error) {
|
||||
errorMessage = error.data.error;
|
||||
}
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
await $swal.fire({
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
html: errorDetails
|
||||
? `<p>${errorMessage}</p><ul style="text-align: left; margin-top: 10px;"><li>${errorDetails.replace(
|
||||
/\n• /g,
|
||||
"</li><li>"
|
||||
)}</li></ul>`
|
||||
: errorMessage,
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Load template when component mounts
|
||||
onMounted(async () => {
|
||||
await loadTemplate();
|
||||
});
|
||||
</script>
|
||||
749
pages/notification/templates/index.vue
Normal file
749
pages/notification/templates/index.vue
Normal file
@@ -0,0 +1,749 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
<rs-card class="mb-5">
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<span title="Info"
|
||||
><Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
></span>
|
||||
Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Manage your notification templates here. You can create, edit, preview, and
|
||||
manage versions of templates for various channels like Email, SMS, Push, and
|
||||
In-App messages.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold">Notification Templates</h2>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="pt-2">
|
||||
<rs-tab fill>
|
||||
<rs-tab-item title="All Templates">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex items-center gap-4 flex-wrap">
|
||||
<FormKit
|
||||
type="select"
|
||||
name="category"
|
||||
placeholder="Filter by Category"
|
||||
:options="categories"
|
||||
v-model="filters.category"
|
||||
@input="filterByCategory"
|
||||
:disabled="isLoading"
|
||||
input-class="formkit-input appearance-none bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md py-2 px-3 text-base leading-normal focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400 disabled:opacity-50"
|
||||
outer-class="flex-grow sm:flex-grow-0 min-w-[180px] mb-0"
|
||||
/>
|
||||
<FormKit
|
||||
type="select"
|
||||
name="status"
|
||||
placeholder="Filter by Status"
|
||||
:options="statusOptions"
|
||||
v-model="filters.status"
|
||||
@input="filterByStatus"
|
||||
:disabled="isLoading"
|
||||
input-class="formkit-input appearance-none bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md py-2 px-3 text-base leading-normal focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400 disabled:opacity-50"
|
||||
outer-class="flex-grow sm:flex-grow-0 min-w-[180px] mb-0"
|
||||
/>
|
||||
<FormKit
|
||||
type="select"
|
||||
name="channel"
|
||||
placeholder="Filter by Channel"
|
||||
:options="channels"
|
||||
v-model="filters.channel"
|
||||
@input="filterByChannel"
|
||||
:disabled="isLoading"
|
||||
input-class="formkit-input appearance-none bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md py-2 px-3 text-base leading-normal focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400 disabled:opacity-50"
|
||||
outer-class="flex-grow sm:flex-grow-0 min-w-[180px] mb-0"
|
||||
/>
|
||||
</div>
|
||||
<rs-button
|
||||
@click="$router.push('/notification/templates/create_template')"
|
||||
class="ml-auto"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
||||
Create Template
|
||||
</rs-button>
|
||||
</div>
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="text-center py-12">
|
||||
<div class="flex justify-center mb-4">
|
||||
<Icon
|
||||
name="ic:outline-refresh"
|
||||
size="2rem"
|
||||
class="text-primary animate-spin"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400">Loading templates...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else-if="!templateList || templateList.length === 0"
|
||||
class="text-center py-12"
|
||||
>
|
||||
<div class="flex justify-center mb-4">
|
||||
<Icon
|
||||
name="material-symbols:inbox-outline"
|
||||
size="3rem"
|
||||
class="text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||
No templates found
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{{
|
||||
filters.category ||
|
||||
filters.channel ||
|
||||
filters.status ||
|
||||
filters.search
|
||||
? "No templates match your current filters."
|
||||
: "Get started by creating your first notification template."
|
||||
}}
|
||||
</p>
|
||||
<rs-button
|
||||
@click="$router.push('/notification/templates/create_template')"
|
||||
>
|
||||
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
||||
Create Your First Template
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- Templates Table -->
|
||||
<rs-table
|
||||
v-else
|
||||
:data="templateList"
|
||||
:columns="columns"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: true,
|
||||
class: 'align-middle',
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template v-slot:channel="{ value }">
|
||||
<div
|
||||
class="flex items-center justify-start gap-1 flex-wrap"
|
||||
style="max-width: 100px"
|
||||
>
|
||||
<template v-if="value.channel && value.channel.length">
|
||||
<template v-for="channel_item in value.channel" :key="channel_item">
|
||||
<span :title="channel_item">
|
||||
<Icon
|
||||
:name="getChannelIcon(channel_item)"
|
||||
class="text-gray-700 dark:text-gray-300"
|
||||
size="18"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:version="{ text }">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>v{{ text }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-slot:status="{ text }">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-medium"
|
||||
:class="getStatusClass(text)"
|
||||
>
|
||||
{{ text == 1 ? "Active" : text == 0 ? "Inactive" : "Draft" }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:action="data">
|
||||
<div class="flex justify-center items-center gap-3">
|
||||
<span title="Edit">
|
||||
<Icon
|
||||
name="material-symbols:edit-outline-rounded"
|
||||
class="text-primary hover:text-primary/90 cursor-pointer"
|
||||
size="20"
|
||||
@click="editTemplate(data.value)"
|
||||
/>
|
||||
</span>
|
||||
<!-- <span title="Preview">
|
||||
<Icon
|
||||
name="material-symbols:preview-outline"
|
||||
class="text-blue-500 hover:text-blue-600 cursor-pointer"
|
||||
size="20"
|
||||
@click="previewTemplate(data.value)"
|
||||
/>
|
||||
</span> -->
|
||||
<span title="Delete">
|
||||
<Icon
|
||||
name="material-symbols:delete-outline"
|
||||
class="text-red-500 hover:text-red-600 cursor-pointer"
|
||||
size="20"
|
||||
@click="openModalDelete(data.value)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</rs-tab-item>
|
||||
</rs-tab>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<rs-modal v-model="showPreview" title="Template Preview" size="lg">
|
||||
<div class="space-y-4 p-1">
|
||||
<div class="flex gap-4 mb-4">
|
||||
<FormKit
|
||||
type="select"
|
||||
name="previewChannel"
|
||||
label="Preview Channel"
|
||||
:options="channels.filter((c) => c.value !== '')"
|
||||
v-model="previewChannel"
|
||||
outer-class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="border rounded-lg p-4 bg-gray-50 dark:bg-gray-800">
|
||||
<h3 class="font-semibold mb-2 text-gray-800 dark:text-gray-200">
|
||||
{{ selectedTemplate?.notificationTitle }}
|
||||
</h3>
|
||||
<div
|
||||
class="prose prose-sm max-w-none dark:prose-invert"
|
||||
v-html="selectedTemplate?.content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<rs-button @click="showPreview = false">Close</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
|
||||
<!-- Version History Modal -->
|
||||
<rs-modal v-model="showVersions" title="Version History" size="lg">
|
||||
<div class="space-y-4 p-1">
|
||||
<div v-if="versionHistory.length">
|
||||
<div
|
||||
v-for="version in versionHistory"
|
||||
:key="version.id"
|
||||
class="border rounded-lg p-4 mb-3 last:mb-0 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150 ease-in-out"
|
||||
:class="{
|
||||
'border-primary-300 bg-primary-50 dark:bg-primary-900/20':
|
||||
version.isCurrent,
|
||||
}"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Version {{ version.version }}</span
|
||||
>
|
||||
<span
|
||||
v-if="version.isCurrent"
|
||||
class="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full dark:bg-green-900 dark:text-green-300"
|
||||
>
|
||||
Current
|
||||
</span>
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-medium rounded-full"
|
||||
:class="getStatusClass(version.status)"
|
||||
>
|
||||
{{ version.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{
|
||||
version.updatedAt
|
||||
}}</span>
|
||||
<rs-button
|
||||
size="sm"
|
||||
@click="restoreVersion(version)"
|
||||
variant="outline"
|
||||
:disabled="version.isCurrent"
|
||||
>Restore</rs-button
|
||||
>
|
||||
<rs-button
|
||||
size="sm"
|
||||
@click="deleteVersion(version)"
|
||||
variant="danger-outline"
|
||||
:disabled="version.isCurrent"
|
||||
>Delete</rs-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ version.name }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ version.subject }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ version.changeDescription }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-gray-500 dark:text-gray-400 py-4">
|
||||
No version history available for this template.
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<rs-button @click="showVersions = false">Close</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Notification Templates",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
|
||||
// Reactive data
|
||||
const isLoading = ref(false);
|
||||
const templateList = ref([]);
|
||||
const totalCount = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
|
||||
// Filters
|
||||
const filters = ref({
|
||||
category: "",
|
||||
channel: "",
|
||||
status: "",
|
||||
search: "",
|
||||
});
|
||||
|
||||
const categories = ref([
|
||||
{ label: "All Categories", value: "" },
|
||||
{ label: "User Management", value: "user_management" },
|
||||
{ label: "Orders & Transactions", value: "orders" },
|
||||
{ label: "Security & Authentication", value: "security" },
|
||||
{ label: "Marketing & Promotions", value: "marketing" },
|
||||
{ label: "System Updates", value: "system" },
|
||||
{ label: "General Information", value: "general" },
|
||||
]);
|
||||
|
||||
const statusOptions = ref([
|
||||
{ label: "All Status", value: "" },
|
||||
{ label: "Active", value: "Active" },
|
||||
{ label: "Inactive", value: "Inactive" },
|
||||
{ label: "Draft", value: "Draft" },
|
||||
{ label: "Archived", value: "Archived" },
|
||||
]);
|
||||
|
||||
const channels = ref([
|
||||
{ label: "All Channels", value: "" },
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "SMS", value: "sms" },
|
||||
{ label: "Push Notification", value: "push" },
|
||||
]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
label: "Title",
|
||||
key: "title",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
label: "Category",
|
||||
key: "category",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
label: "Channels",
|
||||
key: "channel",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
key: "status",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
label: "Action",
|
||||
key: "action",
|
||||
align: "center",
|
||||
},
|
||||
];
|
||||
|
||||
// API functions
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
page: currentPage.value.toString(),
|
||||
limit: pageSize.value.toString(),
|
||||
sortBy: "created_at",
|
||||
sortOrder: "desc",
|
||||
});
|
||||
|
||||
// Add filters if they have values
|
||||
if (filters.value.category) queryParams.append("category", filters.value.category);
|
||||
if (filters.value.channel) queryParams.append("channel", filters.value.channel);
|
||||
if (filters.value.status) queryParams.append("status", filters.value.status);
|
||||
if (filters.value.search) queryParams.append("search", filters.value.search);
|
||||
|
||||
console.log("Fetching templates with params:", queryParams.toString());
|
||||
|
||||
const response = await $fetch(`/api/notifications/templates?${queryParams}`);
|
||||
|
||||
console.log("API Response:", response);
|
||||
|
||||
// Check if response has the expected structure
|
||||
if (
|
||||
response &&
|
||||
response.success &&
|
||||
response.data &&
|
||||
Array.isArray(response.data.templates)
|
||||
) {
|
||||
// Transform API data to match frontend format
|
||||
templateList.value = response.data.templates.map((template) => ({
|
||||
id: template.id,
|
||||
title: template.title,
|
||||
category: template.category,
|
||||
status: template.is_active, // This is now the correct string status from DB
|
||||
createdAt: formatDate(template.created_at),
|
||||
updatedAt: formatDate(template.updated_at),
|
||||
action: null,
|
||||
}));
|
||||
|
||||
totalCount.value = response.data.templates?.length || 0;
|
||||
console.log(`Loaded ${templateList.value.length} templates`);
|
||||
} else {
|
||||
// Handle unexpected response structure
|
||||
console.warn("Unexpected API response structure:", response);
|
||||
templateList.value = [];
|
||||
totalCount.value = 0;
|
||||
|
||||
if (response && !response.success) {
|
||||
const errorMessage =
|
||||
response.data?.error || response.error || "Unknown error occurred";
|
||||
$swal.fire("Error", errorMessage, "error");
|
||||
} else {
|
||||
$swal.fire(
|
||||
"Warning",
|
||||
"Received unexpected response format from server.",
|
||||
"warning"
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
|
||||
// Initialize empty state on error
|
||||
templateList.value = [];
|
||||
totalCount.value = 0;
|
||||
|
||||
// Show user-friendly error message
|
||||
let errorMessage = "Failed to load templates. Please try again.";
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
errorMessage = "Authentication required. Please log in again.";
|
||||
} else if (error.response?.status === 403) {
|
||||
errorMessage = "You don't have permission to access this resource.";
|
||||
} else if (error.response?.status >= 500) {
|
||||
errorMessage = "Server error occurred. Please try again later.";
|
||||
} else if (error.data?.error) {
|
||||
errorMessage = error.data.error;
|
||||
}
|
||||
|
||||
$swal.fire("Error", errorMessage, "error");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to format dates
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "";
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const showPreview = ref(false);
|
||||
const selectedTemplate = ref(null);
|
||||
const previewChannel = ref("email");
|
||||
|
||||
const showVersions = ref(false);
|
||||
const versionHistory = ref([]);
|
||||
|
||||
const getChannelIcon = (channel_item) => {
|
||||
const icons = {
|
||||
email: "material-symbols:mail-outline-rounded",
|
||||
sms: "material-symbols:sms-outline-rounded",
|
||||
push: "material-symbols:notifications-active-outline-rounded",
|
||||
"in-app": "material-symbols:chat-bubble-outline-rounded",
|
||||
};
|
||||
return icons[channel_item] || "material-symbols:help-outline-rounded";
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const classes = {
|
||||
1: "bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-100",
|
||||
0: "bg-red-100 text-red-800 dark:bg-red-700 dark:text-red-100",
|
||||
2: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200",
|
||||
};
|
||||
return (
|
||||
classes[status] || "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
|
||||
);
|
||||
};
|
||||
|
||||
// Filter functions
|
||||
const filterByCategory = async (value) => {
|
||||
filters.value.category = value;
|
||||
currentPage.value = 1; // Reset to first page
|
||||
await fetchTemplates();
|
||||
};
|
||||
|
||||
const filterByChannel = async (value) => {
|
||||
filters.value.channel = value;
|
||||
currentPage.value = 1; // Reset to first page
|
||||
await fetchTemplates();
|
||||
};
|
||||
|
||||
const filterByStatus = async (value) => {
|
||||
filters.value.status = value;
|
||||
currentPage.value = 1; // Reset to first page
|
||||
await fetchTemplates();
|
||||
};
|
||||
|
||||
// Action functions
|
||||
const editTemplate = (template) => {
|
||||
router.push(`/notification/templates/edit/${template.id}`);
|
||||
};
|
||||
|
||||
const previewTemplate = (template) => {
|
||||
selectedTemplate.value = template;
|
||||
showPreview.value = true;
|
||||
};
|
||||
|
||||
const restoreVersion = async (version) => {
|
||||
const result = await $swal.fire({
|
||||
title: "Restore Version?",
|
||||
text: `Are you sure you want to restore version ${version.version} for "${selectedTemplate.value?.title}"? Current content will be overwritten.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Restore",
|
||||
cancelButtonText: "Cancel",
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
// Show loading
|
||||
const loadingSwal = $swal.fire({
|
||||
title: "Restoring Version...",
|
||||
text: "Please wait while we restore the version",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
showConfirmButton: false,
|
||||
didOpen: () => {
|
||||
$swal.showLoading();
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Restoring version:", version.id);
|
||||
|
||||
const response = await $fetch(
|
||||
`/api/notifications/templates/${selectedTemplate.value.id}/versions/${version.id}/restore`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
|
||||
loadingSwal.close();
|
||||
|
||||
if (response.success) {
|
||||
await $swal.fire({
|
||||
title: "Restored!",
|
||||
text: response.data.message,
|
||||
icon: "success",
|
||||
timer: 3000,
|
||||
});
|
||||
|
||||
// Close the version history modal
|
||||
showVersions.value = false;
|
||||
|
||||
// Refresh the templates list to show updated version
|
||||
await fetchTemplates();
|
||||
} else {
|
||||
throw new Error(response.data?.error || "Failed to restore version");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error restoring version:", error);
|
||||
|
||||
let errorMessage = "Failed to restore version. Please try again.";
|
||||
if (error.data?.error) {
|
||||
errorMessage = error.data.error;
|
||||
}
|
||||
|
||||
await $swal.fire({
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteVersion = async (version) => {
|
||||
const result = await $swal.fire({
|
||||
title: "Delete Version?",
|
||||
text: `Are you sure you want to delete version ${version.version} for "${selectedTemplate.value?.title}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
cancelButtonText: "Cancel",
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#3085d6",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
// Show loading
|
||||
const loadingSwal = $swal.fire({
|
||||
title: "Deleting Version...",
|
||||
text: "Please wait while we delete the version",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
showConfirmButton: false,
|
||||
didOpen: () => {
|
||||
$swal.showLoading();
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Deleting version:", version.id);
|
||||
|
||||
const response = await $fetch(
|
||||
`/api/notifications/templates/${selectedTemplate.value.id}/versions/${version.id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
loadingSwal.close();
|
||||
|
||||
if (response.success) {
|
||||
// Remove the deleted version from the local array
|
||||
versionHistory.value = versionHistory.value.filter((v) => v.id !== version.id);
|
||||
|
||||
await $swal.fire({
|
||||
title: "Deleted!",
|
||||
text: response.data.message,
|
||||
icon: "success",
|
||||
timer: 3000,
|
||||
});
|
||||
|
||||
// If no versions left, close the modal
|
||||
if (versionHistory.value.length === 0) {
|
||||
showVersions.value = false;
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.data?.error || "Failed to delete version");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting version:", error);
|
||||
|
||||
let errorMessage = "Failed to delete version. Please try again.";
|
||||
if (error.data?.error) {
|
||||
errorMessage = error.data.error;
|
||||
}
|
||||
|
||||
await $swal.fire({
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const openModalDelete = async (templateToDelete) => {
|
||||
const result = await $swal.fire({
|
||||
title: "Delete Template",
|
||||
text: `Are you sure you want to delete "${templateToDelete.title}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
cancelButtonText: "Cancel",
|
||||
confirmButtonColor: "#d33",
|
||||
cancelButtonColor: "#3085d6",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
await deleteTemplate(templateToDelete);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTemplate = async (templateToDelete) => {
|
||||
try {
|
||||
// Show loading
|
||||
const loadingSwal = $swal.fire({
|
||||
title: "Deleting Template...",
|
||||
text: "Please wait while we delete the template",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
showConfirmButton: false,
|
||||
didOpen: () => {
|
||||
$swal.showLoading();
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Deleting template:", templateToDelete.title);
|
||||
|
||||
const response = await $fetch(`/api/notifications/templates/${templateToDelete.id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
loadingSwal.close();
|
||||
|
||||
if (response.success) {
|
||||
await $swal.fire({
|
||||
position: "center",
|
||||
icon: "success",
|
||||
title: "Deleted!",
|
||||
text: response.data.message,
|
||||
timer: 3000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// Refresh the templates list
|
||||
await fetchTemplates();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting template:", error);
|
||||
|
||||
let errorMessage = "Failed to delete template. Please try again.";
|
||||
if (error.data?.error) {
|
||||
errorMessage = error.data.error;
|
||||
}
|
||||
|
||||
await $swal.fire({
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Load templates when component mounts
|
||||
onMounted(async () => {
|
||||
await fetchTemplates();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user