Added extension functionality

This commit is contained in:
Emil
2026-02-21 11:39:45 +01:00
parent 2f83f232c6
commit 026497ed75
132 changed files with 3533 additions and 60 deletions
+21
View File
@@ -0,0 +1,21 @@
'use client'
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import type { ExtensionCategory } from '@/lib/extensions/types'
const CATEGORY_CONFIG: Record<ExtensionCategory, { label: string; className: string }> = {
accounting: { label: 'Bokföring & Skatt', className: 'bg-rose-100 text-rose-700 border-rose-200' },
reports: { label: 'Branschrapporter', className: 'bg-blue-100 text-blue-700 border-blue-200' },
import: { label: 'Smart Import', className: 'bg-emerald-100 text-emerald-700 border-emerald-200' },
operations: { label: 'Verktyg', className: 'bg-slate-100 text-slate-700 border-slate-200' },
}
export default function CategoryBadge({ category }: { category: ExtensionCategory }) {
const config = CATEGORY_CONFIG[category]
return (
<Badge variant="outline" className={cn('text-[10px] font-medium', config.className)}>
{config.label}
</Badge>
)
}
+44
View File
@@ -0,0 +1,44 @@
'use client'
import { Card, CardContent } from '@/components/ui/card'
import { resolveIcon } from '@/lib/extensions/icon-resolver'
import type { ExtensionDefinition } from '@/lib/extensions/types'
import CategoryBadge from './CategoryBadge'
import ExtensionToggleButton from './ExtensionToggleButton'
import Link from 'next/link'
export default function ExtensionCard({ extension }: { extension: ExtensionDefinition }) {
const Icon = resolveIcon(extension.icon)
return (
<Card className="group relative">
<CardContent className="pt-6">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3 min-w-0">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
<Icon className="h-5 w-5 text-primary" />
</div>
<div className="min-w-0">
<Link
href={`/extensions/${extension.sector}/${extension.slug}`}
className="text-sm font-medium hover:underline"
>
{extension.name}
</Link>
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
{extension.description}
</p>
<div className="mt-2">
<CategoryBadge category={extension.category} />
</div>
</div>
</div>
<ExtensionToggleButton
sectorSlug={extension.sector}
extensionSlug={extension.slug}
/>
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,23 @@
'use client'
import { Switch } from '@/components/ui/switch'
import { useExtensionToggle } from '@/lib/extensions/hooks'
export default function ExtensionToggleButton({
sectorSlug,
extensionSlug,
}: {
sectorSlug: string
extensionSlug: string
}) {
const { enabled, isLoading, toggle } = useExtensionToggle(sectorSlug, extensionSlug)
return (
<Switch
checked={enabled}
onCheckedChange={toggle}
disabled={isLoading}
aria-label={enabled ? 'Inaktivera tillägg' : 'Aktivera tillägg'}
/>
)
}
@@ -0,0 +1,33 @@
'use client'
import type { ExtensionDefinition } from '@/lib/extensions/types'
import { getWorkspaceComponent } from '@/lib/extensions/workspace-registry'
import ExtensionWorkspaceShell from './ExtensionWorkspaceShell'
import EmptyExtensionState from './shared/EmptyExtensionState'
export default function ExtensionWorkspaceLoader({
sector,
slug,
definition,
userId,
}: {
sector: string
slug: string
definition: ExtensionDefinition
userId: string
}) {
const WorkspaceComponent = getWorkspaceComponent(sector, slug)
return (
<ExtensionWorkspaceShell definition={definition}>
{WorkspaceComponent ? (
<WorkspaceComponent userId={userId} />
) : (
<EmptyExtensionState
title="Kommer snart"
description={`${definition.name} \u00e4r under utveckling och kommer snart att vara tillg\u00e4ngligt.`}
/>
)}
</ExtensionWorkspaceShell>
)
}
@@ -0,0 +1,51 @@
'use client'
import type { ExtensionDefinition } from '@/lib/extensions/types'
import { getSector } from '@/lib/extensions/sectors'
import { resolveIcon } from '@/lib/extensions/icon-resolver'
import Link from 'next/link'
export default function ExtensionWorkspaceShell({
definition,
children,
}: {
definition: ExtensionDefinition
children: React.ReactNode
}) {
const Icon = resolveIcon(definition.icon)
const sector = getSector(definition.sector)
return (
<div>
{/* Breadcrumb */}
<nav className="flex items-center gap-1.5 text-sm text-muted-foreground mb-6">
<Link href="/extensions" className="hover:text-foreground transition-colors">
Till\u00e4gg
</Link>
<span>/</span>
<Link
href={`/extensions/${definition.sector}`}
className="hover:text-foreground transition-colors"
>
{sector?.name ?? definition.sector}
</Link>
<span>/</span>
<span className="text-foreground">{definition.name}</span>
</nav>
{/* Header */}
<div className="flex items-start gap-4 mb-8">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 flex-shrink-0">
<Icon className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="text-xl font-semibold tracking-tight">{definition.name}</h1>
<p className="text-sm text-muted-foreground mt-0.5">{definition.description}</p>
</div>
</div>
{/* Extension content */}
{children}
</div>
)
}
+31
View File
@@ -0,0 +1,31 @@
import Link from 'next/link'
import { Card, CardContent } from '@/components/ui/card'
import { resolveIcon } from '@/lib/extensions/icon-resolver'
import type { Sector } from '@/lib/extensions/types'
export default function SectorCard({ sector }: { sector: Sector }) {
const Icon = resolveIcon(sector.icon)
return (
<Link href={`/extensions/${sector.slug}`}>
<Card className="group hover:border-primary/30 transition-colors cursor-pointer">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
<Icon className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="text-sm font-medium group-hover:text-primary transition-colors">
{sector.name}
</h3>
<p className="text-xs text-muted-foreground mt-0.5">{sector.description}</p>
<p className="text-xs text-muted-foreground mt-1.5">
{sector.extensions.length} tillägg
</p>
</div>
</div>
</CardContent>
</Card>
</Link>
)
}
@@ -0,0 +1,14 @@
'use client'
import { FolderKanban } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function ProjectCostWorkspace() {
return (
<EmptyExtensionState
title="Projektkostnadsuppföljning"
description="Uppföljning av kostnader per byggprojekt kommer snart. Du kommer kunna koppla fakturor och transaktioner till specifika projekt."
icon={<FolderKanban className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,14 @@
'use client'
import { Calculator } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function RotCalculatorWorkspace() {
return (
<EmptyExtensionState
title="ROT-avdragsberäkning"
description="Beräkning av ROT-avdrag för hantverkstjänster kommer snart. Du kommer kunna beräkna kundens avdrag och generera underlag till Skatteverket."
icon={<Calculator className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,14 @@
'use client'
import { BarChart3 } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function MultichannelRevenueWorkspace() {
return (
<EmptyExtensionState
title="Flerkanalintäkter"
description="Uppföljning av intäkter per försäljningskanal kommer snart. Du kommer kunna jämföra prestanda mellan webshop, marknadsplatser och fysisk butik."
icon={<BarChart3 className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,14 @@
'use client'
import { ShoppingBag } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function ShopifyImportWorkspace() {
return (
<EmptyExtensionState
title="Shopify-import"
description="Import av ordrar och transaktioner från Shopify kommer snart. Du kommer kunna synkronisera din Shopify-butik automatiskt."
icon={<ShoppingBag className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,15 @@
'use client'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
import { Sparkles } from 'lucide-react'
export default function AiCategorizationWorkspace({ userId }: WorkspaceComponentProps) {
return (
<EmptyExtensionState
title="AI-kategorisering"
description="AI-kategorisering k\u00f6rs automatiskt n\u00e4r nya transaktioner synkas. G\u00e5 till Transaktioner f\u00f6r att se f\u00f6rslag."
icon={<Sparkles className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,15 @@
'use client'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
import { MessageSquare } from 'lucide-react'
export default function AiChatWorkspace({ userId }: WorkspaceComponentProps) {
return (
<EmptyExtensionState
title="AI-assistent"
description="Anv\u00e4nd chattwidgeten i nedre h\u00f6gra h\u00f6rnet f\u00f6r att st\u00e4lla fr\u00e5gor om bokf\u00f6ring och skatt."
icon={<MessageSquare className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,15 @@
'use client'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
import { Landmark } from 'lucide-react'
export default function EnableBankingWorkspace({ userId }: WorkspaceComponentProps) {
return (
<EmptyExtensionState
title="Bankintegration (PSD2)"
description="Koppla ditt bankkonto under Inst\u00e4llningar f\u00f6r att synka transaktioner automatiskt."
icon={<Landmark className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,15 @@
'use client'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
import { Bell } from 'lucide-react'
export default function PushNotificationsWorkspace({ userId }: WorkspaceComponentProps) {
return (
<EmptyExtensionState
title="Push-notiser"
description="Konfigurera push-notiser under Inst\u00e4llningar. Notiser skickas automatiskt vid viktiga h\u00e4ndelser."
icon={<Bell className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,15 @@
'use client'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
import { Camera } from 'lucide-react'
export default function ReceiptOcrWorkspace({ userId }: WorkspaceComponentProps) {
return (
<EmptyExtensionState
title="Kvittoscanning"
description="Ladda upp och skanna kvitton direkt fr\u00e5n till\u00e4ggets arbetsyta. G\u00e5 till Kvitton i sidomenyn f\u00f6r att komma ig\u00e5ng."
icon={<Camera className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,14 @@
'use client'
import { DoorOpen } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function OccupancyWorkspace() {
return (
<EmptyExtensionState
title="Beläggningsgrad"
description="Uppföljning av rumsbeläggning kommer snart. Du kommer kunna registrera beläggning och se trender över tid."
icon={<DoorOpen className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,14 @@
'use client'
import { BedDouble } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function RevparWorkspace() {
return (
<EmptyExtensionState
title="RevPAR-beräkning"
description="Beräkning av intäkt per tillgängligt rum (RevPAR) kommer snart. Du kommer kunna följa upp RevPAR per dag, vecka och månad."
icon={<BedDouble className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,109 @@
'use client'
import { useState, useEffect } from 'react'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import KPICard from '@/components/extensions/shared/KPICard'
import DateRangeFilter from '@/components/extensions/shared/DateRangeFilter'
import DataEntryForm from '@/components/extensions/shared/DataEntryForm'
import ExtensionLoadingSkeleton from '@/components/extensions/shared/ExtensionLoadingSkeleton'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
export default function EarningsPerLiterWorkspace({ userId }: WorkspaceComponentProps) {
const [isLoading, setIsLoading] = useState(true)
const [earningsPerLiter, setEarningsPerLiter] = useState<number>(0)
const [totalLiters, setTotalLiters] = useState<number>(0)
const [totalRevenue, setTotalRevenue] = useState<number>(0)
// Data entry state
const [liters, setLiters] = useState('')
const [entryDate, setEntryDate] = useState(new Date().toISOString().slice(0, 10))
const [isSubmitting, setIsSubmitting] = useState(false)
const now = new Date()
const [dateRange, setDateRange] = useState({
start: new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10),
end: new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10),
})
useEffect(() => {
// In a real implementation, this would fetch liter entries and revenue data
setIsLoading(false)
}, [dateRange, userId])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// In a real implementation, this would save the liter entry via API
setIsSubmitting(false)
setLiters('')
}
if (isLoading) return <ExtensionLoadingSkeleton />
return (
<div className="space-y-6">
<DateRangeFilter
onRangeChange={(start, end) => setDateRange({ start, end })}
/>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<KPICard
label="Intäkt per liter"
value={earningsPerLiter.toLocaleString('sv-SE')}
suffix="kr/l"
/>
<KPICard
label="Totalt liter"
value={totalLiters.toLocaleString('sv-SE')}
suffix="l"
/>
<KPICard
label="Alkoholintäkter"
value={totalRevenue.toLocaleString('sv-SE')}
suffix="kr"
/>
</div>
<DataEntryForm
title="Registrera daglig literförsäljning"
onSubmit={handleSubmit}
submitLabel="Registrera"
isSubmitting={isSubmitting}
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="entry-date">Datum</Label>
<Input
id="entry-date"
type="date"
value={entryDate}
onChange={e => setEntryDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="liters">Antal liter</Label>
<Input
id="liters"
type="number"
step="0.01"
min="0"
placeholder="0.00"
value={liters}
onChange={e => setLiters(e.target.value)}
/>
</div>
</div>
</DataEntryForm>
<div className="rounded-xl border p-6">
<h3 className="text-sm font-semibold mb-4">Så fungerar det</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
Intäkt per liter beräknas genom att dividera alkoholintäkter med totalt antal sålda
liter. Registrera daglig literförsäljning ovan. Alkoholintäkter hämtas automatiskt
från bokföringen.
</p>
</div>
</div>
)
}
@@ -0,0 +1,64 @@
'use client'
import { useState, useEffect } from 'react'
import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry'
import KPICard from '@/components/extensions/shared/KPICard'
import DateRangeFilter from '@/components/extensions/shared/DateRangeFilter'
import ExtensionLoadingSkeleton from '@/components/extensions/shared/ExtensionLoadingSkeleton'
export default function FoodCostWorkspace({ userId }: WorkspaceComponentProps) {
const [isLoading, setIsLoading] = useState(true)
const [foodCost, setFoodCost] = useState<number>(0)
const [revenue, setRevenue] = useState<number>(0)
const [purchases, setPurchases] = useState<number>(0)
// Set initial date range to current month
const now = new Date()
const [dateRange, setDateRange] = useState({
start: new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10),
end: new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10),
})
useEffect(() => {
// In a real implementation, this would fetch journal_entry_lines
// and calculate food cost via the API
setIsLoading(false)
}, [dateRange, userId])
if (isLoading) return <ExtensionLoadingSkeleton />
return (
<div className="space-y-6">
<DateRangeFilter
onRangeChange={(start, end) => setDateRange({ start, end })}
/>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<KPICard
label="Food Cost %"
value={foodCost}
suffix="%"
/>
<KPICard
label="Varuinköp"
value={purchases.toLocaleString('sv-SE')}
suffix="kr"
/>
<KPICard
label="Livsmedelsintäkter"
value={revenue.toLocaleString('sv-SE')}
suffix="kr"
/>
</div>
<div className="rounded-xl border p-6">
<h3 className="text-sm font-semibold mb-4">Så fungerar det</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
Food Cost % beräknas automatiskt utifrån din bokföring. Varuinköp (konton 4000-4999)
divideras med livsmedelsintäkter (konton 3000-3999). En bra riktvärde för restauranger
är 25-35%.
</p>
</div>
</div>
)
}
@@ -0,0 +1,14 @@
'use client'
import { Receipt } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function PosImportWorkspace() {
return (
<EmptyExtensionState
title="Kassa Z-rapport import"
description="Stöd för import av Z-rapporter kommer snart. Du kommer kunna importera dagliga kassarapporter direkt från ditt kassasystem."
icon={<Receipt className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,14 @@
'use client'
import { HandCoins } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function TipTrackingWorkspace() {
return (
<EmptyExtensionState
title="Dricksuppföljning"
description="Registrering av dricks per skift kommer snart. Du kommer kunna följa upp dricksfördelning och bokföra det korrekt."
icon={<HandCoins className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,39 @@
'use client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface DataEntryFormProps {
title: string
onSubmit: (e: React.FormEvent) => void
submitLabel?: string
isSubmitting?: boolean
children: React.ReactNode
className?: string
}
export default function DataEntryForm({
title,
onSubmit,
submitLabel = 'Spara',
isSubmitting = false,
children,
className,
}: DataEntryFormProps) {
return (
<Card className={cn('', className)}>
<CardHeader>
<CardTitle className="text-base">{title}</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={onSubmit} className="space-y-4">
{children}
<Button type="submit" disabled={isSubmitting} size="sm">
{isSubmitting ? 'Sparar...' : submitLabel}
</Button>
</form>
</CardContent>
</Card>
)
}
@@ -0,0 +1,66 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
type Period = 'month' | 'quarter' | 'year' | 'custom'
interface DateRangeFilterProps {
onRangeChange: (start: string, end: string) => void
className?: string
}
export default function DateRangeFilter({ onRangeChange, className }: DateRangeFilterProps) {
const [activePeriod, setActivePeriod] = useState<Period>('month')
const now = new Date()
const handlePeriod = (period: Period) => {
setActivePeriod(period)
const year = now.getFullYear()
const month = now.getMonth()
switch (period) {
case 'month': {
const start = new Date(year, month, 1).toISOString().slice(0, 10)
const end = new Date(year, month + 1, 0).toISOString().slice(0, 10)
onRangeChange(start, end)
break
}
case 'quarter': {
const qStart = Math.floor(month / 3) * 3
const start = new Date(year, qStart, 1).toISOString().slice(0, 10)
const end = new Date(year, qStart + 3, 0).toISOString().slice(0, 10)
onRangeChange(start, end)
break
}
case 'year': {
onRangeChange(`${year}-01-01`, `${year}-12-31`)
break
}
}
}
const periods: { key: Period; label: string }[] = [
{ key: 'month', label: 'M\u00e5nad' },
{ key: 'quarter', label: 'Kvartal' },
{ key: 'year', label: '\u00c5r' },
]
return (
<div className={cn('flex items-center gap-1', className)}>
{periods.map(({ key, label }) => (
<Button
key={key}
variant={activePeriod === key ? 'default' : 'ghost'}
size="sm"
onClick={() => handlePeriod(key)}
className="text-xs"
>
{label}
</Button>
))}
</div>
)
}
@@ -0,0 +1,21 @@
import { Puzzle } from 'lucide-react'
interface EmptyExtensionStateProps {
title?: string
description?: string
icon?: React.ReactNode
}
export default function EmptyExtensionState({
title = 'Ingen data \u00e4nnu',
description = 'Data kommer att visas h\u00e4r n\u00e4r det finns tillg\u00e4ngligt.',
icon,
}: EmptyExtensionStateProps) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
{icon ?? <Puzzle className="h-12 w-12 text-muted-foreground/40 mb-4" />}
<h3 className="text-lg font-medium text-foreground">{title}</h3>
<p className="text-sm text-muted-foreground mt-1 max-w-md">{description}</p>
</div>
)
}
@@ -0,0 +1,23 @@
import { Skeleton } from '@/components/ui/skeleton'
export default function ExtensionLoadingSkeleton() {
return (
<div className="space-y-6">
{/* KPI cards skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[1, 2, 3].map(i => (
<div key={i} className="rounded-xl border p-6 space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-16" />
<Skeleton className="h-3 w-32" />
</div>
))}
</div>
{/* Content skeleton */}
<div className="rounded-xl border p-6 space-y-4">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-40 w-full" />
</div>
</div>
)
}
+34
View File
@@ -0,0 +1,34 @@
'use client'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
interface KPICardProps {
label: string
value: string | number
suffix?: string
trend?: { value: number; label: string }
className?: string
}
export default function KPICard({ label, value, suffix, trend, className }: KPICardProps) {
return (
<Card className={cn('', className)}>
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground">{label}</p>
<div className="flex items-baseline gap-1 mt-1">
<span className="text-2xl font-semibold tracking-tight">{value}</span>
{suffix && <span className="text-sm text-muted-foreground">{suffix}</span>}
</div>
{trend && (
<p className={cn(
'text-xs mt-1',
trend.value > 0 ? 'text-green-600' : trend.value < 0 ? 'text-red-600' : 'text-muted-foreground'
)}>
{trend.value > 0 ? '+' : ''}{trend.value}% {trend.label}
</p>
)}
</CardContent>
</Card>
)
}
@@ -0,0 +1,14 @@
'use client'
import { Clock } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function BillableHoursWorkspace() {
return (
<EmptyExtensionState
title="Debiterbar tid"
description="Tidsrapportering och uppföljning av debiterbara timmar kommer snart. Du kommer kunna logga tid per kund och projekt."
icon={<Clock className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}
@@ -0,0 +1,14 @@
'use client'
import { Layers } from 'lucide-react'
import EmptyExtensionState from '@/components/extensions/shared/EmptyExtensionState'
export default function ProjectBillingWorkspace() {
return (
<EmptyExtensionState
title="Projektfakturering"
description="Uppföljning av fakturering per kundprojekt kommer snart. Du kommer kunna koppla tidrapporter till fakturor automatiskt."
icon={<Layers className="h-12 w-12 text-muted-foreground/40 mb-4" />}
/>
)
}