'use client' import { useState, useMemo } from 'react' import { Plus, Search, Lock, Users2 } from 'lucide-react' import { createClient } from '@/lib/supabase/client' import { toast } from 'sonner' import { PageTitle } from '@/components/ui/page-title' import { KbModal } from './components/kb-modal' import { AddSecretModal } from './components/secret-modal' import { AreaLeadModal } from './components/area-lead-modal' import { KbPanel } from './panels/kb-panel' import { SecretsPanel } from './panels/secrets-panel' import { AreaLeadsPanel } from './panels/area-leads-panel' import { TABS } from './types' import type { OpsClientProps, Tab, KbEntry, AreaLead, Secret } from './types' // ─── Main Component ──────────────────────────────────────────────────────────── export function OpsClient({ spaceId, kbEntries: initial, areaLeads: initialLeads, secrets: initialSecrets, canSeeSecrets, canManageAcl = false, aclRoleOptions = [], aclByEntity = {} }: OpsClientProps) { const [activeTab, setActiveTab] = useState('kb') const [search, setSearch] = useState('') const [kbEntries, setKbEntries] = useState(initial) const [areaLeads, setAreaLeads] = useState(initialLeads) const [secrets, setSecrets] = useState(initialSecrets) // Modals const [showKbModal, setShowKbModal] = useState(false) const [editingKb, setEditingKb] = useState(null) const [showSecretModal, setShowSecretModal] = useState(false) const [showLeadModal, setShowLeadModal] = useState(false) const [editingLead, setEditingLead] = useState(null) // Every predicate is null-safe: title/content/area/handle can all be null // in the database. These memos recompute on every keystroke of the shared // `search` state regardless of the active tab, so an unguarded // .toLowerCase() on a null field throws and takes the whole page down // (this is why KB search appeared broken: filteredLeads referenced a // non-existent `member_name` column). const has = (v: string | null | undefined, q: string) => !!v && v.toLowerCase().includes(q) const filteredKb = useMemo(() => { const q = search.toLowerCase() return kbEntries.filter(e => !e.tags?.includes('process') && (!q || has(e.title, q) || has(e.content, q) || has(e.area, q)), ) }, [kbEntries, search]) const filteredProcesses = useMemo(() => { const q = search.toLowerCase() return kbEntries.filter(e => e.tags?.includes('process') && (!q || has(e.title, q) || has(e.content, q) || has(e.area, q)), ) }, [kbEntries, search]) const filteredSecrets = useMemo(() => { const q = search.toLowerCase() return secrets.filter(s => !q || has(s.title, q) || has(s.label, q) || has(s.area, q)) }, [secrets, search]) const filteredLeads = useMemo(() => { const q = search.toLowerCase() return areaLeads.filter(l => !q || has(l.area_name, q) || has(l.lead_handle, q)) }, [areaLeads, search]) async function deleteLeadFn(id: string) { const supabase = createClient() const { error } = await supabase.from('area_leads').delete().eq('id', id) if (error) { toast.error(error.message); return } setAreaLeads(prev => prev.filter(l => l.id !== id)) toast.success('Area lead removed') } return (
{/* Header */}
Ops & Facilities
{(activeTab === 'kb' || activeTab === 'processes') && ( )} {activeTab === 'secrets' && canSeeSecrets && ( )} {activeTab === 'area-leads' && ( )}
{/* Tabs */}
{TABS.map(tab => ( ))}
{/* Search */} {(activeTab === 'kb' || activeTab === 'processes' || activeTab === 'secrets') && (
setSearch(e.target.value)} placeholder={activeTab === 'kb' ? 'Search knowledge base...' : activeTab === 'processes' ? 'Search processes...' : 'Search credentials...'} className="w-full bg-card border border-border rounded pl-9 pr-4 py-2.5 font-sans text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary transition" />
)} {/* ─── KB Tab ─── */} {activeTab === 'kb' && ( { setEditingKb(null); setShowKbModal(true) }} onEdit={e => { setEditingKb(e); setShowKbModal(true) }} onDelete={id => setKbEntries(prev => prev.filter(e => e.id !== id))} canManageAcl={canManageAcl} aclRoleOptions={aclRoleOptions} aclByEntity={aclByEntity} /> )} {/* ─── Processes Tab ─── */} {activeTab === 'processes' && ( { setEditingKb(null); setShowKbModal(true) }} onEdit={e => { setEditingKb(e); setShowKbModal(true) }} onDelete={id => setKbEntries(prev => prev.filter(e => e.id !== id))} canManageAcl={canManageAcl} aclRoleOptions={aclRoleOptions} aclByEntity={aclByEntity} /> )} {/* ─── Secrets Tab ─── */} {activeTab === 'secrets' && ( setShowSecretModal(true)} onDelete={id => setSecrets(prev => prev.filter(x => x.id !== id))} canManageAcl={canManageAcl} aclRoleOptions={aclRoleOptions} aclByEntity={aclByEntity} /> )} {/* ─── Area Leads Tab ─── */} {activeTab === 'area-leads' && ( { setEditingLead(null); setShowLeadModal(true) }} onEdit={lead => { setEditingLead(lead); setShowLeadModal(true) }} onDelete={deleteLeadFn} /> )}
{/* ─── Modals ─── */} {showKbModal && ( { setShowKbModal(false); setEditingKb(null) }} onSaved={saved => { setKbEntries(prev => editingKb ? prev.map(e => e.id === saved.id ? saved : e) : [saved, ...prev], ) }} /> )} {showSecretModal && ( setShowSecretModal(false)} onSaved={s => setSecrets(prev => [s, ...prev])} spaceId={spaceId} /> )} {showLeadModal && ( { setShowLeadModal(false); setEditingLead(null) }} onSaved={saved => { setAreaLeads(prev => editingLead ? prev.map(l => l.id === saved.id ? saved : l) : [...prev, saved].sort((a, b) => a.area_name.localeCompare(b.area_name)), ) }} spaceId={spaceId} /> )}
) }