Added extension functionality
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user