Retire contacts.contact_type; derive Contacts status from the grid (v0.1.0:106)
The Investors/Prospects distinction is now derived live from the canonical grid (contact_grid_signals -> committed/pipeline_stage), not the mechanically set contact_type column: - Desktop Contacts: drop the Investors/Prospects tabs + TYPE badge; show a derived Status (existing-LP badge + pipeline stage chip). - Dashboard: repoint Total LPs / Prospects onto fundraising_investors entities (committed>0 vs $0, graveyard + blank-row placeholder excluded); fix a total_contacts soft-delete leak. - Stop reading/writing contact_type across the create/update/import/sync paths. The column is left inert in place; a physical drop is deferred to a later signed-off table-rebuild migration (SQLite no-drop-column; contacts is FK-referenced) -- same retire-then-drop path lp_profiles took.
This commit is contained in:
+24
-52
@@ -4373,6 +4373,22 @@
|
||||
);
|
||||
};
|
||||
|
||||
// Derived contact status from the live grid signals the API injects (committed / pipeline_stage) —
|
||||
// replaces the retired contact_type. An existing LP (committed > 0) shows an LP badge; an
|
||||
// in-pipeline contact shows its stage chip (both can show at once — a committed LP still in an
|
||||
// active deal); a contact with neither is a Prospect. Used by the desktop Contacts list + detail.
|
||||
const renderContactStatus = (c) => {
|
||||
const existing = Number((c && c.committed) || 0) > 0;
|
||||
const stage = c && c.pipeline_stage;
|
||||
if (!existing && !stage) return <span className="badge badge-prospect">Prospect</span>;
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{existing && <span className="badge badge-investor">LP</span>}
|
||||
{stage && <StageChip stage={stage} sm />}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Existing-LP earmark — a quiet accent corner-triangle (top-left), the locked existing-investor
|
||||
// signal on cards (dc GridApp:89-91). Reusable: the Grid card now, the Pipeline card (8f). Render
|
||||
// only when the investor has committed capital (existing_investor / committed > 0).
|
||||
@@ -5507,7 +5523,6 @@
|
||||
const [contactsPage, setContactsPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [tab, setTab] = useState('all');
|
||||
const [search, setSearch] = useState('');
|
||||
const [sort, setSort] = useState('last_name');
|
||||
const [order, setOrder] = useState('asc');
|
||||
@@ -5515,19 +5530,18 @@
|
||||
const [deleting, setDeleting] = useState(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState(null);
|
||||
|
||||
const tabFilter = tab === 'all' ? '' : tab === 'investors' ? 'investor' : 'prospect';
|
||||
const contactsOffset = (contactsPage - 1) * CONTACTS_PAGE_SIZE;
|
||||
const contactsMaxPage = Math.max(1, Math.ceil((Number(contactsTotal) || 0) / CONTACTS_PAGE_SIZE));
|
||||
|
||||
useEffect(() => {
|
||||
setContactsPage(1);
|
||||
}, [tab, search, sort, order]);
|
||||
}, [search, sort, order]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchContacts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await api(`/api/contacts?search=${encodeURIComponent(search)}&type=${tabFilter}&sort=${sort}&order=${order}&limit=${CONTACTS_PAGE_SIZE}&offset=${contactsOffset}`, {}, token);
|
||||
const result = await api(`/api/contacts?search=${encodeURIComponent(search)}&sort=${sort}&order=${order}&limit=${CONTACTS_PAGE_SIZE}&offset=${contactsOffset}`, {}, token);
|
||||
setContacts(result.data || []);
|
||||
setContactsTotal(Number(result.total) || 0);
|
||||
} catch (err) {
|
||||
@@ -5538,7 +5552,7 @@
|
||||
};
|
||||
|
||||
fetchContacts();
|
||||
}, [token, tab, search, sort, order, contactsOffset]);
|
||||
}, [token, search, sort, order, contactsOffset]);
|
||||
|
||||
const handleDeleteContact = async (id) => {
|
||||
setDeleting(id);
|
||||
@@ -5555,16 +5569,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const contactTypeTooltip = (type) => {
|
||||
const badges = {
|
||||
'investor': 'badge-investor',
|
||||
'prospect': 'badge-prospect',
|
||||
'advisor': 'badge-advisor',
|
||||
'other': 'badge-other'
|
||||
};
|
||||
return badges[type] || 'badge-other';
|
||||
};
|
||||
|
||||
const handleSort = (column) => {
|
||||
if (sort === column) {
|
||||
setOrder(order === 'asc' ? 'desc' : 'asc');
|
||||
@@ -5579,18 +5583,6 @@
|
||||
<h2 className="section-title">Contacts</h2>
|
||||
|
||||
<div className="section">
|
||||
<div className="tabs">
|
||||
{['all', 'investors', 'prospects'].map(t => (
|
||||
<button
|
||||
key={t}
|
||||
className={`tab ${tab === t ? 'active' : ''}`}
|
||||
onClick={() => setTab(t)}
|
||||
>
|
||||
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="controls">
|
||||
<input
|
||||
type="text"
|
||||
@@ -5617,7 +5609,7 @@
|
||||
<th onClick={() => handleSort('source')} style={{ cursor: 'pointer' }}>
|
||||
Lead Source {sort === 'source' && (order === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Email</th>
|
||||
<th>Last Contact</th>
|
||||
<th>Communications</th>
|
||||
@@ -5629,11 +5621,7 @@
|
||||
<td>{contact.first_name} {contact.last_name}</td>
|
||||
<td>{contact.organization || contact.organization_name || '-'}</td>
|
||||
<td>{contact.source || '-'}</td>
|
||||
<td>
|
||||
<span className={`badge ${contactTypeTooltip(contact.contact_type)}`}>
|
||||
{contact.contact_type}
|
||||
</span>
|
||||
</td>
|
||||
<td>{renderContactStatus(contact)}</td>
|
||||
<td>{contact.email || '-'}</td>
|
||||
<td>{formatDate(contact.last_contact_date)}</td>
|
||||
<td>{contact.communication_count ?? contact.comm_count ?? 0}</td>
|
||||
@@ -5677,7 +5665,7 @@
|
||||
onDelete={() => setConfirmDelete(selectedContact.id)}
|
||||
token={token}
|
||||
onRefresh={() => {
|
||||
const result = api(`/api/contacts?search=${encodeURIComponent(search)}&type=${tabFilter}&sort=${sort}&order=${order}&limit=${CONTACTS_PAGE_SIZE}&offset=${contactsOffset}`, {}, token);
|
||||
const result = api(`/api/contacts?search=${encodeURIComponent(search)}&sort=${sort}&order=${order}&limit=${CONTACTS_PAGE_SIZE}&offset=${contactsOffset}`, {}, token);
|
||||
result.then(r => {
|
||||
setContacts(r.data || []);
|
||||
setContactsTotal(Number(r.total) || 0);
|
||||
@@ -6017,7 +6005,6 @@
|
||||
city: '',
|
||||
state: '',
|
||||
country: '',
|
||||
contact_type: 'prospect',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
@@ -6050,7 +6037,6 @@
|
||||
city: details.city || '',
|
||||
state: details.state || '',
|
||||
country: details.country || '',
|
||||
contact_type: details.contact_type || 'prospect',
|
||||
status: details.status || 'active'
|
||||
});
|
||||
}, [details]);
|
||||
@@ -6076,7 +6062,6 @@
|
||||
city: editDraft.city.trim(),
|
||||
state: editDraft.state.trim(),
|
||||
country: editDraft.country.trim(),
|
||||
contact_type: editDraft.contact_type,
|
||||
status: editDraft.status
|
||||
})
|
||||
}, token);
|
||||
@@ -6156,15 +6141,6 @@
|
||||
<label className="form-label">Country</label>
|
||||
<input className="text-input" value={editDraft.country} onChange={(e) => setEditDraft((d) => ({ ...d, country: e.target.value }))} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Type</label>
|
||||
<select className="select-input" value={editDraft.contact_type} onChange={(e) => setEditDraft((d) => ({ ...d, contact_type: e.target.value }))}>
|
||||
<option value="investor">Investor</option>
|
||||
<option value="prospect">Prospect</option>
|
||||
<option value="advisor">Advisor</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Status</label>
|
||||
<select className="select-input" value={editDraft.status} onChange={(e) => setEditDraft((d) => ({ ...d, status: e.target.value }))}>
|
||||
@@ -6207,12 +6183,8 @@
|
||||
</span>
|
||||
</div>
|
||||
<div className="detail-row">
|
||||
<span className="detail-label">Type</span>
|
||||
<span className="detail-value">
|
||||
<span className={`badge ${details.contact_type === 'investor' ? 'badge-investor' : 'badge-prospect'}`}>
|
||||
{details.contact_type}
|
||||
</span>
|
||||
</span>
|
||||
<span className="detail-label">Status</span>
|
||||
<span className="detail-value">{renderContactStatus(details)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user