import React from 'react'; import BN from 'bn.js'; import { view } from 'react-easy-state'; import { RouteComponentProps, withRouter } from 'react-router'; import { Row, Col, Card, Alert, Button, Collapse, Popconfirm, Input, Tag, message, } from 'antd'; import TextArea from 'antd/lib/input/TextArea'; import store from 'src/store'; import { formatDateSeconds, formatDurationSeconds } from 'util/time'; import { PROPOSAL_STATUS, PROPOSAL_ARBITER_STATUS, MILESTONE_STAGE, PROPOSAL_STAGE, } from 'src/types'; import { Link } from 'react-router-dom'; import Back from 'components/Back'; import Markdown from 'components/Markdown'; import ArbiterControl from 'components/ArbiterControl'; import { toZat, fromZat } from 'src/util/units'; import FeedbackModal from '../FeedbackModal'; import './index.less'; type Props = RouteComponentProps; const STATE = { paidTxId: '', showCancelAndRefundPopover: false, showChangeToAcceptedWithFundingPopover: false, }; type State = typeof STATE; class ProposalDetailNaked extends React.Component { state = STATE; rejectInput: null | TextArea = null; componentDidMount() { this.loadDetail(); } render() { const id = this.getIdFromQuery(); const { proposalDetail: p, proposalDetailFetching } = store; if (!p || (p && p.proposalId !== id) || proposalDetailFetching) { return 'loading proposal...'; } const needsArbiter = PROPOSAL_ARBITER_STATUS.MISSING === p.arbiter.status && p.status === PROPOSAL_STATUS.LIVE && !p.isFailed && p.stage !== PROPOSAL_STAGE.COMPLETED; const refundablePct = p.milestones.reduce((prev, m) => { return m.datePaid ? prev - parseFloat(m.payoutPercent) : prev; }, 100); const { isVersionTwo } = p; const shouldShowArbiter = !isVersionTwo || (isVersionTwo && p.acceptedWithFunding === true); const cancelButtonText = isVersionTwo ? 'Cancel' : 'Cancel & refund'; const shouldShowChangeToAcceptedWithFunding = isVersionTwo && p.acceptedWithFunding === false; const renderCancelControl = () => { const disabled = this.getCancelAndRefundDisabled(); return ( Are you sure you want to cancel proposal and begin
the refund process? This cannot be undone.

} placement="left" cancelText="cancel" okText="confirm" visible={this.state.showCancelAndRefundPopover} okButtonProps={{ loading: store.proposalDetailCanceling, }} onCancel={this.handleCancelCancel} onConfirm={this.handleConfirmCancel} >
); }; const renderChangeToAcceptedWithFundingControl = () => { return ( Are you sure you want to accept the proposal
with funding? This cannot be undone.

} placement="left" cancelText="cancel" okText="confirm" visible={this.state.showChangeToAcceptedWithFundingPopover} okButtonProps={{ loading: store.proposalDetailCanceling, }} onCancel={this.handleChangeToAcceptWithFundingCancel} onConfirm={this.handleChangeToAcceptWithFundingConfirm} >
); }; const renderArbiterControl = () => ( ); const renderApproved = () => p.status === PROPOSAL_STATUS.APPROVED && ( ); const renderReview = () => p.status === PROPOSAL_STATUS.PENDING && (

Please review this proposal and render your judgment.

} /> ); const renderRejected = () => p.status === PROPOSAL_STATUS.REJECTED && (

This proposal has changes requested. The team will be able to re-submit it for approval should they desire to do so.

Reason:
{p.rejectReason} } /> ); const renderNominateArbiter = () => needsArbiter && shouldShowArbiter && (

An arbiter is required to review milestone payout requests.

} /> ); const renderNominatedArbiter = () => PROPOSAL_ARBITER_STATUS.NOMINATED === p.arbiter.status && p.status === PROPOSAL_STATUS.LIVE && (

{p.arbiter.user!.displayName} has been nominated for arbiter of this proposal but has not yet accepted.

} /> ); const renderMilestoneAccepted = () => { if (p.stage === PROPOSAL_STAGE.FAILED || p.stage === PROPOSAL_STAGE.CANCELED) { return; } if ( !( p.status === PROPOSAL_STATUS.LIVE && p.currentMilestone && p.currentMilestone.stage === MILESTONE_STAGE.ACCEPTED ) ) { return; } const ms = p.currentMilestone; const amount = fromZat( toZat(p.target) .mul(new BN(ms.payoutPercent)) .divn(100), ); return (

Milestone {ms.index + 1} - {ms.title} {' '} was accepted on {formatDateSeconds(ms.dateAccepted)}.

{' '} Please make a payment of {amount.toString()} ZEC to:

{' '}
{p.payoutAddress}
this.setState({ paidTxId: e.target.value })} onSearch={this.handlePaidMilestone} /> } /> ); }; const renderFailed = () => p.isFailed && ( This proposal failed to reach its funding goal of {p.target} ZEC by{' '} {formatDateSeconds(p.datePublished + p.deadlineDuration)}. All contributors will need to be refunded. ) : ( <> This proposal was canceled by an admin, and will be refunding contributors{' '} {refundablePct}% of their contributions. ) } /> ); const renderDeetItem = (name: string, val: any) => (
{name} {val}  
); return (

{p.title}

{/* MAIN */} {renderApproved()} {renderReview()} {renderRejected()} {renderNominateArbiter()} {renderNominatedArbiter()} {renderMilestoneAccepted()} {renderFailed()} {p.brief} {p.milestones.map((milestone, i) => ( {milestone.title + ' '} {milestone.immediatePayout && ( Immediate Payout )} } extra={`${milestone.payoutPercent}% Payout`} key={i} > {p.isVersionTwo && (

Estimated Days to Complete:{' '} {milestone.immediatePayout ? 'N/A' : milestone.daysEstimated}{' '}

)}

Estimated Date:{' '} {milestone.dateEstimated ? formatDateSeconds(milestone.dateEstimated) : 'N/A'}{' '}

{milestone.content}

))}
{JSON.stringify(p, null, 4)}
{/* RIGHT SIDE */} {/* ACTIONS */} {renderCancelControl()} {renderArbiterControl()} {shouldShowChangeToAcceptedWithFunding && renderChangeToAcceptedWithFundingControl()} {/* DETAILS */} {renderDeetItem('id', p.proposalId)} {renderDeetItem('created', formatDateSeconds(p.dateCreated))} {renderDeetItem( 'published', p.datePublished ? formatDateSeconds(p.datePublished) : 'n/a', )} {renderDeetItem( 'deadlineDuration', formatDurationSeconds(p.deadlineDuration), )} {p.datePublished && renderDeetItem( '(deadline)', formatDateSeconds(p.datePublished + p.deadlineDuration), )} {renderDeetItem('isFailed', JSON.stringify(p.isFailed))} {renderDeetItem('status', p.status)} {renderDeetItem('stage', p.stage)} {renderDeetItem('target', p.target)} {renderDeetItem('contributed', p.contributed)} {renderDeetItem('funded (inc. matching)', p.funded)} {renderDeetItem('matching', p.contributionMatching)} {renderDeetItem('bounty', p.contributionBounty)} {renderDeetItem('rfpOptIn', JSON.stringify(p.rfpOptIn))} {renderDeetItem( 'acceptedWithFunding', JSON.stringify(p.acceptedWithFunding), )} {renderDeetItem( 'arbiter', <> {p.arbiter.user && ( {p.arbiter.user.displayName} )} ({p.arbiter.status}) , )} {p.rfp && renderDeetItem( 'rfp', {p.rfp.title}, )} {/* TEAM */} {p.team.map(t => (
{t.displayName}
))}
); } private getCancelAndRefundDisabled = () => { const { proposalDetail: p } = store; if (!p) { return true; } return ( p.status !== PROPOSAL_STATUS.LIVE || p.stage === PROPOSAL_STAGE.FAILED || p.stage === PROPOSAL_STAGE.CANCELED || p.isFailed ); }; private handleCancelAndRefundClick = () => { const disabled = this.getCancelAndRefundDisabled(); if (!disabled) { if (!this.state.showCancelAndRefundPopover) { this.setState({ showCancelAndRefundPopover: true }); } } }; private handleChangeToAcceptedWithFunding = () => { this.setState({ showChangeToAcceptedWithFundingPopover: true }); }; private handleChangeToAcceptWithFundingCancel = () => { this.setState({ showChangeToAcceptedWithFundingPopover: false }); }; private handleChangeToAcceptWithFundingConfirm = () => { if (!store.proposalDetail) return; store.changeProposalToAcceptedWithFunding(store.proposalDetail.proposalId); this.setState({ showChangeToAcceptedWithFundingPopover: false }); }; private getIdFromQuery = () => { return Number(this.props.match.params.id); }; private loadDetail = () => { store.fetchProposalDetail(this.getIdFromQuery()); }; private handleCancelCancel = () => { this.setState({ showCancelAndRefundPopover: false }); }; private handleConfirmCancel = () => { if (!store.proposalDetail) return; store.cancelProposal(store.proposalDetail.proposalId); this.setState({ showCancelAndRefundPopover: false }); }; private handleApprove = (withFunding: boolean) => { store.approveProposal(true, withFunding); }; private handleReject = async (reason: string) => { await store.approveProposal(false, false, reason); message.info('Proposal changes requested'); }; private handlePaidMilestone = async () => { const pid = store.proposalDetail!.proposalId; const mid = store.proposalDetail!.currentMilestone!.id; await store.markMilestonePaid(pid, mid, this.state.paidTxId); message.success('Marked milestone paid.'); }; } const ProposalDetail = withRouter(view(ProposalDetailNaked)); export default ProposalDetail;