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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 }>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user