diff --git a/frontend/src/components/design-library/molecules/offers-list/offers-list.stories.tsx b/frontend/src/components/design-library/molecules/offers-list/offers-list.stories.tsx new file mode 100644 index 00000000..42919e42 --- /dev/null +++ b/frontend/src/components/design-library/molecules/offers-list/offers-list.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import OffersList from './offers-list'; + +export default { + title: 'Design Library/Molecules/OffersList', + component: OffersList, +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + offers: [ + { + User: { + username: 'username', + picture_url: 'https://via.placeholder.com/150', + name: 'name', + }, + status: 'status', + value: 100, + suggestedDate: new Date(), + }, + ], + onMessage: (id) => console.log('onMessage', id), + assigned: false, + onAccept: (id) => console.log('onAccept', id), + onReject: (id) => console.log('onReject', id), +}; \ No newline at end of file diff --git a/frontend/src/components/design-library/molecules/offers-list/offers-list.tsx b/frontend/src/components/design-library/molecules/offers-list/offers-list.tsx new file mode 100644 index 00000000..3b9270a9 --- /dev/null +++ b/frontend/src/components/design-library/molecules/offers-list/offers-list.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { + Button, + Chip, + Typography, + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, + Divider +} from '@material-ui/core'; +import MessageIcon from '@mui/icons-material/Message'; +import MomentComponent from 'moment'; +import { FormattedMessage } from 'react-intl'; + +interface OfferListProps { + offers: any; + onMessage?: any; + assigned?: boolean; + onAccept?: any; + onReject?: any; + viewMode?: boolean; +} + + +export default function OffersList({ + offers, + onMessage, + assigned, + onAccept, + onReject, + viewMode +}:OfferListProps) { + const onSendMessage = (id) => { + onMessage(id); + } + + return ( + + {offers?.map((offer) => ( + <> + + + + + + {offer?.User?.username || offer?.User?.name} + + + } + secondary={ +
+
+ + { offer?.User?.name } + + + $ {offer?.value} + + { offer?.suggestedDate && + + Finish {MomentComponent(offer?.suggestedDate).fromNow()} + + } + { offer?.comment && + + Comment:
+ {offer?.comment} +
+ } + { offer?.learn && + + + + } + { offer?.createdAt && + + { MomentComponent(offer?.createdAt).fromNow() } + + } +
+ + { !viewMode ? ( +
+ + + +
+ ) : null } +
+ } + /> +
+ + + ))} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/design-library/templates/offer-drawer/components/invite/invite-input.tsx b/frontend/src/components/design-library/templates/offer-drawer/components/invite/invite-input.tsx new file mode 100644 index 00000000..37755e6a --- /dev/null +++ b/frontend/src/components/design-library/templates/offer-drawer/components/invite/invite-input.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { FormControl, Input, InputLabel } from "@material-ui/core"; + +const EmailInviteInput = ({ + onEmailInviteChange +}) => { + return ( + + + + + + + ) +} + +export default EmailInviteInput; \ No newline at end of file diff --git a/frontend/src/components/design-library/templates/offer-drawer/components/offer-drawer-create.tsx b/frontend/src/components/design-library/templates/offer-drawer/components/offer-drawer-create.tsx new file mode 100644 index 00000000..c7edd759 --- /dev/null +++ b/frontend/src/components/design-library/templates/offer-drawer/components/offer-drawer-create.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import Introduction from '../../../molecules/introduction/introduction'; +import IssueCard from '../../../organisms/issue-card/issue-card'; +import SimpleInfo from '../../../molecules/simple-info/simple-info'; +import DeliveryDate from '../../../organisms/delivery-date/delivery-date'; +import PickupTagList from '../../../molecules/pickup-tag-list/pickup-tag-list'; +import PricePlan from '../../../organisms/price-plan/price-plan'; +import InputComment from '../../../molecules/input-comment/input-comment'; +import OfferDrawerCheckboxes from './offer/offer-drawer-checkboxes'; +import InviteInput from './invite/invite-input'; +import { makeStyles } from '@material-ui/core'; +import CheckboxTerms from '../../../molecules/checkbox-terms/checkbox-terms'; + +const useStyles = makeStyles(theme => ({ + details: { + display: 'flex', + flexDirection: 'column' + }, + spanText: { + display: 'inline-block', + verticalAlign: 'middle' + }, +})); + +interface OfferDrawerCreateProps { + introTitle: any; + introMessage: any; + introImage: any; + issue: any; + simpleInfoText: any; + commentAreaPlaceholder: any; + onDeliveryDateChange: any; + pickupTagListTitle: any; + pickutTagListDescription: any; + setCurrentPrice: any; + currentPrice: any; + onCommentChange: any; + offerCheckboxes: boolean; + onLearnCheckboxChange: any; + onConfirmOfferChange: any; + onTermsCheckboxChange: any; + onEmailInviteChange: any; + hasEmailInput?: boolean; +} + +const OfferDrawerCreate: React.FC = ({ + introTitle, + introMessage, + introImage, + issue, + simpleInfoText, + commentAreaPlaceholder, + onDeliveryDateChange, + pickupTagListTitle, + pickutTagListDescription, + setCurrentPrice, + currentPrice, + onCommentChange, + offerCheckboxes, + onLearnCheckboxChange, + onConfirmOfferChange, + onTermsCheckboxChange, + onEmailInviteChange, + hasEmailInput = false +}) => { + + const classes = useStyles(); + + return ( + <> + + + {introMessage} + + + + + {hasEmailInput && } + + setCurrentPrice(price)} + /> + , + title: , + items: [ + , + , + ], + } + } price={currentPrice} onChange={(price) => setCurrentPrice(price)} /> + + {offerCheckboxes && + + } + + + ); +}; + +export default OfferDrawerCreate; \ No newline at end of file diff --git a/frontend/src/components/design-library/templates/offer-drawer/components/offer-drawer-tabs.tsx b/frontend/src/components/design-library/templates/offer-drawer/components/offer-drawer-tabs.tsx new file mode 100644 index 00000000..5387b501 --- /dev/null +++ b/frontend/src/components/design-library/templates/offer-drawer/components/offer-drawer-tabs.tsx @@ -0,0 +1,28 @@ +import React, { useState } from 'react'; +import { Tab, Tabs } from "@material-ui/core"; + + +const OfferDrawerTabs = ({ tabs, onTabChange }) => { + const [tabValue, setTabValue] = useState(0) + + const handleChange = (event, newValue) => { + setTabValue(newValue) + onTabChange(newValue) + } + + return ( + <> + + {tabs.map((tab) => )} + + {tabs.map((tab) => tab.value === tabValue && tab.component)} + + ); +} + +export default OfferDrawerTabs; \ No newline at end of file diff --git a/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.stories.tsx b/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.stories.tsx index ad2e4c94..08740895 100644 --- a/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.stories.tsx +++ b/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.stories.tsx @@ -58,3 +58,21 @@ Primary.args = { } } }; + +export const WithTabs = Template.bind({}); +WithTabs.args = { + ...Primary.args, + offers: [ + { + User: { + username: 'username', + picture_url: 'https://via.placeholder.com/150', + name: 'name', + }, + status: 'status', + value: 100, + suggestedDate: new Date(), + }, + ], + tabs: true, +}; diff --git a/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.tsx b/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.tsx index 196262a6..a2e5e294 100644 --- a/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.tsx +++ b/frontend/src/components/design-library/templates/offer-drawer/offer-drawer.tsx @@ -1,20 +1,11 @@ import React, { useEffect } from 'react' -import { FormattedMessage } from 'react-intl' import { makeStyles } from '@material-ui/core/styles'; import Drawer from '../../molecules/drawer/drawer' - -import Introduction from '../../molecules/introduction/introduction'; -import IssueCard from '../../organisms/issue-card/issue-card'; -import SimpleInfo from '../../molecules/simple-info/simple-info'; -import DeliveryDate from '../../organisms/delivery-date/delivery-date'; -import PickupTagList from '../../molecules/pickup-tag-list/pickup-tag-list'; -import { FormControl, Input, InputLabel, Typography } from '@material-ui/core'; -import PricePlan from '../../organisms/price-plan/price-plan'; -import InputComment from '../../molecules/input-comment/input-comment'; -import OfferDrawerCheckboxes from './components/offer/offer-drawer-checkboxes'; -import OfferDrawerActions from './components/offer/offer-drawer-actions'; -import CheckboxTerms from '../../molecules/checkbox-terms/checkbox-terms'; +import OfferDrawerCreate from './components/offer-drawer-create'; +import OfferDrawerTabs from './components/offer-drawer-tabs'; +import OffersList from '../../molecules/offers-list/offers-list'; +import { AddCircleTwoTone as AddIcon } from '@material-ui/icons'; const useStyles = makeStyles(theme => ({ @@ -28,7 +19,7 @@ const useStyles = makeStyles(theme => ({ }, })); -type OfferDrawerProps = { +export type OfferDrawerProps = { title: any; introTitle: any; introMessage: any; @@ -50,6 +41,8 @@ type OfferDrawerProps = { onTermsCheckboxChange?: any; onConfirmOfferChange: any; onEmailInviteChange?: any; + tabs?: any; + offersProps?: any; } const OfferDrawer = ({ @@ -73,92 +66,79 @@ const OfferDrawer = ({ onCommentChange, onTermsCheckboxChange, onConfirmOfferChange, - onEmailInviteChange + onEmailInviteChange, + tabs, + offersProps }: OfferDrawerProps) => { const [ currentPrice, setCurrentPrice ] = React.useState(0); + const [ enableActions, setEnableActions ] = React.useState(true); const classes = useStyles(); - const emailInviteInput = () => { - if (hasEmailInput) { - return ( - - - - - - - ) + const createSection = + + + const drawerTabs = [ + { + value: 0, + label: 'Your existing offers', + default: true, + component: + }, + { + value: 1, + label: ( +
+ Make a new offer + +
+ ), + component: createSection } - } + ] useEffect(() => { onChangePrice?.(currentPrice) }, [currentPrice]) + useEffect(() => { + tabs && setEnableActions(false) + }, [tabs]) + return ( - - - {introMessage} - - - - - { emailInviteInput() } - - setCurrentPrice(price)} - /> - , - title: , - items: [ - , - , - ], - } - } price={currentPrice} onChange={(price) => setCurrentPrice(price)} /> - - { offerCheckboxes && - + {tabs ? +
+ value !== 0 ? setEnableActions(true) : setEnableActions(false)} /> +
+ :
+ {createSection} +
} -
) } diff --git a/frontend/src/components/task/offers/task-offer-drawer.tsx b/frontend/src/components/task/offers/task-offer-drawer.tsx new file mode 100644 index 00000000..e320c871 --- /dev/null +++ b/frontend/src/components/task/offers/task-offer-drawer.tsx @@ -0,0 +1,208 @@ +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import OfferDrawer, { type OfferDrawerProps } from '../../design-library/templates/offer-drawer/offer-drawer'; +import { makeStyles, Typography, } from '@material-ui/core'; +import TaskOrderInvoiceConfirm from '../task-order-invoice-confirm'; +import MessageAssignment from '../assignment/messageAssignment'; +const taskCover = require('../../../images/task-cover.png') + +const useStyles = makeStyles(theme => ({ + spanText: { + display: 'inline-block', + verticalAlign: 'middle' + }, +})); + +type TaskOfferDrawerProps = { + issue: any; + open: boolean; + onClose: any; + onMessage: any; + assigned: boolean; + updateTask: any; + offerUpdate: any; + loggedUser: any; + createOrder: any; + assignTask: any; + assigns: any; +} + +const TaskOfferDrawer = ({ + issue, + open, + onClose, + onMessage, + assigned, + updateTask, + offerUpdate, + loggedUser, + createOrder, + assignTask, + assigns +}: TaskOfferDrawerProps +) => { + const classes = useStyles(); + + const { data } = issue + const allOffers = data?.Offers || [] + const userOffers = allOffers.filter(offer => offer.User.id === loggedUser?.user?.id) || [] + const isOwner = data?.User?.id === loggedUser?.user?.id + + const [interestedSuggestedDate, setInterestedSuggestedDate] = React.useState(null); + const [currentPrice, setCurrentPrice] = React.useState(null); + const [interestedComment, setInterestedComment] = React.useState(''); + const [interestedLearn, setInterestedLearn] = React.useState(false); + const [termsAgreed, setTermsAgreed] = React.useState(false); + const [confirmOffer, setConfirmOffer] = React.useState(false); + const [confirmOrderDialog, setConfirmOrderDialog] = React.useState(false); + const [currentOffer, setCurrentOffer] = React.useState(null); + const [messageDialog, setMessageDialog] = React.useState(false); + const [ interested, setInterested ] = React.useState(null) + + const confirmAssignTaskAndCreateOrder = async (event, offer) => { + event.preventDefault() + setConfirmOrderDialog(true) + setCurrentOffer(offer) + } + + const onReject = async (event, offer) => { + event.preventDefault() + offerUpdate(data.id, offer.id, { status: 'rejected' }) + } + + const assignTaskAndCreateOrder = async (event, offer) => { + event.preventDefault() + + const assign = assigns.filter(item => item.userId === offer.userId)[0] + + await data.id && loggedUser.logged && await createOrder({ + provider: 'stripe', + amount: offer.value, + userId: loggedUser?.user?.id, + email: loggedUser?.user?.email, + taskId: data.id, + currency: 'usd', + status: 'open', + source_type: 'invoice-item', + customer_id: loggedUser?.user?.customer_id, + metadata: { + offer_id: data.id, + } + }) + await assignTask(data.id, assign.id) + await offerUpdate(data.id, offer.id, { status: 'accepted' }) + setConfirmOrderDialog(false) + setCurrentOffer(null) + } + + const openMessageDialog = (id) => { + setMessageDialog(true) + setInterested(id) + } + + const handleOfferTask = () => { + updateTask({ + id: data.id, + Offer: { + userId: loggedUser?.user?.id, + suggestedDate: interestedSuggestedDate, + value: currentPrice, + learn: interestedLearn, + comment: interestedComment + } + }) + onClose() + } + + return ( + <> + setMessageDialog(false) } + id={ data.id } + to={ interested } + messageAction={ onMessage } + /> + setConfirmOrderDialog(false)} + onConfirm={(event) => assignTaskAndCreateOrder(event, currentOffer)} + offer={currentOffer} + /> + } + introTitle={ + + } + introMessage={ + + {(msg) => ( + + {msg} + + )} + + } + pickupTagListTitle={ + + + + } + pickutTagListDescription={ + + + + } + simpleInfoText={ + + {(msg) => ( + + {msg} + + )} + + } + commentAreaPlaceholder={ + + } + introImage={taskCover} + issue={issue} + open={open} + onClose={onClose} + actions={ + [ + { + label: , + onClick: onClose + }, + { + label: , + onClick: handleOfferTask, + variant: 'contained', + color: 'secondary', + disabled: !confirmOffer || !termsAgreed || !currentPrice || currentPrice === 0 + } + ] + } + onDeliveryDateChange={(date) => setInterestedSuggestedDate(date)} + onChangePrice={(price) => setCurrentPrice(price)} + onLearnCheckboxChange={(checked) => setInterestedLearn(checked)} + onTermsCheckboxChange={(checked) => setTermsAgreed(checked)} + onConfirmOfferChange={(checked) => setConfirmOffer(checked)} + onCommentChange={(e) => setInterestedComment(e.target.value)} + tabs={isOwner ? !!allOffers.length : !!userOffers.length} + offersProps={{ + offers: isOwner ? allOffers : userOffers, + onMessage: (id) => openMessageDialog(id), + assigned: assigned, + onAccept: (event, offer) => confirmAssignTaskAndCreateOrder(event, offer), + onReject: (event, offer) => onReject(event, offer), + viewMode: data?.User?.id !== loggedUser?.user?.id + }} + /> + + ); +} + +export default TaskOfferDrawer; \ No newline at end of file diff --git a/frontend/src/components/task/task-payment.js b/frontend/src/components/task/task-payment.js index 7b38e4e6..15fdb673 100644 --- a/frontend/src/components/task/task-payment.js +++ b/frontend/src/components/task/task-payment.js @@ -111,16 +111,16 @@ const StyledTab = withStyles({ flexDirection: 'row', alignItems: 'inherit', }, - svgIcon: { + svgIcon: { root: { width: 16, height: 16, }, - } + } })(Tab); class TaskPayment extends Component { - constructor (props) { + constructor(props) { super(props) this.state = { currentTab: 0, @@ -132,7 +132,7 @@ class TaskPayment extends Component { } } - componentDidMount () { + componentDidMount() { } @@ -167,13 +167,13 @@ class TaskPayment extends Component { return possibles[status] } - render () { + render() { const { classes, orders, offers, ...other } = this.props const TabContainer = props => { return ( - - { props.children } + + {props.children} ) } @@ -189,7 +189,7 @@ class TaskPayment extends Component { const assignTaskAndCreateOrder = async (event, offer) => { const { task, loggedUser, createOrder, assignTask, assigns } = this.props event.preventDefault() - + const assign = this.props.assigns.filter(item => item.userId === offer.userId)[0] await task.id && loggedUser.logged && await createOrder({ @@ -233,133 +233,133 @@ class TaskPayment extends Component { return ( - + - { this.props.paid && ( + {this.props.paid && ( - ) } + )}
- + } + icon={} /> } + + value={1} + label={this.props.intl.formatMessage(messages.creditCardPayment)} + icon={} /> } + + value={2} + label={this.props.intl.formatMessage(messages.payPalPayment)} + icon={} /> - - { (this.props.transferId || this.props.task?.Transfer) ? ( + + {(this.props.transferId || this.props.task?.Transfer) ? ( - - } + + } > - + {this.props.transferId ? - { `${this.props.transferId}` } + {`${this.props.transferId}`} - : + :
+ /> - +
}
) : - { orders.length > 0 ? orders.map((order, index) => ( + {orders.length > 0 ? orders.map((order, index) => (
- { order.provider === 'paypal' + {order.provider === 'paypal' ? ( - + - + - { !order.transfer_id + {!order.transfer_id ? ( - + ) : ( - - { (msg) => ( - - ) } + }} > + {(msg) => ( + + )} ) } ) : ( - + - + - - {`$ ${order.amount}`} - - - {order?.User ? 'by ' + order?.User?.name : ''} - -
} - secondary={ `${this.statuses(order.status) + ' ' + MomentComponent(order.createdAt).fromNow() || this.props.intl.formatMessage(messages.labelCreditCard)}` } + primary={ +
+ + {`$ ${order.amount}`} + + + {order?.User ? 'by ' + order?.User?.name : ''} + +
} + secondary={`${this.statuses(order.status) + ' ' + MomentComponent(order.createdAt).fromNow() || this.props.intl.formatMessage(messages.labelCreditCard)}`} /> - + ) } @@ -370,111 +370,91 @@ class TaskPayment extends Component {
- ) } - } + )} + } - -
- { (!this.props.paid || this.props.task?.Transfer?.id) ? ( -
- { this.props.assigned ? - : null - } - { this.props?.assigns?.length > 0 ? -
- - - - await this.props.assignTask(this.props.id, id)} - onReject={async (id) => await this.props.actionAssign(this.props.id, id, false)} - /> -
: null - } - { offers?.length ? -
- - - - openMessageDialog(id, 'offers') } - onAccept={(event, offer) => confirmAssignTaskAndCreateOrder(event, offer)} - onReject={(event, offer) => onReject(event, offer)} - /> - this.setState({ confirmOrderDialog: false })} - onConfirm={(event) => assignTaskAndCreateOrder(event, this.state.currentOffer)} - offer={this.state.currentOffer} - /> -
: null - } - -
- ) : ( -
- -
- ) } -
- + +
+ {(!this.props.paid || this.props.task?.Transfer?.id) ? ( +
+ {this.props.assigned ? + : null + } + {this.props?.assigns?.length > 0 ? +
+ + + + await this.props.assignTask(this.props.id, id)} + onReject={async (id) => await this.props.actionAssign(this.props.id, id, false)} + /> +
: null + } + +
+ ) : ( +
+ +
+ )} +
+ - { hasOrders() ? ( + {hasOrders() ? (
- { !this.props.paid && ( + {!this.props.paid && ( - ) } + )}
- ) : null } - { !this.props.paid ? ( + ) : null} + {!this.props.paid ? ( ) : ( - ) } + )}
) diff --git a/frontend/src/components/task/task.js b/frontend/src/components/task/task.js index 6b4bf689..97de15c8 100644 --- a/frontend/src/components/task/task.js +++ b/frontend/src/components/task/task.js @@ -59,6 +59,7 @@ import TaskPaymentForm from './task-payment-form' import TaskPayments from './task-payments' import TaskLevelSplitButton from './task-level-split-button' import TaskDeadlineForm from './task-deadline-form' +import TaskOfferDrawer from './offers/task-offer-drawer' import TaskStatusIcons from './task-status-icons' @@ -751,8 +752,6 @@ class Task extends Component { - -