Fix/multiple company (#203)

* Refactor onboarding and dashboard logic; add silent team creation for users

- Removed unnecessary useCompany context in DashboardContent and SettingsSidebar components.
- Simplified onboarding setup logic to allow direct access to the dashboard for users without companies.
- Introduced WelcomeOnboarding component to handle user onboarding steps.
- Added migration to create silent teams for all users at signup, backfilling existing users without teams, and cleaning up incomplete companies.

* fix: update greeting logic and improve email handling in TIC extension

* Redirect to onboarding for users without companies and update onboarding flow

* Build issue fix

* Enhance onboarding experience by adding existing companies check
This commit is contained in:
Mattsson
2026-04-09 11:54:12 +02:00
committed by GitHub
parent dec9a37d2d
commit bf5a8d9195
24 changed files with 1101 additions and 1173 deletions
+70 -141
View File
@@ -10,7 +10,7 @@ import { switchCompany } from '@/lib/company/actions'
import { Check, ChevronsUpDown, Plus, Loader2 } from 'lucide-react'
export default function CompanySwitcher() {
const { company, companies, isTeamMember, team } = useCompany()
const { company, companies } = useCompany()
const router = useRouter()
const [open, setOpen] = useState(false)
const [isPending, startTransition] = useTransition()
@@ -43,7 +43,6 @@ export default function CompanySwitcher() {
// Update position when opening (run twice: once to render, once to measure)
useEffect(() => {
if (!open) return
// First frame: portal mounts, second frame: we can measure it
const raf = requestAnimationFrame(() => updatePosition())
return () => cancelAnimationFrame(raf)
}, [open, updatePosition])
@@ -88,124 +87,31 @@ export default function CompanySwitcher() {
})
}
const canOpen = companies.length > 0 || isTeamMember
// For team members: show team name as header, active company below
// For self-service: just show company name
if (team) {
return (
<div>
<button
ref={triggerRef}
onClick={() => canOpen && setOpen(!open)}
className={cn(
'flex flex-col w-full text-left rounded-lg transition-colors',
canOpen && 'hover:bg-muted/40 -mx-1 px-1 py-0.5',
)}
aria-expanded={open}
aria-haspopup="listbox"
>
<div className="flex items-center gap-1.5 w-full">
<p className="text-[13px] font-semibold text-foreground truncate tracking-[-0.01em] flex-1">
{team.name}
</p>
{canOpen && (
<ChevronsUpDown className="h-3 w-3 text-muted-foreground flex-shrink-0" />
)}
</div>
{company && (
<p className="text-[11px] text-muted-foreground truncate">
{company.name}
</p>
)}
</button>
{open && createPortal(
<div
ref={dropdownRef}
className="fixed min-w-56 w-max max-w-[calc(100vw-1rem)] bg-card border border-border/60 rounded-lg shadow-lg z-[60] py-1 animate-in fade-in slide-in-from-top-1 duration-150"
style={{ top: dropdownPos.top, left: dropdownPos.left }}
>
{companies.length > 0 && (
<>
<div className="px-2 py-1.5">
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-[0.08em] px-1.5">
Företag
</p>
</div>
<div className="max-h-48 overflow-y-auto px-1">
{companies.map(({ company: c, role }) => (
<button
key={c.id}
onClick={() => handleSwitch(c.id)}
disabled={isPending}
className={cn(
'flex items-center gap-2 w-full px-2.5 py-2 text-left text-[13px] leading-snug transition-colors rounded-md md:whitespace-nowrap',
c.id === company?.id
? 'text-foreground bg-muted/40'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40',
isPending && 'opacity-50',
)}
role="option"
aria-selected={c.id === company?.id}
>
<span className="flex-1 min-w-0">{c.name}</span>
{role !== 'owner' && (
<span className="text-[10px] text-muted-foreground/60 flex-shrink-0">
{role}
</span>
)}
{c.id === company?.id && (
<Check className="h-3.5 w-3.5 text-primary flex-shrink-0" />
)}
{isPending && c.id !== company?.id && (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground flex-shrink-0" />
)}
</button>
))}
</div>
</>
)}
<div className={cn(companies.length > 0 && 'border-t border-border/40 mt-1 pt-1', 'px-1')}>
<Link
href="/companies/new"
onClick={() => setOpen(false)}
className="flex items-center gap-2 px-2.5 py-2 text-[13px] text-muted-foreground hover:text-foreground hover:bg-muted/40 rounded-md transition-colors md:whitespace-nowrap"
>
<Plus className="h-3.5 w-3.5" />
Lägg till företag
</Link>
</div>
</div>,
document.body
)}
</div>
)
}
// Self-service user (no team) — simple company display
// Always allow opening the dropdown (to show "Lägg till företag")
const hasMultiple = companies.length > 1
const canOpen = companies.length > 0
// No companies yet — hide the switcher entirely
if (!company && companies.length === 0) {
return null
}
return (
<div>
<button
ref={triggerRef}
onClick={() => hasMultiple && setOpen(!open)}
className={cn(
'flex items-center gap-1.5 w-full text-left rounded-lg transition-colors',
hasMultiple && 'hover:bg-muted/40 -mx-1 px-1 py-0.5',
)}
onClick={() => setOpen(!open)}
className="flex items-center gap-1.5 w-full text-left rounded-lg border border-transparent hover:border-border/60 hover:bg-muted/40 -mx-1 px-2 py-1.5 transition-all duration-150"
aria-expanded={open}
aria-haspopup="listbox"
>
<p className="text-[13px] font-semibold text-foreground truncate tracking-[-0.01em] flex-1">
{company?.name || 'Min verksamhet'}
</p>
{hasMultiple && (
<ChevronsUpDown className="h-3 w-3 text-muted-foreground flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="text-[10px] text-muted-foreground/60 uppercase tracking-[0.06em] leading-none mb-1">Företag</p>
<p className="text-[13px] font-semibold text-foreground truncate tracking-[-0.01em]">
{company?.name || 'Min verksamhet'}
</p>
</div>
<ChevronsUpDown className="h-3 w-3 text-muted-foreground flex-shrink-0" />
</button>
{open && createPortal(
@@ -214,36 +120,59 @@ export default function CompanySwitcher() {
className="fixed min-w-56 w-max max-w-[calc(100vw-1rem)] bg-card border border-border/60 rounded-lg shadow-lg z-[60] py-1 animate-in fade-in slide-in-from-top-1 duration-150"
style={{ top: dropdownPos.top, left: dropdownPos.left }}
>
<div className="max-h-48 overflow-y-auto px-1">
{companies.map(({ company: c, role }) => (
<button
key={c.id}
onClick={() => handleSwitch(c.id)}
disabled={isPending}
className={cn(
'flex items-center gap-2 w-full px-2.5 py-2 text-left text-[13px] leading-snug transition-colors rounded-md md:whitespace-nowrap',
c.id === company?.id
? 'text-foreground bg-muted/40'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40',
isPending && 'opacity-50',
)}
role="option"
aria-selected={c.id === company?.id}
>
<span className="flex-1 min-w-0">{c.name}</span>
{role !== 'owner' && (
<span className="text-[10px] text-muted-foreground/60 flex-shrink-0">
{role}
</span>
)}
{c.id === company?.id && (
<Check className="h-3.5 w-3.5 text-primary flex-shrink-0" />
)}
{isPending && c.id !== company?.id && (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground flex-shrink-0" />
)}
</button>
))}
{companies.length > 0 && (
<>
{hasMultiple && (
<div className="px-2 py-1.5">
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-[0.08em] px-1.5">
Företag
</p>
</div>
)}
<div className="max-h-48 overflow-y-auto px-1">
{companies.map(({ company: c, role }) => (
<button
key={c.id}
onClick={() => handleSwitch(c.id)}
disabled={isPending}
className={cn(
'flex items-center gap-2 w-full px-2.5 py-2 text-left text-[13px] leading-snug transition-colors rounded-md md:whitespace-nowrap',
c.id === company?.id
? 'text-foreground bg-muted/40'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40',
isPending && 'opacity-50',
)}
role="option"
aria-selected={c.id === company?.id}
>
<span className="flex-1 min-w-0">{c.name}</span>
{role !== 'owner' && (
<span className="text-[10px] text-muted-foreground/60 flex-shrink-0">
{role}
</span>
)}
{c.id === company?.id && (
<Check className="h-3.5 w-3.5 text-primary flex-shrink-0" />
)}
{isPending && c.id !== company?.id && (
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground flex-shrink-0" />
)}
</button>
))}
</div>
</>
)}
<div className={cn(companies.length > 0 && 'border-t border-border/40 mt-1 pt-1', 'px-1')}>
<Link
href="/onboarding"
onClick={() => setOpen(false)}
className="flex items-center gap-2 px-2.5 py-2 text-[13px] text-muted-foreground hover:text-foreground hover:bg-muted/40 rounded-md transition-colors md:whitespace-nowrap"
>
<Plus className="h-3.5 w-3.5" />
Lägg till företag
</Link>
</div>
</div>,
document.body