feat: integrate Grafana dashboards into BOC DashboardPage

- Add Infrastructure Health section with CPU/Memory/Disk panels
- Add Service Status section with PM2/Docker panels
- Create GrafanaPanel component for iframe embedding
- Build passes successfully
This commit is contained in:
Bernt
2026-07-29 19:03:06 +00:00
parent af874040ca
commit e5623d2f84
77 changed files with 11338 additions and 779 deletions
+202
View File
@@ -0,0 +1,202 @@
import { Badge } from '@/components/ui/Badge'
import { formatCurrency } from '@/lib/utils'
import {
AlertTriangle,
TrendingUp,
TrendingDown,
AlertCircle,
Briefcase,
Clock,
ArrowRight,
} from 'lucide-react'
interface BriefingCardProps {
title: string
value: string | number
subtitle?: string
icon?: React.ReactNode
trend?: 'up' | 'down' | 'neutral'
trendValue?: string
variant?: 'default' | 'success' | 'warning' | 'danger'
}
export function BriefingCard({
title,
value,
subtitle,
icon,
trend,
trendValue,
variant = 'default',
}: BriefingCardProps) {
const variantStyles = {
default: 'bg-surface border-border',
success: 'bg-success/5 border-success/20',
warning: 'bg-warning/5 border-warning/20',
danger: 'bg-danger/5 border-danger/20',
}
return (
<div className={`p-4 rounded-xl border ${variantStyles[variant]}`}>
<div className="flex items-start justify-between">
<div className="space-y-1">
<p className="text-sm text-text-secondary">{title}</p>
<p className="text-2xl font-semibold">{value}</p>
{subtitle && <p className="text-xs text-text-tertiary">{subtitle}</p>}
</div>
{icon && <div className="text-text-secondary">{icon}</div>}
</div>
{trend && (
<div className="flex items-center gap-1 mt-2">
{trend === 'up' && <TrendingUp size={14} className="text-success" />}
{trend === 'down' && <TrendingDown size={14} className="text-danger" />}
{trend === 'neutral' && <AlertCircle size={14} className="text-text-secondary" />}
<span className={`text-xs ${trend === 'up' ? 'text-success' : trend === 'down' ? 'text-danger' : 'text-text-secondary'}`}>
{trendValue}
</span>
</div>
)}
</div>
)
}
interface AlertCardProps {
alert: {
level: string
title: string
description: string
source: string
}
}
export function AlertCard({ alert }: AlertCardProps) {
const levelStyles = {
info: { icon: <AlertCircle size={16} />, color: 'text-primary bg-primary/5 border-primary/20' },
warning: { icon: <AlertTriangle size={16} />, color: 'text-warning bg-warning/5 border-warning/20' },
critical: { icon: <AlertTriangle size={16} />, color: 'text-danger bg-danger/5 border-danger/20' },
}
const style = levelStyles[alert.level as keyof typeof levelStyles] || levelStyles.info
return (
<div className={`p-3 rounded-lg border ${style.color}`}>
<div className="flex items-start gap-2">
{style.icon}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{alert.title}</p>
<p className="text-xs opacity-80 mt-0.5">{alert.description}</p>
</div>
<Badge variant="default" className="text-xs">{alert.source}</Badge>
</div>
</div>
)
}
interface ActionCardProps {
action: {
priority: number
title: string
description: string
impact: string
time_estimate: string
}
}
export function ActionCard({ action }: ActionCardProps) {
const priorityColors = {
10: 'bg-danger text-white',
9: 'bg-danger/80 text-white',
8: 'bg-warning text-white',
7: 'bg-warning/80 text-white',
6: 'bg-primary text-white',
5: 'bg-primary/80 text-white',
}
return (
<div className="p-3 rounded-lg border border-border bg-surface hover:bg-surface-hover transition-colors cursor-pointer group">
<div className="flex items-start gap-3">
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium shrink-0 ${priorityColors[action.priority as keyof typeof priorityColors] || 'bg-text-secondary text-white'}`}>
{action.priority}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium group-hover:text-primary transition-colors">{action.title}</p>
<p className="text-xs text-text-secondary mt-0.5">{action.description}</p>
<div className="flex items-center gap-3 mt-2">
<span className="text-xs text-text-tertiary flex items-center gap-1">
<Clock size={12} />
{action.time_estimate}
</span>
<span className="text-xs text-text-tertiary">{action.impact}</span>
</div>
</div>
<ArrowRight size={16} className="text-text-tertiary group-hover:text-primary transition-colors shrink-0" />
</div>
</div>
)
}
interface TaskCardProps {
task: {
id: string
title: string
priority: string
status: string
days_open: number
category: string
}
}
export function TaskCard({ task }: TaskCardProps) {
const priorityVariants = {
critical: 'danger',
high: 'warning',
medium: 'default',
low: 'success',
}
return (
<div className="flex items-center gap-3 p-2 rounded-lg hover:bg-surface-hover transition-colors">
<div className="w-2 h-2 rounded-full bg-warning shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm truncate">{task.title}</p>
<div className="flex items-center gap-2 mt-0.5">
<Badge variant={priorityVariants[task.priority as keyof typeof priorityVariants] as any} className="text-xs">
{task.priority}
</Badge>
<span className="text-xs text-text-tertiary">{task.category}</span>
</div>
</div>
<span className="text-xs text-text-tertiary shrink-0">{task.days_open}d</span>
</div>
)
}
interface DealCardProps {
deal: {
id: string
name: string
customer: string
value: number
currency: string
stage: string
probability: number
}
}
export function DealCard({ deal }: DealCardProps) {
return (
<div className="flex items-center gap-3 p-2 rounded-lg hover:bg-surface-hover transition-colors">
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
<Briefcase size={16} className="text-primary" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{deal.name}</p>
<p className="text-xs text-text-secondary">{deal.customer}</p>
</div>
<div className="text-right shrink-0">
<p className="text-sm font-medium">{formatCurrency(deal.value)}</p>
<p className="text-xs text-text-tertiary">{deal.probability}%</p>
</div>
</div>
)
}
+287
View File
@@ -0,0 +1,287 @@
import { useEffect, useState } from 'react'
import { briefingApi, type RealDailyBriefing } from '@/lib/briefingApi'
import { BriefingCard, AlertCard, ActionCard, TaskCard, DealCard } from '@/components/BriefingCard'
import { Card, CardHeader } from '@/components/ui/Card'
import { Badge } from '@/components/ui/Badge'
import { Skeleton } from '@/components/ui/Skeleton'
import { formatCurrency, formatNumber } from '@/lib/utils'
import {
DollarSign,
Users,
Briefcase,
AlertTriangle,
CheckCircle2,
Target,
} from 'lucide-react'
export function BriefingDashboard() {
const [briefing, setBriefing] = useState<RealDailyBriefing | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
async function fetchBriefing() {
try {
setLoading(true)
const data = await briefingApi.getRealBriefing()
setBriefing(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load briefing')
} finally {
setLoading(false)
}
}
fetchBriefing()
// Uppdatera var 5:e minut
const interval = setInterval(fetchBriefing, 300000)
return () => clearInterval(interval)
}, [])
if (loading) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-[120px]" />
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Skeleton className="h-[400px] lg:col-span-2" />
<Skeleton className="h-[400px]" />
</div>
</div>
)
}
if (error) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<AlertTriangle size={48} className="text-danger mx-auto mb-4" />
<p className="text-danger font-medium">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors"
>
Försök igen
</button>
</div>
</div>
)
}
if (!briefing) return null
const { company_health, my_tasks, my_deals, team_overview, financial_status, alerts, actions } = briefing
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Dagsöversikt</h1>
<p className="text-text-secondary">
{briefing.user.name} {new Date(briefing.generated_at).toLocaleDateString('sv-SE', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${company_health.status === 'healthy' ? 'bg-success' : company_health.status === 'warning' ? 'bg-warning' : 'bg-danger'}`} />
<span className="text-sm text-text-secondary capitalize">{company_health.status}</span>
</div>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<BriefingCard
title="Cash Position"
value={formatCurrency(company_health.cash_position)}
subtitle="Eget kapital"
icon={<DollarSign size={20} />}
trend={company_health.profit_margin > 0 ? 'up' : 'down'}
trendValue={`${formatNumber(company_health.profit_margin)}% marginal`}
variant={company_health.cash_position > 1000000 ? 'success' : company_health.cash_position > 500000 ? 'warning' : 'danger'}
/>
<BriefingCard
title="Pipeline"
value={formatCurrency(company_health.total_pipeline)}
subtitle={`${company_health.active_deals} aktiva deals`}
icon={<Briefcase size={20} />}
trend="up"
trendValue="+12% vs förra månaden"
/>
<BriefingCard
title="Team"
value={`${team_overview.active_now}/${team_overview.total_members}`}
subtitle={`${team_overview.on_leave} på leave`}
icon={<Users size={20} />}
trend={team_overview.on_leave > 0 ? 'neutral' : 'up'}
trendValue={team_overview.on_leave > 0 ? `${team_overview.on_leave} borta idag` : 'Alla närvarande'}
/>
<BriefingCard
title="Mina Tasks"
value={my_tasks.length}
subtitle={`${my_tasks.filter(t => t.priority === 'critical').length} kritiska`}
icon={<Target size={20} />}
variant={my_tasks.filter(t => t.priority === 'critical').length > 0 ? 'danger' : my_tasks.filter(t => t.priority === 'high').length > 0 ? 'warning' : 'success'}
/>
</div>
{/* Alerts */}
{alerts.length > 0 && (
<div className="space-y-2">
<h2 className="text-lg font-semibold flex items-center gap-2">
<AlertTriangle size={20} className="text-warning" />
Varningar
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{alerts.map((alert, i) => (
<AlertCard key={i} alert={alert} />
))}
</div>
</div>
)}
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Actions & Tasks */}
<div className="lg:col-span-2 space-y-6">
{/* Recommended Actions */}
{actions.length > 0 && (
<Card>
<CardHeader
title="Rekommenderade Åtgärder"
subtitle="Prioriterade baserat på din roll och aktuell data"
/>
<div className="p-4 space-y-3">
{actions.map((action, i) => (
<ActionCard key={i} action={action} />
))}
</div>
</Card>
)}
{/* My Tasks */}
<Card>
<CardHeader
title="Mina Uppgifter"
subtitle={`${my_tasks.length} öppna uppgifter`}
/>
<div className="p-4 space-y-1">
{my_tasks.length === 0 ? (
<div className="text-center py-8 text-text-secondary">
<CheckCircle2 size={48} className="mx-auto mb-2 text-success" />
<p>Alla uppgifter är klara!</p>
</div>
) : (
my_tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))
)}
</div>
</Card>
{/* My Deals */}
{my_deals.length > 0 && (
<Card>
<CardHeader
title="Mina Deals"
subtitle={`${my_deals.length} aktiva förhandlingar`}
/>
<div className="p-4 space-y-1">
{my_deals.map((deal) => (
<DealCard key={deal.id} deal={deal} />
))}
</div>
</Card>
)}
</div>
{/* Right Column - Team & Finance */}
<div className="space-y-6">
{/* Team Overview */}
<Card>
<CardHeader
title="Teamöversikt"
subtitle={`${team_overview.total_members} medlemmar`}
/>
<div className="p-4">
<div className="grid grid-cols-3 gap-2 mb-4">
<div className="text-center p-2 bg-success/5 rounded-lg">
<p className="text-2xl font-semibold text-success">{team_overview.active_now}</p>
<p className="text-xs text-text-secondary">Aktiva</p>
</div>
<div className="text-center p-2 bg-warning/5 rounded-lg">
<p className="text-2xl font-semibold text-warning">{team_overview.on_leave}</p>
<p className="text-xs text-text-secondary">Borta</p>
</div>
<div className="text-center p-2 bg-primary/5 rounded-lg">
<p className="text-2xl font-semibold text-primary">{team_overview.open_tickets}</p>
<p className="text-xs text-text-secondary">Tickets</p>
</div>
</div>
<div className="space-y-2">
{team_overview.members.slice(0, 5).map((member, i) => (
<div key={i} className="flex items-center gap-2 p-2 rounded-lg hover:bg-surface-hover transition-colors">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-medium text-primary">
{member.name.split(' ').map(n => n[0]).join('')}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{member.name}</p>
<p className="text-xs text-text-secondary">{member.role}</p>
</div>
{member.open_tasks > 0 && (
<Badge variant="warning" className="text-xs">{member.open_tasks}</Badge>
)}
</div>
))}
</div>
</div>
</Card>
{/* Financial Status */}
<Card>
<CardHeader
title="Finansiell Status"
subtitle="Från ledger i realtid"
/>
<div className="p-4 space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-text-secondary">Tillgångar</span>
<span className="text-sm font-medium">{formatCurrency(financial_status.total_assets)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-text-secondary">Skulder</span>
<span className="text-sm font-medium">{formatCurrency(financial_status.total_liabilities)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-text-secondary">Eget Kapital</span>
<span className="text-sm font-medium text-success">{formatCurrency(financial_status.equity)}</span>
</div>
{financial_status.runway_months > 0 && (
<div className="flex justify-between items-center">
<span className="text-sm text-text-secondary">Runway</span>
<span className="text-sm font-medium">{formatNumber(financial_status.runway_months)} mån</span>
</div>
)}
<div className="pt-2 border-t border-border">
<p className="text-xs text-text-tertiary mb-2">Top Konton</p>
{financial_status.top_accounts?.slice(0, 3).map((account, i) => (
<div key={i} className="flex justify-between items-center py-1">
<span className="text-xs text-text-secondary">{account.name}</span>
<span className="text-xs font-medium">{formatCurrency(account.amount)}</span>
</div>
))}
</div>
</div>
</Card>
</div>
</div>
</div>
)
}
+404
View File
@@ -0,0 +1,404 @@
import { useState } from 'react'
import { Button } from '@/components/ui/Button'
import { Card } from '@/components/ui/Card'
import { Badge } from '@/components/ui/Badge'
import {
CheckCircle2,
ArrowRight,
ArrowLeft,
Building2,
Users,
Settings,
Sparkles,
Briefcase,
} from 'lucide-react'
interface OnboardingStep {
id: string
title: string
description: string
icon: React.ElementType
component: React.ReactNode
}
interface OnboardingModalProps {
isOpen: boolean
onComplete: () => void
tenantName?: string
}
export function OnboardingModal({ isOpen, onComplete, tenantName = 'din verksamhet' }: OnboardingModalProps) {
const [currentStep, setCurrentStep] = useState(0)
const [completedSteps, setCompletedSteps] = useState<string[]>([])
const steps: OnboardingStep[] = [
{
id: 'welcome',
title: 'Välkommen till BOC',
description: `Låt oss konfigurera ${tenantName}`,
icon: Sparkles,
component: <WelcomeStep tenantName={tenantName} />,
},
{
id: 'company',
title: 'Företagsinformation',
description: 'Grundläggande uppgifter om verksamheten',
icon: Building2,
component: <CompanyStep />,
},
{
id: 'users',
title: 'Lägg till användare',
description: 'Bjud in teammedlemmar',
icon: Users,
component: <UsersStep />,
},
{
id: 'settings',
title: 'Inställningar',
description: 'Konfigurera bokföring och moms',
icon: Settings,
component: <SettingsStep />,
},
{
id: 'complete',
title: 'Klart!',
description: 'Du är redo att börja',
icon: CheckCircle2,
component: <CompleteStep onComplete={onComplete} />,
},
]
const handleNext = () => {
if (currentStep < steps.length - 1) {
setCompletedSteps([...completedSteps, steps[currentStep].id])
setCurrentStep(currentStep + 1)
}
}
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1)
}
}
const handleSkip = () => {
onComplete()
}
if (!isOpen) return null
const step = steps[currentStep]
const isLastStep = currentStep === steps.length - 1
const isFirstStep = currentStep === 0
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-2xl mx-4">
<Card className="overflow-hidden">
{/* Progress Header */}
<div className="bg-gradient-to-r from-primary to-primary-hover p-6 text-white">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<step.icon size={24} />
<span className="text-sm font-medium">Steg {currentStep + 1} av {steps.length}</span>
</div>
{!isLastStep && (
<button onClick={handleSkip} className="text-sm text-white/80 hover:text-white">
Hoppa över
</button>
)}
</div>
<h2 className="text-2xl font-bold mb-1">{step.title}</h2>
<p className="text-white/80">{step.description}</p>
{/* Progress Bar */}
<div className="flex items-center gap-2 mt-6">
{steps.map((s, i) => (
<div key={s.id} className="flex-1 flex items-center gap-2">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
i <= currentStep ? 'bg-white text-primary' : 'bg-white/20 text-white/60'
}`}>
{completedSteps.includes(s.id) ? (
<CheckCircle2 size={16} />
) : (
i + 1
)}
</div>
{i < steps.length - 1 && (
<div className={`flex-1 h-1 rounded-full transition-colors ${
i < currentStep ? 'bg-white' : 'bg-white/20'
}`} />
)}
</div>
))}
</div>
</div>
{/* Step Content */}
<div className="p-6 min-h-[300px]">
{step.component}
</div>
{/* Footer */}
<div className="p-6 border-t border-border flex items-center justify-between">
<Button
variant="secondary"
onClick={handleBack}
disabled={isFirstStep}
icon={<ArrowLeft size={16} />}
>
Tillbaka
</Button>
{isLastStep ? (
<Button onClick={onComplete} icon={<Sparkles size={16} />}>
Kom igång
</Button>
) : (
<Button onClick={handleNext} icon={<ArrowRight size={16} />}>
Nästa steg
</Button>
)}
</div>
</Card>
</div>
</div>
)
}
function WelcomeStep({ tenantName }: { tenantName: string }) {
return (
<div className="text-center space-y-4">
<div className="w-20 h-20 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto">
<Sparkles size={40} className="text-primary" />
</div>
<h3 className="text-xl font-semibold">Välkommen till Business Operations Center</h3>
<p className="text-text-secondary">
BOC är ditt kompletta affärssystem för att hantera {tenantName}.
Vi ska igenom några snabba steg för att komma igång.
</p>
<div className="grid grid-cols-2 gap-4 mt-6">
{[
{ icon: Building2, label: 'Företagsinfo', desc: 'Organisationsnummer, adress' },
{ icon: Users, label: 'Team', desc: 'Bjud in kollegor' },
{ icon: Briefcase, label: 'Bokföring', desc: 'BAS-kontoplan, moms' },
{ icon: Settings, label: 'Inställningar', desc: 'Valuta, språk' },
].map((item) => (
<div key={item.label} className="p-4 rounded-xl bg-surface border border-border">
<item.icon size={24} className="text-primary mb-2" />
<p className="font-medium text-sm">{item.label}</p>
<p className="text-xs text-text-secondary">{item.desc}</p>
</div>
))}
</div>
</div>
)
}
function CompanyStep() {
const [formData, setFormData] = useState({
orgNumber: '',
companyName: 'Landvex AB',
address: '',
city: '',
postalCode: '',
vatNumber: '',
})
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Företagsnamn</label>
<input
type="text"
value={formData.companyName}
onChange={(e) => setFormData({ ...formData, companyName: e.target.value })}
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Organisationsnummer</label>
<input
type="text"
placeholder="559141-7042"
value={formData.orgNumber}
onChange={(e) => setFormData({ ...formData, orgNumber: e.target.value })}
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Adress</label>
<input
type="text"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Postnummer</label>
<input
type="text"
value={formData.postalCode}
onChange={(e) => setFormData({ ...formData, postalCode: e.target.value })}
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Ort</label>
<input
type="text"
value={formData.city}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Momsregistreringsnummer</label>
<input
type="text"
placeholder="SE559141704201"
value={formData.vatNumber}
onChange={(e) => setFormData({ ...formData, vatNumber: e.target.value })}
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
</div>
)
}
function UsersStep() {
const [emails, setEmails] = useState([''])
const addEmail = () => setEmails([...emails, ''])
const updateEmail = (index: number, value: string) => {
const newEmails = [...emails]
newEmails[index] = value
setEmails(newEmails)
}
return (
<div className="space-y-4">
<p className="text-text-secondary">Bjud in dina teammedlemmar via e-post. De får en inbjudan att med i verksamheten.</p>
{emails.map((email, index) => (
<div key={index} className="flex items-center gap-2">
<input
type="email"
placeholder="namn@foretag.se"
value={email}
onChange={(e) => updateEmail(index, e.target.value)}
className="flex-1 h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
<select className="h-10 px-3 rounded-[14px] border bg-surface text-sm">
<option value="admin">Admin</option>
<option value="manager">Manager</option>
<option value="user">Användare</option>
</select>
</div>
))}
<Button variant="secondary" onClick={addEmail} className="w-full">
+ Lägg till fler
</Button>
<div className="p-4 bg-primary/5 rounded-xl border border-primary/20">
<p className="text-sm font-medium text-primary">💡 Tips</p>
<p className="text-xs text-text-secondary mt-1">
Du kan alltid bjuda in fler användare senare från HR-modulen.
</p>
</div>
</div>
)
}
function SettingsStep() {
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Valuta</label>
<select className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm">
<option value="SEK">SEK (Svenska kronor)</option>
<option value="EUR">EUR (Euro)</option>
<option value="USD">USD (US Dollar)</option>
<option value="NOK">NOK (Norska kronor)</option>
<option value="DKK">DKK (Danska kronor)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Bokföringsstandard</label>
<select className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm">
<option value="BAS2024">BAS 2024 (Sverige)</option>
<option value="BAS2023">BAS 2023 (Sverige)</option>
<option value="EU">EU-standard</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Momssats (%)</label>
<input
type="number"
defaultValue={25}
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Räkenskapsår start</label>
<input
type="date"
defaultValue="2026-01-01"
className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Språk</label>
<select className="w-full h-10 px-4 rounded-[14px] border bg-surface text-sm">
<option value="sv">Svenska</option>
<option value="en">English</option>
<option value="no">Norsk</option>
<option value="da">Dansk</option>
</select>
</div>
<div className="p-4 bg-warning/5 rounded-xl border border-warning/20">
<p className="text-sm font-medium text-warning"> Viktigt</p>
<p className="text-xs text-text-secondary mt-1">
Dessa inställningar påverkar hela verksamhetens bokföring. Kontrollera med din revisor om du är osäker.
</p>
</div>
</div>
)
}
function CompleteStep({ onComplete: _onComplete }: { onComplete: () => void }) {
return (
<div className="text-center space-y-4">
<div className="w-20 h-20 rounded-full bg-success/10 flex items-center justify-center mx-auto">
<CheckCircle2 size={40} className="text-success" />
</div>
<h3 className="text-xl font-semibold">Allt är klart!</h3>
<p className="text-text-secondary">
Din verksamhet är nu konfigurerad och du är redo att börja använda BOC.
</p>
<div className="grid grid-cols-3 gap-4 mt-6">
{[
{ label: 'Företag', status: 'Klart' },
{ label: 'Team', status: 'Klart' },
{ label: 'Inställningar', status: 'Klart' },
].map((item) => (
<div key={item.label} className="p-3 rounded-xl bg-surface border border-border">
<Badge variant="success" className="mb-2">{item.status}</Badge>
<p className="text-sm font-medium">{item.label}</p>
</div>
))}
</div>
</div>
)
}
+13 -3
View File
@@ -10,7 +10,9 @@ import {
User,
Settings,
ChevronDown,
Building2,
} from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { getInitials, formatRelativeTime } from '@/lib/utils'
import { cn } from '@/lib/utils'
@@ -41,6 +43,7 @@ const notifications = [
export function Header() {
const { user, logout } = useAuthStore()
const { toggleSidebar } = useUIStore()
const navigate = useNavigate()
const [notifOpen, setNotifOpen] = useState(false)
const [profileOpen, setProfileOpen] = useState(false)
@@ -177,13 +180,20 @@ export function Header() {
<p className="text-xs text-text-secondary">{user?.email || 'user@amos.com'}</p>
</div>
<div className="py-1">
<button className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-text-secondary hover:bg-bg transition-colors">
<button
onClick={() => { navigate('/profile'); setProfileOpen(false) }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-text-secondary hover:bg-bg transition-colors"
>
<User size={15} />
Profile
Min Profil
</button>
<button className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-text-secondary hover:bg-bg transition-colors">
<Building2 size={15} />
Byt Verksamhet
</button>
<button className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-text-secondary hover:bg-bg transition-colors">
<Settings size={15} />
Settings
Inställningar
</button>
<div className="border-t border-border/60 mt-1 pt-1">
<button
+3 -1
View File
@@ -13,11 +13,13 @@ import {
Zap,
ChevronLeft,
ChevronRight,
Newspaper,
} from 'lucide-react'
import { cn } from '@/lib/utils'
const navItems = [
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
{ path: '/', label: 'Briefing', icon: Newspaper },
{ path: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ path: '/crm', label: 'CRM', icon: Users },
{ path: '/sales', label: 'Sales', icon: TrendingUp },
{ path: '/finance', label: 'Finance', icon: Wallet },