Files
hackerspace.sh/lib/actions/area-leads.ts
T
Moheeb ZaraandClaude Opus 4.8 2226f8f746 polish(ops): friendly not-found message on cross-space area-lead update
updateAreaLead used .single(), which errors (raw PostgREST message) rather than
returning null when the id isn't in the caller's space. Switch to .maybeSingle()
so the existing 'Area lead not found' guard is reached, matching deleteAreaLead.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-04 21:26:55 -07:00

158 lines
5.1 KiB
TypeScript

'use server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
import { requireMemberWithRole, parseInput, logActivity } from '@/lib/auth-helpers'
import { ADMIN_ROLES } from '@/lib/permissions'
import { upsertAreaLeadSchema, createAreaLeadSchema, updateAreaLeadSchema, uuidSchema } from '@/lib/validations'
export async function upsertAreaLead(formData: {
area_code: string
area_name: string
lead_id?: string
lead_handle?: string
status?: string
}) {
const v = parseInput(upsertAreaLeadSchema, formData)
if (!v.ok) return { error: v.error }
// area_code is the (space_id, area_code) upsert conflict target. Postgres
// treats NULLs as distinct in unique constraints, so a null/empty code
// would let duplicate area-lead rows accumulate. Require it.
if (!v.data.area_code || !v.data.area_code.trim()) {
return { error: 'An area code is required.' }
}
const supabase = await createClient()
const auth = await requireMemberWithRole(supabase, ADMIN_ROLES, 'Admin access required')
if (!auth.ok) return { error: auth.error }
const { member } = auth
// If a lead member is referenced, confirm they belong to this space (the
// row is space-scoped and RLS scopes reads, but reject a foreign id at the
// boundary for consistency with the rest of the codebase).
if (v.data.lead_id) {
const { data: lead } = await supabase
.from('space_members')
.select('id')
.eq('id', v.data.lead_id)
.eq('space_id', member.space_id)
.maybeSingle()
if (!lead) return { error: 'The selected lead is not a member of this space.' }
}
const { data, error } = await supabase
.from('area_leads')
.upsert(
{
space_id: member.space_id,
area_code: v.data.area_code,
area_name: v.data.area_name,
lead_id: v.data.lead_id ?? null,
lead_handle: v.data.lead_handle ?? null,
status: v.data.status,
updated_at: new Date().toISOString(),
},
{ onConflict: 'space_id,area_code' },
)
.select()
.single()
if (error) return { error: error.message }
revalidatePath('/ops')
return { data }
}
// ─── Ops area-lead roster (area_name-keyed) ──────────────────────────────────
// The Ops UI manages a plain roster of "who leads which area". These are a
// separate concern from upsertAreaLead's area_code-keyed role interface above;
// area_code is left null here.
export async function createAreaLead(formData: {
area_name: string
lead_handle: string
description?: string | null
}) {
const v = parseInput(createAreaLeadSchema, formData)
if (!v.ok) return { error: v.error }
const supabase = await createClient()
const auth = await requireMemberWithRole(supabase, ADMIN_ROLES, 'Admin access required')
if (!auth.ok) return { error: auth.error }
const { member } = auth
const { data, error } = await supabase
.from('area_leads')
.insert({
space_id: member.space_id,
area_name: v.data.area_name,
lead_handle: v.data.lead_handle,
description: v.data.description ?? null,
})
.select()
.single()
if (error) return { error: error.message }
await logActivity(supabase, member, 'created', 'area_lead', data.id as string)
revalidatePath('/ops')
return { data }
}
export async function updateAreaLead(
id: string,
updates: { area_name: string; lead_handle: string; description?: string | null },
) {
const v = parseInput(updateAreaLeadSchema, { id, ...updates })
if (!v.ok) return { error: v.error }
const supabase = await createClient()
const auth = await requireMemberWithRole(supabase, ADMIN_ROLES, 'Admin access required')
if (!auth.ok) return { error: auth.error }
const { member } = auth
const { data, error } = await supabase
.from('area_leads')
.update({
area_name: v.data.area_name,
lead_handle: v.data.lead_handle,
description: v.data.description ?? null,
})
.eq('id', v.data.id)
.eq('space_id', member.space_id)
.select()
.maybeSingle()
if (error) return { error: error.message }
// A cross-space / missing id matches 0 rows; surface the friendly message
// (maybeSingle returns null rather than erroring, unlike single()).
if (!data) return { error: 'Area lead not found' }
await logActivity(supabase, member, 'updated', 'area_lead', v.data.id)
revalidatePath('/ops')
return { data }
}
export async function deleteAreaLead(id: string) {
const v = parseInput(uuidSchema, id)
if (!v.ok) return { error: 'Invalid area lead ID' }
const supabase = await createClient()
const auth = await requireMemberWithRole(supabase, ADMIN_ROLES, 'Admin access required')
if (!auth.ok) return { error: auth.error }
const { member } = auth
const { error, count } = await supabase
.from('area_leads')
.delete({ count: 'exact' })
.eq('id', v.data)
.eq('space_id', member.space_id)
if (error) return { error: error.message }
if (count === 0) return { error: 'Area lead not found' }
await logActivity(supabase, member, 'deleted', 'area_lead', v.data)
revalidatePath('/ops')
return { success: true as const }
}