= p => {
+ if (!p.isCurrent || !p.isFunded || p.stage === MILESTONE_STAGE.PAID) {
+ return null;
+ }
+ if (!p.hasArbiter && !p.isTeamMember) {
+ return null;
+ }
+
+ // TEAM INFO
+ const team = {
+ [MILESTONE_STAGE.IDLE]: () => (
+ <>
+ Payment Request
+ {p.immediatePayout && (
+
+ Congratulations on getting funded! You can now begin the process of receiving
+ your initial payment. Click below to request the first milestone payout. It
+ will instantly be approved, and you’ll receive your funds shortly thereafter.
+
+ )}
+ {!p.immediatePayout &&
+ p.index === 0 && (
+
+ Congratulations on getting funded! Click below to request your first
+ milestone payout.
+
+ )}
+ {!p.immediatePayout &&
+ p.index > 0 && You can request a payment for this milestone.
}
+ p.requestPayout(p.proposalId, p.id)} block>
+ {(p.immediatePayout && 'Request initial payout') || 'Request payout'}
+
+ >
+ ),
+ [MILESTONE_STAGE.REQUESTED]: () => (
+ <>
+ Payment Requested
+
+ The milestone payout was requested on {fmtDate(p.dateRequested)}. You will be
+ notified when it has been reviewed.
+
+ >
+ ),
+ [MILESTONE_STAGE.REJECTED]: () => (
+ <>
+ Payment Rejected
+ The request for payout was rejected for the following reason:
+ {p.rejectReason}
+ You may request payout again when you are ready.
+ p.requestPayout(p.proposalId, p.id)} block>
+ Request payout
+
+ >
+ ),
+ [MILESTONE_STAGE.ACCEPTED]: () => (
+ <>
+ Awaiting Payment
+
+ Payout approved on {fmtDate(p.dateAccepted)}! You will receive payment shortly.
+
+ >
+ ),
+ [MILESTONE_STAGE.PAID]: () => <>>,
+ } as { [key in MILESTONE_STAGE]: () => ReactNode };
+
+ // OUTSIDERS/OTHERS INFO
+ const others = {
+ [MILESTONE_STAGE.IDLE]: () => (
+ <>
+ Payment Request
+ The team may request a payout for this milestone at any time.
+ >
+ ),
+ [MILESTONE_STAGE.REQUESTED]: () => (
+ <>
+ Payment Requested
+
+ The team requested a payout on {fmtDate(p.dateRequested)}, and awaits approval.
+
+ >
+ ),
+ [MILESTONE_STAGE.REJECTED]: () => (
+ <>
+ Payment Rejected
+
+ The payout request was denied on {fmtDate(p.dateRejected)} for the following
+ reason:
+
+ {p.rejectReason}
+ >
+ ),
+ [MILESTONE_STAGE.ACCEPTED]: () => (
+ <>
+ Awaiting Payment
+ The payout request was approved on {fmtDate(p.dateAccepted)}.
+ >
+ ),
+ [MILESTONE_STAGE.PAID]: () => <>>,
+ } as { [key in MILESTONE_STAGE]: () => ReactNode };
+
+ // ARBITER INFO
+ const arbiter = {
+ [MILESTONE_STAGE.IDLE]: () => (
+ <>
+ Payment Request
+
+ The team may request a payout for this milestone at any time. As arbiter you
+ will be responsible for reviewing these requests.
+
+ >
+ ),
+ [MILESTONE_STAGE.REQUESTED]: () => (
+ <>
+ Payment Requested
+
+ The team requested a payout on {fmtDate(p.dateRequested)}, and awaits your
+ approval.
+
+
+ p.acceptPayout(p.proposalId, p.id)}>
+ Accept
+
+ p.showRejectPayout(p.id)}>
+ Reject
+
+
+ >
+ ),
+ [MILESTONE_STAGE.REJECTED]: () => (
+ <>
+ Payment Rejected
+
+ You rejected this payment request on {fmtDate(p.dateRejected)} for the following
+ reason:
+
+ {p.rejectReason}
+ >
+ ),
+ [MILESTONE_STAGE.ACCEPTED]: () => (
+ <>
+ Awaiting Payment
+ You approved this payment request on {fmtDate(p.dateAccepted)}.
+ >
+ ),
+ [MILESTONE_STAGE.PAID]: () => <>>,
+ } as { [key in MILESTONE_STAGE]: () => ReactNode };
+
+ let content = null;
+ if (p.isTeamMember) {
+ content = team[p.stage]();
+ } else if (p.isArbiter) {
+ content = arbiter[p.stage]();
+ } else {
+ content = others[p.stage]();
+ }
+
+ // special warning if no arbiter is set for team members
+ if (!p.hasArbiter && p.isTeamMember) {
+ content = (
+
+ We are sorry for the inconvenience, but in order to have milestone payouts
+ reviewed an arbiter must be assigned. Please{' '}
+
+ contact support
+ {' '}
+ for help.
+
+ }
+ />
+ );
+ }
+
+ return (
+ <>
+
+ {content}
+ >
+ );
+};
+
+const ConnectedProposalMilestones = connect<{}, DispatchProps, OwnProps, AppState>(
+ undefined,
+ {
+ requestPayout: proposalActions.requestPayout,
+ acceptPayout: proposalActions.acceptPayout,
+ rejectPayout: proposalActions.rejectPayout,
+ },
+)(ProposalMilestones);
export default ConnectedProposalMilestones;
diff --git a/frontend/client/modules/create/utils.ts b/frontend/client/modules/create/utils.ts
index 3389d7b0..afc06c7b 100644
--- a/frontend/client/modules/create/utils.ts
+++ b/frontend/client/modules/create/utils.ts
@@ -1,10 +1,19 @@
-import { ProposalDraft, CreateMilestone, STATUS, PROPOSAL_ARBITER_STATUS } from 'types';
+import {
+ ProposalDraft,
+ CreateMilestone,
+ STATUS,
+ MILESTONE_STAGE,
+ PROPOSAL_ARBITER_STATUS,
+} from 'types';
import { User } from 'types';
import { getAmountError, isValidAddress } from 'utils/validators';
-import { MILESTONE_STATE, Proposal } from 'types';
import { Zat, toZat } from 'utils/units';
import { ONE_DAY } from 'utils/time';
-import { PROPOSAL_CATEGORY } from 'api/constants';
+import { PROPOSAL_CATEGORY, PROPOSAL_STAGE } from 'api/constants';
+import {
+ ProposalDetail,
+ PROPOSAL_DETAIL_INITIAL_STATE,
+} from 'modules/proposals/reducers';
export const TARGET_ZEC_LIMIT = 1000;
@@ -170,7 +179,7 @@ export function proposalToContractData(form: ProposalDraft): any {
}
// This is kind of a disgusting function, sorry.
-export function makeProposalPreviewFromDraft(draft: ProposalDraft): Proposal {
+export function makeProposalPreviewFromDraft(draft: ProposalDraft): ProposalDetail {
const { invites, ...rest } = draft;
const target = parseFloat(draft.target);
@@ -189,22 +198,23 @@ export function makeProposalPreviewFromDraft(draft: ProposalDraft): Proposal {
funded: Zat('0'),
contributionMatching: 0,
percentFunded: 0,
- stage: 'preview',
+ stage: PROPOSAL_STAGE.PREVIEW,
category: draft.category || PROPOSAL_CATEGORY.CORE_DEV,
isStaked: true,
arbiter: {
status: PROPOSAL_ARBITER_STATUS.ACCEPTED,
},
milestones: draft.milestones.map((m, idx) => ({
+ id: idx,
index: idx,
title: m.title,
content: m.content,
amount: toZat(target * (parseInt(m.payoutPercent, 10) / 100)),
dateEstimated: m.dateEstimated,
immediatePayout: m.immediatePayout,
- isPaid: false,
payoutPercent: m.payoutPercent.toString(),
- state: MILESTONE_STATE.WAITING,
+ stage: MILESTONE_STAGE.IDLE,
})),
+ ...PROPOSAL_DETAIL_INITIAL_STATE,
};
}
diff --git a/frontend/client/modules/proposals/actions.ts b/frontend/client/modules/proposals/actions.ts
index dcb91d5e..02e083c5 100644
--- a/frontend/client/modules/proposals/actions.ts
+++ b/frontend/client/modules/proposals/actions.ts
@@ -6,6 +6,9 @@ import {
getProposalUpdates,
getProposalContributions,
postProposalComment as apiPostProposalComment,
+ requestProposalPayout,
+ acceptProposalPayout,
+ rejectProposalPayout,
} from 'api/api';
import { Dispatch } from 'redux';
import { Proposal, Comment, ProposalPageParams } from 'types';
@@ -14,6 +17,52 @@ import { getProposalPageSettings } from './selectors';
type GetState = () => AppState;
+function addProposalUserRoles(p: Proposal, state: AppState) {
+ if (state.auth.user) {
+ const authUserId = state.auth.user.userid;
+ if (p.arbiter.user) {
+ p.isArbiter = p.arbiter.user.userid === authUserId;
+ }
+ if (p.team.find(t => t.userid === authUserId)) {
+ p.isTeamMember = true;
+ }
+ }
+ return p;
+}
+
+export function requestPayout(proposalId: number, milestoneId: number) {
+ return async (dispatch: Dispatch) => {
+ return dispatch({
+ type: types.PROPOSAL_PAYOUT_REQUEST,
+ payload: async () => {
+ return (await requestProposalPayout(proposalId, milestoneId)).data;
+ },
+ });
+ };
+}
+
+export function acceptPayout(proposalId: number, milestoneId: number) {
+ return async (dispatch: Dispatch) => {
+ return dispatch({
+ type: types.PROPOSAL_PAYOUT_ACCEPT,
+ payload: async () => {
+ return (await acceptProposalPayout(proposalId, milestoneId)).data;
+ },
+ });
+ };
+}
+
+export function rejectPayout(proposalId: number, milestoneId: number, reason: string) {
+ return async (dispatch: Dispatch) => {
+ return dispatch({
+ type: types.PROPOSAL_PAYOUT_REJECT,
+ payload: async () => {
+ return (await rejectProposalPayout(proposalId, milestoneId, reason)).data;
+ },
+ });
+ };
+}
+
// change page, sort, filter, search
export function setProposalPage(pageParams: Partial) {
return async (dispatch: Dispatch, getState: GetState) => {
@@ -49,7 +98,7 @@ export function fetchProposals() {
export type TFetchProposal = typeof fetchProposal;
export function fetchProposal(proposalId: Proposal['proposalId']) {
- return async (dispatch: Dispatch) => {
+ return async (dispatch: Dispatch, getState: GetState) => {
dispatch({
type: types.PROPOSAL_DATA_PENDING,
payload: { proposalId },
@@ -58,7 +107,7 @@ export function fetchProposal(proposalId: Proposal['proposalId']) {
const proposal = (await getProposal(proposalId)).data;
return dispatch({
type: types.PROPOSAL_DATA_FULFILLED,
- payload: proposal,
+ payload: addProposalUserRoles(proposal, getState()),
});
} catch (error) {
dispatch({
diff --git a/frontend/client/modules/proposals/reducers.ts b/frontend/client/modules/proposals/reducers.ts
index 36e4074a..df14a1fd 100644
--- a/frontend/client/modules/proposals/reducers.ts
+++ b/frontend/client/modules/proposals/reducers.ts
@@ -10,10 +10,19 @@ import {
} from 'types';
import { PROPOSAL_SORT } from 'api/constants';
+export interface ProposalDetail extends Proposal {
+ isRequestingPayout: boolean;
+ requestPayoutError: string;
+ isRejectingPayout: boolean;
+ rejectPayoutError: string;
+ isAcceptingPayout: boolean;
+ acceptPayoutError: string;
+}
+
export interface ProposalState {
page: LoadableProposalPage;
- detail: null | Proposal;
+ detail: null | ProposalDetail;
isFetchingDetail: boolean;
detailError: null | string;
@@ -36,6 +45,15 @@ export interface ProposalState {
deleteContributionError: null | string;
}
+export const PROPOSAL_DETAIL_INITIAL_STATE = {
+ isRequestingPayout: false,
+ requestPayoutError: '',
+ isRejectingPayout: false,
+ rejectPayoutError: '',
+ isAcceptingPayout: false,
+ acceptPayoutError: '',
+};
+
export const INITIAL_STATE: ProposalState = {
page: {
page: 1,
@@ -203,14 +221,14 @@ export default (state = INITIAL_STATE, action: any) => {
// if requesting same proposal, leave the detail object
state.detail && state.detail.proposalId === payload.proposalId
? state.detail
- : loadedInPage || null,
+ : { ...loadedInPage, ...PROPOSAL_DETAIL_INITIAL_STATE } || null,
isFetchingDetail: true,
detailError: null,
};
case types.PROPOSAL_DATA_FULFILLED:
return {
...state,
- detail: payload,
+ detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
isFetchingDetail: false,
};
case types.PROPOSAL_DATA_REJECTED:
@@ -221,6 +239,78 @@ export default (state = INITIAL_STATE, action: any) => {
detailError: (payload && payload.message) || payload.toString(),
};
+ case types.PROPOSAL_PAYOUT_REQUEST_PENDING:
+ return {
+ ...state,
+ detail: {
+ ...state.detail,
+ isRequestingPayout: true,
+ requestPayoutError: '',
+ },
+ };
+ case types.PROPOSAL_PAYOUT_REQUEST_FULFILLED:
+ return {
+ ...state,
+ detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
+ };
+ case types.PROPOSAL_PAYOUT_REQUEST_REJECTED:
+ return {
+ ...state,
+ detail: {
+ ...state.detail,
+ isRequestingPayout: false,
+ requestPayoutError: (payload && payload.message) || payload.toString(),
+ },
+ };
+
+ case types.PROPOSAL_PAYOUT_REJECT_PENDING:
+ return {
+ ...state,
+ detail: {
+ ...state.detail,
+ isRejectingPayout: true,
+ rejectPayoutError: '',
+ },
+ };
+ case types.PROPOSAL_PAYOUT_REJECT_FULFILLED:
+ return {
+ ...state,
+ detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
+ };
+ case types.PROPOSAL_PAYOUT_REJECT_REJECTED:
+ return {
+ ...state,
+ detail: {
+ ...state.detail,
+ isRejectingPayout: false,
+ rejectPayoutError: (payload && payload.message) || payload.toString(),
+ },
+ };
+
+ case types.PROPOSAL_PAYOUT_ACCEPT_PENDING:
+ return {
+ ...state,
+ detail: {
+ ...state.detail,
+ isAcceptingPayout: true,
+ acceptPayoutError: '',
+ },
+ };
+ case types.PROPOSAL_PAYOUT_ACCEPT_FULFILLED:
+ return {
+ ...state,
+ detail: { ...payload, ...PROPOSAL_DETAIL_INITIAL_STATE },
+ };
+ case types.PROPOSAL_PAYOUT_ACCEPT_REJECTED:
+ return {
+ ...state,
+ detail: {
+ ...state.detail,
+ isAcceptingPayout: false,
+ acceptPayoutError: (payload && payload.message) || payload.toString(),
+ },
+ };
+
case types.PROPOSAL_COMMENTS_PENDING:
return {
...state,
diff --git a/frontend/client/modules/proposals/types.ts b/frontend/client/modules/proposals/types.ts
index 7be80b5a..0f06fe03 100644
--- a/frontend/client/modules/proposals/types.ts
+++ b/frontend/client/modules/proposals/types.ts
@@ -32,6 +32,21 @@ enum proposalTypes {
POST_PROPOSAL_CONTRIBUTION = 'POST_PROPOSAL_CONTRIBUTION',
SET_PROPOSAL_PAGE = 'SET_PROPOSAL_PAGE',
+
+ PROPOSAL_PAYOUT_REQUEST = 'PROPOSAL_PAYOUT_REQUEST',
+ PROPOSAL_PAYOUT_REQUEST_FULFILLED = 'PROPOSAL_PAYOUT_REQUEST_FULFILLED',
+ PROPOSAL_PAYOUT_REQUEST_REJECTED = 'PROPOSAL_PAYOUT_REQUEST_REJECTED',
+ PROPOSAL_PAYOUT_REQUEST_PENDING = 'PROPOSAL_PAYOUT_REQUEST_PENDING',
+
+ PROPOSAL_PAYOUT_REJECT = 'PROPOSAL_PAYOUT_REJECT',
+ PROPOSAL_PAYOUT_REJECT_FULFILLED = 'PROPOSAL_PAYOUT_REJECT_FULFILLED',
+ PROPOSAL_PAYOUT_REJECT_REJECTED = 'PROPOSAL_PAYOUT_REJECT_REJECTED',
+ PROPOSAL_PAYOUT_REJECT_PENDING = 'PROPOSAL_PAYOUT_REJECT_PENDING',
+
+ PROPOSAL_PAYOUT_ACCEPT = 'PROPOSAL_PAYOUT_ACCEPT',
+ PROPOSAL_PAYOUT_ACCEPT_FULFILLED = 'PROPOSAL_PAYOUT_ACCEPT_FULFILLED',
+ PROPOSAL_PAYOUT_ACCEPT_REJECTED = 'PROPOSAL_PAYOUT_ACCEPT_REJECTED',
+ PROPOSAL_PAYOUT_ACCEPT_PENDING = 'PROPOSAL_PAYOUT_ACCEPT_PENDING',
}
export default proposalTypes;
diff --git a/frontend/client/utils/api.ts b/frontend/client/utils/api.ts
index 2a8686d2..4eb6ab21 100644
--- a/frontend/client/utils/api.ts
+++ b/frontend/client/utils/api.ts
@@ -6,7 +6,6 @@ import {
PageParams,
UserProposal,
RFP,
- MILESTONE_STATE,
ProposalPage,
} from 'types';
import { UserState } from 'modules/users/reducers';
@@ -91,16 +90,12 @@ export function formatProposalFromGet(p: any): Proposal {
? 0
: proposal.funded.div(proposal.target.divn(100)).toNumber();
if (proposal.milestones) {
- proposal.milestones = proposal.milestones.map((m: any, index: number) => {
- return {
- ...m,
- index,
- amount: proposal.target.mul(new BN(m.payoutPercent)).divn(100),
- // TODO: Get data from backend
- state: MILESTONE_STATE.WAITING,
- isPaid: false,
- };
+ const msToFe = (m: any) => ({
+ ...m,
+ amount: proposal.target.mul(new BN(m.payoutPercent)).divn(100),
});
+ proposal.milestones = proposal.milestones.map(msToFe);
+ proposal.currentMilestone = msToFe(proposal.currentMilestone);
}
return proposal;
}
diff --git a/frontend/stories/ProposalMilestones.story.tsx b/frontend/stories/ProposalMilestones.story.tsx
index f9c25ef7..96be4161 100644
--- a/frontend/stories/ProposalMilestones.story.tsx
+++ b/frontend/stories/ProposalMilestones.story.tsx
@@ -5,25 +5,25 @@ import { Provider } from 'react-redux';
import { configureStore } from 'store/configure';
import { combineInitialState } from 'store/reducers';
import Milestones from 'components/Proposal/Milestones';
-import { MILESTONE_STATE } from 'types';
-const { WAITING, ACTIVE, PAID, REJECTED } = MILESTONE_STATE;
+import { MILESTONE_STAGE } from 'types';
+const { IDLE, ACCEPTED, PAID, REJECTED } = MILESTONE_STAGE;
import 'styles/style.less';
import 'components/Proposal/style.less';
import 'components/Proposal/Governance/style.less';
import { generateProposal } from './props';
-const msWaiting = { state: WAITING, isPaid: false };
-const msPaid = { state: PAID, isPaid: true };
-const msActive = { state: ACTIVE, isPaid: false };
-const msRejected = { state: REJECTED, isPaid: false };
+const msWaiting = { stage: IDLE };
+const msPaid = { stage: PAID };
+const msActive = { stage: ACCEPTED };
+const msRejected = { stage: REJECTED };
const trustee = 'z123';
const contributor = 'z456';
-const geometryCases = [...Array(10).keys()].map(i =>
- generateProposal({ milestoneCount: i + 1 }),
-);
+// const geometryCases = [...Array(10).keys()].map(i =>
+// generateProposal({ milestoneCount: i + 1 }),
+// );
const cases: { [index: string]: any } = {
// trustee - first
@@ -38,11 +38,7 @@ const cases: { [index: string]: any } = {
['first - not paid']: generateProposal({
amount: 5,
funded: 5,
- milestoneOverrides: [
- { state: PAID, isPaid: false },
- msWaiting,
- msWaiting,
- ],
+ milestoneOverrides: [{ stage: PAID }, msWaiting, msWaiting],
}),
// trustee - second
@@ -59,20 +55,12 @@ const cases: { [index: string]: any } = {
['second - not paid']: generateProposal({
amount: 5,
funded: 5,
- milestoneOverrides: [
- msPaid,
- { state: PAID, isPaid: false },
- msWaiting,
- ],
+ milestoneOverrides: [msPaid, { stage: PAID }, msWaiting],
}),
['second - no vote']: generateProposal({
amount: 5,
funded: 5,
- milestoneOverrides: [
- msPaid,
- { state: ACTIVE, isPaid: false },
- msWaiting,
- ],
+ milestoneOverrides: [msPaid, { stage: ACCEPTED }, msWaiting],
contributorOverrides: [{ milestoneNoVotes: [false, true, false] }],
}),
['second - rejected']: generateProposal({
@@ -95,20 +83,12 @@ const cases: { [index: string]: any } = {
['final - not paid']: generateProposal({
amount: 5,
funded: 5,
- milestoneOverrides: [
- msPaid,
- msPaid,
- { state: PAID, isPaid: false },
- ],
+ milestoneOverrides: [msPaid, msPaid, { stage: PAID }],
}),
['final - no vote']: generateProposal({
amount: 5,
funded: 5,
- milestoneOverrides: [
- msPaid,
- msPaid,
- { state: ACTIVE, isPaid: false },
- ],
+ milestoneOverrides: [msPaid, msPaid, { stage: ACCEPTED }],
contributorOverrides: [{ milestoneNoVotes: [false, true, false] }],
}),
['final - rejected']: generateProposal({
@@ -169,14 +149,14 @@ for (const key of Object.keys(cases)) {
));
}
-const geometryStories = storiesOf('Proposal/Milestones/geometry', module);
+// const geometryStories = storiesOf('Proposal/Milestones/geometry', module);
-geometryCases.forEach((gc, idx) =>
- geometryStories.add(`${idx + 1} steps`, () => (
-
- )),
-);
+// geometryCases.forEach((gc, idx) =>
+// geometryStories.add(`${idx + 1} steps`, () => (
+//
+// )),
+// );
diff --git a/frontend/stories/props.tsx b/frontend/stories/props.tsx
index 63ae1635..2842b1f1 100644
--- a/frontend/stories/props.tsx
+++ b/frontend/stories/props.tsx
@@ -1,13 +1,12 @@
import {
Contributor,
- Milestone,
- MILESTONE_STATE,
+ MILESTONE_STAGE,
Proposal,
ProposalMilestone,
STATUS,
PROPOSAL_ARBITER_STATUS,
} from 'types';
-import { PROPOSAL_CATEGORY } from 'api/constants';
+import { PROPOSAL_CATEGORY, PROPOSAL_STAGE } from 'api/constants';
import BN from 'bn.js';
import moment from 'moment';
@@ -41,7 +40,7 @@ export function generateProposal({
funded?: number;
created?: number;
deadline?: number;
- milestoneOverrides?: Array>;
+ milestoneOverrides?: Array>;
contributorOverrides?: Array>;
milestoneCount?: number;
}) {
@@ -110,15 +109,15 @@ export function generateProposal({
}
const defaults: ProposalMilestone = {
+ id: 0,
title: 'Milestone A',
content: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.`,
dateEstimated: moment().unix(),
immediatePayout: true,
index: 0,
- state: MILESTONE_STATE.WAITING,
+ stage: MILESTONE_STAGE.IDLE,
amount: amountBn,
- isPaid: false,
payoutPercent: '33',
};
return { ...defaults, ...overrides };
@@ -126,6 +125,7 @@ export function generateProposal({
const milestones = [...Array(milestoneCount).keys()].map(i => {
const overrides = {
+ id: i,
index: i,
title: genMilestoneTitle(),
immediatePayout: i === 0,
@@ -158,7 +158,7 @@ export function generateProposal({
title: 'Crowdfund Title',
brief: 'A cool test crowdfund',
content: 'body',
- stage: 'FUNDING_REQUIRED',
+ stage: PROPOSAL_STAGE.WIP,
category: PROPOSAL_CATEGORY.COMMUNITY,
isStaked: true,
arbiter: {
diff --git a/frontend/types/milestone.ts b/frontend/types/milestone.ts
index 46661931..2809503f 100644
--- a/frontend/types/milestone.ts
+++ b/frontend/types/milestone.ts
@@ -7,16 +7,31 @@ export enum MILESTONE_STATE {
PAID = 'PAID',
}
+// NOTE: sync with /backend/grand/utils/enums.py MilestoneStage
+export enum MILESTONE_STAGE {
+ IDLE = 'IDLE',
+ REQUESTED = 'REQUESTED',
+ REJECTED = 'REJECTED',
+ ACCEPTED = 'ACCEPTED',
+ PAID = 'PAID',
+}
+
export interface Milestone {
index: number;
- state: MILESTONE_STATE;
+ stage: MILESTONE_STAGE;
amount: Zat;
- isPaid: boolean;
immediatePayout: boolean;
dateEstimated: number;
+ dateRequested?: number;
+ dateRejected?: number;
+ dateAccepted?: number;
+ datePaid?: number;
+ rejectReason?: string;
+ paidTxId?: string;
}
export interface ProposalMilestone extends Milestone {
+ id: number;
content: string;
payoutPercent: string;
title: string;
diff --git a/frontend/types/proposal.ts b/frontend/types/proposal.ts
index 64b1c66c..143d9033 100644
--- a/frontend/types/proposal.ts
+++ b/frontend/types/proposal.ts
@@ -1,5 +1,5 @@
import { Zat } from 'utils/units';
-import { PROPOSAL_CATEGORY } from 'api/constants';
+import { PROPOSAL_CATEGORY, PROPOSAL_STAGE } from 'api/constants';
import { CreateMilestone, Update, User, Comment, ContributionWithUser } from 'types';
import { ProposalMilestone } from './milestone';
import { RFP } from './rfp';
@@ -36,7 +36,7 @@ export interface ProposalDraft {
brief: string;
category: PROPOSAL_CATEGORY;
content: string;
- stage: string;
+ stage: PROPOSAL_STAGE;
target: string;
payoutAddress: string;
deadlineDuration: number;
@@ -56,9 +56,12 @@ export interface Proposal extends Omit {
percentFunded: number;
contributionMatching: number;
milestones: ProposalMilestone[];
+ currentMilestone?: ProposalMilestone;
datePublished: number | null;
dateApproved: number | null;
arbiter: ProposalProposalArbiter;
+ isTeamMember?: boolean; // FE derived
+ isArbiter?: boolean; // FE derived
}
export interface TeamInviteWithProposal extends TeamInvite {