Skip to content

Commit 6fddaf3

Browse files
authored
Merge pull request #328 from codeunia-dev/feat/company-team-roles-and-invite-flow
feat(company): Implement team member role system and pending invitation handling
2 parents df2d5f7 + ff12f08 commit 6fddaf3

17 files changed

Lines changed: 127 additions & 32 deletions

File tree

app/api/companies/[slug]/members/[userId]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export const runtime = 'nodejs'
1111

1212
// Validation schema for role update
1313
const updateRoleSchema = z.object({
14-
role: z.enum(['owner', 'admin', 'editor', 'member'], {
15-
errorMap: () => ({ message: 'Role must be owner, admin, editor, or member' }),
14+
role: z.enum(['owner', 'admin', 'editor', 'viewer'], {
15+
errorMap: () => ({ message: 'Role must be owner, admin, editor, or viewer' }),
1616
}),
1717
})
1818

app/api/companies/[slug]/members/invite/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export const runtime = 'nodejs'
1313
// Validation schema for invite request
1414
const inviteSchema = z.object({
1515
email: z.string().email('Invalid email address'),
16-
role: z.enum(['admin', 'editor', 'member'], {
17-
errorMap: () => ({ message: 'Role must be admin, editor, or member' }),
16+
role: z.enum(['admin', 'editor', 'viewer'], {
17+
errorMap: () => ({ message: 'Role must be admin, editor, or viewer' }),
1818
}),
1919
})
2020

app/dashboard/company/[slug]/accept-invitation/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export default function AcceptInvitationPage() {
199199
<>
200200
<div className="bg-zinc-800/50 border border-zinc-700 rounded-lg p-4">
201201
<p className="text-sm text-zinc-300 mb-2">
202-
You&apos;ve been invited to join <strong className="text-white">{companyName}</strong> on CodeUnia.
202+
You&apos;ve been invited to join <strong className="text-white">{companyName}</strong> on Codeunia.
203203
</p>
204204
<p className="text-sm text-zinc-400">
205205
By accepting this invitation, you&apos;ll be able to collaborate with the team and manage events.

app/dashboard/company/[slug]/analytics/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
99
import { Skeleton } from '@/components/ui/skeleton'
1010
import { AnalyticsCharts } from '@/components/dashboard/AnalyticsCharts'
1111
import { useCompanyContext } from '@/contexts/CompanyContext'
12+
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
1213
import { CompanyAnalytics } from '@/types/company'
1314
import { format, subDays, startOfMonth, endOfMonth, subMonths } from 'date-fns'
1415
import { CalendarIcon, AlertCircle, TrendingUp } from 'lucide-react'
@@ -25,6 +26,7 @@ export default function AnalyticsPage() {
2526
const params = useParams()
2627
const companySlug = params?.slug as string
2728
const { currentCompany, loading: companyLoading } = useCompanyContext()
29+
const isPendingInvitation = usePendingInvitationRedirect()
2830

2931
const [analytics, setAnalytics] = useState<CompanyAnalytics[]>([])
3032
const [loading, setLoading] = useState(true)
@@ -67,6 +69,14 @@ export default function AnalyticsPage() {
6769
}
6870
}, [currentCompany, fetchAnalytics])
6971

72+
if (companyLoading || isPendingInvitation) {
73+
return (
74+
<div className="flex items-center justify-center min-h-[60vh]">
75+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
76+
</div>
77+
)
78+
}
79+
7080
const handlePresetChange = (preset: PresetRange) => {
7181
setSelectedPreset(preset)
7282
const today = new Date()

app/dashboard/company/[slug]/events/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState, useEffect, useCallback } from 'react'
44
import { useCompanyContext } from '@/contexts/CompanyContext'
5+
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
56
import { Button } from '@/components/ui/button'
67
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
78
import { Badge } from '@/components/ui/badge'
@@ -25,6 +26,7 @@ import {
2526

2627
export default function CompanyEventsPage() {
2728
const { currentCompany, loading: companyLoading } = useCompanyContext()
29+
const isPendingInvitation = usePendingInvitationRedirect()
2830
const [events, setEvents] = useState<Event[]>([])
2931
const [loading, setLoading] = useState(true)
3032
const [searchTerm, setSearchTerm] = useState('')
@@ -58,6 +60,14 @@ export default function CompanyEventsPage() {
5860
}
5961
}, [currentCompany, fetchEvents])
6062

63+
if (companyLoading || isPendingInvitation) {
64+
return (
65+
<div className="flex items-center justify-center min-h-[60vh]">
66+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
67+
</div>
68+
)
69+
}
70+
6171
const filteredEvents = events.filter(event =>
6272
event.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
6373
event.category.toLowerCase().includes(searchTerm.toLowerCase())

app/dashboard/company/[slug]/hackathons/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState, useEffect, useCallback } from 'react'
44
import { useCompanyContext } from '@/contexts/CompanyContext'
5+
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
56
import { Button } from '@/components/ui/button'
67
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
78
import { Badge } from '@/components/ui/badge'
@@ -29,6 +30,7 @@ interface Hackathon {
2930

3031
export default function CompanyHackathonsPage() {
3132
const { currentCompany, loading: companyLoading } = useCompanyContext()
33+
const isPendingInvitation = usePendingInvitationRedirect()
3234
const [hackathons, setHackathons] = useState<Hackathon[]>([])
3335
const [loading, setLoading] = useState(true)
3436
const [searchTerm, setSearchTerm] = useState('')
@@ -61,6 +63,14 @@ export default function CompanyHackathonsPage() {
6163
}
6264
}, [currentCompany, fetchHackathons])
6365

66+
if (companyLoading || isPendingInvitation) {
67+
return (
68+
<div className="flex items-center justify-center min-h-[60vh]">
69+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
70+
</div>
71+
)
72+
}
73+
6474
const filteredHackathons = hackathons.filter(hackathon =>
6575
hackathon.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
6676
hackathon.category?.toLowerCase().includes(searchTerm.toLowerCase())

app/dashboard/company/[slug]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import React from 'react'
44
import { useCompanyContext } from '@/contexts/CompanyContext'
5+
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
56
import { Button } from '@/components/ui/button'
67
import { Badge } from '@/components/ui/badge'
78
import Link from 'next/link'
@@ -20,8 +21,9 @@ import { useSubscription } from '@/hooks/useSubscription'
2021
export default function CompanySlugDashboardPage() {
2122
const { currentCompany, userRole, loading, error } = useCompanyContext()
2223
const { usage } = useSubscription(currentCompany?.slug)
24+
const isPendingInvitation = usePendingInvitationRedirect()
2325

24-
if (loading) {
26+
if (loading || isPendingInvitation) {
2527
return (
2628
<div className="flex items-center justify-center min-h-[60vh]">
2729
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>

app/dashboard/company/[slug]/settings/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import React, { useState } from 'react'
44
import { useCompanyContext } from '@/contexts/CompanyContext'
5+
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
56
import { Button } from '@/components/ui/button'
67
import { Input } from '@/components/ui/input'
78
import { Label } from '@/components/ui/label'
@@ -34,6 +35,7 @@ import { useSubscription } from '@/hooks/useSubscription'
3435

3536
export default function CompanySettingsPage() {
3637
const { currentCompany, userRole, loading: contextLoading, refreshCompany } = useCompanyContext()
38+
const isPendingInvitation = usePendingInvitationRedirect()
3739
const { usage } = useSubscription(currentCompany?.slug)
3840
const { toast } = useToast()
3941
const [loading, setLoading] = useState(false)
@@ -103,6 +105,14 @@ export default function CompanySettingsPage() {
103105
}
104106
}, [currentCompany])
105107

108+
if (contextLoading || isPendingInvitation) {
109+
return (
110+
<div className="flex items-center justify-center min-h-[60vh]">
111+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
112+
</div>
113+
)
114+
}
115+
106116
const handleInputChange = (field: string, value: string) => {
107117
setFormData((prev) => ({ ...prev, [field]: value }))
108118
}

app/dashboard/company/[slug]/subscription/page.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,23 @@ export default async function SubscriptionPage({ params }: PageProps) {
4242
.select('role, status')
4343
.eq('company_id', company.id)
4444
.eq('user_id', user.id)
45-
.eq('status', 'active')
4645
.single()
4746

47+
// Redirect if no membership found
4848
if (!membership) {
4949
redirect('/dashboard/company')
5050
}
5151

52+
// Redirect pending invitations to accept page
53+
if (membership.status === 'pending') {
54+
redirect(`/dashboard/company/${slug}/accept-invitation`)
55+
}
56+
57+
// Only active members can access subscription
58+
if (membership.status !== 'active') {
59+
redirect('/dashboard/company')
60+
}
61+
5262
// Only owners and admins can manage subscription
5363
if (!['owner', 'admin'].includes(membership.role)) {
5464
redirect(`/dashboard/company/${slug}`)

app/dashboard/company/[slug]/team/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react'
44
import { useParams } from 'next/navigation'
55
import { TeamManagement } from '@/components/dashboard/TeamManagement'
66
import { useCompanyContext } from '@/contexts/CompanyContext'
7+
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
78
import { Skeleton } from '@/components/ui/skeleton'
89
import { Alert, AlertDescription } from '@/components/ui/alert'
910
import { AlertCircle } from 'lucide-react'
@@ -12,8 +13,9 @@ export default function TeamPage() {
1213
const params = useParams()
1314
const companySlug = params?.slug as string
1415
const { currentCompany, userRole, loading, error } = useCompanyContext()
16+
const isPendingInvitation = usePendingInvitationRedirect()
1517

16-
if (loading) {
18+
if (loading || isPendingInvitation) {
1719
return (
1820
<div className="container mx-auto py-8 px-4 max-w-7xl">
1921
<div className="space-y-6">

0 commit comments

Comments
 (0)