Retire lp_profiles + LP Tracker; repoint Dashboard committed to the grid (v0.1.0:78)

The fundraising grid + email capture is the canonical system of record. lp_profiles
was a superseded single-fund model with no reachable create/edit path, and the LP
Tracker page was already orphaned (no nav entry + a redirect bouncing it to the grid).

- Remove /api/lp-profiles* endpoints + handlers, the unused lp-breakdown report,
  the contact-dossier LP section, the demo-seed LP block, and (frontend) the
  LPTrackerPage component + its lp-tracker->fundraising-grid redirect.
- Dashboard "Total Committed" now sums fundraising_investors.total_invested
  (graveyarded investors excluded) instead of the orphaned lp_profiles table, which
  read ~$0. "Total Funded" dropped: the grid tracks commitments, not a funded amount,
  and the frontend never rendered it.
- Leave the empty lp_profiles table/index, the contact-delete soft-delete cascade,
  and the --reset-all-data clear in place (never-hard-delete).
- Tests: add test_dashboard_report.py; update test_soft_delete_reads.py. 21/21 green.
This commit is contained in:
Keysat
2026-06-16 10:48:53 -05:00
parent 5cda84a7c0
commit 108210d8e1
8 changed files with 180 additions and 486 deletions
-286
View File
@@ -3826,32 +3826,6 @@
)}
</div>
{details.lp_profile && (
<div className="detail-section">
<div className="detail-section-title">LP Profile</div>
<div className="detail-row">
<span className="detail-label">Commitment</span>
<span className="detail-value">{formatCurrencyLong(details.lp_profile.commitment_amount)}</span>
</div>
<div className="detail-row">
<span className="detail-label">Funded</span>
<span className="detail-value">{formatCurrencyLong(details.lp_profile.funded_amount)}</span>
</div>
<div className="detail-row">
<span className="detail-label">Fund</span>
<span className="detail-value">{details.lp_profile.fund_name || '-'}</span>
</div>
<div className="detail-row">
<span className="detail-label">Docs Signed</span>
<span className="detail-value">{details.lp_profile.legal_docs_signed ? '✓' : '✗'}</span>
</div>
<div className="detail-row">
<span className="detail-label">Wire Received</span>
<span className="detail-value">{details.lp_profile.wire_received ? '✓' : '✗'}</span>
</div>
</div>
)}
{details.opportunities && details.opportunities.length > 0 && (
<div className="detail-section">
<div className="detail-section-title">Opportunities</div>
@@ -4591,260 +4565,6 @@
);
};
const LPTrackerPage = ({ token, onShowToast }) => {
const [lps, setLps] = useState([]);
const [contacts, setContacts] = useState([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({ contact_id: '' });
const [formError, setFormError] = useState('');
const [selectedLP, setSelectedLP] = useState(null);
const [confirmDelete, setConfirmDelete] = useState(null);
useEffect(() => {
const fetchLPs = async () => {
try {
setLoading(true);
const [lpResult, contactResult] = await Promise.all([
api(`/api/lp-profiles?search=${search}`, {}, token),
api('/api/contacts?limit=1000', {}, token)
]);
setLps(lpResult.data || []);
setContacts(contactResult.data || []);
} catch (err) {
onShowToast(getErrorMessage(err, 'Failed to load LP profiles'), 'error');
} finally {
setLoading(false);
}
};
fetchLPs();
}, [token, search, onShowToast]);
const handleAddLP = async (e) => {
e.preventDefault();
setFormError('');
try {
await api('/api/lp-profiles', {
method: 'POST',
body: JSON.stringify(formData)
}, token);
setShowForm(false);
setFormData({ contact_id: '' });
const result = await api(`/api/lp-profiles?search=${search}`, {}, token);
setLps(result.data || []);
onShowToast('LP profile created', 'success');
} catch (err) {
setFormError(err.message);
}
};
const handleDeleteLP = async (id) => {
if (!MOCK_MODE) {
onShowToast('LP delete endpoint is not available yet in backend', 'error');
return;
}
try {
await api(`/api/lp-profiles/${id}`, { method: 'DELETE' }, token);
setLps(lps.filter(l => l.id !== id));
setConfirmDelete(null);
setSelectedLP(null);
onShowToast('LP deleted', 'success');
} catch (err) {
onShowToast(err.message, 'error');
}
};
const totalCommitted = useMemo(() => lps.reduce((sum, lp) => sum + (lp.commitment_amount || 0), 0), [lps]);
const totalFunded = useMemo(() => lps.reduce((sum, lp) => sum + (lp.funded_amount || 0), 0), [lps]);
const avgCheck = lps.length > 0 ? totalCommitted / lps.length : 0;
return (
<div className="page-container">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2 className="section-title">LP Tracker</h2>
<button onClick={() => setShowForm(true)}>+ Add LP Profile</button>
</div>
<div className="kpi-grid">
<div className="kpi-card">
<div className="kpi-label">Total Committed</div>
<div className="kpi-value">{formatCurrencyLong(totalCommitted)}</div>
</div>
<div className="kpi-card">
<div className="kpi-label">Total Funded</div>
<div className="kpi-value">{formatCurrencyLong(totalFunded)}</div>
</div>
<div className="kpi-card">
<div className="kpi-label">Avg Check Size</div>
<div className="kpi-value">{formatCurrencyLong(avgCheck)}</div>
</div>
<div className="kpi-card">
<div className="kpi-label">Number of LPs</div>
<div className="kpi-value">{lps.length}</div>
</div>
</div>
<div className="section">
<div className="controls">
<input
type="text"
className="search-input"
placeholder="Search LPs..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
{loading ? (
<SkeletonBlock lines={8} />
) : lps.length === 0 ? (
<div className="empty-state">No LP profiles</div>
) : (
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Organization</th>
<th>Commitment</th>
<th>Funded</th>
<th>Docs</th>
<th>Wire</th>
<th>K1</th>
<th></th>
</tr>
</thead>
<tbody>
{lps.map(lp => (
<tr key={lp.id} onClick={() => setSelectedLP(lp)}>
<td>{contactName(lp)}</td>
<td>{lp.organization || lp.organization_name || '-'}</td>
<td>{formatCurrencyLong(lp.commitment_amount)}</td>
<td>{formatCurrencyLong(lp.funded_amount)}</td>
<td>{lp.legal_docs_signed ? '✓' : '✗'}</td>
<td>{lp.wire_received ? '✓' : '✗'}</td>
<td>{lp.k1_sent ? '✓' : '✗'}</td>
<td>
<button
className="button-danger"
style={{ padding: '4px 8px', fontSize: '11px' }}
onClick={(e) => {
e.stopPropagation();
setConfirmDelete(lp.id);
}}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{showForm && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">Add LP Profile</div>
{formError && <div className="toast error" style={{ position: 'static', marginBottom: '16px' }}>{formError}</div>}
<form onSubmit={handleAddLP}>
<div className="form-group">
<label className="form-label">Contact *</label>
<select
className="select-input"
value={formData.contact_id || ''}
onChange={(e) => setFormData({ ...formData, contact_id: e.target.value })}
required
>
<option value="">Select contact</option>
{contacts.map((c) => (
<option key={c.id} value={c.id}>{contactName(c)}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Commitment Amount</label>
<input
type="number"
className="text-input"
value={formData.commitment_amount || ''}
onChange={(e) => setFormData({ ...formData, commitment_amount: parseFloat(e.target.value) })}
/>
</div>
<div className="form-group">
<label className="form-label">Funded Amount</label>
<input
type="number"
className="text-input"
value={formData.funded_amount || ''}
onChange={(e) => setFormData({ ...formData, funded_amount: parseFloat(e.target.value) })}
/>
</div>
<div className="form-group">
<label className="form-label">Fund Name</label>
<input
type="text"
className="text-input"
value={formData.fund_name || ''}
onChange={(e) => setFormData({ ...formData, fund_name: e.target.value })}
/>
</div>
<div className="form-group">
<label className="form-label">
<input
type="checkbox"
checked={formData.legal_docs_signed || false}
onChange={(e) => setFormData({ ...formData, legal_docs_signed: e.target.checked })}
/>
{' '}Docs Signed
</label>
</div>
<div className="form-group">
<label className="form-label">
<input
type="checkbox"
checked={formData.wire_received || false}
onChange={(e) => setFormData({ ...formData, wire_received: e.target.checked })}
/>
{' '}Wire Received
</label>
</div>
<div className="form-group">
<label className="form-label">
<input
type="checkbox"
checked={formData.k1_sent || false}
onChange={(e) => setFormData({ ...formData, k1_sent: e.target.checked })}
/>
{' '}K1 Sent
</label>
</div>
<div className="form-actions">
<button type="button" className="button-secondary" onClick={() => setShowForm(false)}>Cancel</button>
<button type="submit">Create</button>
</div>
</form>
</div>
</div>
)}
{confirmDelete && (
<ConfirmDialog
title="Delete LP"
message="Are you sure?"
onConfirm={() => handleDeleteLP(confirmDelete)}
onCancel={() => setConfirmDelete(null)}
/>
)}
</div>
);
};
const FeatureRequestsPage = ({ token, onShowToast, user }) => {
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(true);
@@ -10937,12 +10657,6 @@
setToasts(t => [...t, { id, message, type }]);
}, []);
useEffect(() => {
if (page === 'lp-tracker') {
setPage('fundraising-grid');
}
}, [page]);
if (!token) {
return <LoginPage />;
}