chore: clear audit backlog (types sync, email norm, docs, members shortcut)

Finishes all remaining tracked work from the full-app audit.

- types/database.ts synced: added the 8 missing tables (forum_threads,
  comments, space_tiers, space_invites, space_onboarding_steps,
  space_role_labels, space_custom_roles, space_member_custom_roles),
  the comment_entity_type enum (type + Constants), and the missing
  columns (space_members.tier_id/onboarding_completed_at/
  onboarding_progress, secrets.encrypted_value/encryption_version,
  knowledge_base.render_markdown).
- Email canonicalization: new emailField() helper (trim+lowercase)
  applied to signIn/signUp/addMember/updateMember/createContact and
  the import path; prevents case-variant duplicates and sign-in
  mismatches.
- Members directory: per-row "make area lead" picker (admin/board)
  assigns a member to any area-lead role; mirrors Customize.
- Docs refreshed and re-dated: DATABASE_SCHEMA.md (migrations 014-024
  summary), DB_SCHEMA_MAP.md (new-table quick map), API_REFERENCE.md
  (all server actions from migrations 016-024). schema.sql named as
  source of truth.

Backlog is now empty. 271/271 tests pass, build clean. No migration.
This commit is contained in:
Moheeb Zara
2026-05-15 19:01:52 -07:00
parent 86d44f6efc
commit 4c9b55147b
9 changed files with 232 additions and 20 deletions
+30 -2
View File
@@ -2,14 +2,18 @@
import { useState } from 'react'
import { Plus, X, ChevronDown } from 'lucide-react'
import { addMember, updateMember, approveMember, removeMember } from '@/lib/actions'
import { addMember, updateMember, approveMember, removeMember, assignAreaLead } from '@/lib/actions'
import { toast } from 'sonner'
import { useRouter } from 'next/navigation'
import type { Tables } from '@/types/database'
type Member = Tables<'space_members'>
type AreaLeadRole = { id: string; area_name: string; lead_id: string | null }
interface Props {
members: Member[]
currentRole: string
areaLeadRoles?: AreaLeadRole[]
}
const TIER_COLORS: Record<string, string> = {
@@ -21,8 +25,17 @@ const TIER_COLORS: Record<string, string> = {
const isAdmin = (role: string) => role === 'admin' || role === 'board'
export function MembersClient({ members: initialMembers, currentRole }: Props) {
export function MembersClient({ members: initialMembers, currentRole, areaLeadRoles = [] }: Props) {
const router = useRouter()
const [members, setMembers] = useState<Member[]>(initialMembers)
async function makeAreaLead(memberId: string, areaLeadRoleId: string) {
if (!areaLeadRoleId) return
const result = await assignAreaLead({ area_lead_role_id: areaLeadRoleId, member_id: memberId })
if (result.error) { toast.error(result.error); return }
toast.success('Assigned as area lead')
router.refresh()
}
const [activeTab, setActiveTab] = useState<'all' | 'payment_issues' | 'unverified' | 'inactive'>('all')
const [search, setSearch] = useState('')
const [tierFilter, setTierFilter] = useState('')
@@ -326,6 +339,21 @@ export function MembersClient({ members: initialMembers, currentRole }: Props) {
>
REMOVE
</button>
{areaLeadRoles.length > 0 && (
<select
defaultValue=""
onChange={e => { makeAreaLead(m.id, e.target.value); e.currentTarget.value = '' }}
className="font-mono text-[10px] border border-border px-1.5 py-0.5 rounded bg-background text-muted-foreground hover:border-primary hover:text-primary transition focus:outline-none"
title="Assign this member as an area lead"
>
<option value="">+ area lead</option>
{areaLeadRoles.map(r => (
<option key={r.id} value={r.id}>
{r.area_name}{r.lead_id ? ' (reassign)' : ' (vacant)'}
</option>
))}
</select>
)}
</div>
</td>
)}
+12
View File
@@ -57,10 +57,22 @@ export default async function MembersPage() {
.eq('space_id', self.space_id)
.order('joined_at')
// Area-lead roles, so admin/board can assign a lead straight from a member
// row (the same capability also lives in Customize -> Area leads).
const canManageLeads = self.role === 'admin' || self.role === 'board'
const { data: areaLeadRoles } = canManageLeads
? await supabase
.from('area_leads')
.select('id, area_name, lead_id')
.eq('space_id', self.space_id)
.order('area_name')
: { data: [] }
return (
<MembersClient
members={members ?? []}
currentRole={self.role}
areaLeadRoles={(areaLeadRoles ?? []) as Array<{ id: string; area_name: string; lead_id: string | null }>}
/>
)
}