615 lines
20 KiB
Vue
615 lines
20 KiB
Vue
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
|
|
<!-- Info Card -->
|
|
<rs-card class="mb-5">
|
|
<template #header>
|
|
<div class="flex">
|
|
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon>
|
|
Edit Notification
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<p class="mb-4">
|
|
Edit and update your notification settings. Modify delivery options, content,
|
|
and targeting to improve your notification campaign 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 notification details...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<rs-card v-else-if="error" class="mb-5">
|
|
<template #body>
|
|
<div class="text-center py-8">
|
|
<div class="flex justify-center mb-4">
|
|
<Icon name="ic:outline-error" size="4rem" class="text-red-400" />
|
|
</div>
|
|
<h3 class="text-lg font-medium text-gray-500 mb-2">
|
|
Error Loading Notification
|
|
</h3>
|
|
<p class="text-gray-500 mb-4">
|
|
{{ error }}
|
|
</p>
|
|
<rs-button @click="$router.push('/notification/list')">
|
|
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
|
Back to List
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Notification Not Found -->
|
|
<rs-card v-else-if="!notification">
|
|
<template #body>
|
|
<div class="text-center py-8">
|
|
<div class="flex justify-center mb-4">
|
|
<Icon name="ic:outline-error" size="4rem" class="text-red-400" />
|
|
</div>
|
|
<h3 class="text-lg font-medium text-gray-500 mb-2">Notification Not Found</h3>
|
|
<p class="text-gray-500 mb-4">
|
|
The notification you're trying to edit doesn't exist or has been deleted.
|
|
</p>
|
|
<rs-button @click="$router.push('/notification/list')">
|
|
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
|
Back to List
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Edit Form -->
|
|
<rs-card v-else>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-semibold">Edit Notification</h2>
|
|
<div class="flex gap-3">
|
|
<rs-button @click="$router.push('/notification/list')" variant="outline">
|
|
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
|
Back to List
|
|
</rs-button>
|
|
<rs-button @click="viewNotification" variant="primary">
|
|
<Icon name="ic:outline-visibility" class="mr-1"></Icon>
|
|
View Details
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="pt-2">
|
|
<FormKit
|
|
type="form"
|
|
@submit="handleUpdateNotification"
|
|
:actions="false"
|
|
class="w-full"
|
|
>
|
|
<!-- Basic Information -->
|
|
<div class="space-y-6 mb-8">
|
|
<h3
|
|
class="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2"
|
|
>
|
|
Basic Information
|
|
</h3>
|
|
<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="Notification Title"
|
|
placeholder="Enter notification title"
|
|
validation="required"
|
|
v-model="notificationForm.title"
|
|
help="This is for internal identification purposes"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
name="type"
|
|
label="Notification Type"
|
|
:options="notificationTypes"
|
|
validation="required"
|
|
v-model="notificationForm.type"
|
|
help="Choose between single targeted notification or bulk notification"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
name="priority"
|
|
label="Priority Level"
|
|
:options="priorityLevels"
|
|
validation="required"
|
|
v-model="notificationForm.priority"
|
|
help="Set the importance level of this notification"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
name="category"
|
|
label="Category"
|
|
:options="categoryOptions"
|
|
validation="required"
|
|
v-model="notificationForm.category"
|
|
help="Categorize your notification for better organization"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Right Column -->
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="checkbox"
|
|
name="channels"
|
|
label="Delivery Channels"
|
|
:options="channelOptions"
|
|
validation="required|min:1"
|
|
v-model="notificationForm.channels"
|
|
decorator-icon="material-symbols:check"
|
|
options-class="grid grid-cols-1 gap-y-2 pt-1"
|
|
help="Select one or more delivery channels"
|
|
/>
|
|
|
|
<FormKit
|
|
v-if="notificationForm.channels.includes('email')"
|
|
type="text"
|
|
name="emailSubject"
|
|
label="Email Subject Line"
|
|
placeholder="Enter email subject"
|
|
validation="required"
|
|
v-model="notificationForm.emailSubject"
|
|
help="This will be the email subject line"
|
|
/>
|
|
|
|
<FormKit
|
|
type="datetime-local"
|
|
name="expiresAt"
|
|
label="Expiration Date & Time (Optional)"
|
|
v-model="notificationForm.expiresAt"
|
|
help="Set when this notification should expire"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scheduling -->
|
|
<div class="space-y-6 mb-8">
|
|
<h3
|
|
class="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2"
|
|
>
|
|
Scheduling
|
|
</h3>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="radio"
|
|
name="deliveryType"
|
|
label="Delivery Schedule"
|
|
:options="deliveryTypes"
|
|
validation="required"
|
|
v-model="notificationForm.deliveryType"
|
|
decorator-icon="material-symbols:radio-button-checked"
|
|
options-class="space-y-3 pt-1"
|
|
/>
|
|
|
|
<div
|
|
v-if="notificationForm.deliveryType === 'scheduled'"
|
|
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
|
>
|
|
<FormKit
|
|
type="datetime-local"
|
|
name="scheduledAt"
|
|
label="Scheduled Date & Time"
|
|
validation="required"
|
|
v-model="notificationForm.scheduledAt"
|
|
help="When should this notification be sent?"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
name="timezone"
|
|
label="Timezone"
|
|
:options="timezoneOptions"
|
|
validation="required"
|
|
v-model="notificationForm.timezone"
|
|
help="Select the timezone for scheduling"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="space-y-4"
|
|
v-if="notificationForm.deliveryType === 'scheduled'"
|
|
>
|
|
<div class="p-4 border rounded-lg bg-blue-50 dark:bg-blue-900/20">
|
|
<h4 class="font-semibold text-blue-800 dark:text-blue-200 mb-2">
|
|
Scheduling Information
|
|
</h4>
|
|
<div class="text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
|
<p v-if="notificationForm.scheduledAt">
|
|
<strong>Scheduled for:</strong>
|
|
{{ formatScheduledTime() }}
|
|
</p>
|
|
<p>
|
|
<strong>Timezone:</strong>
|
|
{{ notificationForm.timezone }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Target Audience -->
|
|
<div class="space-y-6 mb-8">
|
|
<h3
|
|
class="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2"
|
|
>
|
|
Target Audience
|
|
</h3>
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="radio"
|
|
name="audienceType"
|
|
label="Target Audience"
|
|
:options="audienceTypeOptions"
|
|
validation="required"
|
|
v-model="notificationForm.audienceType"
|
|
decorator-icon="material-symbols:radio-button-checked"
|
|
options-class="space-y-3 pt-1"
|
|
/>
|
|
|
|
<div
|
|
v-if="notificationForm.audienceType === 'specific'"
|
|
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
|
>
|
|
<FormKit
|
|
type="textarea"
|
|
name="specificUsers"
|
|
label="Specific Users"
|
|
placeholder="Enter email addresses or user IDs (one per line)"
|
|
validation="required"
|
|
v-model="notificationForm.specificUsers"
|
|
rows="6"
|
|
help="Enter one email address or user ID per line"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="p-4 border rounded-lg bg-green-50 dark:bg-green-900/20">
|
|
<h4 class="font-semibold text-green-800 dark:text-green-200 mb-2">
|
|
Estimated Reach
|
|
</h4>
|
|
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
|
|
{{ estimatedReach.toLocaleString() }} users
|
|
</div>
|
|
<p class="text-sm text-green-700 dark:text-green-300 mt-1">
|
|
Based on your audience selection
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div
|
|
class="flex justify-between items-center pt-6 border-t border-gray-200 dark:border-gray-700"
|
|
>
|
|
<div class="flex gap-3">
|
|
<rs-button @click="handleSaveDraft" variant="outline">
|
|
<Icon name="material-symbols:save" class="mr-1"></Icon>
|
|
Save as Draft
|
|
</rs-button>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<rs-button @click="$router.push('/notification/list')" variant="outline">
|
|
Cancel
|
|
</rs-button>
|
|
<rs-button btnType="submit">
|
|
<Icon name="material-symbols:update" class="mr-1"></Icon>
|
|
Update Notification
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</FormKit>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { useNotifications } from "~/composables/useNotifications";
|
|
|
|
const router = useRouter();
|
|
|
|
definePageMeta({
|
|
title: "Edit Notification",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{
|
|
name: "Dashboard",
|
|
path: "/dashboard",
|
|
},
|
|
{
|
|
name: "Notification",
|
|
path: "/notification",
|
|
},
|
|
{
|
|
name: "List",
|
|
path: "/notification/list",
|
|
},
|
|
{
|
|
name: "Edit",
|
|
path: "",
|
|
type: "current",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Get route params
|
|
const route = useRoute();
|
|
const notificationId = route.params.id;
|
|
|
|
// Use the notifications composable
|
|
const {
|
|
isLoading,
|
|
error,
|
|
getNotificationById,
|
|
updateNotification,
|
|
saveDraft,
|
|
testSendNotification,
|
|
getAudiencePreview,
|
|
} = useNotifications();
|
|
|
|
// Reactive data
|
|
const notification = ref(null);
|
|
const testEmail = ref("");
|
|
const estimatedReach = ref(0);
|
|
|
|
// Form data
|
|
const notificationForm = ref({
|
|
title: "",
|
|
type: "single",
|
|
priority: "medium",
|
|
category: "",
|
|
channels: [],
|
|
emailSubject: "",
|
|
expiresAt: "",
|
|
deliveryType: "immediate",
|
|
scheduledAt: "",
|
|
timezone: "UTC",
|
|
audienceType: "all",
|
|
specificUsers: "",
|
|
userSegments: [],
|
|
userStatus: "",
|
|
registrationPeriod: "",
|
|
excludeUnsubscribed: true,
|
|
});
|
|
|
|
// Form options
|
|
const notificationTypes = [
|
|
{ label: "Single Notification", value: "single" },
|
|
{ label: "Bulk Notification", value: "bulk" },
|
|
];
|
|
|
|
const priorityLevels = [
|
|
{ label: "Low", value: "low" },
|
|
{ label: "Medium", value: "medium" },
|
|
{ label: "High", value: "high" },
|
|
{ label: "Critical", value: "critical" },
|
|
];
|
|
|
|
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: "Push Notification", value: "push" },
|
|
];
|
|
|
|
const deliveryTypes = [
|
|
{ label: "Send Immediately", value: "immediate" },
|
|
{ label: "Schedule for Later", value: "scheduled" },
|
|
];
|
|
|
|
const timezoneOptions = [
|
|
{ label: "UTC", value: "UTC" },
|
|
{ label: "Asia/Kuala_Lumpur", value: "Asia/Kuala_Lumpur" },
|
|
{ label: "America/New_York", value: "America/New_York" },
|
|
{ label: "Europe/London", value: "Europe/London" },
|
|
{ label: "Asia/Tokyo", value: "Asia/Tokyo" },
|
|
];
|
|
|
|
const audienceTypeOptions = [
|
|
{ label: "All Users", value: "all" },
|
|
{ label: "Specific Users", value: "specific" },
|
|
];
|
|
|
|
// Methods
|
|
const formatScheduledTime = () => {
|
|
if (!notificationForm.value.scheduledAt) return "";
|
|
return new Date(notificationForm.value.scheduledAt).toLocaleDateString("en-US", {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
};
|
|
|
|
const viewNotification = () => {
|
|
router.push(`/notification/view/${notificationId}`);
|
|
};
|
|
|
|
const handleSaveDraft = async () => {
|
|
try {
|
|
await saveDraft(notificationForm.value);
|
|
$swal.fire("Success", "Notification saved as draft", "success");
|
|
} catch (error) {
|
|
console.error("Error saving draft:", error);
|
|
$swal.fire("Error", "Failed to save draft", "error");
|
|
}
|
|
};
|
|
|
|
const handleUpdateNotification = async () => {
|
|
try {
|
|
// Validate channels data
|
|
if (
|
|
!Array.isArray(notificationForm.value.channels) ||
|
|
notificationForm.value.channels.length === 0
|
|
) {
|
|
throw new Error("Please select at least one delivery channel");
|
|
}
|
|
|
|
// Validate that all selected channels are valid
|
|
const validChannels = channelOptions.map((option) => option.value);
|
|
const hasInvalidChannel = notificationForm.value.channels.some(
|
|
(channel) => !validChannels.includes(channel)
|
|
);
|
|
if (hasInvalidChannel) {
|
|
throw new Error("Invalid delivery channel selected");
|
|
}
|
|
|
|
// Transform channels data into the required format
|
|
const formData = {
|
|
...notificationForm.value,
|
|
channels: notificationForm.value.channels.map((channel) => ({
|
|
type: channel,
|
|
is_enabled: true,
|
|
})),
|
|
};
|
|
|
|
await updateNotification(notificationId, formData);
|
|
|
|
$swal
|
|
.fire({
|
|
title: "Success!",
|
|
text: "Notification has been updated successfully.",
|
|
icon: "success",
|
|
confirmButtonText: "Back to List",
|
|
})
|
|
.then((result) => {
|
|
if (result.isConfirmed) {
|
|
router.push("/notification/list");
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating notification:", error);
|
|
const errorMessage =
|
|
error.data?.message || error.message || "Failed to update notification";
|
|
$swal.fire("Error", errorMessage, "error");
|
|
}
|
|
};
|
|
|
|
const loadNotification = async () => {
|
|
try {
|
|
const data = await getNotificationById(notificationId);
|
|
notification.value = data;
|
|
|
|
// Transform channels data from backend format to frontend format
|
|
const channels =
|
|
data.notification_channels?.map((channel) => channel.channel_type) || [];
|
|
|
|
// Populate form with existing data
|
|
notificationForm.value = {
|
|
title: data.title,
|
|
type: data.type || "single",
|
|
priority: data.priority,
|
|
category: data.category?.value,
|
|
channels: channels,
|
|
emailSubject: data.emailSubject,
|
|
expiresAt: data.expiresAt,
|
|
deliveryType: data.deliveryType,
|
|
scheduledAt: data.scheduledAt,
|
|
timezone: data.timezone || "UTC",
|
|
audienceType: data.audienceType,
|
|
specificUsers: data.specificUsers,
|
|
userSegments: data.userSegments || [],
|
|
userStatus: data.userStatus,
|
|
registrationPeriod: data.registrationPeriod,
|
|
excludeUnsubscribed: data.excludeUnsubscribed !== false,
|
|
};
|
|
|
|
// Update estimated reach
|
|
estimatedReach.value = data.analytics?.estimatedReach || 0;
|
|
} catch (error) {
|
|
console.error("Error loading notification:", error);
|
|
notification.value = null;
|
|
$swal
|
|
.fire({
|
|
title: "Error",
|
|
text: "Failed to load notification details",
|
|
icon: "error",
|
|
confirmButtonText: "Back to List",
|
|
})
|
|
.then((result) => {
|
|
if (result.isConfirmed) {
|
|
router.push("/notification/list");
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Watch for audience type changes to update estimated reach
|
|
watch(
|
|
() => [
|
|
notificationForm.value.audienceType,
|
|
notificationForm.value.specificUsers,
|
|
notificationForm.value.userSegments,
|
|
notificationForm.value.userStatus,
|
|
notificationForm.value.registrationPeriod,
|
|
notificationForm.value.excludeUnsubscribed,
|
|
notificationForm.value.channels,
|
|
],
|
|
async () => {
|
|
try {
|
|
// Skip if audience type is not set
|
|
if (!notificationForm.value.audienceType) return;
|
|
|
|
const response = await getAudiencePreview({
|
|
audienceType: notificationForm.value.audienceType,
|
|
specificUsers: notificationForm.value.specificUsers,
|
|
userSegments: notificationForm.value.userSegments || [],
|
|
userStatus: notificationForm.value.userStatus,
|
|
registrationPeriod: notificationForm.value.registrationPeriod,
|
|
excludeUnsubscribed: notificationForm.value.excludeUnsubscribed,
|
|
channels: notificationForm.value.channels,
|
|
});
|
|
|
|
if (response.success) {
|
|
estimatedReach.value = response.data.totalCount;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error getting audience preview:", error);
|
|
// Don't show error to user for background calculation
|
|
estimatedReach.value = 0;
|
|
}
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadNotification();
|
|
});
|
|
</script>
|