refactor: consolidate extension system to general-only with manifest-driven architecture
- Remove all sector-specific extensions (construction, ecommerce, export, hotel, restaurant, tech) — only general-purpose extensions remain - Move NE-bilaga and SRU export from extensions to core reports (lib/reports/) - Move moms-box-mapping from extensions/export/shared to lib/vat/ - Replace per-extension API routes with catch-all dispatcher (app/api/extensions/ext/[...path]/route.ts) - Add manifest.json for each extension with metadata, env vars, and deps - Add api-routes.ts pattern for extension-defined API endpoints - Add code generation scripts (generate-extension-registry, create-extension) - Add extensions.config.json for opt-in extension loading - Add extensions.schema.json for config validation - Add email service interface with noop default (lib/email/service.ts) - Add CI workflow (core-build.yml) to verify core builds with zero extensions - Add migration 045: expand account_type CHECK for untaxed_reserves - Update CLAUDE.md with comprehensive extension system documentation - Update all report engines and bookkeeping services for new imports - Clean up extensions.schema.json to only list existing extensions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
39e407644d
commit
03b569d708
@@ -1,3 +1,40 @@
|
||||
import { resolve, join, dirname } from 'path'
|
||||
import { readdirSync, readFileSync } from 'fs'
|
||||
|
||||
/**
|
||||
* Build EXTENSION_DEFINITIONS from manifest.json files so the test
|
||||
* is independent of extensions.config.json.
|
||||
*/
|
||||
function buildDefinitionsFromManifests(): Record<string, unknown[]> {
|
||||
const extensionsDir = resolve(__dirname, '../../../extensions')
|
||||
const result: Record<string, unknown[]> = {}
|
||||
|
||||
function walk(dir: string) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath)
|
||||
} else if (entry.name === 'manifest.json') {
|
||||
const manifest = JSON.parse(readFileSync(fullPath, 'utf-8'))
|
||||
const sector: string = manifest.sector
|
||||
if (!result[sector]) result[sector] = []
|
||||
result[sector].push({
|
||||
slug: manifest.id,
|
||||
sector: manifest.sector,
|
||||
...manifest.definition,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(extensionsDir)
|
||||
return result
|
||||
}
|
||||
|
||||
vi.mock('@/lib/extensions/_generated/sector-definitions', () => ({
|
||||
EXTENSION_DEFINITIONS: buildDefinitionsFromManifests(),
|
||||
}))
|
||||
|
||||
import {
|
||||
SECTORS,
|
||||
getSector,
|
||||
@@ -7,12 +44,12 @@ import {
|
||||
} from '../sectors'
|
||||
|
||||
describe('sectors registry', () => {
|
||||
it('should have 7 sectors', () => {
|
||||
expect(SECTORS.length).toBe(7)
|
||||
it('should have 1 sector', () => {
|
||||
expect(SECTORS.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should have 24 total extensions', () => {
|
||||
expect(getAllExtensions().length).toBe(24)
|
||||
it('should have 9 total extensions', () => {
|
||||
expect(getAllExtensions().length).toBe(9)
|
||||
})
|
||||
|
||||
it('should have unique slugs within each sector', () => {
|
||||
@@ -30,10 +67,10 @@ describe('sectors registry', () => {
|
||||
})
|
||||
|
||||
it('getSector returns correct sector', () => {
|
||||
const sector = getSector('restaurant')
|
||||
const sector = getSector('general')
|
||||
expect(sector).toBeDefined()
|
||||
expect(sector!.slug).toBe('restaurant')
|
||||
expect(sector!.name).toBe('Restaurang & Café')
|
||||
expect(sector!.slug).toBe('general')
|
||||
expect(sector!.name).toBe('Generella verktyg')
|
||||
})
|
||||
|
||||
it('getSector returns undefined for unknown slug', () => {
|
||||
@@ -42,21 +79,21 @@ describe('sectors registry', () => {
|
||||
})
|
||||
|
||||
it('getExtensionDefinition returns correct extension', () => {
|
||||
const ext = getExtensionDefinition('restaurant', 'food-cost')
|
||||
const ext = getExtensionDefinition('general', 'ai-chat')
|
||||
expect(ext).toBeDefined()
|
||||
expect(ext!.slug).toBe('food-cost')
|
||||
expect(ext!.name).toBe('Food Cost %')
|
||||
expect(ext!.sector).toBe('restaurant')
|
||||
expect(ext!.slug).toBe('ai-chat')
|
||||
expect(ext!.name).toBe('AI-assistent')
|
||||
expect(ext!.sector).toBe('general')
|
||||
})
|
||||
|
||||
it('getExtensionDefinition returns undefined for unknown extension', () => {
|
||||
const ext = getExtensionDefinition('restaurant', 'nonexistent')
|
||||
const ext = getExtensionDefinition('general', 'nonexistent')
|
||||
expect(ext).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getExtensionsBySector returns extensions for a sector', () => {
|
||||
const extensions = getExtensionsBySector('restaurant')
|
||||
expect(extensions.length).toBe(4)
|
||||
const extensions = getExtensionsBySector('general')
|
||||
expect(extensions.length).toBe(9)
|
||||
})
|
||||
|
||||
it('all extensions have required fields', () => {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
|
||||
export const ENABLED_EXTENSION_IDS: ReadonlySet<string> = new Set([
|
||||
])
|
||||
@@ -0,0 +1,5 @@
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
import type { Extension } from '../types'
|
||||
|
||||
export const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
import type { ExtensionDefinition } from '../types'
|
||||
|
||||
export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { ComponentType } from 'react'
|
||||
import type { WorkspaceComponentProps } from '../workspace-registry'
|
||||
|
||||
export const WORKSPACES: Record<string, ComponentType<WorkspaceComponentProps>> = {
|
||||
}
|
||||
@@ -1,47 +1,14 @@
|
||||
import { extensionRegistry } from './registry'
|
||||
import { receiptOcrExtension } from '@/extensions/general/receipt-ocr'
|
||||
import { aiCategorizationExtension } from '@/extensions/general/ai-categorization'
|
||||
import { pushNotificationsExtension } from '@/extensions/general/push-notifications'
|
||||
import { aiChatExtension } from '@/extensions/general/ai-chat'
|
||||
import { invoiceInboxExtension } from '@/extensions/general/invoice-inbox'
|
||||
import { calendarExtension } from '@/extensions/general/calendar'
|
||||
import { userDescriptionMatchExtension } from '@/extensions/general/user-description-match'
|
||||
import { euSalesListExtension } from '@/extensions/export/eu-sales-list'
|
||||
import { vatMonitorExtension } from '@/extensions/export/vat-monitor'
|
||||
import { intrastatExtension } from '@/extensions/export/intrastat'
|
||||
import { currencyReceivablesExtension } from '@/extensions/export/currency-receivables'
|
||||
import type { Extension } from './types'
|
||||
|
||||
import { enableBankingExtension } from '@/extensions/general/enable-banking'
|
||||
|
||||
/**
|
||||
* Explicit list of first-party extensions.
|
||||
*
|
||||
* Next.js bundling requires static imports — no dynamic filesystem scanning.
|
||||
* Add extensions here as they are built.
|
||||
*/
|
||||
const FIRST_PARTY_EXTENSIONS: Extension[] = [
|
||||
receiptOcrExtension,
|
||||
aiCategorizationExtension,
|
||||
pushNotificationsExtension,
|
||||
aiChatExtension,
|
||||
invoiceInboxExtension,
|
||||
calendarExtension,
|
||||
userDescriptionMatchExtension,
|
||||
enableBankingExtension,
|
||||
|
||||
// ── Export sector ──────────────────────────────────────────
|
||||
euSalesListExtension,
|
||||
vatMonitorExtension,
|
||||
intrastatExtension,
|
||||
currencyReceivablesExtension,
|
||||
]
|
||||
import { FIRST_PARTY_EXTENSIONS } from './_generated/extension-list'
|
||||
|
||||
let loaded = false
|
||||
|
||||
/**
|
||||
* Load and register all first-party extensions.
|
||||
* Load and register all configured extensions.
|
||||
* Idempotent — safe to call multiple times.
|
||||
*
|
||||
* Extensions are controlled by extensions.config.json.
|
||||
* Run `npm run setup:extensions` to regenerate after config changes.
|
||||
*/
|
||||
export function loadExtensions(): void {
|
||||
if (loaded) return
|
||||
|
||||
+12
-374
@@ -1,393 +1,31 @@
|
||||
import type { Sector, SectorSlug, ExtensionDefinition } from './types'
|
||||
import { EXTENSION_DEFINITIONS } from './_generated/sector-definitions'
|
||||
|
||||
// ============================================================
|
||||
// Sector & Extension Registry
|
||||
// ============================================================
|
||||
//
|
||||
// Pure data file. No React, no database calls.
|
||||
// All sector and extension metadata lives here.
|
||||
// Sector shells are structural and always present.
|
||||
// Extension definitions per sector come from the generated file
|
||||
// (controlled by extensions.config.json).
|
||||
// ============================================================
|
||||
|
||||
export const SECTORS: Sector[] = [
|
||||
// ── General ──────────────────────────────────────────────
|
||||
/** Sector shells — structural metadata, always available */
|
||||
const SECTOR_SHELLS: Omit<Sector, 'extensions'>[] = [
|
||||
{
|
||||
slug: 'general',
|
||||
name: 'Generella verktyg',
|
||||
icon: 'Layers',
|
||||
description: 'Verktyg som passar alla verksamheter',
|
||||
extensions: [
|
||||
{
|
||||
slug: 'receipt-ocr',
|
||||
name: 'Kvittoscanning',
|
||||
sector: 'general',
|
||||
category: 'import',
|
||||
icon: 'Camera',
|
||||
dataPattern: 'manual',
|
||||
hasOwnData: true,
|
||||
description: 'Skanna kvitton och extrahera data automatiskt',
|
||||
longDescription:
|
||||
'Ladda upp kvittofoton och låt systemet automatiskt extrahera leverantör, belopp, moms och datum. Sparar tid och minskar manuell inmatning.',
|
||||
},
|
||||
{
|
||||
slug: 'ai-categorization',
|
||||
name: 'AI-kategorisering',
|
||||
sector: 'general',
|
||||
category: 'operations',
|
||||
icon: 'Sparkles',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['transactions'],
|
||||
description: 'AI-drivna kategoriförslag för transaktioner',
|
||||
longDescription:
|
||||
'Använder AI för att automatiskt föreslå BAS-kontokategorier för dina banktransaktioner. Lär sig från dina tidigare bokföringsval.',
|
||||
},
|
||||
{
|
||||
slug: 'ai-chat',
|
||||
name: 'AI-assistent',
|
||||
sector: 'general',
|
||||
category: 'operations',
|
||||
icon: 'MessageSquare',
|
||||
dataPattern: 'manual',
|
||||
hasOwnData: true,
|
||||
description: 'AI-assistent för skatte- och bokföringsfrågor',
|
||||
longDescription:
|
||||
'Ställ frågor om skatt, bokföring och företagande till en AI-assistent som förstår svensk redovisning. Svar baserade på aktuella regler och praxis.',
|
||||
quickAction: {
|
||||
label: 'AI-assistent',
|
||||
description: 'Fråga om bokföring',
|
||||
icon: 'MessageSquare',
|
||||
event: 'open-ai-chat',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'push-notifications',
|
||||
name: 'Push-notiser',
|
||||
sector: 'general',
|
||||
category: 'operations',
|
||||
icon: 'Bell',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['journal_entries', 'invoices', 'receipts'],
|
||||
description: 'Händelsenotiser för bokföringsaktiviteter',
|
||||
longDescription:
|
||||
'Få push-notiser direkt i webbläsaren när viktiga händelser sker — nya fakturor, förfallna betalningar, slutförda bokföringar med mera.',
|
||||
},
|
||||
{
|
||||
slug: 'invoice-inbox',
|
||||
name: 'Dokumentinkorg',
|
||||
sector: 'general',
|
||||
category: 'import',
|
||||
icon: 'Inbox',
|
||||
dataPattern: 'manual',
|
||||
hasOwnData: true,
|
||||
description: 'Ta emot alla dokument via e-post — fakturor, kvitton och myndighetspost',
|
||||
longDescription:
|
||||
'Skicka alla affärsdokument till en dedikerad e-postadress. AI klassificerar automatiskt dokumenttyp (faktura, kvitto, myndighetspost), extraherar data och matchar mot transaktioner. En inkorg för alla dokument.',
|
||||
},
|
||||
{
|
||||
slug: 'calendar',
|
||||
name: 'Kalender',
|
||||
sector: 'general',
|
||||
category: 'operations',
|
||||
icon: 'Calendar',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['invoices', 'deadlines', 'customers'],
|
||||
description: 'Fullstandig kalendervy med manads-, vecko- och dagsvisning',
|
||||
longDescription:
|
||||
'Se alla fakturadatum och deadlines i en interaktiv kalender med manads-, vecko- och dagsvy.',
|
||||
},
|
||||
{
|
||||
slug: 'user-description-match',
|
||||
name: 'Beskrivningsmatchning',
|
||||
sector: 'general',
|
||||
category: 'operations',
|
||||
icon: 'TextSearch',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['transactions', 'mapping_rules'],
|
||||
description: 'Matcha transaktioner med egna beskrivningar',
|
||||
longDescription:
|
||||
'Beskriv vad en transaktion gäller med egna ord och få smarta bokföringsförslag. Systemet lär sig av dina beskrivningar och applicerar automatiskt på framtida transaktioner från samma leverantör.',
|
||||
},
|
||||
{
|
||||
slug: 'enable-banking',
|
||||
name: 'Bankintegration (PSD2)',
|
||||
sector: 'general',
|
||||
category: 'import',
|
||||
icon: 'Landmark',
|
||||
dataPattern: 'manual',
|
||||
hasOwnData: true,
|
||||
description: 'Automatisk banktransaktionssynk via PSD2',
|
||||
longDescription:
|
||||
'Koppla ditt bankkonto direkt och synka transaktioner automatiskt via säker PSD2-bankintegration. Stöder de flesta svenska banker.',
|
||||
subscriptionNotice:
|
||||
'Denna integration kräver ett aktivt Enable Banking-abonnemang. Utan abonnemang kommer bankintegration inte att fungera.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Restaurant ───────────────────────────────────────────
|
||||
{
|
||||
slug: 'restaurant',
|
||||
name: 'Restaurang & Café',
|
||||
icon: 'UtensilsCrossed',
|
||||
description: 'Branschverktyg för restauranger och caféverksamhet',
|
||||
extensions: [
|
||||
{
|
||||
slug: 'food-cost',
|
||||
name: 'Food Cost %',
|
||||
sector: 'restaurant',
|
||||
category: 'reports',
|
||||
icon: 'ChefHat',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['journal_entry_lines'],
|
||||
description: 'Beräkna råvarukostnad i procent av omsättning',
|
||||
longDescription:
|
||||
'Beräkna och följ upp din råvarukostnadsprocent (food cost) automatiskt utifrån bokföringen. Jämför inköpskonton (4000-serien) mot livsmedelsomsättning (3000-serien) och se trender över tid.',
|
||||
},
|
||||
{
|
||||
slug: 'earnings-per-liter',
|
||||
name: 'Intäkt per liter alkohol',
|
||||
sector: 'restaurant',
|
||||
category: 'reports',
|
||||
icon: 'Wine',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['journal_entry_lines'],
|
||||
hasOwnData: true,
|
||||
description: 'Beräkna intäkt per såld liter alkohol',
|
||||
longDescription:
|
||||
'Kombinerar alkoholintäkter från bokföringen med manuellt inmatade literuppgifter för att räkna ut intäkt per liter. Följ trender och optimera ditt sortiment.',
|
||||
},
|
||||
{
|
||||
slug: 'pos-import',
|
||||
name: 'Kassa Z-rapport Import',
|
||||
sector: 'restaurant',
|
||||
category: 'import',
|
||||
icon: 'FileSpreadsheet',
|
||||
dataPattern: 'manual',
|
||||
hasOwnData: true,
|
||||
description: 'Importera Z-rapporter från kassasystem',
|
||||
longDescription:
|
||||
'Importera dagliga Z-rapporter från ditt kassasystem (CSV/Excel). Se daglig försäljningsstatistik, betalsättsfördelning och trender.',
|
||||
},
|
||||
{
|
||||
slug: 'tip-tracking',
|
||||
name: 'Dricksuppföljning',
|
||||
sector: 'restaurant',
|
||||
category: 'operations',
|
||||
icon: 'HandCoins',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['journal_entry_lines'],
|
||||
hasOwnData: true,
|
||||
description: 'Spåra dricks per skift och anställd',
|
||||
longDescription:
|
||||
'Registrera dricks per skift och anställd. Se totaler, snitt per anställd och dricks som andel av omsättningen.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Construction ─────────────────────────────────────────
|
||||
{
|
||||
slug: 'construction',
|
||||
name: 'Bygg & Anläggning',
|
||||
icon: 'HardHat',
|
||||
description: 'Branschverktyg för byggföretag och hantverkare',
|
||||
extensions: [
|
||||
{
|
||||
slug: 'rot-calculator',
|
||||
name: 'ROT-kalkylator',
|
||||
sector: 'construction',
|
||||
category: 'accounting',
|
||||
icon: 'Calculator',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['invoices'],
|
||||
hasOwnData: true,
|
||||
description: 'Beräkna ROT-avdrag per kund och jobb',
|
||||
longDescription:
|
||||
'Beräkna ROT-avdrag (30% av arbetskostnad, max 50 000 kr/år per kund). Håll koll på utnyttjad kvot per kund och undvik att överskrida maxbeloppet.',
|
||||
},
|
||||
{
|
||||
slug: 'project-cost',
|
||||
name: 'Projektkostnad',
|
||||
sector: 'construction',
|
||||
category: 'reports',
|
||||
icon: 'FolderKanban',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['journal_entry_lines', 'invoices'],
|
||||
hasOwnData: true,
|
||||
description: 'Följ upp kostnader och intäkter per byggprojekt',
|
||||
longDescription:
|
||||
'Samla alla kostnader och intäkter för varje byggprojekt. Se marginaler, jämför budget mot utfall och identifiera olönsamma projekt.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Hotel ────────────────────────────────────────────────
|
||||
{
|
||||
slug: 'hotel',
|
||||
name: 'Hotell & Logi',
|
||||
icon: 'Hotel',
|
||||
description: 'Branschverktyg för hotell och logi',
|
||||
extensions: [
|
||||
{
|
||||
slug: 'revpar',
|
||||
name: 'RevPAR',
|
||||
sector: 'hotel',
|
||||
category: 'reports',
|
||||
icon: 'TrendingUp',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['journal_entry_lines'],
|
||||
hasOwnData: true,
|
||||
description: 'Revenue Per Available Room',
|
||||
longDescription:
|
||||
'Beräkna Revenue Per Available Room (RevPAR) — det viktigaste nyckeltalet inom hotellbranschen. Kombinerar beläggningsgrad och snittpris per rum.',
|
||||
},
|
||||
{
|
||||
slug: 'occupancy',
|
||||
name: 'Beläggningsgrad',
|
||||
sector: 'hotel',
|
||||
category: 'reports',
|
||||
icon: 'BedDouble',
|
||||
dataPattern: 'manual',
|
||||
hasOwnData: true,
|
||||
description: 'Spåra beläggning och rumsstatus',
|
||||
longDescription:
|
||||
'Registrera daglig beläggning, tillgängliga rum och belagda rum. Se beläggningsgrad över tid och identifiera säsongsmönster.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Tech ─────────────────────────────────────────────────
|
||||
{
|
||||
slug: 'tech',
|
||||
name: 'IT & Konsulting',
|
||||
icon: 'Monitor',
|
||||
description: 'Branschverktyg för IT-konsulter och teknikföretag',
|
||||
extensions: [
|
||||
{
|
||||
slug: 'billable-hours',
|
||||
name: 'Debiterbar tid',
|
||||
sector: 'tech',
|
||||
category: 'reports',
|
||||
icon: 'Clock',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['invoices'],
|
||||
hasOwnData: true,
|
||||
description: 'Följ debiteringsgrad och effektiv timtaxa',
|
||||
longDescription:
|
||||
'Registrera arbetade timmar per projekt och beräkna debiteringsgrad (debiterbar/total tid). Se effektiv timtaxa och optimera din tidsanvändning.',
|
||||
},
|
||||
{
|
||||
slug: 'project-billing',
|
||||
name: 'Projektfakturering',
|
||||
sector: 'tech',
|
||||
category: 'reports',
|
||||
icon: 'ReceiptText',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['invoices', 'journal_entry_lines'],
|
||||
hasOwnData: true,
|
||||
description: 'Fakturerade belopp per projekt och kund',
|
||||
longDescription:
|
||||
'Följ fakturerade belopp per projekt och kund. Jämför mot budget, se olönsamma projekt och identifiera dina mest lönsamma kunder.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── E-commerce ───────────────────────────────────────────
|
||||
{
|
||||
slug: 'ecommerce',
|
||||
name: 'E-handel',
|
||||
icon: 'ShoppingCart',
|
||||
description: 'Branschverktyg för nätbutiker och e-handel',
|
||||
extensions: [
|
||||
{
|
||||
slug: 'shopify-import',
|
||||
name: 'Shopify-import',
|
||||
sector: 'ecommerce',
|
||||
category: 'import',
|
||||
icon: 'Store',
|
||||
dataPattern: 'manual',
|
||||
hasOwnData: true,
|
||||
description: 'Importera ordrar från Shopify',
|
||||
longDescription:
|
||||
'Importera orderdata från Shopify-export (CSV). Se intäkter per produkt, ordertrender och genomsnittligt ordervärde.',
|
||||
},
|
||||
{
|
||||
slug: 'multichannel-revenue',
|
||||
name: 'Flerkanalsintäkter',
|
||||
sector: 'ecommerce',
|
||||
category: 'reports',
|
||||
icon: 'BarChart3',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['journal_entry_lines'],
|
||||
hasOwnData: true,
|
||||
description: 'Intäktsanalys per försäljningskanal',
|
||||
longDescription:
|
||||
'Analysera intäkter fördelat på försäljningskanaler — webshop, marknadsplatser, fysisk butik. Identifiera dina mest lönsamma kanaler.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ── Export ──────────────────────────────────────────────
|
||||
{
|
||||
slug: 'export',
|
||||
name: 'Export & Utrikeshandel',
|
||||
icon: 'Ship',
|
||||
description: 'Branschverktyg för export och utrikeshandel',
|
||||
extensions: [
|
||||
{
|
||||
slug: 'eu-sales-list',
|
||||
name: 'Periodisk sammanställning',
|
||||
sector: 'export',
|
||||
category: 'accounting',
|
||||
icon: 'FileText',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['invoices', 'customers'],
|
||||
hasOwnData: false,
|
||||
description: 'Generera periodisk sammanställning (EC Sales List) för Skatteverket',
|
||||
longDescription:
|
||||
'Sammanställer automatiskt alla momsfria EU-försäljningar grupperat per kund och momsregistreringsnummer. Genererar nedladdningsbar fil (CSV/XML) för uppladdning till Skatteverket. Validerar kundernas VAT-nummer via VIES och flaggar saknade uppgifter. Korsvaliderar mot momsdeklarationens ruta 35 och 39.',
|
||||
},
|
||||
{
|
||||
slug: 'vat-monitor',
|
||||
name: 'Exportmoms-monitor',
|
||||
sector: 'export',
|
||||
category: 'reports',
|
||||
icon: 'Shield',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['journal_entry_lines', 'journal_entries', 'invoices'],
|
||||
hasOwnData: false,
|
||||
description: 'Övervaka momsbehandling för export och EU-handel',
|
||||
longDescription:
|
||||
'Visar intäkter uppdelat på inhemsk försäljning, EU B2B (reverse charge) och export utanför EU. Mappar automatiskt till rätt rutor i momsdeklarationen (ruta 05, 35, 36, 39, 40). Flaggar potentiella fel som saknat momsregistreringsnummer på EU-kunder eller felaktig momsbehandling.',
|
||||
},
|
||||
{
|
||||
slug: 'intrastat',
|
||||
name: 'Intrastat-generator',
|
||||
sector: 'export',
|
||||
category: 'accounting',
|
||||
icon: 'BarChart3',
|
||||
dataPattern: 'both',
|
||||
readsCoreTables: ['invoices', 'customers'],
|
||||
hasOwnData: true,
|
||||
description: 'Generera Intrastat-deklarationer för rapportering till SCB',
|
||||
longDescription:
|
||||
'Tagga produkter med CN-koder (Combined Nomenclature), vikt och ursprungsland. Genererar kompletta Intrastat-deklarationer i CSV-format för uppladdning till SCB:s IDEP.web. Övervakar tröskelvärdet på 12 MSEK för utförsel och varnar när rapporteringsskyldighet uppstår.',
|
||||
},
|
||||
{
|
||||
slug: 'currency-receivables',
|
||||
name: 'Valutafordringar',
|
||||
sector: 'export',
|
||||
category: 'reports',
|
||||
icon: 'TrendingUp',
|
||||
dataPattern: 'core',
|
||||
readsCoreTables: ['invoices', 'journal_entry_lines', 'transactions'],
|
||||
hasOwnData: false,
|
||||
description: 'Övervaka valutaexponering och orealiserade kursvinster/-förluster',
|
||||
longDescription:
|
||||
'Visar öppna kundfordringar per valuta med aktuellt SEK-värde baserat på Riksbankens dagskurser. Beräknar orealiserade valutakursvinster och -förluster. Visar realiserade kursdifferenser per period (konto 3960/7960). Ger en samlad bild av företagets valutarisk.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/** Full sectors with extensions merged from generated definitions */
|
||||
export const SECTORS: Sector[] = SECTOR_SHELLS.map(shell => ({
|
||||
...shell,
|
||||
extensions: EXTENSION_DEFINITIONS[shell.slug] ?? [],
|
||||
}))
|
||||
|
||||
// ============================================================
|
||||
// Helper functions
|
||||
// ============================================================
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { EntityType, RawTransaction, IngestResult } from '@/types'
|
||||
export type ExtensionCategory = 'accounting' | 'reports' | 'import' | 'operations'
|
||||
|
||||
/** Sector slugs for extension organization */
|
||||
export type SectorSlug = 'general' | 'restaurant' | 'construction' | 'hotel' | 'tech' | 'ecommerce' | 'export'
|
||||
export type SectorSlug = 'general'
|
||||
|
||||
/** How an extension gets its data */
|
||||
export type ExtensionDataPattern = 'core' | 'manual' | 'both'
|
||||
@@ -176,6 +176,7 @@ export interface Extension {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
sector?: SectorSlug
|
||||
|
||||
// Surfaces
|
||||
routes?: RouteDefinition[]
|
||||
@@ -188,6 +189,10 @@ export interface Extension {
|
||||
taxCodes?: TaxCodeDefinition[]
|
||||
dimensionTypes?: DimensionDefinition[]
|
||||
|
||||
/** Named services this extension provides to core via registry lookup */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
services?: Record<string, (...args: any[]) => Promise<any>>
|
||||
|
||||
// Lifecycle hooks
|
||||
onInstall?(ctx: ExtensionContext): Promise<void>
|
||||
onUninstall?(ctx: ExtensionContext): Promise<void>
|
||||
|
||||
@@ -1,49 +1,13 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { ComponentType } from 'react'
|
||||
import { WORKSPACES } from './_generated/workspace-map'
|
||||
|
||||
export interface WorkspaceComponentProps {
|
||||
userId: string
|
||||
}
|
||||
|
||||
type WorkspaceKey = `${string}/${string}`
|
||||
|
||||
const WORKSPACES: Record<WorkspaceKey, ComponentType<WorkspaceComponentProps>> = {
|
||||
// General
|
||||
'general/receipt-ocr': dynamic(() => import('@/components/extensions/general/ReceiptOcrWorkspace')),
|
||||
'general/ai-categorization': dynamic(() => import('@/components/extensions/general/AiCategorizationWorkspace')),
|
||||
'general/ai-chat': dynamic(() => import('@/components/extensions/general/AiChatWorkspace')),
|
||||
'general/push-notifications': dynamic(() => import('@/components/extensions/general/PushNotificationsWorkspace')),
|
||||
'general/invoice-inbox': dynamic(() => import('@/components/extensions/general/DocumentInboxWorkspace')),
|
||||
'general/calendar': dynamic(() => import('@/components/extensions/general/CalendarWorkspace')),
|
||||
'general/enable-banking': dynamic(() => import('@/components/extensions/general/EnableBankingWorkspace')),
|
||||
'general/user-description-match': dynamic(() => import('@/components/extensions/general/UserDescriptionMatchWorkspace')),
|
||||
// Restaurant
|
||||
'restaurant/food-cost': dynamic(() => import('@/components/extensions/restaurant/FoodCostWorkspace')),
|
||||
'restaurant/earnings-per-liter': dynamic(() => import('@/components/extensions/restaurant/EarningsPerLiterWorkspace')),
|
||||
'restaurant/pos-import': dynamic(() => import('@/components/extensions/restaurant/PosImportWorkspace')),
|
||||
'restaurant/tip-tracking': dynamic(() => import('@/components/extensions/restaurant/TipTrackingWorkspace')),
|
||||
// Construction
|
||||
'construction/rot-calculator': dynamic(() => import('@/components/extensions/construction/RotCalculatorWorkspace')),
|
||||
'construction/project-cost': dynamic(() => import('@/components/extensions/construction/ProjectCostWorkspace')),
|
||||
// Hotel
|
||||
'hotel/revpar': dynamic(() => import('@/components/extensions/hotel/RevparWorkspace')),
|
||||
'hotel/occupancy': dynamic(() => import('@/components/extensions/hotel/OccupancyWorkspace')),
|
||||
// Tech
|
||||
'tech/billable-hours': dynamic(() => import('@/components/extensions/tech/BillableHoursWorkspace')),
|
||||
'tech/project-billing': dynamic(() => import('@/components/extensions/tech/ProjectBillingWorkspace')),
|
||||
// E-commerce
|
||||
'ecommerce/shopify-import': dynamic(() => import('@/components/extensions/ecommerce/ShopifyImportWorkspace')),
|
||||
'ecommerce/multichannel-revenue': dynamic(() => import('@/components/extensions/ecommerce/MultichannelRevenueWorkspace')),
|
||||
// Export
|
||||
'export/eu-sales-list': dynamic(() => import('@/components/extensions/export/EuSalesListWorkspace')),
|
||||
'export/vat-monitor': dynamic(() => import('@/components/extensions/export/VatMonitorWorkspace')),
|
||||
'export/intrastat': dynamic(() => import('@/components/extensions/export/IntrastatWorkspace')),
|
||||
'export/currency-receivables': dynamic(() => import('@/components/extensions/export/CurrencyReceivablesWorkspace')),
|
||||
}
|
||||
|
||||
export function getWorkspaceComponent(
|
||||
sector: string,
|
||||
slug: string
|
||||
): ComponentType<WorkspaceComponentProps> | null {
|
||||
return WORKSPACES[`${sector}/${slug}` as WorkspaceKey] ?? null
|
||||
return WORKSPACES[`${sector}/${slug}`] ?? null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user