'use client' import { useRouter } from 'next/navigation' import { useEffect, useMemo, useRef, useState } from 'react' import { useAuth } from '@/lib/auth' const statusOptions = [ 'Intake', 'Writing', 'Review', 'Final QA', 'Submitted', 'Active', 'Draft', 'In Progress', 'Under Review', 'Won', 'Lost', 'No Bid', ] type RfpRow = { id: string title: string agency?: string | null status: string submissionDeadline: string lastFollowedUp?: string | null notes?: string | null partnerSelections?: string[] } type PartnerOption = { id: string name: string } export default function RFPStatusTable({ rfps, partners }: { rfps: RfpRow[]; partners: PartnerOption[] }) { const router = useRouter() const { user } = useAuth() const [updatingId, setUpdatingId] = useState(null) const [editingNotes, setEditingNotes] = useState<{ [key: string]: string }>({}) const [updatingPartnerId, setUpdatingPartnerId] = useState(null) const [updatingFollowUpId, setUpdatingFollowUpId] = useState(null) // Filter state const [statusFilter, setStatusFilter] = useState('all') const [partnerFilter, setPartnerFilter] = useState('all') // Partner dropdown open state const [openPartnerDropdown, setOpenPartnerDropdown] = useState(null) // Track if we're doing a local update (don't sync from props) const skipNextSync = useRef(false) // Close partner dropdown when clicking outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (openPartnerDropdown && !(e.target as Element).closest('.partner-dropdown-container')) { setOpenPartnerDropdown(null) } } document.addEventListener('click', handleClickOutside) return () => document.removeEventListener('click', handleClickOutside) }, [openPartnerDropdown]) // Track follow-up dates locally const [followUpMap, setFollowUpMap] = useState>(() => { const initial: Record = {} rfps.forEach((r) => { initial[r.id] = r.lastFollowedUp || null }) return initial }) const [statusMap, setStatusMap] = useState>(() => { const initial: Record = {} rfps.forEach((r) => { initial[r.id] = r.status }) return initial }) // Keep local partner selections so the UI reflects changes immediately const [partnerSelectionsMap, setPartnerSelectionsMap] = useState>(() => { const initial: Record = {} rfps.forEach((r) => { const list = r.partnerSelections && r.partnerSelections.length ? r.partnerSelections : ['Own'] initial[r.id] = list }) return initial }) // Sync state when rfps props change (after router.refresh()) - but only for NEW RFPs useEffect(() => { if (skipNextSync.current) { skipNextSync.current = false return } const newFollowUpMap: Record = {} const newStatusMap: Record = {} const newPartnerMap: Record = {} rfps.forEach((r) => { // Preserve local state if we already have it, otherwise use prop value newFollowUpMap[r.id] = followUpMap[r.id] !== undefined ? followUpMap[r.id] : (r.lastFollowedUp || null) newStatusMap[r.id] = statusMap[r.id] !== undefined ? statusMap[r.id] : r.status newPartnerMap[r.id] = partnerSelectionsMap[r.id] !== undefined ? partnerSelectionsMap[r.id] : (r.partnerSelections && r.partnerSelections.length ? r.partnerSelections : ['Own']) }) setFollowUpMap(newFollowUpMap) setStatusMap(newStatusMap) setPartnerSelectionsMap(newPartnerMap) }, [rfps]) // Filtered RFPs const filteredRfps = useMemo(() => { return rfps.filter((rfp) => { // Status filter if (statusFilter !== 'all' && (statusMap[rfp.id] || rfp.status) !== statusFilter) { return false } // Partner filter const selections = partnerSelectionsMap[rfp.id] || ['Own'] if (partnerFilter === 'own' && !selections.includes('Own')) { return false } if (partnerFilter === 'partner' && selections.length === 1 && selections[0] === 'Own') { return false } return true }) }, [rfps, statusFilter, partnerFilter, statusMap, partnerSelectionsMap]) const handleStatusChange = async (id: string, newStatus: string, prevStatus: string) => { setUpdatingId(id) try { if (!user?.id) { alert('Please log in to update status') setStatusMap((prev) => ({ ...prev, [id]: prevStatus })) return } await fetch(`/api/rfps/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'x-user-id': user?.id || '', }, body: JSON.stringify({ status: newStatus }), }) router.refresh() } catch (err) { console.error('Failed to update status', err) setStatusMap((prev) => ({ ...prev, [id]: prevStatus })) alert('Could not update status. Please try again.') } finally { setUpdatingId(null) } } const handlePartnerChange = async (id: string, newPartnerSelections: string[]) => { setUpdatingPartnerId(id) skipNextSync.current = true try { if (!user?.id) { alert('Please log in to update partner selection') return } await fetch(`/api/rfps/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'x-user-id': user?.id || '', }, body: JSON.stringify({ partnerSelections: JSON.stringify(newPartnerSelections) }), }) // Don't call router.refresh() - just keep local state } catch (err) { console.error('Failed to update partner selection', err) alert('Could not update partner selection. Please try again.') } finally { setUpdatingPartnerId(null) } } const handleNotesBlur = async (id: string, notes: string) => { skipNextSync.current = true try { if (!user?.id) { alert('Please log in to update notes') return } await fetch(`/api/rfps/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'x-user-id': user?.id || '', }, body: JSON.stringify({ notes }), }) // Don't call router.refresh() - just keep local state } catch (err) { console.error('Failed to update notes', err) alert('Could not update notes. Please try again.') } } const handleFollowUpChange = async (id: string, dateValue: string) => { setUpdatingFollowUpId(id) setFollowUpMap((prev) => ({ ...prev, [id]: dateValue || null })) skipNextSync.current = true try { if (!user?.id) { alert('Please log in to update follow-up date') return } await fetch(`/api/rfps/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'x-user-id': user?.id || '', }, body: JSON.stringify({ lastFollowedUp: dateValue || null }), }) // Don't call router.refresh() - just keep local state } catch (err) { console.error('Failed to update follow-up date', err) alert('Could not update follow-up date. Please try again.') } finally { setUpdatingFollowUpId(null) } } return (

Manual Status Table

{filteredRfps.map((rfp) => { const selectedList = partnerSelectionsMap[rfp.id] || ['Own'] const isOwnOnly = selectedList.length === 1 && selectedList[0] === 'Own' const isDropdownOpen = openPartnerDropdown === rfp.id const selectedPartnerNames = selectedList .filter((id) => id !== 'Own') .map((id) => { const p = partners.find((p) => p.id === id) return p ? p.name : id }) return (
Title Agency Submission Deadline Partners Status Last Followed Up Notes
{rfp.title} {rfp.agency || '—'} {new Date(rfp.submissionDeadline).toLocaleDateString()}
{isDropdownOpen && (
{partners.map((partner) => { const checked = selectedList.includes(partner.id) return ( ) })} {partners.length === 0 && (

No partners configured

)}
)} {!isOwnOnly && selectedPartnerNames.length > 0 && (
{selectedPartnerNames.join(', ')}
)}
handleFollowUpChange(rfp.id, e.target.value)} />