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:
Jakob Wennberg
2026-02-13 15:39:13 +01:00
co-authored by Claude Opus 4.6
parent a25d10a528
commit 0aecf5b41c
184 changed files with 1418 additions and 24763 deletions
-111
View File
@@ -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
View File
@@ -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
*/