diff --git a/src/pages/ProposalDetailPage.tsx b/src/pages/ProposalDetailPage.tsx index bc3c3d2..0e43f69 100644 --- a/src/pages/ProposalDetailPage.tsx +++ b/src/pages/ProposalDetailPage.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router-dom' import api from '@/services/api' -import type { Proposal, Milestone } from '@/types' +import type { Proposal, Milestone, Essential, EssentialType } from '@/types' import dayjs from 'dayjs' import CopyableCode from '@/components/CopyableCode' @@ -24,6 +24,18 @@ export default function ProposalDetailPage() { const [editDescription, setEditDescription] = useState('') const [editLoading, setEditLoading] = useState(false) + // Essential state + const [essentials, setEssentials] = useState([]) + const [loadingEssentials, setLoadingEssentials] = useState(false) + + // Essential create/edit state + const [showEssentialModal, setShowEssentialModal] = useState(false) + const [editingEssential, setEditingEssential] = useState(null) + const [essentialTitle, setEssentialTitle] = useState('') + const [essentialType, setEssentialType] = useState('feature') + const [essentialDesc, setEssentialDesc] = useState('') + const [essentialLoading, setEssentialLoading] = useState(false) + const fetchProposal = () => { if (!projectId) return api.get(`/projects/${projectId}/proposals/${id}`).then(({ data }) => setProposal(data)) @@ -39,6 +51,18 @@ export default function ProposalDetailPage() { .then(({ data }) => setMilestones(data.filter((m) => m.status === 'open'))) } + const fetchEssentials = () => { + if (!projectId || !id) return + setLoadingEssentials(true) + api.get(`/projects/${projectId}/proposals/${id}/essentials`) + .then(({ data }) => setEssentials(data)) + .finally(() => setLoadingEssentials(false)) + } + + useEffect(() => { + fetchEssentials() + }, [id, projectId]) + const handleAccept = async () => { if (!selectedMilestone || !projectId) return setActionLoading(true) @@ -109,6 +133,64 @@ export default function ProposalDetailPage() { } } + // Essential handlers + const openEssentialModal = (essential?: Essential) => { + if (essential) { + setEditingEssential(essential) + setEssentialTitle(essential.title) + setEssentialType(essential.type) + setEssentialDesc(essential.description || '') + } else { + setEditingEssential(null) + setEssentialTitle('') + setEssentialType('feature') + setEssentialDesc('') + } + setError('') + setShowEssentialModal(true) + } + + const handleSaveEssential = async () => { + if (!projectId || !id) return + setEssentialLoading(true) + setError('') + try { + if (editingEssential) { + await api.patch(`/projects/${projectId}/proposals/${id}/essentials/${editingEssential.id}`, { + title: essentialTitle, + type: essentialType, + description: essentialDesc || null, + }) + } else { + await api.post(`/projects/${projectId}/proposals/${id}/essentials`, { + title: essentialTitle, + type: essentialType, + description: essentialDesc || null, + }) + } + setShowEssentialModal(false) + fetchEssentials() + } catch (err: any) { + setError(err.response?.data?.detail || 'Save failed') + } finally { + setEssentialLoading(false) + } + } + + const handleDeleteEssential = async (essentialId: number) => { + if (!projectId || !id) return + if (!confirm('Are you sure you want to delete this Essential?')) return + setEssentialLoading(true) + try { + await api.delete(`/projects/${projectId}/proposals/${id}/essentials/${essentialId}`) + fetchEssentials() + } catch (err: any) { + setError(err.response?.data?.detail || 'Delete failed') + } finally { + setEssentialLoading(false) + } + } + if (!proposal) return
Loading...
const statusBadgeClass = (s: string) => { @@ -150,6 +232,56 @@ export default function ProposalDetailPage() {

{proposal.description || 'No description'}

+ {/* Essentials section */} +
+
+

Essentials ({essentials.length})

+ {proposal.status === 'open' && ( + + )} +
+ {loadingEssentials ? ( +
Loading...
+ ) : essentials.length === 0 ? ( +
+ No essentials yet. {proposal.status === 'open' ? 'Add one to define deliverables for this proposal.' : ''} +
+ ) : ( +
+ {essentials.map((e) => ( +
+
+ {e.type} + {e.essential_code && } +

{e.title}

+ {proposal.status === 'open' && ( +
+ + +
+ )} +
+ {e.description && ( +

{e.description}

+ )} +
+ ))} +
+ )} +
+ {/* Generated tasks (shown when accepted) */} {proposal.status === 'accepted' && proposal.generated_tasks && proposal.generated_tasks.length > 0 && (
@@ -278,6 +410,56 @@ export default function ProposalDetailPage() {
)} + {/* Essential create/edit modal */} + {showEssentialModal && ( +
setShowEssentialModal(false)}> +
e.stopPropagation()}> +

{editingEssential ? 'Edit Essential' : 'New Essential'}

+ + +