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>