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 (
{value}
{sub}
Open proposals awaiting your vote
All{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}%
) })}Incidents needing response
All{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{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(' · ')}
Active Projects
All projects{project.title}
{project.status.replace(/_/g, ' ').toUpperCase()}{project.description}
}
Payment Alerts
ReconcileConnect a payment platform {' '}to see alerts here.
Recent Activity
{item.display_name || 'System'}{' '}{item.action}
{new Date(item.created_at).toLocaleDateString()}