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:
+2
-112
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user