Add module detail page with toggle for enabling/disabling modules

- Delete scaffolded static sector route files that conflicted with dynamic routing
- Add Supabase migration for module_toggles table with RLS policies
- Extend modules-data.ts with longDesc field and getModuleBySlug helper
- Create ModuleToggle client component with optimistic upsert to Supabase
- Create [sector]/[module] detail page with description, badges, and toggle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Emil
2026-02-18 09:33:30 +01:00
co-authored by Claude Opus 4.6
parent 71edcfd550
commit f1733e2174
6 changed files with 1441 additions and 254 deletions
@@ -0,0 +1,145 @@
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { ArrowLeft, BookOpen, BarChart3, FileDown, Cog } from 'lucide-react'
import { getModuleBySlug, CATEGORY_LABELS, type ModuleCategory } from '@/lib/modules-data'
import { Badge } from '@/components/ui/badge'
import { ModuleToggle } from '@/components/modules/ModuleToggle'
const CATEGORY_META: Record<ModuleCategory, {
icon: typeof BookOpen
color: string
bgColor: string
borderColor: string
}> = {
bokforing: {
icon: BookOpen,
color: 'text-primary',
bgColor: 'bg-primary/8',
borderColor: 'border-primary/10',
},
rapport: {
icon: BarChart3,
color: 'text-accent',
bgColor: 'bg-accent/8',
borderColor: 'border-accent/10',
},
import: {
icon: FileDown,
color: 'text-success',
bgColor: 'bg-success/8',
borderColor: 'border-success/10',
},
operativ: {
icon: Cog,
color: 'text-muted-foreground',
bgColor: 'bg-muted/60',
borderColor: 'border-border',
},
}
export default async function ModuleDetailPage({
params,
}: {
params: Promise<{ sector: string; module: string }>
}) {
const { sector: sectorSlug, module: moduleSlug } = await params
const result = getModuleBySlug(sectorSlug, moduleSlug)
if (!result) {
notFound()
}
const { sector, module: mod } = result
const SectorIcon = sector.icon
const catMeta = CATEGORY_META[mod.cat]
const CatIcon = catMeta.icon
return (
<div className="space-y-8 stagger-enter">
{/* Navigation */}
<div>
<Link
href={`/modules/${sectorSlug}`}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="h-3.5 w-3.5" />
{sector.name}
</Link>
</div>
{/* Header */}
<div className="space-y-4">
<div className="flex items-start gap-5">
<div className={`flex items-center justify-center w-12 h-12 rounded-xl ${catMeta.bgColor} ring-1 ${catMeta.borderColor}`}>
<CatIcon className={`h-6 w-6 ${catMeta.color}`} strokeWidth={1.5} />
</div>
<div className="flex-1 min-w-0">
<h1 className="font-display text-2xl font-semibold tracking-tight">
{mod.name}
</h1>
<p className="text-muted-foreground mt-1">
{mod.desc}
</p>
</div>
</div>
{/* Badges */}
<div className="flex items-center gap-2">
<Badge
className={`${catMeta.bgColor} ${catMeta.color} border-transparent`}
>
{CATEGORY_LABELS[mod.cat]}
</Badge>
<Badge variant="secondary" className="gap-1.5">
<SectorIcon className="h-3 w-3" strokeWidth={1.5} />
{sector.name}
</Badge>
</div>
</div>
{/* Separator */}
<div className="h-px bg-border" />
{/* Long description */}
<div className="prose prose-sm max-w-none">
<div className="rounded-xl border border-border bg-card p-6 space-y-4">
{mod.longDesc.split('. Nyckelfunktioner: ').map((part, i) => {
if (i === 0) {
return (
<div key={i}>
<h2 className="text-base font-semibold text-foreground mt-0 mb-2">
Beskrivning
</h2>
<p className="text-sm text-muted-foreground leading-relaxed m-0">
{part}{mod.longDesc.includes('Nyckelfunktioner') ? '' : ''}
</p>
</div>
)
}
return (
<div key={i}>
<h2 className="text-base font-semibold text-foreground mt-0 mb-2">
Nyckelfunktioner
</h2>
<ul className="space-y-1.5 m-0 p-0 list-none">
{part.split('; ').map((feature, j) => (
<li
key={j}
className="flex items-start gap-2 text-sm text-muted-foreground"
>
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-primary/40 flex-shrink-0" />
{feature.replace(/,\s*$/, '')}
</li>
))}
</ul>
</div>
)
})}
</div>
</div>
{/* Toggle */}
<ModuleToggle sectorSlug={sectorSlug} moduleSlug={moduleSlug} />
</div>
)
}
+9 -93
View File
@@ -1,56 +1,10 @@
import Link from 'next/link'
import {
UtensilsCrossed,
BedDouble,
Monitor,
HardHat,
HeartPulse,
Store,
ShoppingCart,
Scissors,
Truck,
Scale,
GraduationCap,
Wheat,
Film,
Dumbbell,
Wrench,
UserPlus,
Factory,
Briefcase,
PartyPopper,
Building,
Layers,
ChevronRight,
} from 'lucide-react'
const sectors = [
{ name: 'Restaurang & Café', slug: 'restaurang', icon: UtensilsCrossed, description: 'Menyhantering, bordsbokning & kassa' },
{ name: 'Hotell & Boende', slug: 'hotell', icon: BedDouble, description: 'Rumsbokning, gäster & städschema' },
{ name: 'Tech & IT', slug: 'tech', icon: Monitor, description: 'Projekthantering, tid & SLA' },
{ name: 'Bygg & Entreprenad', slug: 'bygg', icon: HardHat, description: 'Projektkalkyler, underentreprenörer' },
{ name: 'Hälsa & Sjukvård', slug: 'halsa', icon: HeartPulse, description: 'Patientbokning, journal & recept' },
{ name: 'Detaljhandel', slug: 'detaljhandel', icon: Store, description: 'Lager, kassa & leverantörer' },
{ name: 'E-handel', slug: 'ehandel', icon: ShoppingCart, description: 'Ordrar, frakt & returer' },
{ name: 'Frisör & Skönhet', slug: 'frisor', icon: Scissors, description: 'Tidsbokning, kunder & produkter' },
{ name: 'Transport & Logistik', slug: 'transport', icon: Truck, description: 'Flotta, rutter & leveranser' },
{ name: 'Juridik & Redovisning', slug: 'juridik', icon: Scale, description: 'Ärenden, tidrapport & klienter' },
{ name: 'Utbildning & Förskola', slug: 'utbildning', icon: GraduationCap, description: 'Schema, elever & närvaro' },
{ name: 'Jordbruk & Livsmedel', slug: 'jordbruk', icon: Wheat, description: 'Skörd, djurhållning & spårbarhet' },
{ name: 'Media & Kommunikation', slug: 'media', icon: Film, description: 'Projekt, innehåll & publicering' },
{ name: 'Fitness & Sport', slug: 'fitness', icon: Dumbbell, description: 'Medlemskap, klasser & pass' },
{ name: 'Fordon & Verkstad', slug: 'fordon', icon: Wrench, description: 'Arbetsorder, delar & fordon' },
{ name: 'Bemanning & HR', slug: 'bemanning', icon: UserPlus, description: 'Kandidater, uppdrag & timmar' },
{ name: 'Tillverkning & Industri', slug: 'tillverkning', icon: Factory, description: 'Produktion, lager & kvalitet' },
{ name: 'Konsult & Rådgivning', slug: 'konsult', icon: Briefcase, description: 'Uppdrag, tid & debitering' },
{ name: 'Event & Underhållning', slug: 'event', icon: PartyPopper, description: 'Evenemang, biljetter & artister' },
{ name: 'Fastighetsförvaltning', slug: 'fastighet', icon: Building, description: 'Objekt, hyresgäster & underhåll' },
] as const
import { Layers } from 'lucide-react'
import { sectors } from '@/lib/modules-data'
export default function ModulesPage() {
return (
<div className="space-y-8">
{/* Page header */}
<div>
<h1 className="font-display text-2xl font-semibold tracking-tight">
Företagsmoduler
@@ -60,75 +14,37 @@ export default function ModulesPage() {
</p>
</div>
{/* Visa alla — inline as first item in the grid */}
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8 gap-1">
{/* Visa alla tile */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
<Link
href="/modules/alla"
className="group flex flex-col items-center gap-2 rounded-xl px-2 py-4 transition-all duration-200 hover:bg-accent/10"
className="group flex items-center gap-3.5 rounded-xl border border-accent/20 bg-accent/5 px-4 py-3.5 transition-all duration-200 hover:border-accent/40 hover:bg-accent/10"
>
<div className="flex items-center justify-center w-11 h-11 rounded-xl bg-accent/12 text-accent transition-all duration-200 group-hover:bg-accent/20 group-hover:scale-110">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-accent/12 text-accent shrink-0 transition-transform duration-200 group-hover:scale-105">
<Layers className="h-5 w-5" strokeWidth={1.5} />
</div>
<span className="text-[11px] font-medium text-accent text-center leading-tight">
<span className="text-sm font-medium text-accent truncate">
Visa alla
</span>
</Link>
{/* Sector tiles */}
{sectors.map((sector) => {
const Icon = sector.icon
return (
<Link
key={sector.slug}
href={`/modules/${sector.slug}`}
className="group relative flex flex-col items-center gap-2 rounded-xl px-2 py-4 transition-all duration-200 hover:bg-secondary/60"
className="group flex items-center gap-3.5 rounded-xl border border-border/50 bg-card px-4 py-3.5 transition-all duration-200 hover:border-border hover:bg-secondary/50 hover:shadow-sm"
>
<div className="flex items-center justify-center w-11 h-11 rounded-xl bg-muted/60 text-muted-foreground transition-all duration-200 group-hover:bg-primary/10 group-hover:text-primary group-hover:scale-110">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-muted/60 text-muted-foreground shrink-0 transition-all duration-200 group-hover:bg-primary/10 group-hover:text-primary group-hover:scale-105">
<Icon className="h-5 w-5" strokeWidth={1.5} />
</div>
<span className="text-[11px] font-medium text-muted-foreground text-center leading-tight transition-colors duration-200 group-hover:text-foreground">
<span className="text-sm font-medium text-foreground/80 truncate transition-colors duration-200 group-hover:text-foreground">
{sector.name}
</span>
{/* Hover tooltip with description */}
<div className="pointer-events-none absolute -bottom-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10">
<div className="whitespace-nowrap rounded-md bg-foreground px-2.5 py-1 text-[10px] text-background shadow-lg">
{sector.description}
</div>
</div>
</Link>
)
})}
</div>
{/* Compact list below for scanning */}
<div>
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-3">
Alla branscher
</p>
<div className="columns-1 sm:columns-2 lg:columns-3 gap-0">
{sectors.map((sector) => {
const Icon = sector.icon
return (
<Link
key={sector.slug}
href={`/modules/${sector.slug}`}
className="group flex items-center gap-2.5 py-2 px-2 -mx-2 rounded-lg transition-colors duration-150 hover:bg-secondary/50 break-inside-avoid"
>
<Icon className="h-3.5 w-3.5 text-muted-foreground/60 flex-shrink-0 transition-colors duration-150 group-hover:text-primary" strokeWidth={1.5} />
<span className="text-sm text-muted-foreground transition-colors duration-150 group-hover:text-foreground truncate">
{sector.name}
</span>
<span className="hidden sm:inline text-xs text-muted-foreground/50 truncate transition-colors duration-150 group-hover:text-muted-foreground">
{sector.description}
</span>
<ChevronRight className="h-3 w-3 text-muted-foreground/30 ml-auto flex-shrink-0 opacity-0 transition-all duration-150 group-hover:opacity-100" />
</Link>
)
})}
</div>
</div>
</div>
)
}