Retire the Pipeline page's "+ New Opportunity" button (v0.1.0:88)

Opportunities are now born only from a fundraising-grid investor row
("+ Pipeline"), which matches how the team works — they live in the grid,
not on the board. The old "+ New Opportunity" button created a deal by
picking a contact, a path that contradicts the grid-is-canonical model and
the contact-vs-investor framing.

Remove the button, its create-by-contact modal, the now-dead handler/state,
and the Pipeline page's unused /api/contacts fetch. Replace the button with a
muted "Add deals from the Fundraising Grid" hint. The board is now a view +
stage-management surface. Frontend-only; no backend or schema change.

Render-smoke green.
This commit is contained in:
Keysat
2026-06-18 08:25:14 -05:00
parent 4df104b119
commit 114916b789
6 changed files with 31 additions and 118 deletions
+2 -112
View File
@@ -3891,11 +3891,7 @@
const PipelinePage = ({ token, onShowToast }) => {
const [opportunities, setOpportunities] = useState([]);
const [contacts, setContacts] = useState([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({ stage: 'lead', priority: 'medium', contact_id: '' });
const [formError, setFormError] = useState('');
const [selectedOpp, setSelectedOpp] = useState(null);
const [confirmDelete, setConfirmDelete] = useState(null);
@@ -3905,12 +3901,8 @@
const fetchOpportunities = async () => {
try {
setLoading(true);
const [oppResult, contactResult] = await Promise.all([
api('/api/opportunities?limit=1000', {}, token),
api('/api/contacts?limit=1000', {}, token)
]);
const oppResult = await api('/api/opportunities?limit=1000', {}, token);
setOpportunities(oppResult.data || []);
setContacts(contactResult.data || []);
} catch (err) {
onShowToast(getErrorMessage(err, 'Failed to load pipeline'), 'error');
} finally {
@@ -3921,27 +3913,6 @@
fetchOpportunities();
}, [token, onShowToast]);
const handleAddOpportunity = async (e) => {
e.preventDefault();
setFormError('');
try {
await api('/api/opportunities', {
method: 'POST',
body: JSON.stringify(formData)
}, token);
setShowForm(false);
setFormData({ stage: 'lead', priority: 'medium', contact_id: '' });
const result = await api('/api/opportunities?limit=1000', {}, token);
setOpportunities(result.data || []);
onShowToast('Opportunity created', 'success');
} catch (err) {
setFormError(err.message);
}
};
const handleDeleteOpp = async (id) => {
try {
await api(`/api/opportunities/${id}`, { method: 'DELETE' }, token);
@@ -4000,7 +3971,7 @@
<div className="page-container">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2 className="section-title">Pipeline</h2>
<button onClick={() => setShowForm(true)}>+ New Opportunity</button>
<span style={{ fontSize: '12px', color: '#8ea2b7' }}>Add deals from the Fundraising Grid — "+ Pipeline" on an investor row</span>
</div>
{loading ? (
@@ -4041,87 +4012,6 @@
</>
)}
{showForm && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">New Opportunity</div>
{formError && <div className="toast error" style={{ position: 'static', marginBottom: '16px' }}>{formError}</div>}
<form onSubmit={handleAddOpportunity}>
<div className="form-group">
<label className="form-label">Opportunity Name *</label>
<input
type="text"
className="text-input"
value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<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">Stage</label>
<select
className="select-input"
value={formData.stage}
onChange={(e) => setFormData({ ...formData, stage: e.target.value })}
>
{stages.map(s => (
<option key={s} value={s}>{s.replace(/_/g, ' ')}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Expected Amount</label>
<input
type="number"
className="text-input"
value={formData.expected_amount || ''}
onChange={(e) => setFormData({ ...formData, expected_amount: parseFloat(e.target.value) })}
/>
</div>
<div className="form-group">
<label className="form-label">Priority</label>
<select
className="select-input"
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: e.target.value })}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</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-actions">
<button type="button" className="button-secondary" onClick={() => setShowForm(false)}>Cancel</button>
<button type="submit">Create</button>
</div>
</form>
</div>
</div>
)}
{selectedOpp && (
<OpportunityDetailPanel
opp={selectedOpp}