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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user