Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions src/app/(main)/ocia-sessions/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createClient } from '@/lib/supabase/server'
import { redirect, notFound } from 'next/navigation'
import { BreadcrumbSetter } from '@/components/breadcrumb-setter'
import { OciaSessionFormWrapper } from '../../ocia-session-form-wrapper'
import { getOciaSessionWithRelations } from '@/lib/actions/ocia-sessions'

interface PageProps {
params: Promise<{ id: string }>
}

export default async function EditOciaSessionPage({ params }: PageProps) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')

const { id } = await params
const ociaSession = await getOciaSessionWithRelations(id)
if (!ociaSession) notFound()

const breadcrumbs = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Our OCIA Sessions', href: '/ocia-sessions' },
{ label: ociaSession.name || 'View', href: `/ocia-sessions/${id}` },
{ label: 'Edit', href: `/ocia-sessions/${id}/edit` }
]

return (
<>
<BreadcrumbSetter breadcrumbs={breadcrumbs} />
<OciaSessionFormWrapper
ociaSession={ociaSession}
title="Edit OCIA Session"
description="Update OCIA session details and manage candidates in this session."
/>
</>
)
}
159 changes: 159 additions & 0 deletions src/app/(main)/ocia-sessions/[id]/ocia-session-view-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"use client"

import { OciaSessionWithRelations, updateOciaSession, deleteOciaSession } from '@/lib/actions/ocia-sessions'
import { ModuleViewContainer } from '@/components/module-view-container'
import { Button } from '@/components/ui/button'
import { ModuleStatusLabel } from '@/components/module-status-label'
import { Edit, Users } from 'lucide-react'
import Link from 'next/link'
import { PersonAvatarGroup } from '@/components/person-avatar-group'
import { useEnrichedEntityWithAvatars } from '@/hooks/use-enriched-entity-with-avatars'
import { useCallback } from 'react'

interface OciaSessionViewClientProps {
ociaSession: OciaSessionWithRelations
}

export function OciaSessionViewClient({ ociaSession }: OciaSessionViewClientProps) {
// Extract people (candidates) from entity
const getPeople = useCallback(
(session: OciaSessionWithRelations) => session.candidates || [],
[]
)

// Enrich entity with signed avatar URLs
const enrichEntity = useCallback(
(session: OciaSessionWithRelations, avatarUrls: Record<string, string>) => ({
...session,
candidates: session.candidates?.map(candidate => ({
...candidate,
avatar_url: candidate.id && avatarUrls[candidate.id]
? avatarUrls[candidate.id]
: candidate.avatar_url
}))
}),
[]
)

// Get entity enriched with signed avatar URLs
const enrichedOciaSession = useEnrichedEntityWithAvatars(
ociaSession,
getPeople,
enrichEntity
)

// Generate filename for downloads (placeholder for future feature)
const generateFilename = (extension: string) => {
const name = ociaSession.name.replace(/[^a-zA-Z0-9]/g, '-')
const date = ociaSession.ocia_event?.start_date || 'no-date'
return `${name}-${date}-ocia-session.${extension}`
}

// Generate action buttons
const actionButtons = (
<>
<Button asChild className="w-full">
<Link href={`/ocia-sessions/${ociaSession.id}/edit`}>
<Edit className="h-4 w-4 mr-2" />
Edit OCIA Session
</Link>
</Button>
</>
)

// Details section content
const detailsContent = (
<>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<span className="font-medium">Coordinator:</span>
<p className="text-sm text-muted-foreground">
{enrichedOciaSession.coordinator?.full_name || 'Not assigned'}
</p>
</div>

<div>
<span className="font-medium">Status:</span>
<div className="mt-1">
<ModuleStatusLabel status={enrichedOciaSession.status} statusType="module" />
</div>
</div>

{enrichedOciaSession.ocia_event && enrichedOciaSession.ocia_event.start_date && (
<>
<div>
<span className="font-medium">Session Date:</span>
<p className="text-sm text-muted-foreground">
{new Date(enrichedOciaSession.ocia_event.start_date).toLocaleDateString()}
{enrichedOciaSession.ocia_event.start_time && (
<> at {enrichedOciaSession.ocia_event.start_time}</>
)}
</p>
</div>

{enrichedOciaSession.ocia_event.location && (
<div>
<span className="font-medium">Location:</span>
<p className="text-sm text-muted-foreground">
{enrichedOciaSession.ocia_event.location.name}
</p>
</div>
)}
</>
)}

<div>
<span className="font-medium">Candidates:</span>
<p className="text-sm text-muted-foreground">
{enrichedOciaSession.candidates?.length || 0} candidate{enrichedOciaSession.candidates?.length !== 1 ? 's' : ''}
</p>
</div>

{enrichedOciaSession.candidates && enrichedOciaSession.candidates.length > 0 && (
<div className="lg:col-span-2">
<span className="font-medium block mb-2">Candidate Roster:</span>
<PersonAvatarGroup
people={enrichedOciaSession.candidates.map(candidate => ({
id: candidate.id,
first_name: candidate.first_name || '',
last_name: candidate.last_name || '',
full_name: candidate.full_name,
avatar_url: candidate.avatar_url
}))}
type="group"
maxDisplay={10}
size="md"
/>
</div>
)}
</div>

{enrichedOciaSession.note && (
<div className="pt-2 border-t">
<span className="font-medium">Notes:</span>
<p className="text-sm text-muted-foreground mt-1 whitespace-pre-wrap">{enrichedOciaSession.note}</p>
</div>
)}
</>
)

// Get candidate count for cascade delete message
const candidateCount = enrichedOciaSession.candidates?.length || 0

return (
<ModuleViewContainer
entity={enrichedOciaSession}
entityType="OCIA Session"
modulePath="ocia-sessions"
actionButtons={actionButtons}
details={detailsContent}
onDelete={deleteOciaSession}
cascadeDelete={{
label: 'Also delete all linked candidates',
description: `Delete this OCIA session and all ${candidateCount} linked candidate${candidateCount !== 1 ? 's' : ''}. The people records will be permanently deleted.`,
}}
>
{null}
</ModuleViewContainer>
)
}
39 changes: 39 additions & 0 deletions src/app/(main)/ocia-sessions/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createClient } from '@/lib/supabase/server'
import { redirect, notFound } from 'next/navigation'
import { PageContainer } from '@/components/page-container'
import { BreadcrumbSetter } from '@/components/breadcrumb-setter'
import { OciaSessionViewClient } from './ocia-session-view-client'
import { getOciaSessionWithRelations } from '@/lib/actions/ocia-sessions'

