diff --git a/api/models/index.js b/api/models/index.js index 39b2a370..6d9bf9e4 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -56,7 +56,7 @@ const associations = ({ Allocation, Client, Contributor, Issue, Payment, Permiss Contributor.hasMany(Permission, { foreignKey: 'contributor_id' }) Payment.hasMany(Allocation, { foreignKey: 'payment_id' }) Project.hasMany(Allocation, { foreignKey: 'project_id' }) - Project.belongsTo(Client, { foreignKey: 'client_id' }) + Client.hasMany(Project, { foreignKey: 'client_id' }) Project.hasMany(Permission, { foreignKey: 'project_id' }) Issue.belongsTo(Project, { foreignKey: 'project_id' }) Rate.hasMany(Allocation, { foreignKey: 'rate_id' }) diff --git a/api/modules/dataSyncs.js b/api/modules/dataSyncs.js index 3a9a01c1..a82f539e 100644 --- a/api/modules/dataSyncs.js +++ b/api/modules/dataSyncs.js @@ -93,8 +93,6 @@ const dataSyncs = module.exports = (() => { const syncInvoicelyCSV = async () => { const invoiceFile = INVOICELY_CSV_PATH - console.log('invoiceFile') - console.log(invoiceFile) return ( amazon.fetchFile({ file: invoiceFile }) .then(file => { diff --git a/api/schema/resolvers/ContributorResolver.js b/api/schema/resolvers/ContributorResolver.js index a3e21f0b..33f70eb2 100644 --- a/api/schema/resolvers/ContributorResolver.js +++ b/api/schema/resolvers/ContributorResolver.js @@ -1,8 +1,39 @@ const { AuthenticationError } = require('apollo-server') +const { col, fn } = require('sequelize') + const toggl = require('../../handlers/toggl') module.exports = { Contributor: { + paidByCurrency: async (contributor, args, { models }) => { + const totalPaidByCurrencyQuery = await models.Client.findAll({ + group: 'currency', + attributes: ['currency'], + raw: true, + include: { + model: models.Project, + attributes: [], + required: true, + include: { + model: models.Allocation, + attributes: [[fn('sum', col('amount')), 'amount']], + where: { + contributor_id: contributor.id + }, + } + } + }) + totalPaidByCurrency = [] + totalPaidByCurrencyQuery.map(t => { + totalPaidByCurrency.push( + { + currency: t['currency'], + amount: t['Projects.Allocations.amount'] + } + ) + }) + return totalPaidByCurrency + }, permissions: (contributor, args, { models }) => { return models.Permission.findAll({ where: { @@ -17,7 +48,7 @@ module.exports = { } }) }, - total_paid: async (contributor, args, { models }) => { + totalPaid: async (contributor, args, { models }) => { const totalPaid = await models.Allocation.sum('amount', { where: { contributor_id: contributor.id diff --git a/api/schema/resolvers/ProjectResolver.js b/api/schema/resolvers/ProjectResolver.js index 2a41ab06..26a6c60c 100644 --- a/api/schema/resolvers/ProjectResolver.js +++ b/api/schema/resolvers/ProjectResolver.js @@ -1,7 +1,7 @@ const moment = require('moment') -const sequelize = require('sequelize'); +const sequelize = require('sequelize') const { ApolloError } = require('apollo-server') -const { col, fn, Op } = require('sequelize'); +const { col, fn, Op } = require('sequelize') const { split } = require('lodash') const { GITHUB, TOGGL } = require('../../config/credentials'); @@ -9,7 +9,7 @@ const github = require('../../handlers/github') const toggl = require('../../handlers/toggl') const { validateDatesFormat } = require('../helpers/inputValidation') const { dataSyncs } = require('../../modules') -const apiModules = require('../../modules'); +const apiModules = require('../../modules') module.exports = { @@ -132,7 +132,7 @@ module.exports = { } ) || 0 return { - fromPayments: parseInt( totalPaidFromClient / totalIssues, 10), + fromPayments: parseInt(totalPaidFromClient / totalIssues, 10), fromAllocations: parseInt(totalPaidFromAllocation / totalIssues, 10) } }, diff --git a/api/schema/types/ContributorType.js b/api/schema/types/ContributorType.js index 63637d44..1789e075 100644 --- a/api/schema/types/ContributorType.js +++ b/api/schema/types/ContributorType.js @@ -5,7 +5,6 @@ module.exports = gql` type Contributor { id: Int! toggl_id: Int - total_paid: Int! name: String! external_data_url: String github_id: String @@ -15,6 +14,9 @@ module.exports = gql` permissions: [Permission] timeEntries: [TimeEntry] rates: [Rate] + "The following attributes are calculated and aren't on the database" + totalPaid: Int! + paidByCurrency: [TotalAllocatedByCurrency] } input CreateContributorInput { @@ -38,6 +40,11 @@ module.exports = gql` github_handle: String } + type TotalAllocatedByCurrency { + amount: Int + currency: String + } + type Query { checkSession: Contributor getContributorById(id: Int!): Contributor diff --git a/api/schema/types/ProjectType.js b/api/schema/types/ProjectType.js index 154b582e..484aa929 100644 --- a/api/schema/types/ProjectType.js +++ b/api/schema/types/ProjectType.js @@ -54,10 +54,11 @@ module.exports = gql` toDate: String, confirmedOnly: Boolean ): Int + "The total paid from the client" totalPaid( fromDate: String, toDate: String - ): Int # The total paid from the client, takes start date and ed date as attributes + ): Int totalIncurredPayments: Int # The total incurred not paid from the client } diff --git a/src/components/AllocationAddForm.js b/src/components/AllocationAddForm.js index edcd2e90..63e91be5 100644 --- a/src/components/AllocationAddForm.js +++ b/src/components/AllocationAddForm.js @@ -108,7 +108,7 @@ const AllocationAddForm = (props) => { rate_id: allocationRate.id } }) - if (loadingNewAllocation) return loading... + if (loadingNewAllocation) return '' else if (allocationCreated.errors) { console.log('Error adding the allocation'); } else { @@ -231,17 +231,6 @@ const AllocationAddForm = (props) => { variables: { contributorId: contributor ? contributor.id : null, projectId: project ? project.id : null - - } - }, { - query: GET_PAYMENT_ALLOCATIONS, - variables: { - paymentId: selectedPayment ? selectedPayment.id : null - } - }, { - query: GET_PAYMENT_TOTAL_ALLOCATED, - variables: { - paymentId: selectedPayment ? selectedPayment.id : null } }, { query: GET_PROJECT_PAYMENTS, @@ -370,8 +359,7 @@ const AllocationAddForm = (props) => { const activeContributors = activeAllocations.map(a => { return a.contributor }) - - if (!rateCurrency) { + if (!rateCurrency && clientCurrency) { setRateCurrency(clientCurrency) } @@ -495,6 +483,7 @@ const AllocationAddForm = (props) => { { { - { - c.github_access_token && + {c.github_access_token && } diff --git a/src/components/AllocationOverview.js b/src/components/AllocationOverview.js new file mode 100644 index 00000000..156ab59a --- /dev/null +++ b/src/components/AllocationOverview.js @@ -0,0 +1,290 @@ +import React, { useEffect, useState } from 'react' +import { useMutation, useQuery, useLazyQuery } from '@apollo/client' +import { + Box, + Button, + Dialog, + DialogTitle, + Grid, + Typography +} from '@material-ui/core/' +import DeleteOutlinedIcon from '@material-ui/icons/DeleteOutlined' +import moment from 'moment' +import { findKey } from 'lodash' + +import DeleteConfirmationDialog from './DeleteConfirmationDialog' +import EditAllocationInfo from './EditAllocationInfo' +import EditAllocationRate from './EditAllocationRate' +import LoadingProgress from './LoadingProgress' +import { GET_ALLOCATIONS, GET_ALLOCATION_INFO } from '../operations/queries/AllocationQueries' +import { GET_CLIENT_PAYMENTS } from '../operations/queries/ClientQueries' +import { GET_CONTRIBUTORS, GET_CONTRIBUTOR_ALLOCATIONS, GET_CONTRIBUTOR_RATES } from '../operations/queries/ContributorQueries' +import { GET_PAYMENT_ALLOCATIONS } from '../operations/queries/PaymentQueries' +import { GET_PROJECT_CONTRIBUTORS, GET_PROJECT_PAYMENTS } from '../operations/queries/ProjectQueries' +import { DELETE_ALLOCATION, UPDATE_ALLOCATION } from '../operations/mutations/AllocationMutations' +import { CREATE_RATE } from '../operations/mutations/RateMutations' + +const AllocationOverview = (props) => { + + const { + allocationInfo, + onClose, + open + } = props + + const [clientPayments, setClientPayments] = useState(null) + const [contributorAllocation, setContributorAllocation] = useState(null) + const [contributorRates, setContributorRates] = useState(null) + const [updatedAllocationPayment, setUpdatedAllocationPayment] = useState(null) + const [updatedAllocationRate, setUpdatedAllocationRate] = useState({}) + const [openDeleteAllocation, setOpenDeleteAllocation] = useState(false) + const [selectedCurrency, setSelectedCurrency] = useState(null) + const [updatedAllocationStartDate, setUpdatedAllocationStartDate] = useState(null) + const [updatedAllocationEndDate, setUpdatedAllocationEndDate] = useState(null) + + const { + data: dataAllocation, + error: errorAllocation, + loading: loadingAllocation + } = useQuery(GET_ALLOCATION_INFO, { + fetchPolicy: 'cache-and-network', + variables: { + id: allocationInfo.id + } + }) + + const [getClientPayments, { + data: dataClientPayments, + loading: loadingClientPayments, + error: errorClientPayments + }] = useLazyQuery(GET_CLIENT_PAYMENTS, { + onCompleted: dataClientPayments => { + setClientPayments(dataClientPayments.getClientById) + } + }) + + const [getContributorRates, { + data: dataContributorRates, + loading: loadingContributorRates, + error: errorContributorRates + }] = useLazyQuery(GET_CONTRIBUTOR_RATES, { + onCompleted: dataContributorRates => { + setContributorRates(dataContributorRates.getContributorById) + } + }) + + const [createRate, { + dataNewRate, + loadingNewRate, + errorNewRate + }] = useMutation(CREATE_RATE) + + const [deleteAllocation, { + dataDeletedPayment, + loadingDeletedPayment, + errorDeletedPayment + }] = useMutation(DELETE_ALLOCATION, { + variables: { + id: allocationInfo.id + }, + refetchQueries: [{ + query: GET_ALLOCATIONS, + variables: { + projectId: contributorAllocation ? contributorAllocation.project.id : null, + contributorId: contributorAllocation ? contributorAllocation.contributor.id : null + } + }, { + query: GET_PROJECT_CONTRIBUTORS, + variables: { + id: contributorAllocation ? contributorAllocation.project.id : null + } + }] + }) + + const [updateAllocation, { + dataUpdatedAllocation, + loadingUpdatedAllocation, + errorUpdatedAllocation + }] = useMutation(UPDATE_ALLOCATION, { + refetchQueries: [{ + query: GET_CONTRIBUTOR_ALLOCATIONS, + variables: { + id: contributorAllocation ? contributorAllocation.contributor.id : null + } + }] + }) + + useEffect(() => { + if (contributorAllocation) { + getClientPayments({ + variables: { + clientId: contributorAllocation.project.client.id + } + }) + getContributorRates({ + variables: { + id: contributorAllocation.contributor.id + } + }) + setUpdatedAllocationEndDate(moment(allocation.end_date, 'x')['_d']) + setUpdatedAllocationPayment(contributorAllocation.payment) + setUpdatedAllocationStartDate(moment(allocation.start_date, 'x')['_d']) + } + }, [contributorAllocation]) + + const handleClose = () => { + setContributorAllocation(null) + onClose() + } + + const handleDeleteAllocation = async () => { + const paymentDeleted = await deleteAllocation() + onClose() + } + + const handleUpdateAllocation = async ({ + allocation, + contributor, + contributorRates, + endDate, + payment, + rate, + startDate, + }) => { + //look for rate with same values + const selectedRate = {} + const existingRate = findKey( + contributorRates, + { + 'hourly_rate': rate.hourly_rate.toString(), + 'total_expected_hours': Number(rate.total_expected_hours), + 'type': rate.type, + 'currency': selectedCurrency + } + ) + if (existingRate) { + selectedRate.id = contributorRates[existingRate].id + } else { + //create rate + const newRate = await createRate({ + variables: { + hourly_rate: rate.hourly_rate.toString(), + total_expected_hours: Number(rate.total_expected_hours), + type: rate.type, + currency: selectedCurrency, + contributor_id: contributor.id + } + }) + selectedRate.id = newRate.data.createRate.id + } + //update allocation with that rate id + try { + const updatedAllocation = await updateAllocation({ + variables: { + id: allocation.id, + amount: Number(rate.total_amount), + start_date: moment(startDate).format('YYYY-MM-DD'), + end_date: moment(endDate).format('YYYY-MM-DD'), + date_paid: null, + rate_id: Number(selectedRate.id), + payment_id: payment ? payment.id : null + } + }) + if (loadingUpdatedAllocation) return '' + else if (updatedAllocation.errors) { + throw updatedAllocation.errors + } else { + onClose() + } + } catch (error) { + console.log(`error ${error}`); + onClose() + } + + } + + if (loadingAllocation || loadingClientPayments) return + if (errorAllocation || errorClientPayments) return `Error` + + const { getAllocationById: allocation } = dataAllocation + const payments = [null] + if (clientPayments) { + payments.unshift(...clientPayments.payments) + } + + if (!contributorAllocation) { + setContributorAllocation(allocation) + } + + return ( + handleClose()} + open={open} + > + + {`Allocation Detail`} + + + + +
+
+ + + + + + + + + + + + handleDeleteAllocation()} + deleteItem={`allocation`} + open={openDeleteAllocation} + onClose={() => setOpenDeleteAllocation(false)} + /> +
+
+ ) +} + +export default AllocationOverview diff --git a/src/components/AllocationTile.js b/src/components/AllocationTile.js index caac03e1..cca77dcf 100644 --- a/src/components/AllocationTile.js +++ b/src/components/AllocationTile.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { Box, Card, @@ -9,6 +9,7 @@ import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet' import accounting from 'accounting-js' import moment from 'moment' +import AllocationOverview from './AllocationOverview' import { formatAmount, selectCurrencyInformation @@ -21,8 +22,10 @@ const AllocationTile = (props) => { allocation } = props + const [openAllocationOverview, setOpenAllocationOverview] = useState(false) + const currencyInformation = selectCurrencyInformation({ - currency: 'USD' + currency: allocation.rate.currency }) const paymentAmount = formatAmount({ amount: allocation.amount / 100, @@ -33,8 +36,12 @@ const AllocationTile = (props) => { return ( - - + + setOpenAllocationOverview(true)}> @@ -101,6 +108,11 @@ const AllocationTile = (props) => { + setOpenAllocationOverview(false)} + open={openAllocationOverview} + /> ) diff --git a/src/components/ContributorAllocations.js b/src/components/ContributorAllocations.js index 30c82cf8..1732e607 100644 --- a/src/components/ContributorAllocations.js +++ b/src/components/ContributorAllocations.js @@ -6,11 +6,16 @@ import { Grid, Typography } from '@material-ui/core' +import { isEmpty } from 'lodash' import AllocationAddForm from './AllocationAddForm' import AllocationTile from './AllocationTile' +import EmptyState from './EmptyState' import LoadingProgress from './LoadingProgress' -import { GET_CONTRIBUTOR_ALLOCATIONS, GET_CONTRIBUTOR_INFO } from '../operations/queries/ContributorQueries' +import { + GET_CONTRIBUTOR_ALLOCATIONS, + GET_CONTRIBUTOR_INFO +} from '../operations/queries/ContributorQueries' import { white } from '../styles/colors.scss' const ContributorAllocations = (props) => { @@ -88,7 +93,16 @@ const ContributorAllocations = (props) => { - {renderAllocations({ allocations: contributorAllocations.allocations })} + { + !isEmpty(contributorAllocations.allocations) + ? renderAllocations({ allocations: contributorAllocations.allocations }) + : ( + + ) + } diff --git a/src/components/ContributorInfoTile.js b/src/components/ContributorInfoTile.js index bee40bf4..a7ce9d81 100644 --- a/src/components/ContributorInfoTile.js +++ b/src/components/ContributorInfoTile.js @@ -6,6 +6,11 @@ import { CardActions, CardContent, Grid, + Icon, + List, + ListItem, + ListItemAvatar, + ListItemText, Typography } from '@material-ui/core' import CheckCircleIcon from '@material-ui/icons/CheckCircle' @@ -45,15 +50,39 @@ const ContributorInfoTile = (props) => { currency: 'USD' }) const paymentAmount = formatAmount({ - amount: contributor.total_paid / 100, + amount: contributor.totalPaid / 100, currencyInformation: currencyInformation }) + const renderPaidToContributorByCurrency = (props) => { + const { + paidByCurrency + } = props + + return paidByCurrency.map(p => { + const currencyInformation = selectCurrencyInformation({ + currency: p.currency + }) + const paymentAmount = formatAmount({ + amount: p.amount / 100, + currencyInformation: currencyInformation + }) + return ( + + + + + + + ) + }) + } + return ( - + @@ -62,10 +91,7 @@ const ContributorInfoTile = (props) => { - + @@ -76,19 +102,28 @@ const ContributorInfoTile = (props) => { - - - {`Total paid from allocations: ${paymentAmount}`} - - + + + + + + {`Total paid from allocations:`} + + + + {renderPaidToContributorByCurrency({ paidByCurrency: contributor.paidByCurrency })} + + + + ) diff --git a/src/components/ContributorProjectsCollab.js b/src/components/ContributorProjectsCollab.js index f61661dc..31553689 100644 --- a/src/components/ContributorProjectsCollab.js +++ b/src/components/ContributorProjectsCollab.js @@ -6,9 +6,10 @@ import { Grid, Typography } from '@material-ui/core' -import { find } from 'lodash' +import { find, isEmpty } from 'lodash' import moment from 'moment' +import EmptyState from './EmptyState' import LoadingProgress from './LoadingProgress' import ProjectTile from './ProjectTile' import { GET_CONTRIBUTOR_PROJECTS } from '../operations/queries/ContributorQueries' @@ -31,8 +32,6 @@ const ContributorProjectsCollab = (props) => { const renderProjects = ({ propjects }) => { return projects.map(p => { - console.log('p'); - console.log(p); return ( @@ -68,11 +67,20 @@ const ContributorProjectsCollab = (props) => { - {`Project Involvment`} + {`Project Involvement`} - {renderProjects({ projects: projects })} + { + !isEmpty(projects) + ? renderProjects({ projects: projects }) + : ( + + ) + } diff --git a/src/components/ContributorTile.js b/src/components/ContributorTile.js index 8453d5c8..62ef8071 100644 --- a/src/components/ContributorTile.js +++ b/src/components/ContributorTile.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { useQuery } from '@apollo/client' import { Accordion, @@ -15,6 +15,7 @@ import moment from 'moment' import { filter, sortBy } from 'lodash' import { useHistory } from 'react-router-dom' +import AllocationOverview from './AllocationOverview' import { GET_ALLOCATIONS } from '../operations/queries/AllocationQueries' import { formatAmount, @@ -32,9 +33,16 @@ const ContributorTile = (props) => { } = props const history = useHistory() + const [openAllocationOverview, setOpenAllocationOverview] = useState(false) + const [selectedAllocation, setSelectedAllocation] = useState(null) + const handleAddButton = () => { onAddButton({ contributor }) } + const handleAllocationOverview = ({ value, allocation }) => { + setSelectedAllocation(allocation) + setOpenAllocationOverview(value) + } const redirectToContributorDetails = ({ contributor }) => { history.push(`/contributor/${contributor.id}`) } @@ -65,7 +73,7 @@ const ContributorTile = (props) => { return sortedAllocations.map(a => { const currencyInformation = selectCurrencyInformation({ - currency: a.payment ? a.payment.client.currency : '' + currency: a.rate.currency }) const allocationAmount = formatAmount({ amount: a.amount / 100, @@ -78,7 +86,11 @@ const ContributorTile = (props) => { const isActiveAllocation = moment(a.start_date, 'x').isBefore(moment()) return ( - + handleAllocationOverview({ value: true, allocation: a })} + >
@@ -234,6 +246,13 @@ const ContributorTile = (props) => { + {selectedAllocation && + handleAllocationOverview({ value: false })} + open={openAllocationOverview} + /> + } ) diff --git a/src/components/DeletePayment.js b/src/components/DeletePayment.js index 41a28e18..a01f9d3d 100644 --- a/src/components/DeletePayment.js +++ b/src/components/DeletePayment.js @@ -44,7 +44,6 @@ const DeletePayment = (props) => { handleDeletePayment()} deleteItem={`payment`} - payment={payment} open={open} onClose={onClose} /> diff --git a/src/components/EditAllocationInfo.js b/src/components/EditAllocationInfo.js new file mode 100644 index 00000000..099b2834 --- /dev/null +++ b/src/components/EditAllocationInfo.js @@ -0,0 +1,173 @@ +import React, { useEffect, useState } from 'react' +import { + Collapse, + Grid, + List, + ListItem, + ListItemText, + Typography +} from '@material-ui/core/' +import PaymentIcon from '@material-ui/icons/Payment' +import { + ExpandLess, + ExpandMore +} from '@material-ui/icons' +import moment from 'moment' +import { + differenceWith, + findIndex, + isEqual +} from 'lodash' + +import { formatAmount, selectCurrencyInformation } from '../scripts/selectors' + +const EditAllocationInfo = (props) => { + + const { + allocation, + payments, + selectedPayment, + setSelectedPayment + } = props + + const currencyInformation = selectCurrencyInformation({ + currency: allocation.project.client.currency + }) + + const [openPayments, setOpenPayments] = useState(false) + + const handleClickPayments = () => { + setOpenPayments(!openPayments) + } + const onClickPayment = (payment) => { + if (payment) { + setSelectedPayment(payment) + } else { + setSelectedPayment(null) + } + setOpenPayments(false) + } + + const paymentAmount = ( + selectedPayment + ? formatAmount({ + amount: selectedPayment.amount / 100, + currencyInformation: currencyInformation + }) + : 'Proposed' + ) + const datePaid = ( + selectedPayment + ? selectedPayment.date_paid + ? moment(selectedPayment.date_paid, 'x').format('MM/DD/YYYY') + : selectedPayment.date_incurred + ? 'Warning: This payment has not been paid' + : 'Proposed' + : '' + ) + + const listPayments = (payments) => { + const paymentsList = differenceWith(payments, [selectedPayment], isEqual) + return paymentsList.map(payment => { + const paymentAmount = ( + payment + ? (formatAmount({ + amount: payment.amount / 100, + currencyInformation: currencyInformation + })) + : null + ) + return ( + + onClickPayment(payment)}> + + + + + + + {`${payment + ? payment.date_paid + ? moment(payment.date_paid, 'x').format('MM/DD/YYYY') + : 'Not paid yet' + : 'Proposed' + }`} + + + + + + + ) + }) + } + + return ( + + + + {`Project`} + + + {`${allocation.project.name}`} + + + + + {`Contributor`} + + {`${allocation.contributor.name}`} + + + + + + {`Client`} + + + {`${allocation.project.client.name}`} + + + + + {`Payment`} + + + + + + + + + + + {openPayments + ? + : + } + + + + + {payments.length > 1 && + listPayments(payments) + } + + + + {`Date paid: ${datePaid}`} + + + + ) +} + +export default EditAllocationInfo diff --git a/src/components/EditAllocationRate.js b/src/components/EditAllocationRate.js new file mode 100644 index 00000000..06534bc6 --- /dev/null +++ b/src/components/EditAllocationRate.js @@ -0,0 +1,131 @@ +import React, { useState } from 'react' +import { useMutation, useQuery } from '@apollo/client' +import { + Box, + Button, + ButtonGroup, + Grid, + Typography +} from '@material-ui/core/' +import moment from 'moment' +import DatePicker from 'react-datepicker' +import { findKey } from 'lodash' + +import LoadingProgress from './LoadingProgress' +import RateProratedMonthlyForm from './RateProratedMonthlyForm' +import RateMaxBudgetForm from './RateMaxBudgetForm' +import { + GET_CONTRIBUTOR_ALLOCATIONS, + GET_CONTRIBUTOR_RATES +} from '../operations/queries/ContributorQueries' +import { CREATE_RATE } from '../operations/mutations/RateMutations' +import { UPDATE_ALLOCATION } from '../operations/mutations/AllocationMutations' + +const EditAllocationRate = (props) => { + + const { + allocation, + currency, + endDate, + rate, + selectedPayment, + setEndDate, + setNewAllocationRate, + setSelectedCurrency, + setStartDate, + startDate + } = props + + const [selectedRateType, setSelectedRateType] = useState(rate.type) + + // console.log('selectedPayment'); + // console.log(selectedPayment); + + const getRangedTimeEntries = (dates) => { + const [start, end] = dates + setStartDate(start) + setEndDate(end) + } + + return ( + + + + + + + + + + getRangedTimeEntries(date)} + customInput={ + + {`${ + startDate + ? moment(startDate).format('MM/DD/YYYY') + : 'Start date' + } - ${ + endDate + ? moment(endDate).format('MM/DD/YYYY') + : ' End date' + }`} + + } + /> + + + {selectedRateType == 'prorated_monthly' + ? ( + + ) : ( + + ) + } + + ) +} + +export default EditAllocationRate diff --git a/src/components/EmptyState.js b/src/components/EmptyState.js new file mode 100644 index 00000000..9808e346 --- /dev/null +++ b/src/components/EmptyState.js @@ -0,0 +1,30 @@ +import React, { useState } from 'react'; +import { + Box, + Grid, + Icon, + Typography +} from '@material-ui/core' +import GroupAddIcon from '@material-ui/icons/GroupAdd'; + +const EmptyState = (props) => { + const { + description, + iconClassname + } = props + + return ( + + + + {`${description}`} + + + + + + + ) +} + +export default EmptyState diff --git a/src/components/PaymentTile.js b/src/components/PaymentTile.js index 696ffadc..b34f4768 100644 --- a/src/components/PaymentTile.js +++ b/src/components/PaymentTile.js @@ -16,6 +16,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore' import MonetizationOnIcon from '@material-ui/icons/MonetizationOn' import AllocationAddForm from './AllocationAddForm' +import AllocationOverview from './AllocationOverview' import DeletePayment from './DeletePayment' import PaymentEditDialog from './PaymentEditDialog' import { @@ -26,7 +27,6 @@ import { formatAmount, selectCurrencyInformation } from '../scripts/selectors' - import { red } from '../styles/colors.scss' const PaymentTile = (props) => { @@ -58,13 +58,19 @@ const PaymentTile = (props) => { const [paymentClicked, setPaymentClicked] = useState(null) const [openAddAllocationDialog, setOpenAddAllocationDialog] = useState(false) + const [openAllocationOverview, setOpenAllocationOverview] = useState(false) const [openDeletePayment, setOpenDeletePayment] = useState(false) const [openEditPayment, setOpenEditPayment] = useState(false) + const [selectedAllocation, setSelectedAllocation] = useState(null) const addAllocation = (props) => { setOpenAddAllocationDialog(true) setPaymentClicked(props.payment) } + const handleAllocationClicked = ({ value, allocation }) => { + setSelectedAllocation(allocation) + setOpenAllocationOverview(value) + } const handleAddAllocationClose = () => { setOpenAddAllocationDialog(false) } @@ -99,7 +105,7 @@ const PaymentTile = (props) => { currencyInformation } = props - return allocations.map(a => { + return allocations.map((a, i) => { const { amount, contributor, @@ -113,7 +119,10 @@ const PaymentTile = (props) => { return ( - + handleAllocationClicked({ value: true, allocation: a })} + > {`${contributor.name}`} @@ -160,9 +169,7 @@ const PaymentTile = (props) => { - { - `${paymentAmount}` - } + {`${paymentAmount}`} @@ -171,10 +178,9 @@ const PaymentTile = (props) => { align='left' color='secondary' > - { - `${paymentHasBeenMade - ? formattedDatePaid - : formattedDateIncurred}` + {`${paymentHasBeenMade + ? formattedDatePaid + : formattedDateIncurred}` } @@ -219,7 +225,6 @@ const PaymentTile = (props) => { > - @@ -264,6 +269,13 @@ const PaymentTile = (props) => { open={openEditPayment} onClose={() => handleEditPayment(false)} /> + {selectedAllocation && + handleAllocationClicked(false)} + open={openAllocationOverview} + /> + } { }, []) useEffect(() => { - if (githubContributors.length) { - setContributors(contributors.concat(...githubContributors)) + try { + if (githubContributors.length) { + setContributors(contributors.concat(...githubContributors)) + } + } catch (error) { + console.log(`error: ${error}`) } }, [githubContributors]) diff --git a/src/components/ProjectPayments.js b/src/components/ProjectPayments.js index 399e191d..03d51cc6 100644 --- a/src/components/ProjectPayments.js +++ b/src/components/ProjectPayments.js @@ -10,7 +10,7 @@ import { } from '@material-ui/core' import AddIcon from '@material-ui/icons/Add' import MonetizationOnIcon from '@material-ui/icons/MonetizationOn' -import { filter, orderBy } from 'lodash' +import { filter, isEmpty, orderBy } from 'lodash' import LoadingProgress from './LoadingProgress' import PaymentsEmptyState from './PaymentsEmptyState' @@ -100,13 +100,13 @@ const ProjectPayments = (props) => { project={getProjectById} currencyInformation={currencyInformation} /> - {allocatedPayments.length != 0 && + {!isEmpty(allocatedPayments) && } - {proposedAllocations.length && + {!isEmpty(proposedAllocations) && ( diff --git a/src/components/ProjectProposedAllocationsTile.js b/src/components/ProjectProposedAllocationsTile.js index 6619dcf9..6519d6b3 100644 --- a/src/components/ProjectProposedAllocationsTile.js +++ b/src/components/ProjectProposedAllocationsTile.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import { useQuery } from '@apollo/client' import { Accordion, @@ -11,10 +11,9 @@ import { Typography } from '@material-ui/core' +import AllocationOverview from './AllocationOverview' import { GET_PROJECT_TOTAL_PROPOSED } from '../operations/queries/ProjectQueries' -import { - formatAmount -} from '../scripts/selectors' +import { formatAmount } from '../scripts/selectors' import moment from 'moment' import ExpandMoreIcon from '@material-ui/icons/ExpandMore' @@ -37,6 +36,14 @@ const ProjectProposedAllocationsTile = (props) => { } }) + const [openAllocationOverview, setOpenAllocationOverview] = useState(null) + const [selectedAllocation, setSelectedAllocation] = useState(null) + + const handleAllocationClicked = ({ value, allocation }) => { + setSelectedAllocation(allocation) + setOpenAllocationOverview(value) + } + const renderAllocations = ({ allocations, currencyInformation }) => { return allocations.map(a => { @@ -49,7 +56,10 @@ const ProjectProposedAllocationsTile = (props) => { return ( - + handleAllocationClicked({ value: true, allocation: a })} + > {`${a.contributor.name}`} @@ -62,9 +72,9 @@ const ProjectProposedAllocationsTile = (props) => { - {`${currencyInformation['symbol']}${a.rate.hourly_rate}/hr ( - ${a.rate.type == 'monthly_rate' ? 'monthly rate' : 'max budget'} - )`} + {` + ${currencyInformation['symbol']}${a.rate.hourly_rate}/hr (${a.rate.type == 'monthly_rate' ? 'monthly rate' : 'max budget'}) + `} @@ -117,7 +127,6 @@ const ProjectProposedAllocationsTile = (props) => { {`${totalAllocatedNotPaid}`} - @@ -129,10 +138,15 @@ const ProjectProposedAllocationsTile = (props) => { + {selectedAllocation && + handleAllocationClicked(false)} + open={openAllocationOverview} + /> + } - ) - } export default ProjectProposedAllocationsTile diff --git a/src/components/RateMaxBudgetForm.js b/src/components/RateMaxBudgetForm.js index a96d6cd4..2d9eaa33 100644 --- a/src/components/RateMaxBudgetForm.js +++ b/src/components/RateMaxBudgetForm.js @@ -19,8 +19,10 @@ const RateMaxBudgetForm = (props) => { const { clientCurrency, currentRate, + currentTotal, createRate, endDate, + selectedPayment, setCurrency, setNewAllocationRate, startDate @@ -28,7 +30,7 @@ const RateMaxBudgetForm = (props) => { const [currentRateInput, setCurrentRateInput] = useState(null) const [rateCurrency, setRateCurrency] = useState(clientCurrency) - const [totalAmount, setTotalAmount] = useState(0) + const [totalAmount, setTotalAmount] = useState(currentTotal ? currentTotal / 100 : 0) const [totalWeeks, setTotalWeeks] = useState(null) const [totalHours, setTotalHours] = useState(0) @@ -37,12 +39,19 @@ const RateMaxBudgetForm = (props) => { setCurrency(rateCurrency) } }, [rateCurrency]) + useEffect(() => { setTotalWeeks(endDate.diff(startDate, 'days') / 7) setCurrentRateInput(currentRate ? currentRate.hourly_rate : 0) setRateCurrency(currentRate ? currentRate.currency : clientCurrency) }, [currentRate]) + useEffect(() => { + if (selectedPayment) { + setRateCurrency(clientCurrency) + } + }, [selectedPayment]) + useEffect(() => { setTotalHours(totalAmount && currentRateInput ? ((totalAmount / 100) / currentRateInput).toFixed(2) : 0) setNewAllocationRate({ @@ -85,7 +94,8 @@ const RateMaxBudgetForm = (props) => { name='Currency' fullWidth onChange={(event) => setRateCurrency(event.target.value)} - value={rateCurrency} + value={selectedPayment ? clientCurrency : rateCurrency} + disabled={selectedPayment} > {renderCurrencies(CURRENCIES)} @@ -113,7 +123,7 @@ const RateMaxBudgetForm = (props) => { variant='filled' currencySymbol={`${currencyInformation['symbol']}`} minimumValue='0' - defautltValue='0' + defaultValue={currentTotal / 100} outputFormat='string' decimalCharacter={`${currencyInformation['decimal']}`} digitGroupSeparator={`${currencyInformation['thousand']}`} diff --git a/src/components/RateProratedMonthlyForm.js b/src/components/RateProratedMonthlyForm.js index bbdc5019..ce7f5bd5 100644 --- a/src/components/RateProratedMonthlyForm.js +++ b/src/components/RateProratedMonthlyForm.js @@ -21,6 +21,7 @@ const RateProratedMonthlyForm = (props) => { clientCurrency, currentRate, endDate, + selectedPayment, setNewAllocationRate, setCurrency, startDate @@ -28,17 +29,11 @@ const RateProratedMonthlyForm = (props) => { const [monthlyHoursInput, setMonthlyhoursInput] = useState(null) const [currentRateInput, setCurrentRateInput] = useState(null) - const [rateCurrency, setRateCurrency] = useState(clientCurrency) + const [rateCurrency, setRateCurrency] = useState(null) const [totalAmount, setTotalAmount] = useState(null) const [totalWeeks, setTotalWeeks] = useState(null) const [totalHours, setTotalHours] = useState(0) - useEffect(() => { - if (rateCurrency) { - setCurrency(rateCurrency) - } - }, [rateCurrency]) - useEffect(() => { setTotalWeeks(endDate.diff(startDate, 'days') / 7) setCurrentRateInput(currentRate ? currentRate.hourly_rate : 0) @@ -46,6 +41,19 @@ const RateProratedMonthlyForm = (props) => { setRateCurrency(currentRate ? currentRate.currency : clientCurrency) }, [currentRate]) + useEffect(() => { + if (selectedPayment) { + setRateCurrency(clientCurrency) + } + }, [selectedPayment]) + + useEffect(() => { + if (!rateCurrency) { + setRateCurrency(clientCurrency) + } + setCurrency(rateCurrency) + }, [rateCurrency]) + useEffect(() => { setTotalAmount(currentRateInput * monthlyHoursInput) }, [monthlyHoursInput, currentRateInput]) @@ -96,7 +104,8 @@ const RateProratedMonthlyForm = (props) => { name='Currency' fullWidth onChange={(event) => setRateCurrency(event.target.value)} - value={rateCurrency} + value={selectedPayment ? clientCurrency : rateCurrency} + disabled={selectedPayment} > {renderCurrencies(CURRENCIES)} diff --git a/src/constants/index.js b/src/constants/index.js index 6790b215..8dd5f136 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -98,6 +98,10 @@ export const NAV_TITLES = [ title: 'Clients', locations: ['/home/clients'] }, + { + title: 'Contributor', + locations: ['/contributor'] + }, { title: 'Login', locations: ['/login'] diff --git a/src/operations/mutations/AllocationMutations.js b/src/operations/mutations/AllocationMutations.js index c6112faa..e1f4646e 100644 --- a/src/operations/mutations/AllocationMutations.js +++ b/src/operations/mutations/AllocationMutations.js @@ -30,3 +30,35 @@ export const CREATE_ALLOCATION = gql` } } ` + +export const DELETE_ALLOCATION = gql` + mutation DeleteAllocation( + $id: Int! + ) { + deleteAllocationById(id: $id) + } +` + +export const UPDATE_ALLOCATION = gql` + mutation UpdateAllocation( + $id: Int!, + $amount: Int!, + $start_date: String!, + $end_date: String!, + $rate_id: Int!, + $payment_id: Int + ) { + updateAllocationById( + id: $id + updateFields: { + amount: $amount + start_date: $start_date + end_date: $end_date + rate_id: $rate_id + payment_id: $payment_id + } + ){ + id + } + } +` diff --git a/src/operations/queries/AllocationQueries.js b/src/operations/queries/AllocationQueries.js index c9a146e4..024b007a 100644 --- a/src/operations/queries/AllocationQueries.js +++ b/src/operations/queries/AllocationQueries.js @@ -8,6 +8,10 @@ export const GET_ALLOCATIONS = gql` start_date end_date date_paid + contributor { + id + name + } rate { id active @@ -18,12 +22,59 @@ export const GET_ALLOCATIONS = gql` } payment { id + amount + client { + id + name + currency + } + } + project { + id + name + client { + id + name + currency + } + } + } + } +` + +export const GET_ALLOCATION_INFO = gql` + query Allocation($id: Int!) { + getAllocationById(id: $id) { + id + amount + start_date + end_date + payment { + id + amount + date_incurred + date_paid + } + contributor { + id + name + } + project { + id + name client { id name currency } } + rate { + id + hourly_rate + total_expected_hours + type + currency + } } } ` diff --git a/src/operations/queries/ClientQueries.js b/src/operations/queries/ClientQueries.js index 0ec85ae8..14647434 100644 --- a/src/operations/queries/ClientQueries.js +++ b/src/operations/queries/ClientQueries.js @@ -73,6 +73,8 @@ query ClientPayments($clientId: Int!) { payments { id amount + date_incurred + date_paid } } } diff --git a/src/operations/queries/ContributorQueries.js b/src/operations/queries/ContributorQueries.js index f1e64dcd..38950e67 100644 --- a/src/operations/queries/ContributorQueries.js +++ b/src/operations/queries/ContributorQueries.js @@ -35,6 +35,9 @@ export const GET_CONTRIBUTOR_ALLOCATIONS = gql` allocations { id amount + contributor { + id + } active start_date end_date @@ -68,7 +71,11 @@ export const GET_CONTRIBUTOR_INFO = gql` github_id github_handle github_access_token - total_paid + totalPaid + paidByCurrency { + currency + amount + } } } ` @@ -83,6 +90,7 @@ export const GET_CONTRIBUTOR_RATES = gql` active type hourly_rate + currency total_expected_hours } diff --git a/src/pages/ContributorDetailPage.js b/src/pages/ContributorDetailPage.js index adbc609c..351ffd45 100644 --- a/src/pages/ContributorDetailPage.js +++ b/src/pages/ContributorDetailPage.js @@ -23,12 +23,6 @@ class ContributorDetailPage extends React.Component { align='center' className='ContributorDetailPage' > -
{ location } = props const locationTitle = NAV_TITLES.find(tl => { - return tl.locations.find(l => location.match(l)) + return tl.locations.find(l => { + return location.includes(l) + }) }) return locationTitle }