Initial copy from influencer-biz
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
/**
|
||||
* Notification scheduling and quiet hours handling
|
||||
*/
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import {
|
||||
sendPushToMultiple,
|
||||
createTaxDeadlinePayload,
|
||||
createInvoicePayload,
|
||||
createCampaignDeadlinePayload,
|
||||
type PushSubscriptionKeys,
|
||||
} from './web-push'
|
||||
import type { NotificationSettings, NotificationType } from '@/types'
|
||||
|
||||
/**
|
||||
* Check if current time is within quiet hours (Sweden timezone)
|
||||
*/
|
||||
export function isWithinQuietHours(
|
||||
settings: NotificationSettings,
|
||||
now: Date = new Date()
|
||||
): boolean {
|
||||
// Convert to Sweden timezone
|
||||
const swedenTime = new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'Europe/Stockholm',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(now)
|
||||
|
||||
const [hours, minutes] = swedenTime.split(':').map(Number)
|
||||
const currentMinutes = hours * 60 + minutes
|
||||
|
||||
// Parse quiet hours
|
||||
const [startHours, startMinutes] = settings.quiet_start.split(':').map(Number)
|
||||
const [endHours, endMinutes] = settings.quiet_end.split(':').map(Number)
|
||||
|
||||
const quietStartMinutes = startHours * 60 + startMinutes
|
||||
const quietEndMinutes = endHours * 60 + endMinutes
|
||||
|
||||
// Handle overnight quiet hours (e.g., 21:00 to 08:00)
|
||||
if (quietStartMinutes > quietEndMinutes) {
|
||||
// Quiet if after start OR before end
|
||||
return currentMinutes >= quietStartMinutes || currentMinutes < quietEndMinutes
|
||||
}
|
||||
|
||||
// Normal range (e.g., 01:00 to 06:00)
|
||||
return currentMinutes >= quietStartMinutes && currentMinutes < quietEndMinutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a notification has already been sent (prevent duplicates)
|
||||
*/
|
||||
async function wasNotificationSent(
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
notificationType: NotificationType,
|
||||
referenceId: string,
|
||||
daysBefore: number
|
||||
): Promise<boolean> {
|
||||
const { data } = await supabase
|
||||
.from('notification_log')
|
||||
.select('id')
|
||||
.eq('user_id', userId)
|
||||
.eq('notification_type', notificationType)
|
||||
.eq('reference_id', referenceId)
|
||||
.eq('days_before', daysBefore)
|
||||
.single()
|
||||
|
||||
return !!data
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a sent notification
|
||||
*/
|
||||
async function logNotification(
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
notificationType: NotificationType,
|
||||
referenceId: string,
|
||||
daysBefore: number
|
||||
): Promise<void> {
|
||||
await supabase.from('notification_log').insert({
|
||||
user_id: userId,
|
||||
notification_type: notificationType,
|
||||
reference_id: referenceId,
|
||||
days_before: daysBefore,
|
||||
delivery_status: 'sent',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's push subscriptions
|
||||
*/
|
||||
async function getUserSubscriptions(
|
||||
supabase: SupabaseClient,
|
||||
userId: string
|
||||
): Promise<PushSubscriptionKeys[]> {
|
||||
const { data } = await supabase
|
||||
.from('push_subscriptions')
|
||||
.select('endpoint, p256dh, auth')
|
||||
.eq('user_id', userId)
|
||||
.eq('is_active', true)
|
||||
|
||||
return data || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's notification settings
|
||||
*/
|
||||
async function getUserNotificationSettings(
|
||||
supabase: SupabaseClient,
|
||||
userId: string
|
||||
): Promise<NotificationSettings | null> {
|
||||
const { data } = await supabase
|
||||
.from('notification_settings')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.single()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable gone (unsubscribed) push subscriptions
|
||||
*/
|
||||
async function disableGoneSubscriptions(
|
||||
supabase: SupabaseClient,
|
||||
endpoints: string[]
|
||||
): Promise<void> {
|
||||
if (endpoints.length === 0) return
|
||||
|
||||
await supabase
|
||||
.from('push_subscriptions')
|
||||
.update({ is_active: false })
|
||||
.in('endpoint', endpoints)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send tax deadline notifications
|
||||
*/
|
||||
export async function sendTaxDeadlineNotifications(
|
||||
supabase: SupabaseClient
|
||||
): Promise<{ sent: number; skipped: number }> {
|
||||
let sent = 0
|
||||
let skipped = 0
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayStr = today.toISOString().split('T')[0]
|
||||
|
||||
// Calculate dates for 7 days and 1 day before
|
||||
const in7Days = new Date(today)
|
||||
in7Days.setDate(in7Days.getDate() + 7)
|
||||
const in7DaysStr = in7Days.toISOString().split('T')[0]
|
||||
|
||||
const tomorrow = new Date(today)
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
const tomorrowStr = tomorrow.toISOString().split('T')[0]
|
||||
|
||||
// Fetch tax deadlines due in 7 days or 1 day
|
||||
const { data: deadlines } = await supabase
|
||||
.from('deadlines')
|
||||
.select('id, user_id, title, due_date')
|
||||
.eq('deadline_type', 'tax')
|
||||
.eq('is_completed', false)
|
||||
.in('status', ['upcoming', 'action_needed'])
|
||||
.in('due_date', [in7DaysStr, tomorrowStr, todayStr])
|
||||
|
||||
if (!deadlines || deadlines.length === 0) {
|
||||
return { sent: 0, skipped: 0 }
|
||||
}
|
||||
|
||||
// Group by user
|
||||
const userDeadlines = new Map<string, typeof deadlines>()
|
||||
for (const deadline of deadlines) {
|
||||
const list = userDeadlines.get(deadline.user_id) || []
|
||||
list.push(deadline)
|
||||
userDeadlines.set(deadline.user_id, list)
|
||||
}
|
||||
|
||||
for (const [userId, userDls] of userDeadlines) {
|
||||
// Get user settings
|
||||
const settings = await getUserNotificationSettings(supabase, userId)
|
||||
|
||||
// Skip if tax deadlines disabled
|
||||
if (settings && !settings.tax_deadlines_enabled) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if push disabled
|
||||
if (settings && !settings.push_enabled) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if within quiet hours
|
||||
if (settings && isWithinQuietHours(settings)) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
// Get subscriptions
|
||||
const subscriptions = await getUserSubscriptions(supabase, userId)
|
||||
if (subscriptions.length === 0) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
for (const deadline of userDls) {
|
||||
const daysUntil = Math.ceil(
|
||||
(new Date(deadline.due_date).getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
|
||||
)
|
||||
|
||||
// Check if already sent for this day count
|
||||
const alreadySent = await wasNotificationSent(
|
||||
supabase,
|
||||
userId,
|
||||
'tax_deadline',
|
||||
deadline.id,
|
||||
daysUntil
|
||||
)
|
||||
|
||||
if (alreadySent) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// Create and send notification
|
||||
const payload = createTaxDeadlinePayload(
|
||||
deadline.title,
|
||||
deadline.due_date,
|
||||
daysUntil,
|
||||
deadline.id
|
||||
)
|
||||
|
||||
const result = await sendPushToMultiple(subscriptions, payload)
|
||||
|
||||
// Log the notification
|
||||
await logNotification(supabase, userId, 'tax_deadline', deadline.id, daysUntil)
|
||||
|
||||
// Disable gone subscriptions
|
||||
if (result.goneSubscriptions.length > 0) {
|
||||
await disableGoneSubscriptions(supabase, result.goneSubscriptions)
|
||||
}
|
||||
|
||||
if (result.successful > 0) {
|
||||
sent++
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { sent, skipped }
|
||||
}
|
||||
|
||||
/**
|
||||
* Send invoice reminder notifications
|
||||
*/
|
||||
export async function sendInvoiceNotifications(
|
||||
supabase: SupabaseClient
|
||||
): Promise<{ sent: number; skipped: number }> {
|
||||
let sent = 0
|
||||
let skipped = 0
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayStr = today.toISOString().split('T')[0]
|
||||
|
||||
// Calculate dates
|
||||
const in3Days = new Date(today)
|
||||
in3Days.setDate(in3Days.getDate() + 3)
|
||||
const in3DaysStr = in3Days.toISOString().split('T')[0]
|
||||
|
||||
const daysAgo3 = new Date(today)
|
||||
daysAgo3.setDate(daysAgo3.getDate() - 3)
|
||||
const daysAgo3Str = daysAgo3.toISOString().split('T')[0]
|
||||
|
||||
const daysAgo7 = new Date(today)
|
||||
daysAgo7.setDate(daysAgo7.getDate() - 7)
|
||||
const daysAgo7Str = daysAgo7.toISOString().split('T')[0]
|
||||
|
||||
// Fetch invoices: due soon (3d, 0d) or overdue (+3d, +7d)
|
||||
const { data: invoices } = await supabase
|
||||
.from('invoices')
|
||||
.select('id, user_id, invoice_number, total, due_date, customer:customers(name)')
|
||||
.in('status', ['sent', 'overdue'])
|
||||
.in('due_date', [in3DaysStr, todayStr, daysAgo3Str, daysAgo7Str])
|
||||
|
||||
if (!invoices || invoices.length === 0) {
|
||||
return { sent: 0, skipped: 0 }
|
||||
}
|
||||
|
||||
// Group by user
|
||||
const userInvoices = new Map<string, typeof invoices>()
|
||||
for (const invoice of invoices) {
|
||||
const list = userInvoices.get(invoice.user_id) || []
|
||||
list.push(invoice)
|
||||
userInvoices.set(invoice.user_id, list)
|
||||
}
|
||||
|
||||
for (const [userId, userInvs] of userInvoices) {
|
||||
const settings = await getUserNotificationSettings(supabase, userId)
|
||||
|
||||
if (settings && !settings.invoice_reminders_enabled) {
|
||||
skipped += userInvs.length
|
||||
continue
|
||||
}
|
||||
|
||||
if (settings && !settings.push_enabled) {
|
||||
skipped += userInvs.length
|
||||
continue
|
||||
}
|
||||
|
||||
if (settings && isWithinQuietHours(settings)) {
|
||||
skipped += userInvs.length
|
||||
continue
|
||||
}
|
||||
|
||||
const subscriptions = await getUserSubscriptions(supabase, userId)
|
||||
if (subscriptions.length === 0) {
|
||||
skipped += userInvs.length
|
||||
continue
|
||||
}
|
||||
|
||||
for (const invoice of userInvs) {
|
||||
const dueDate = new Date(invoice.due_date)
|
||||
const daysUntil = Math.ceil((dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const isOverdue = daysUntil < 0
|
||||
|
||||
const notificationType: NotificationType = isOverdue ? 'invoice_overdue' : 'invoice_due'
|
||||
|
||||
const alreadySent = await wasNotificationSent(
|
||||
supabase,
|
||||
userId,
|
||||
notificationType,
|
||||
invoice.id,
|
||||
Math.abs(daysUntil)
|
||||
)
|
||||
|
||||
if (alreadySent) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// customer is a single object from the join, but TypeScript sees it as an array from select()
|
||||
const customer = invoice.customer as unknown as { name: string } | null
|
||||
const customerName = customer?.name || 'Okänd kund'
|
||||
|
||||
const payload = createInvoicePayload(
|
||||
invoice.invoice_number,
|
||||
customerName,
|
||||
invoice.total,
|
||||
invoice.due_date,
|
||||
isOverdue,
|
||||
invoice.id
|
||||
)
|
||||
|
||||
const result = await sendPushToMultiple(subscriptions, payload)
|
||||
|
||||
await logNotification(supabase, userId, notificationType, invoice.id, Math.abs(daysUntil))
|
||||
|
||||
if (result.goneSubscriptions.length > 0) {
|
||||
await disableGoneSubscriptions(supabase, result.goneSubscriptions)
|
||||
}
|
||||
|
||||
if (result.successful > 0) {
|
||||
sent++
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { sent, skipped }
|
||||
}
|
||||
|
||||
/**
|
||||
* Send campaign deadline notifications
|
||||
*/
|
||||
export async function sendCampaignNotifications(
|
||||
supabase: SupabaseClient
|
||||
): Promise<{ sent: number; skipped: number }> {
|
||||
let sent = 0
|
||||
let skipped = 0
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayStr = today.toISOString().split('T')[0]
|
||||
|
||||
const in3Days = new Date(today)
|
||||
in3Days.setDate(in3Days.getDate() + 3)
|
||||
const in3DaysStr = in3Days.toISOString().split('T')[0]
|
||||
|
||||
// Fetch campaign deadlines due in 3 days or today
|
||||
const { data: deadlines } = await supabase
|
||||
.from('deadlines')
|
||||
.select('id, user_id, title, due_date, campaign:campaigns(name)')
|
||||
.neq('deadline_type', 'tax')
|
||||
.not('campaign_id', 'is', null)
|
||||
.eq('is_completed', false)
|
||||
.in('due_date', [in3DaysStr, todayStr])
|
||||
|
||||
if (!deadlines || deadlines.length === 0) {
|
||||
return { sent: 0, skipped: 0 }
|
||||
}
|
||||
|
||||
// Group by user
|
||||
const userDeadlines = new Map<string, typeof deadlines>()
|
||||
for (const deadline of deadlines) {
|
||||
const list = userDeadlines.get(deadline.user_id) || []
|
||||
list.push(deadline)
|
||||
userDeadlines.set(deadline.user_id, list)
|
||||
}
|
||||
|
||||
for (const [userId, userDls] of userDeadlines) {
|
||||
const settings = await getUserNotificationSettings(supabase, userId)
|
||||
|
||||
if (settings && !settings.campaign_deadlines_enabled) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
if (settings && !settings.push_enabled) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
if (settings && isWithinQuietHours(settings)) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
const subscriptions = await getUserSubscriptions(supabase, userId)
|
||||
if (subscriptions.length === 0) {
|
||||
skipped += userDls.length
|
||||
continue
|
||||
}
|
||||
|
||||
for (const deadline of userDls) {
|
||||
const daysUntil = Math.ceil(
|
||||
(new Date(deadline.due_date).getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
|
||||
)
|
||||
|
||||
const alreadySent = await wasNotificationSent(
|
||||
supabase,
|
||||
userId,
|
||||
'campaign_deadline',
|
||||
deadline.id,
|
||||
daysUntil
|
||||
)
|
||||
|
||||
if (alreadySent) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// campaign is a single object from the join, but TypeScript sees it as an array from select()
|
||||
const campaign = deadline.campaign as unknown as { name: string } | null
|
||||
const campaignName = campaign?.name || 'Kampanj'
|
||||
|
||||
const payload = createCampaignDeadlinePayload(
|
||||
campaignName,
|
||||
deadline.title,
|
||||
deadline.due_date,
|
||||
daysUntil,
|
||||
deadline.id
|
||||
)
|
||||
|
||||
const result = await sendPushToMultiple(subscriptions, payload)
|
||||
|
||||
await logNotification(supabase, userId, 'campaign_deadline', deadline.id, daysUntil)
|
||||
|
||||
if (result.goneSubscriptions.length > 0) {
|
||||
await disableGoneSubscriptions(supabase, result.goneSubscriptions)
|
||||
}
|
||||
|
||||
if (result.successful > 0) {
|
||||
sent++
|
||||
} else {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { sent, skipped }
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Web Push configuration and sending utilities
|
||||
*/
|
||||
|
||||
import webpush from 'web-push'
|
||||
|
||||
// Configure web-push with VAPID keys
|
||||
const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY
|
||||
const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY
|
||||
const vapidSubject = process.env.VAPID_SUBJECT || 'mailto:support@influencer-biz.se'
|
||||
|
||||
if (vapidPublicKey && vapidPrivateKey) {
|
||||
webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Push subscription keys from the browser
|
||||
*/
|
||||
export interface PushSubscriptionKeys {
|
||||
endpoint: string
|
||||
p256dh: string
|
||||
auth: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification payload
|
||||
*/
|
||||
export interface NotificationPayload {
|
||||
title: string
|
||||
body: string
|
||||
icon?: string
|
||||
badge?: string
|
||||
tag?: string
|
||||
data?: {
|
||||
url?: string
|
||||
type?: string
|
||||
id?: string
|
||||
}
|
||||
actions?: Array<{
|
||||
action: string
|
||||
title: string
|
||||
icon?: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a push notification to a subscription
|
||||
*/
|
||||
export async function sendPushNotification(
|
||||
subscription: PushSubscriptionKeys,
|
||||
payload: NotificationPayload
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
if (!vapidPublicKey || !vapidPrivateKey) {
|
||||
console.error('VAPID keys not configured')
|
||||
return { success: false, error: 'VAPID keys not configured' }
|
||||
}
|
||||
|
||||
const pushSubscription = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
p256dh: subscription.p256dh,
|
||||
auth: subscription.auth,
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
await webpush.sendNotification(
|
||||
pushSubscription,
|
||||
JSON.stringify(payload),
|
||||
{
|
||||
TTL: 86400, // 24 hours
|
||||
urgency: 'normal',
|
||||
}
|
||||
)
|
||||
return { success: true }
|
||||
} catch (error: unknown) {
|
||||
const err = error as { statusCode?: number; message?: string }
|
||||
|
||||
// Handle subscription gone (user unsubscribed)
|
||||
if (err.statusCode === 410) {
|
||||
return { success: false, error: 'subscription_gone' }
|
||||
}
|
||||
|
||||
console.error('Push notification error:', err)
|
||||
return { success: false, error: err.message || 'Unknown error' }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send push notifications to multiple subscriptions
|
||||
*/
|
||||
export async function sendPushToMultiple(
|
||||
subscriptions: PushSubscriptionKeys[],
|
||||
payload: NotificationPayload
|
||||
): Promise<{
|
||||
successful: number
|
||||
failed: number
|
||||
goneSubscriptions: string[]
|
||||
}> {
|
||||
let successful = 0
|
||||
let failed = 0
|
||||
const goneSubscriptions: string[] = []
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
subscriptions.map((sub) => sendPushNotification(sub, payload))
|
||||
)
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled' && result.value.success) {
|
||||
successful++
|
||||
} else {
|
||||
failed++
|
||||
if (result.status === 'fulfilled' && result.value.error === 'subscription_gone') {
|
||||
goneSubscriptions.push(subscriptions[index].endpoint)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { successful, failed, goneSubscriptions }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public VAPID key for client-side subscription
|
||||
*/
|
||||
export function getVapidPublicKey(): string | null {
|
||||
return vapidPublicKey || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification payload for tax deadline
|
||||
*/
|
||||
export function createTaxDeadlinePayload(
|
||||
title: string,
|
||||
dueDate: string,
|
||||
daysUntil: number,
|
||||
deadlineId: string
|
||||
): NotificationPayload {
|
||||
const urgencyText = daysUntil === 0 ? 'IDAG!' : daysUntil === 1 ? 'imorgon' : `om ${daysUntil} dagar`
|
||||
|
||||
return {
|
||||
title: `📋 ${title}`,
|
||||
body: `Deadline ${urgencyText} (${formatDate(dueDate)})`,
|
||||
icon: '/icons/icon-192.png',
|
||||
badge: '/icons/badge-72.png',
|
||||
tag: `tax-deadline-${deadlineId}`,
|
||||
data: {
|
||||
url: '/calendar',
|
||||
type: 'tax_deadline',
|
||||
id: deadlineId,
|
||||
},
|
||||
actions: [
|
||||
{ action: 'view', title: 'Visa' },
|
||||
{ action: 'dismiss', title: 'Avfärda' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification payload for invoice reminder
|
||||
*/
|
||||
export function createInvoicePayload(
|
||||
invoiceNumber: string,
|
||||
customerName: string,
|
||||
amount: number,
|
||||
dueDate: string,
|
||||
isOverdue: boolean,
|
||||
invoiceId: string
|
||||
): NotificationPayload {
|
||||
const title = isOverdue
|
||||
? `⚠️ Obetald faktura #${invoiceNumber}`
|
||||
: `💰 Faktura #${invoiceNumber} förfaller`
|
||||
|
||||
const body = isOverdue
|
||||
? `${customerName} - ${amount.toLocaleString('sv-SE')} kr (förföll ${formatDate(dueDate)})`
|
||||
: `${customerName} - ${amount.toLocaleString('sv-SE')} kr (${formatDate(dueDate)})`
|
||||
|
||||
return {
|
||||
title,
|
||||
body,
|
||||
icon: '/icons/icon-192.png',
|
||||
badge: '/icons/badge-72.png',
|
||||
tag: `invoice-${invoiceId}`,
|
||||
data: {
|
||||
url: `/invoices/${invoiceId}`,
|
||||
type: isOverdue ? 'invoice_overdue' : 'invoice_due',
|
||||
id: invoiceId,
|
||||
},
|
||||
actions: [
|
||||
{ action: 'view', title: 'Visa faktura' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notification payload for campaign deadline
|
||||
*/
|
||||
export function createCampaignDeadlinePayload(
|
||||
campaignName: string,
|
||||
deliverableTitle: string,
|
||||
dueDate: string,
|
||||
daysUntil: number,
|
||||
deadlineId: string
|
||||
): NotificationPayload {
|
||||
const urgencyText = daysUntil === 0 ? 'IDAG!' : daysUntil === 1 ? 'imorgon' : `om ${daysUntil} dagar`
|
||||
|
||||
return {
|
||||
title: `🎬 ${campaignName}`,
|
||||
body: `${deliverableTitle} - leverans ${urgencyText}`,
|
||||
icon: '/icons/icon-192.png',
|
||||
badge: '/icons/badge-72.png',
|
||||
tag: `campaign-deadline-${deadlineId}`,
|
||||
data: {
|
||||
url: '/calendar',
|
||||
type: 'campaign_deadline',
|
||||
id: deadlineId,
|
||||
},
|
||||
actions: [
|
||||
{ action: 'view', title: 'Visa' },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for notifications
|
||||
*/
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('sv-SE', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user