interface PageProps {
params: Promise<{ id: string }>
}

export default async function OciaSessionPage({ params }: PageProps) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')

const { id } = await params
const ociaSession = await getOciaSessionWithRelations(id)
if (!ociaSession) notFound()

// Build dynamic title from OCIA session data
const title = ociaSession.name

const breadcrumbs = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Our OCIA Sessions', href: '/ocia-sessions' },
{ label: 'View' }
]

return (
<PageContainer
title={title}
description="View OCIA session details and manage candidates."
>
<BreadcrumbSetter breadcrumbs={breadcrumbs} />
<OciaSessionViewClient ociaSession={ociaSession} />
</PageContainer>
)
}
98 changes: 98 additions & 0 deletions src/app/(main)/ocia-sessions/candidates-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client"

import { useState } from 'react'
import { addCandidateToSession, removeCandidateFromSession } from '@/lib/actions/ocia-sessions'
import type { OciaSessionWithRelations } from '@/lib/actions/ocia-sessions'
import { ListCard, CardListItem } from '@/components/list-card'
import { PeoplePicker } from '@/components/people-picker'
import { toast } from 'sonner'
import { useRouter } from 'next/navigation'
import Link from 'next/link'

interface CandidatesSectionProps {
ociaSession: OciaSessionWithRelations
}

export function CandidatesSection({ ociaSession }: CandidatesSectionProps) {
const router = useRouter()
const [showCandidatePicker, setShowCandidatePicker] = useState(false)

const handleAddCandidate = async (person: any) => {
try {
await addCandidateToSession(ociaSession.id, person.id)
toast.success('Candidate added to session')
router.refresh()
setShowCandidatePicker(false)
} catch (error: any) {
console.error('Failed to add candidate:', error)
toast.error(error.message || 'Failed to add candidate to session')
}
}

const handleRemoveCandidate = async (personId: string) => {
try {
await removeCandidateFromSession(personId)
toast.success('Candidate removed from session')
router.refresh()
} catch (error: any) {
console.error('Failed to remove candidate:', error)
toast.error(error.message || 'Failed to remove candidate from session')
}
}

return (
<>
<div data-testid="ocia-candidates-list">
<ListCard
title="Candidates in This Session"
description={`${ociaSession.candidates?.length || 0} ${ociaSession.candidates?.length === 1 ? 'candidate' : 'candidates'} in this OCIA session`}
items={ociaSession.candidates || []}
getItemId={(candidate) => candidate.id}
onAdd={() => setShowCandidatePicker(true)}
addButtonLabel="Add Candidate"
emptyMessage="No candidates in this session yet. Click 'Add Candidate' to add an existing person."
renderItem={(candidate) => {
const candidateName = candidate.full_name || 'No name assigned'

return (
<CardListItem
id={candidate.id}
onDelete={() => handleRemoveCandidate(candidate.id)}
deleteConfirmTitle="Remove Candidate from Session?"
deleteConfirmDescription={`Are you sure you want to remove ${candidateName} from this OCIA session? The person record will remain in the system and can be re-added to this or another session later.`}
deleteActionLabel="Remove from Session"
>
<div className="flex flex-col gap-1">
<Link
href={`/people/${candidate.id}`}
className="font-medium hover:underline"
onClick={(e) => e.stopPropagation()}
>
{candidateName}
</Link>
{candidate.email && (
<p className="text-sm text-muted-foreground">
{candidate.email}
</p>
)}
{candidate.phone_number && (
<p className="text-sm text-muted-foreground">
{candidate.phone_number}
</p>
)}
</div>
</CardListItem>
)
}}
/>
</div>

{/* Candidate Picker Dialog */}
<PeoplePicker
open={showCandidatePicker}
onOpenChange={setShowCandidatePicker}
onSelect={handleAddCandidate}
/>
</>
)
}
26 changes: 26 additions & 0 deletions src/app/(main)/ocia-sessions/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { BreadcrumbSetter } from '@/components/breadcrumb-setter'
import { OciaSessionFormWrapper } from '../ocia-session-form-wrapper'

export default async function CreateOciaSessionPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')

const breadcrumbs = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Our OCIA Sessions', href: '/ocia-sessions' },
{ label: 'Create', href: '/ocia-sessions/create' }
]

return (
<>
<BreadcrumbSetter breadcrumbs={breadcrumbs} />
<OciaSessionFormWrapper
title="Create OCIA Session"
description="Create a new OCIA session for managing candidates."
/>
</>
)
}
Loading