Refactor to generic ERP base
Remove influencer-specific features (campaigns, TikTok, gifts, shadow ledger, contracts, briefings) and consolidate into a clean ERP foundation with core bookkeeping, invoicing, receipts, tax reporting, and calendar functionality. Reorganize database migrations into a clean numbered sequence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a25d10a528
commit
0aecf5b41c
@@ -7,7 +7,6 @@ import {
|
||||
sendPushToMultiple,
|
||||
createTaxDeadlinePayload,
|
||||
createInvoicePayload,
|
||||
createCampaignDeadlinePayload,
|
||||
type PushSubscriptionKeys,
|
||||
} from './web-push'
|
||||
import type { NotificationSettings, NotificationType } from '@/types'
|
||||
@@ -376,113 +375,3 @@ export async function sendInvoiceNotifications(
|
||||
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 }
|
||||
}
|
||||
|
||||
+1
-30
@@ -7,7 +7,7 @@ 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'
|
||||
const vapidSubject = process.env.VAPID_SUBJECT || 'mailto:support@erp-base.se'
|
||||
|
||||
if (vapidPublicKey && vapidPrivateKey) {
|
||||
webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey)
|
||||
@@ -191,35 +191,6 @@ export function createInvoicePayload(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user