import { createClient } from '@/lib/supabase/server' import Link from 'next/link' import { redirect } from 'next/navigation' import type { Tables } from '@/types/database' import { PageTitle } from '@/components/ui/page-title' import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' import { listSelfEntryDoors } from '@/lib/actions' import { DoorSelfEntry } from './door-self-entry' function IcoUsers({ className }: { className?: string }) { return } function IcoDues({ className }: { className?: string }) { return } function IcoTask({ className }: { className?: string }) { return } function IcoPay({ className }: { className?: string }) { return } function IcoPlus({ className }: { className?: string }) { return } function IcoDoor({ className }: { className?: string }) { return } export default async function DashboardPage() { const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) redirect('/login') const { data: member } = await supabase .from('space_members') .select('id, space_id, display_name, role') .eq('user_id', user.id) .in('status', ['current', 'unverified', 'late']) .maybeSingle() if (!member?.space_id) redirect('/signup') const spaceId = member.space_id const memberId = (member as { id: string }).id const isAdminOrBoard = member.role === 'admin' || member.role === 'board' const [ { count: activeMembers }, { count: openTasks }, { count: unlinkedPayments }, { data: tasks }, { data: projects }, { data: activity }, { data: openProposalsRaw }, { data: myVotesRaw }, { data: pendingIncidentsRaw }, ] = await Promise.all([ supabase.from('space_members').select('*', { count: 'exact', head: true }).eq('space_id', spaceId).eq('status', 'current'), supabase.from('tasks').select('*', { count: 'exact', head: true }).eq('space_id', spaceId).in('status', ['open', 'claimed', 'in_progress']), supabase.from('payments').select('*', { count: 'exact', head: true }).eq('space_id', spaceId).eq('link_status', 'unlinked'), supabase.from('tasks').select('*').eq('space_id', spaceId).in('status', ['open', 'claimed', 'in_progress', 'overdue', 'due_today', 'blocked']).order('due_date', { ascending: true, nullsFirst: false }).limit(5), supabase.from('projects').select('*').eq('space_id', spaceId).in('status', ['in_progress', 'blocked', 'review']).limit(3), supabase.from('activity_log').select('*').eq('space_id', spaceId).order('created_at', { ascending: false }).limit(6), supabase.from('proposals').select('*').eq('space_id', spaceId).eq('status', 'open').order('voting_closes_at', { ascending: true, nullsFirst: false }).limit(10), supabase.from('proposal_votes').select('proposal_id').eq('member_id', memberId), isAdminOrBoard ? supabase.from('incidents').select('*').eq('space_id', spaceId).in('status', ['received', 'under_review']).order('created_at', { ascending: false }).limit(5) : Promise.resolve({ data: [] as unknown[] }), ]) const votedProposalIds = new Set( ((myVotesRaw ?? []) as Array<{ proposal_id: string }>).map(v => v.proposal_id), ) type ProposalSummary = { id: string title: string voting_closes_at: string | null proposal_type: string quorum_required: number total_voters: number quorum_met: boolean | null } const pendingProposals = ((openProposalsRaw ?? []) as unknown as ProposalSummary[]) .filter(p => !votedProposalIds.has(p.id)) .slice(0, 5) const pendingIncidents = (pendingIncidentsRaw ?? []) as unknown as Array<{ id: string title: string severity: string status: string sla_response_by: string | null created_at: string }> const overdueTasks = (tasks ?? []).filter((t) => t.due_date && new Date(t.due_date) < new Date()) const stats = [ { label: 'Active Members', value: activeMembers ?? 0, Ico: IcoUsers, sub: 'in your space', warn: false }, { label: 'Dues Current', value: Math.max(0, (activeMembers ?? 0) - (unlinkedPayments ?? 0)), Ico: IcoDues, sub: `${unlinkedPayments ?? 0} unpaid / unverified`, warn: (unlinkedPayments ?? 0) > 0 }, { label: 'Open Tasks', value: openTasks ?? 0, Ico: IcoTask, sub: `${overdueTasks.length} overdue`, warn: (openTasks ?? 0) > 0 }, { label: 'Unlinked Payments', value: unlinkedPayments ?? 0, Ico: IcoPay, sub: 'needs reconciliation', warn: (unlinkedPayments ?? 0) > 0 }, ] // First-run: an admin/board member alone in a space with nothing set up yet // gets a guided checklist instead of a grid of zeros. const isFirstRun = isAdminOrBoard && (activeMembers ?? 0) <= 1 && (openTasks ?? 0) === 0 && (unlinkedPayments ?? 0) === 0 && (projects ?? []).length === 0 && (activity ?? []).length === 0 && pendingProposals.length === 0 && pendingIncidents.length === 0 if (isFirstRun) { const steps = [ { href: '/members', Ico: IcoUsers, title: 'Invite or add members', desc: 'Add people directly, or generate a shareable join link right there.' }, { href: '/customize', Ico: IcoPlus, title: 'Set up onboarding', desc: 'Decide what new members see and agree to when they join.' }, { href: '/tasks', Ico: IcoTask, title: 'Create your first task or chore', desc: 'Capture the recurring upkeep your space depends on.' }, { href: '/settings', Ico: IcoPay, title: 'Connect payments', desc: 'Link a payment platform to track dues and reconcile income.' }, ] const firstName = member.display_name?.split(' ')[0] || 'there' return (
Dashboard
Welcome, {firstName}. Let's set up your space. Your space is empty. These first steps get it running. This panel goes away once there is activity.
{steps.map(({ href, Ico, title, desc }) => ( {title} {desc} ))}
) } const selfEntryRes = await listSelfEntryDoors() const selfEntryDoors = ('data' in selfEntryRes ? selfEntryRes.data : []) as { id: string; name: string }[] return (
Dashboard Quick Task Task
{stats.map(({ label, value, Ico, sub, warn }) => (

{label}

{value}

{sub}

))}
{(pendingProposals.length > 0 || pendingIncidents.length > 0) && (
{pendingProposals.length > 0 && (

Open proposals awaiting your vote

All
{pendingProposals.map(p => { const totalCounted = p.total_voters const quorumPct = p.quorum_required > 0 ? Math.min(100, Math.round((totalCounted / p.quorum_required) * 100)) : 0 return (

{p.title}

{p.proposal_type.replace(/_/g, ' ')} {p.voting_closes_at ? ` · closes ${new Date(p.voting_closes_at).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}` : ''} {' · '} quorum {quorumPct}%

) })}
)} {isAdminOrBoard && pendingIncidents.length > 0 && (

Incidents needing response

All
{pendingIncidents.map(i => { const overdueSla = i.sla_response_by && new Date(i.sla_response_by) < new Date() return (

{i.title}

{i.severity} · {i.status.replace(/_/g, ' ')} {i.sla_response_by && ( <> {' · '} respond by {new Date(i.sla_response_by).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} )}

) })}
)}
)}

Quick Chores

All tasks
{(tasks ?? []).length > 0 ? (tasks ?? []).map((task) => { const isOverdue = task.due_date && new Date(task.due_date) < new Date() const isDueToday = task.due_date && new Date(task.due_date).toDateString() === new Date().toDateString() return (

{task.title}

{[task.area, task.recurrence && task.recurrence !== 'none' ? `${task.recurrence}` : null, task.due_date ? `due ${new Date(task.due_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` : null].filter(Boolean).join(' · ')}

{isOverdue ? ( OVERDUE ) : isDueToday ? ( TODAY ) : ( OPEN )} CLAIM
) }) : ( No open tasks Nothing needs doing right now. New chores will show up here. )}

Active Projects

All projects
{(projects ?? []).length > 0 ? (projects ?? []).map((project) => (

{project.title}

{project.status.replace(/_/g, ' ').toUpperCase()}
{project.description &&

{project.description}

}
)) : ( No active projects Projects in progress, blocked, or in review appear here. )}
{selfEntryDoors.length > 0 && (

Door access

)}

Payment Alerts

Reconcile

Connect a payment platform {' '}to see alerts here.

Recent Activity

{(activity ?? []).length > 0 ? (activity ?? []).map((item) => (

{item.display_name || 'System'}{' '}{item.action}

{new Date(item.created_at).toLocaleDateString()}

)) : ( No recent activity yet Actions taken in your space will be listed here. )}
) }