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