From 2debf3dfdd19ce5a84990a0f893e3e5262718ffa Mon Sep 17 00:00:00 2001 From: Gwynn Dandridge-Perry Date: Sun, 26 Feb 2023 14:26:43 -0800 Subject: [PATCH 1/5] fix: note field needs to be a non-empty string for api when approving capture --- src/api/treeTrackerApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/treeTrackerApi.js b/src/api/treeTrackerApi.js index 05fce190e..3dede20e4 100644 --- a/src/api/treeTrackerApi.js +++ b/src/api/treeTrackerApi.js @@ -98,7 +98,7 @@ export default { lon: capture.lon, gps_accuracy: capture.gps_accuracy, captured_at: capture.captured_at, - note: capture.note ? capture.note : null, + note: capture.note ? capture.note : ' ', // temporary measure because the api requires a non-empty string, but adding notes is not in the UX yet age: age, morphology, species_id: speciesId, From 772031af09cbd4fd5e2babc336119084d0e2928e Mon Sep 17 00:00:00 2001 From: Gwynn Dandridge-Perry Date: Sun, 19 Mar 2023 17:56:36 -0700 Subject: [PATCH 2/5] feat: add contracts section --- .env.development | 1 + src/api/contracts.js | 74 ++++ src/components/Contracts/ContractsTable.js | 288 +++++++++++++++ src/components/Contracts/CreateContract.js | 340 ++++++++++++++++++ src/context/AppContext.js | 20 ++ src/models/auth.js | 1 + src/views/ContractsView/ContractsView.js | 31 ++ .../ContractsView/ContractsView.styles.js | 37 ++ 8 files changed, 792 insertions(+) create mode 100644 src/api/contracts.js create mode 100644 src/components/Contracts/ContractsTable.js create mode 100644 src/components/Contracts/CreateContract.js create mode 100644 src/views/ContractsView/ContractsView.js create mode 100644 src/views/ContractsView/ContractsView.styles.js diff --git a/.env.development b/.env.development index baa1c7f14..96b126af9 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,6 @@ REACT_APP_WEBMAP_DOMAIN=https://beta-map.treetracker.org REACT_APP_API_ROOT=https://dev-k8s.treetracker.org/api/admin +REACT_APP_CONTRACTS_ROOT=https://dev-k8s.treetracker.org/contract REACT_APP_EARNINGS_ROOT=https://dev-k8s.treetracker.org/earnings REACT_APP_FIELD_DATA_API_ROOT=https://dev-k8s.treetracker.org/field-data REACT_APP_GROWER_QUERY_API_ROOT=https://dev-k8s.treetracker.org/grower-account-query diff --git a/src/api/contracts.js b/src/api/contracts.js new file mode 100644 index 000000000..4ab12a01d --- /dev/null +++ b/src/api/contracts.js @@ -0,0 +1,74 @@ +import axios from 'axios'; +import { session } from '../models/auth'; + +const apiUrl = `${process.env.REACT_APP_CONTRACTS_ROOT}`; +const Axios = axios.create({ baseURL: apiUrl }); + +export default { + /** + * @function getContracts + * @description Get Contracts from the API + * @returns {Promise} + */ + async getContracts(params) { + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + return Axios.get(`contract`, { params, headers }).then((res) => res.data); + }, + + /** + * @function createContract + * @description Get Contracts from the API + * @returns {Promise} + */ + async createContract(params) { + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + return Axios.post(`contract`, { params, headers }).then((res) => res.data); + }, + + /** + * @function patchEarning + * @description Patch earning from the API + * + * @param {object} earning - earning to patch + * @returns {Promise} + */ + async patchContract(contract) { + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + return Axios.patch(`contracts`, contract, { headers }).then( + (res) => res.data + ); + }, + + /** + * @funtion batchPatchContracts + * @description Batch patch Contracts + * @param {File} csv file + * @returns {Promise} + * */ + async batchPatchContracts(file) { + const formData = new FormData(); + formData.append('csv', file); + const headers = { + accept: 'multipart/form-data', + Authorization: session.token, + }; + + return Axios.patch(`contracts/batch`, formData, { headers }) + .then((res) => res.data) + .catch((error) => { + throw new Error('Contracts Batch Upload Failed!', { cause: error }); + }); + }, +}; diff --git a/src/components/Contracts/ContractsTable.js b/src/components/Contracts/ContractsTable.js new file mode 100644 index 000000000..fc4c6b4b1 --- /dev/null +++ b/src/components/Contracts/ContractsTable.js @@ -0,0 +1,288 @@ +import React, { useEffect, useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { format } from 'date-fns'; +import contractsAPI from '../../api/contracts'; +import CustomTable from '../common/CustomTable/CustomTable'; +import { + convertDateStringToHumanReadableFormat, + generateActiveDateRangeFilterString, +} from 'utilities'; +import CustomTableFilter from 'components/common/CustomTableFilter/CustomTableFilter'; +import CustomTableItemDetails from 'components/common/CustomTableItemDetails/CustomTableItemDetails'; +import CreateContract from './CreateContract'; + +const useStyles = makeStyles({ + flex: { + display: 'flex', + alignItems: 'center', + }, + spaceBetween: { + justifyContent: 'space-between', + }, + margin: { + marginTop: '3rem', + marginBottom: '2rem', + }, +}); + +/** + * @constant + * @name contractsTableMetaData + * @description contains table meta data + * @type {Object[]} + * @param {string} contractsTableMetaData[].name - earning property used to get earning property value from earning object to display in table + * @param {string} contractsTableMetaData[].description - column description/label to be displayed in table + * @param {boolean} contractsTableMetaData[].sortable - determines if column is sortable + * @param {boolean} contractsTableMetaData[].showInfoIcon - determines if column has info icon + */ +const contractsTableMetaData = [ + { + description: 'ID', + name: 'id', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Type', + name: 'type', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Organization', + name: 'organization', + sortable: true, + showInfoIcon: false, + // align: 'right', + }, + { + description: 'Contractor', + name: 'contractor', + sortable: false, + showInfoIcon: false, + }, + { + description: 'Total Trees', + name: 'total', + sortable: true, + showInfoIcon: false, + // showInfoIcon: + // 'The effective data is the date on which captures were consolidated and the contracts record was created', + align: 'right', + }, + { + description: 'Status', + name: 'status', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Last Modified', + name: 'last_modified', + sortable: true, + showInfoIcon: false, + }, +]; + +/** + * @function + * @name prepareRows + * @description transform rows such that are well formated compatible with the table meta data + * @param {object} rows - rows to be transformed + * @returns {Array} - transformed rows + */ +const prepareRows = (rows) => + rows.map((row) => { + return { + ...row, + csv_start_date: row.consolidation_period_start, + csv_end_date: row.consolidation_period_end, + consolidation_period_start: convertDateStringToHumanReadableFormat( + row.consolidation_period_start, + 'yyyy-MM-dd' + ), + consolidation_period_end: convertDateStringToHumanReadableFormat( + row.consolidation_period_end, + 'yyyy-MM-dd' + ), + calculated_at: convertDateStringToHumanReadableFormat( + row.calculated_at, + 'yyyy-MM-dd' + ), + payment_confirmed_at: convertDateStringToHumanReadableFormat( + row.payment_confirmed_at + ), + paid_at: row.paid_at ? format(new Date(row.paid_at), 'yyyy-MM-dd') : '', + }; + }); + +/** + * @function + * @name ContractsTable + * @description renders the contracts table + * + * @returns {React.Component} - contracts table component + * */ +function ContractsTable() { + const classes = useStyles(); + // state for contracts table + const [contracts, setContracts] = useState([]); + const [activeDateRangeString, setActiveDateRangeString] = useState(''); + const [filter, setFilter] = useState({}); + const [page, setPage] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [contractsPerPage, setContractsPerPage] = useState(20); + const [sortBy, setSortBy] = useState({ + field: 'status', + order: 'desc', + }); + const [isDateFilterOpen, setIsDateFilterOpen] = useState(false); + const [isMainFilterOpen, setIsMainFilterOpen] = useState(false); + const [totalContracts, setTotalContracts] = useState(0); + const [selectedEarning, setSelectedEarning] = useState(null); + const [isDetailShown, setDetailShown] = useState(false); + + async function getContracts(fetchAll = false) { + // console.warn('getContracts with fetchAll: ', fetchAll); + setIsLoading(true); // show loading indicator when fetching data + + const { results, totalCount = 0 } = await getContractsReal(fetchAll); + console.log('results:', results, 'totalCount:', totalCount); + setContracts(results); + setTotalContracts(totalCount); + + setIsLoading(false); // hide loading indicator when data is fetched + } + + async function getContractsReal(fetchAll = false) { + // console.warn('fetchAll:', fetchAll); + const filtersToSubmit = { ...filter }; + // filter out keys we don't want to submit + Object.keys(filtersToSubmit).forEach((k) => { + if (k === 'grower') { + return; + } else { + if ( + filtersToSubmit[k] === 'all' || + filtersToSubmit[k] === '' || + k === 'organization_id' + ) { + delete filtersToSubmit[k]; + } + } + }); + + const queryParams = { + offset: fetchAll ? 0 : page * contractsPerPage, + limit: fetchAll ? 90000 : contractsPerPage, + // sort_by: sortBy?.field, + // order: sortBy?.order, + ...filtersToSubmit, + }; + + // log.debug('queryParams', queryParams); + + const response = await contractsAPI.getContracts(queryParams); + // log.debug('getContracts response: ', response); + + const results = prepareRows(response.contracts); + return { + results, + totalCount: response.query.count, + }; + } + + const handleOpenMainFilter = () => setIsMainFilterOpen(true); + const handleOpenDateFilter = () => setIsDateFilterOpen(true); + + useEffect(() => { + if (filter?.start_date && filter?.end_date) { + const dateRangeString = generateActiveDateRangeFilterString( + filter?.start_date, + filter?.end_date + ); + setActiveDateRangeString(dateRangeString); + } else { + setActiveDateRangeString(''); + } + + getContracts(); + }, [page, contractsPerPage, /*sortBy,*/ filter]); + + return ( + <> +
+ +
+ + { + setSelectedEarning(value); + setDetailShown(true); + }} + selectedRow={selectedEarning} + tableMetaData={contractsTableMetaData} + activeFiltersCount={ + Object.keys(filter).filter((key) => { + return key === 'start_date' || + key === 'end_date' || + key === 'organization_id' || + filter[key] === 'all' || + filter[key] === '' + ? false + : true; + }).length + } + headerTitle="Contracts" + mainFilterComponent={ + + } + dateFilterComponent={ + + } + rowDetails={ + selectedEarning ? ( + { + setDetailShown(false); + setSelectedEarning(null); + }} + /> + ) : null + } + actionButtonType="export" + exportDataFetch={getContractsReal} + /> + + ); +} + +export default ContractsTable; diff --git a/src/components/Contracts/CreateContract.js b/src/components/Contracts/CreateContract.js new file mode 100644 index 000000000..770e7fc77 --- /dev/null +++ b/src/components/Contracts/CreateContract.js @@ -0,0 +1,340 @@ +import React, { useContext, useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { + Button, + Checkbox, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Grid, + MenuItem, + FormControl, + FormControlLabel, + FormLabel, + TextField, +} from '@material-ui/core'; +import { AppContext } from '../../context/AppContext'; +import DateFnsUtils from '@date-io/date-fns'; +import contractsAPI from '../../api/contracts'; +import * as loglevel from 'loglevel'; + +const log = loglevel.getLogger('./Add.js'); + +const useStyles = makeStyles({ + root: { + width: '48%', + margin: '5px', + '& .MuiFormControl-fullWidth': { + width: '100%', + margin: '5px', + }, + '& .MuiOutlinedInput-root': { + position: 'relative', + borderRadius: '4px', + }, + }, + radioGroup: { + flexDirection: 'row', + }, +}); + +const initialState = { + id: '', + type: '', + organization: '', + contractor: '', + totalTrees: '', + status: '', + lastModified: '', +}; + +// "id": "", +// "agreement_id": "", +// "worker_id": "", +// "status": "active", +// "notes": "", +// "type": "CBO", +// "organization": "Freetown", +// "contractor": "gwynn", +// "totalTrees": "", +// "updated_at": "" + +export default function CreateContract() { + const classes = useStyles(); + const { orgList } = useContext(AppContext); + const [formData, setFormData] = useState(initialState); + const [open, setOpen] = useState(false); + const [errors, setErrors] = useState({}); + + const openModal = () => { + setOpen(true); + }; + + const closeModal = () => { + setFormData(initialState); + setErrors({}); + setOpen(false); + }; + + const validateData = () => { + let errors = {}; + if (!formData.type) { + errors = { ...errors, type: 'Please select a type' }; + } else if (formData.type === 'Organization' && !formData.org_name) { + errors = { ...errors, org_name: 'Please enter an organization name' }; + } else if (formData.type === 'Person') { + if (!formData.first_name) + errors = { ...errors, first_name: 'Please enter a first name' }; + if (!formData.last_name) + errors = { ...errors, last_name: 'Please enter a last name' }; + } + + if (!formData.email || /^[\w\d]@[\w\d]/.test(formData.email)) { + errors = { ...errors, email: 'Please enter an email' }; + } + if (!formData.phone) { + errors = { ...errors, phone: 'Please enter a phone number' }; + } + + setErrors(errors); + return errors; + }; + + const handleSubmit = () => { + log.debug('submitted', formData); + // valildate formData then post request + const errors = validateData(formData); + + if (Object.keys(errors).length === 0) { + contractsAPI + .createContract(formData) + .then((data) => console.log(data)) + .catch((e) => console.error(e)); + } + }; + + const handleEnterPress = (e) => { + e.key === 'Enter' && handleSubmit(e); + }; + + // const defaultTypeList = [ + // { + // name: 'Organization', + // value: 'Organization', + // }, + // { + // name: 'Person', + // value: 'Person', + // }, + // ]; + + // const defaultOrgList = [ + // { + // id: ORGANIZATION_NOT_SET, + // name: 'Not Set', + // value: '', + // }, + // ]; + + return ( + <> + + + Add Contract + + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.type} + helperText={errors.type} + className={classes.textField} + data-testid="type-dropdown" + label="type" + htmlFor="type" + id="type" + name="type" + /> + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.status} + helperText={errors.status} + className={classes.textField} + data-testid="status" + label="status" + htmlFor="status" + id="status" + name="status" + /> + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.agreement} + helperText={errors.agreement} + className={classes.textField} + data-testid="agreement" + label="agreement" + htmlFor="agreement" + id="agreement" + name="agreement" + /> + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.organization} + helperText={errors.organization} + className={classes.textField} + data-testid="organization" + label="organization" + htmlFor="organization" + id="organization" + name="organization" + /> + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.areas} + helperText={errors.areas} + className={classes.textField} + data-testid="areas" + label="areas" + htmlFor="areas" + id="areas" + name="areas" + /> + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.contractor} + helperText={errors.contractor} + className={classes.textField} + data-testid="contractor" + label="contractor" + htmlFor="contractor" + id="contractor" + name="contractor" + /> + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.phone} + helperText={errors.phone} + className={classes.textField} + data-testid="phone" + label="phone" + htmlFor="phone" + id="phone" + name="phone" + /> + + + + + setFormData({ ...formData, [e.target.name]: e.target.value }) + } + onKeyDown={handleEnterPress} + error={!!errors?.notes} + helperText={errors.notes} + className={classes.textField} + data-testid="notes" + label="notes" + htmlFor="notes" + id="notes" + name="notes" + /> + + + + + + + + + + ); +} diff --git a/src/context/AppContext.js b/src/context/AppContext.js index 82cd44d6c..c4678534e 100644 --- a/src/context/AppContext.js +++ b/src/context/AppContext.js @@ -5,6 +5,7 @@ import axios from 'axios'; import VerifyView from '../views/VerifyView'; import GrowersView from '../views/GrowersView'; import CapturesView from '../views/CapturesView'; +import ContractsView from '../views/ContractsView/ContractsView'; import EarningsView from '../views/EarningsView/EarningsView'; import PaymentsView from '../views/PaymentsView/PaymentsView'; import MessagingView from 'views/MessagingView'; @@ -124,6 +125,25 @@ function getRoutes(user) { POLICIES.LIST_PAYMENTS, ]), }, + { + name: 'Contracts', + children: [ + { + name: 'Contracts', + linkTo: '/contracts', + component: ContractsView, + icon: AccountBalanceIcon, + disabled: !hasPermission(user, [ + POLICIES.SUPER_PERMISSION, + POLICIES.MANAGE_CONTRACTS, + ]), + }, + ], + disabled: !hasPermission(user, [ + POLICIES.SUPER_PERMISSION, + POLICIES.MANAGE_CONTRACTS, + ]), + }, { name: 'Growers', linkTo: '/growers', diff --git a/src/models/auth.js b/src/models/auth.js index 758711bee..723c28eda 100644 --- a/src/models/auth.js +++ b/src/models/auth.js @@ -6,6 +6,7 @@ const PERMISSIONS = { const POLICIES = { SUPER_PERMISSION: 'super_permission', + MANAGE_CONTRACTS: 'manage_contracts', MANAGE_EARNINGS: 'manage_earnings', MANAGE_GROWER: 'manage_planter', MANAGE_PAYMENTS: 'manage_payments', diff --git a/src/views/ContractsView/ContractsView.js b/src/views/ContractsView/ContractsView.js new file mode 100644 index 000000000..7c814d078 --- /dev/null +++ b/src/views/ContractsView/ContractsView.js @@ -0,0 +1,31 @@ +import React, { useEffect } from 'react'; +import { Grid } from '@material-ui/core'; +import Navbar from '../../components/Navbar'; +import ContractsTable from '../../components/Contracts/ContractsTable'; +import { documentTitle } from '../../common/variables'; + +/** + * @function + * @name ContractsView + * @description View for the earnings page + * + * @returns {React.Component} + */ +function ContractsView() { + useEffect(() => { + document.title = `Contracts - ${documentTitle}`; + }, []); + + return ( + + + + + ); +} + +export default ContractsView; diff --git a/src/views/ContractsView/ContractsView.styles.js b/src/views/ContractsView/ContractsView.styles.js new file mode 100644 index 000000000..d569d0a90 --- /dev/null +++ b/src/views/ContractsView/ContractsView.styles.js @@ -0,0 +1,37 @@ +import { makeStyles } from '@material-ui/core/styles'; +import { MENU_WIDTH } from '../../components/common/Menu'; + +/** + * @constant + * @name earningsViewStyles + * @description styles for earnings view + * @type {object} + */ +const earningsViewLeftMenu = { + earningsViewLeftMenu: { + height: '100%', + width: MENU_WIDTH, + }, +}; + +/** + * @constant + * @name earningsViewStyles + * @description styles for earnings view + * @type {object} + */ +const earningsViewStyles = {}; + +/** + * @function + * @name useStyles + * @description combines and makes styles for earnings view component + * + * @returns {object} earnings view styles + */ +const useStyles = makeStyles(() => ({ + ...earningsViewStyles, + ...earningsViewLeftMenu, +})); + +export default useStyles; From 915a98f407cdaeb9074f6a3fe84719660c5c773e Mon Sep 17 00:00:00 2001 From: Gwynn Dandridge-Perry Date: Thu, 23 Mar 2023 19:57:34 -0700 Subject: [PATCH 3/5] feat: add contract agreements section --- src/api/contracts.js | 123 +++++- src/common/variables.js | 38 ++ .../Contracts/ContractAgreementsTable.js | 320 ++++++++++++++ src/components/Contracts/ContractsTable.js | 4 +- src/components/Contracts/CreateContract.js | 92 +++- .../Contracts/CreateContractAgreement.js | 405 ++++++++++++++++++ src/context/AppContext.js | 10 + src/views/ContractsView/ContractsView.js | 11 +- 8 files changed, 968 insertions(+), 35 deletions(-) create mode 100644 src/components/Contracts/ContractAgreementsTable.js create mode 100644 src/components/Contracts/CreateContractAgreement.js diff --git a/src/api/contracts.js b/src/api/contracts.js index 4ab12a01d..e3835fff3 100644 --- a/src/api/contracts.js +++ b/src/api/contracts.js @@ -1,5 +1,6 @@ import axios from 'axios'; import { session } from '../models/auth'; +import { handleResponse, handleError } from './apiUtils'; const apiUrl = `${process.env.REACT_APP_CONTRACTS_ROOT}`; const Axios = axios.create({ baseURL: apiUrl }); @@ -30,45 +31,135 @@ export default { Authorization: session.token, }; + // EXAMPLE POST + // { + // "agreement_id": "7bf1f932-2474-4211-8a07-a764ca95c80f", + // "worker_id": "93a026d2-a511-404f-958c-a0a36892af0f", + // "notes": "test contract notes" + // } + return Axios.post(`contract`, { params, headers }).then((res) => res.data); }, /** - * @function patchEarning + * @function getContractAgreements + * @description Get Contracts from the API + * @returns {Promise} + */ + async getContractAgreements(params) { + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + return Axios.get(`agreement`, { params, headers }).then((res) => res.data); + }, + + /** + * @function createContractAgreement + * @description Get Contracts from the API + * @returns {Promise} + */ + async createContractAgreement(agreement) { + const abortController = new AbortController(); + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + // EXAMPLE POST + // { + // "type": "grower", + // "owner_id": "08c71152-c552-42e7-b094-f510ff44e9cb", + // "funder_id":"c558a80a-f319-4c10-95d4-4282ef745b4b", + // "consolidation_rule_id": "6ff67c3a-e588-40e3-ba86-0df623ec6435", + // "name": "test agreement", + // "species_agreement_id": "e14b78c8-8f71-4c42-bb86-5a7f71996336" + // } + + try { + const query = `${apiUrl}/agreement`; + + const result = await fetch(query, { + method: 'POST', + headers, + body: JSON.stringify(agreement), + signal: abortController?.signal, + }).then(handleResponse); + + console.log('result ----', result); + return result; + } catch (error) { + handleError(error); + } + + // const result = await Axios.post(`/agreement`, { + // body: JSON.stringify(params), + // headers, + // }).then((res) => res.data); + }, + + /** + * @function patchContractAgreement * @description Patch earning from the API * * @param {object} earning - earning to patch * @returns {Promise} */ - async patchContract(contract) { + async patchContractAgreement(contract) { const headers = { 'content-type': 'application/json', Authorization: session.token, }; - return Axios.patch(`contracts`, contract, { headers }).then( + return Axios.patch(`/agreement`, contract, { headers }).then( (res) => res.data ); }, /** - * @funtion batchPatchContracts - * @description Batch patch Contracts - * @param {File} csv file + * @function createConsolidationRule + * @description Get Contracts from the API * @returns {Promise} - * */ - async batchPatchContracts(file) { - const formData = new FormData(); - formData.append('csv', file); + */ + async createConsolidationRule(params) { const headers = { - accept: 'multipart/form-data', + 'content-type': 'application/json', Authorization: session.token, }; - return Axios.patch(`contracts/batch`, formData, { headers }) - .then((res) => res.data) - .catch((error) => { - throw new Error('Contracts Batch Upload Failed!', { cause: error }); - }); + // EXAMPLE POST + // { + // "name": "test", + // "owner_id": "af7c1fe6-d669-414e-b066-e9733f0de7a8", + // "lambda": "something" + // } + + return Axios.post(`contract/consolidation_rule`, { params, headers }).then( + (res) => res.data + ); + }, + + /** + * @function createSpeciesAgreement + * @description Get Contracts from the API + * @returns {Promise} + */ + async createSpeciesAgreement(params) { + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + // EXAMPLE POST + // { + // "name": "test species agreement", + // "owner_id": "af7c1fe6-d669-414e-b066-e9733f0de7a8", + // "description": "test species agreement description" + // } + + return Axios.post(`contract/species_agreement`, { params, headers }).then( + (res) => res.data + ); }, }; diff --git a/src/common/variables.js b/src/common/variables.js index 182e6252d..db9b6916b 100644 --- a/src/common/variables.js +++ b/src/common/variables.js @@ -21,6 +21,44 @@ export const verificationStatesArr = [ verificationStates.REJECTED, ]; +export const CONTRACT_STATUS = { + // unsigned: 'unsigned', // db default state + signed: 'signed', + completed: 'completed', + aborted: 'aborted', + cancelled: 'cancelled', +}; + +export const COORDINATOR_ROLES = { + supervisor: 'supervisor', + area_manager: 'area_manager', +}; + +export const CURRENCY = { + USD: 'USD', + SLL: 'SLL', +}; + +export const AGREEMENT_STATUS = { + // planning: 'planning', // db default state + open: 'open', + closed: 'closed', + aborted: 'aborted', +}; + +export const AGREEMENT_TYPE = { + grower: 'grower', + nursury: 'nursury', + village_champion: 'village_champion', +}; + +export const SPECIES_TYPE = { + other: 'other', + any: 'any', + specific: 'specific', + genus: 'genus', +}; + // These are the default min/max dates for the MUI KeyboardDatePicker component // See https://material-ui-pickers.dev/api/KeyboardDatePicker // If we set minDate or maxDate to null on this component, the fwd/back buttons are disabled diff --git a/src/components/Contracts/ContractAgreementsTable.js b/src/components/Contracts/ContractAgreementsTable.js new file mode 100644 index 000000000..7672493d6 --- /dev/null +++ b/src/components/Contracts/ContractAgreementsTable.js @@ -0,0 +1,320 @@ +import React, { useEffect, useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { format } from 'date-fns'; +import contractsAPI from '../../api/contracts'; +import CustomTable from '../common/CustomTable/CustomTable'; +import { + convertDateStringToHumanReadableFormat, + generateActiveDateRangeFilterString, +} from 'utilities'; +import CustomTableFilter from 'components/common/CustomTableFilter/CustomTableFilter'; +import CustomTableItemDetails from 'components/common/CustomTableItemDetails/CustomTableItemDetails'; +import CreateContractAgreement from './CreateContractAgreement'; + +const useStyles = makeStyles({ + flex: { + display: 'flex', + alignItems: 'center', + }, + spaceBetween: { + justifyContent: 'space-between', + }, + margin: { + marginTop: '3rem', + marginBottom: '2rem', + }, +}); + +/** + * @constant + * @name contractsTableMetaData + * @description contains table meta data + * @type {Object[]} + * @param {string} contractsTableMetaData[].name - earning property used to get earning property value from earning object to display in table + * @param {string} contractsTableMetaData[].description - column description/label to be displayed in table + * @param {boolean} contractsTableMetaData[].sortable - determines if column is sortable + * @param {boolean} contractsTableMetaData[].showInfoIcon - determines if column has info icon + */ +const contractsTableMetaData = [ + { + description: 'Name', + name: 'name', + sortable: true, + showInfoIcon: false, + }, + { + description: 'ID', + name: 'id', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Type', + name: 'type', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Funder ID', + name: 'funder_id', + sortable: true, + showInfoIcon: false, + // align: 'right', + }, + { + description: 'Owner ID', + name: 'owner_id', + sortable: true, + showInfoIcon: false, + // align: 'right', + }, + { + description: 'Organization', + name: 'growing_organization_id', + sortable: true, + showInfoIcon: false, + // align: 'right', + }, + { + description: 'Species Agreement ID', + name: 'species_agreement_id', + sortable: false, + showInfoIcon: false, + }, + { + description: 'Consolidation Rule ID', + name: 'consolidation_rule_id', + sortable: false, + showInfoIcon: false, + }, + // { + // description: 'Total Trees', + // name: 'total', + // sortable: true, + // showInfoIcon: false, + // // showInfoIcon: + // // 'The effective data is the date on which captures were consolidated and the contracts record was created', + // align: 'right', + // }, + { + description: 'Status', + name: 'status', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Last Modified', + name: 'updated_at', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Created At', + name: 'created_at', + sortable: true, + showInfoIcon: false, + }, +]; + +/** + * @function + * @name prepareRows + * @description transform rows such that are well formated compatible with the table meta data + * @param {object} rows - rows to be transformed + * @returns {Array} - transformed rows + */ +const prepareRows = (rows) => + rows.map((row) => { + return { + ...row, + csv_start_date: row.consolidation_period_start, + csv_end_date: row.consolidation_period_end, + consolidation_period_start: convertDateStringToHumanReadableFormat( + row.consolidation_period_start, + 'yyyy-MM-dd' + ), + consolidation_period_end: convertDateStringToHumanReadableFormat( + row.consolidation_period_end, + 'yyyy-MM-dd' + ), + calculated_at: convertDateStringToHumanReadableFormat( + row.calculated_at, + 'yyyy-MM-dd' + ), + payment_confirmed_at: convertDateStringToHumanReadableFormat( + row.payment_confirmed_at + ), + paid_at: row.paid_at ? format(new Date(row.paid_at), 'yyyy-MM-dd') : '', + }; + }); + +/** + * @function + * @name ContractAgreementsTable + * @description renders the contracts table + * + * @returns {React.Component} - contracts table component + * */ +function ContractAgreementsTable() { + const classes = useStyles(); + // state for contracts table + const [contracts, setContracts] = useState([]); + const [activeDateRangeString, setActiveDateRangeString] = useState(''); + const [filter, setFilter] = useState({}); + const [page, setPage] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [contractsPerPage, setContractsPerPage] = useState(20); + const [sortBy, setSortBy] = useState({ + field: 'status', + order: 'desc', + }); + const [isDateFilterOpen, setIsDateFilterOpen] = useState(false); + const [isMainFilterOpen, setIsMainFilterOpen] = useState(false); + const [totalContracts, setTotalContracts] = useState(0); + const [selectedEarning, setSelectedEarning] = useState(null); + const [isDetailShown, setDetailShown] = useState(false); + + async function getContracts(fetchAll = false) { + // console.warn('getContracts with fetchAll: ', fetchAll); + setIsLoading(true); // show loading indicator when fetching data + + const { results, totalCount = 0 } = await getContractAgreements(fetchAll); + console.log('results:', results, 'totalCount:', totalCount); + setContracts(results); + setTotalContracts(totalCount); + + setIsLoading(false); // hide loading indicator when data is fetched + } + + async function getContractAgreements(fetchAll = false) { + // console.warn('fetchAll:', fetchAll); + const filtersToSubmit = { ...filter }; + // filter out keys we don't want to submit + Object.keys(filtersToSubmit).forEach((k) => { + if (k === 'grower') { + return; + } else { + if ( + filtersToSubmit[k] === 'all' || + filtersToSubmit[k] === '' || + k === 'organization_id' + ) { + delete filtersToSubmit[k]; + } + } + }); + + const queryParams = { + offset: fetchAll ? 0 : page * contractsPerPage, + limit: fetchAll ? 90000 : contractsPerPage, + // sort_by: sortBy?.field, + // order: sortBy?.order, + ...filtersToSubmit, + }; + + // log.debug('queryParams', queryParams); + + const response = await contractsAPI.getContractAgreements(queryParams); + console.log('getContractAgreements response: ', response); + + const results = prepareRows(response.agreements); + return { + results, + totalCount: response.query.count, + }; + } + + const handleOpenMainFilter = () => setIsMainFilterOpen(true); + const handleOpenDateFilter = () => setIsDateFilterOpen(true); + + useEffect(() => { + if (filter?.start_date && filter?.end_date) { + const dateRangeString = generateActiveDateRangeFilterString( + filter?.start_date, + filter?.end_date + ); + setActiveDateRangeString(dateRangeString); + } else { + setActiveDateRangeString(''); + } + + getContracts(); + }, [page, contractsPerPage, /*sortBy,*/ filter]); + + return ( + <> +
+ +
+ + { + setSelectedEarning(value); + setDetailShown(true); + }} + selectedRow={selectedEarning} + tableMetaData={contractsTableMetaData} + activeFiltersCount={ + Object.keys(filter).filter((key) => { + return key === 'start_date' || + key === 'end_date' || + key === 'organization_id' || + filter[key] === 'all' || + filter[key] === '' + ? false + : true; + }).length + } + headerTitle="Contract Agreements" + mainFilterComponent={ + + } + dateFilterComponent={ + + } + rowDetails={ + selectedEarning ? ( + { + setDetailShown(false); + setSelectedEarning(null); + }} + /> + ) : null + } + actionButtonType="export" + exportDataFetch={getContractAgreements} + /> + + ); +} + +export default ContractAgreementsTable; diff --git a/src/components/Contracts/ContractsTable.js b/src/components/Contracts/ContractsTable.js index fc4c6b4b1..26f2fcf08 100644 --- a/src/components/Contracts/ContractsTable.js +++ b/src/components/Contracts/ContractsTable.js @@ -183,7 +183,7 @@ function ContractsTable() { // log.debug('queryParams', queryParams); const response = await contractsAPI.getContracts(queryParams); - // log.debug('getContracts response: ', response); + console.log('getContracts response: ', response); const results = prepareRows(response.contracts); return { @@ -279,7 +279,7 @@ function ContractsTable() { ) : null } actionButtonType="export" - exportDataFetch={getContractsReal} + exportDataFetch={getContracts} /> ); diff --git a/src/components/Contracts/CreateContract.js b/src/components/Contracts/CreateContract.js index 770e7fc77..f424d0c27 100644 --- a/src/components/Contracts/CreateContract.js +++ b/src/components/Contracts/CreateContract.js @@ -2,20 +2,21 @@ import React, { useContext, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { Button, - Checkbox, + // Checkbox, Dialog, DialogTitle, DialogContent, DialogActions, Grid, - MenuItem, + // MenuItem, FormControl, - FormControlLabel, + // FormControlLabel, FormLabel, TextField, } from '@material-ui/core'; import { AppContext } from '../../context/AppContext'; -import DateFnsUtils from '@date-io/date-fns'; +// import DateFnsUtils from '@date-io/date-fns'; +import SelectOrg from '../common/SelectOrg'; import contractsAPI from '../../api/contracts'; import * as loglevel from 'loglevel'; @@ -39,6 +40,59 @@ const useStyles = makeStyles({ }, }); +/* +POST https://dev-k8s.treetracker.org/contract/contract +{ + "agreement_id": "7bf1f932-2474-4211-8a07-a764ca95c80f", + "worker_id": "93a026d2-a511-404f-958c-a0a36892af0f", + "notes": "test contract notes" +} +GET https://dev-k8s.treetracker.org/contract/contract +{ + "id": "5de33643-2c9a-4d1c-9643-285d7a75e820", + "agreement_id": "7bf1f932-2474-4211-8a07-a764ca95c80f", + "worker_id": "93a026d2-a511-404f-958c-a0a36892af0f", + "status": "unsigned", + "notes": "test contract notes", + "created_at": "2023-03-05T19:57:59.555Z", + "updated_at": "2023-03-05T19:57:59.555Z", + "signed_at": null, + "closed_at": null, + "listed": true + + type + organization + contractor + total trees +} + + +POST https://dev-k8s.treetracker.org/contract/agreement +{ + "type": "grower", + "owner_id": "08c71152-c552-42e7-b094-f510ff44e9cb", + "funder_id":"c558a80a-f319-4c10-95d4-4282ef745b4b", + "consolidation_rule_id": "6ff67c3a-e588-40e3-ba86-0df623ec6435", + "name": "test agreement", + "species_agreement_id": "e14b78c8-8f71-4c42-bb86-5a7f71996336" +} + +POST https://dev-k8s.treetracker.org/contract/consolidation_rule +{ + "name": "test", + "owner_id": "af7c1fe6-d669-414e-b066-e9733f0de7a8", + "lambda": "something" +} + +POST https://dev-k8s.treetracker.org/contract/species_agreement +{ + "name": "test species agreement", + "owner_id": "af7c1fe6-d669-414e-b066-e9733f0de7a8", + "description": "test species agreement description" +} + +*/ + const initialState = { id: '', type: '', @@ -49,20 +103,23 @@ const initialState = { lastModified: '', }; -// "id": "", +// "id": "", -- from database? // "agreement_id": "", -// "worker_id": "", -// "status": "active", +// "worker_id": "", -- ?? +// "status": "", -- from database? // "notes": "", -// "type": "CBO", -// "organization": "Freetown", -// "contractor": "gwynn", -// "totalTrees": "", -// "updated_at": "" +// // "type": "CBO", +// // "organization": "Freetown", -- from logged in org_id +// // "contractor": "gwynn", +// // "totalTrees": "", +// // "signed_at": "", +// // "closed_at": null, +// // "listed": true -export default function CreateContract() { +export default function CreateContractAgreement() { const classes = useStyles(); - const { orgList } = useContext(AppContext); + const context = useContext(AppContext); + console.log('context', context); const [formData, setFormData] = useState(initialState); const [open, setOpen] = useState(false); const [errors, setErrors] = useState({}); @@ -108,7 +165,7 @@ export default function CreateContract() { if (Object.keys(errors).length === 0) { contractsAPI - .createContract(formData) + .createContractAgreement(formData) .then((data) => console.log(data)) .catch((e) => console.error(e)); } @@ -213,6 +270,9 @@ export default function CreateContract() { /> + + + {/* - + */} { + setOpen(true); + }; + + const closeModal = () => { + log.debug('Cancelled: close modal'); + setFormData(initialState); + setErrors({}); + setOpen(false); + }; + + const validateData = () => { + let errors = {}; + if (!formData.type) { + errors = { + ...errors, + type: 'Please select a Type', + }; + } + if (formData.name === '') { + errors = { + ...errors, + org_name: 'Please enter an Agreement Name', + }; + } + if (!formData.owner_id) { + errors = { + ...errors, + first_name: 'Please select an Organization as Owner', + }; + } + if (!formData.funder_id) { + errors = { + ...errors, + last_name: 'Please select a Funder Organization', + }; + } + if (!formData.consolidation_rule_id) { + errors = { + ...errors, + last_name: 'Please select a Consolidation Rule', + }; + } + if (!formData.species_agreement_id) { + errors = { + ...errors, + last_name: 'Please select a Species Agreement', + }; + } + + setErrors(errors); + return errors; + }; + + const handleSubmit = () => { + log.debug('submitted', formData); + // valildate formData then post request + const errors = validateData(formData); + log.debug('ERRORS:', errors); + + if (Object.keys(errors).length === 0) { + contractsAPI + .createContractAgreement(formData) + .then((data) => console.log(data)) + .catch((e) => console.error(e)); + } + }; + + const handleEnterPress = (e) => { + e.key === 'Enter' && handleSubmit(e); + }; + + const defaultOrgList = userHasOrg + ? [ + { + id: ALL_ORGANIZATIONS, + stakeholder_uuid: ALL_ORGANIZATIONS, + name: 'All', + value: 'All', + }, + ] + : [ + { + id: ALL_ORGANIZATIONS, + stakeholder_uuid: ALL_ORGANIZATIONS, + name: 'All', + value: 'All', + }, + { + id: ORGANIZATION_NOT_SET, + stakeholder_uuid: ORGANIZATION_NOT_SET, + name: 'Not set', + value: null, + }, + ]; + + return ( + <> + + + Add Contract + + + + + + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + onKeyDown={handleEnterPress} + error={!!errors?.name} + helperText={errors.name} + className={classes.textField} + data-testid="name" + label="name" + htmlFor="name" + id="name" + name="name" + /> + + + + + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + onKeyDown={handleEnterPress} + error={!!errors?.type} + helperText={errors.type} + className={classes.textField} + data-testid="type-dropdown" + label="Type" + htmlFor="type" + id="type" + name="type" + > + {Object.entries(AGREEMENT_TYPE).map(([key, value]) => ( + + {value} + + ))} + + + + + { + console.log( + 'handleSelection SelectOrg', + org.stakeholder_uuid, + orgId + ); + setFormData({ + ...formData, + owner_id: org?.stakeholder_uuid, + }); + }} + /> + + + + + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + onKeyDown={handleEnterPress} + error={!!errors?.funder_id} + helperText={errors.funder_id} + className={classes.textField} + data-testid="funder_id" + label="funder_id" + htmlFor="funder_id" + id="funder_id" + name="funder_id" + aria-required + > + {[...defaultOrgList, ...orgList].map((org) => ( + + {org.name} + + ))} + + + + + + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + onKeyDown={handleEnterPress} + error={!!errors?.consolidation_rule_id} + helperText={errors.consolidation_rule_id} + className={classes.textField} + data-testid="consolidation_rule_id" + label="consolidation_rule_id" + htmlFor="consolidation_rule_id" + id="consolidation_rule_id" + name="consolidation_rule_id" + /> + + + + + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }) + } + onKeyDown={handleEnterPress} + error={!!errors?.species_agreement_id} + helperText={errors.species_agreement_id} + className={classes.textField} + data-testid="species_agreement_id" + label="species_agreement_id" + htmlFor="species_agreement_id" + id="species_agreement_id" + name="species_agreement_id" + /> + + + + + + + + + + ); +} diff --git a/src/context/AppContext.js b/src/context/AppContext.js index c4678534e..cf52f1d04 100644 --- a/src/context/AppContext.js +++ b/src/context/AppContext.js @@ -138,6 +138,16 @@ function getRoutes(user) { POLICIES.MANAGE_CONTRACTS, ]), }, + { + name: 'Agreements', + linkTo: '/agreements', + component: ContractsView, + icon: AccountBalanceIcon, + disabled: !hasPermission(user, [ + POLICIES.SUPER_PERMISSION, + POLICIES.MANAGE_CONTRACTS, + ]), + }, ], disabled: !hasPermission(user, [ POLICIES.SUPER_PERMISSION, diff --git a/src/views/ContractsView/ContractsView.js b/src/views/ContractsView/ContractsView.js index 7c814d078..5b0d37a5f 100644 --- a/src/views/ContractsView/ContractsView.js +++ b/src/views/ContractsView/ContractsView.js @@ -1,7 +1,9 @@ import React, { useEffect } from 'react'; +import { Route, Switch } from 'react-router'; import { Grid } from '@material-ui/core'; import Navbar from '../../components/Navbar'; import ContractsTable from '../../components/Contracts/ContractsTable'; +import ContractAgreementsTable from '../../components/Contracts/ContractAgreementsTable'; import { documentTitle } from '../../common/variables'; /** @@ -23,7 +25,14 @@ function ContractsView() { style={{ flexWrap: 'nowrap', height: '100%' }} > - + + + + + + + + ); } From 30f84d498b0d4b23645452dc77ca80f787442307 Mon Sep 17 00:00:00 2001 From: Gwynn Dandridge-Perry Date: Tue, 28 Mar 2023 12:03:26 -0700 Subject: [PATCH 4/5] feat: update custom filter and item detail to work with contracts --- src/common/variables.js | 9 +- .../Contracts/ContractAgreementsTable.js | 58 +++-- src/components/Contracts/ContractsTable.js | 71 +++--- src/components/Contracts/CreateContract.js | 2 +- .../CustomTableFilter/CustomTableFilter.js | 114 +++++++++- .../CustomTableItemDetails.js | 215 +++++++++++------- 6 files changed, 334 insertions(+), 135 deletions(-) diff --git a/src/common/variables.js b/src/common/variables.js index db9b6916b..1c4366860 100644 --- a/src/common/variables.js +++ b/src/common/variables.js @@ -22,7 +22,8 @@ export const verificationStatesArr = [ ]; export const CONTRACT_STATUS = { - // unsigned: 'unsigned', // db default state + all: 'all', + unsigned: 'unsigned', // db default state signed: 'signed', completed: 'completed', aborted: 'aborted', @@ -30,23 +31,27 @@ export const CONTRACT_STATUS = { }; export const COORDINATOR_ROLES = { + all: 'all', supervisor: 'supervisor', area_manager: 'area_manager', }; export const CURRENCY = { + all: 'all', USD: 'USD', SLL: 'SLL', }; export const AGREEMENT_STATUS = { - // planning: 'planning', // db default state + all: 'all', + planning: 'planning', // db default state open: 'open', closed: 'closed', aborted: 'aborted', }; export const AGREEMENT_TYPE = { + all: 'all', grower: 'grower', nursury: 'nursury', village_champion: 'village_champion', diff --git a/src/components/Contracts/ContractAgreementsTable.js b/src/components/Contracts/ContractAgreementsTable.js index 7672493d6..a6a0a44a5 100644 --- a/src/components/Contracts/ContractAgreementsTable.js +++ b/src/components/Contracts/ContractAgreementsTable.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { format } from 'date-fns'; +// import { format } from 'date-fns'; import contractsAPI from '../../api/contracts'; import CustomTable from '../common/CustomTable/CustomTable'; import { @@ -59,21 +59,18 @@ const contractsTableMetaData = [ name: 'funder_id', sortable: true, showInfoIcon: false, - // align: 'right', }, { description: 'Owner ID', name: 'owner_id', sortable: true, showInfoIcon: false, - // align: 'right', }, { description: 'Organization', name: 'growing_organization_id', sortable: true, showInfoIcon: false, - // align: 'right', }, { description: 'Species Agreement ID', @@ -127,24 +124,32 @@ const prepareRows = (rows) => rows.map((row) => { return { ...row, - csv_start_date: row.consolidation_period_start, - csv_end_date: row.consolidation_period_end, - consolidation_period_start: convertDateStringToHumanReadableFormat( - row.consolidation_period_start, + created_at: convertDateStringToHumanReadableFormat( + row.created_at, 'yyyy-MM-dd' ), - consolidation_period_end: convertDateStringToHumanReadableFormat( - row.consolidation_period_end, + updated_at: convertDateStringToHumanReadableFormat( + row.updated_at, 'yyyy-MM-dd' ), - calculated_at: convertDateStringToHumanReadableFormat( - row.calculated_at, - 'yyyy-MM-dd' - ), - payment_confirmed_at: convertDateStringToHumanReadableFormat( - row.payment_confirmed_at - ), - paid_at: row.paid_at ? format(new Date(row.paid_at), 'yyyy-MM-dd') : '', + // csv_start_date: row.consolidation_period_start, + // csv_end_date: row.consolidation_period_end, + // consolidation_period_start: convertDateStringToHumanReadableFormat( + // row.consolidation_period_start, + // 'yyyy-MM-dd' + // ), + // consolidation_period_end: convertDateStringToHumanReadableFormat( + // row.consolidation_period_end, + // 'yyyy-MM-dd' + // ), + // calculated_at: convertDateStringToHumanReadableFormat( + // row.calculated_at, + // 'yyyy-MM-dd' + // ), + // payment_confirmed_at: convertDateStringToHumanReadableFormat( + // row.payment_confirmed_at + // ), + // paid_at: row.paid_at ? format(new Date(row.paid_at), 'yyyy-MM-dd') : '', }; }); @@ -171,7 +176,9 @@ function ContractAgreementsTable() { const [isDateFilterOpen, setIsDateFilterOpen] = useState(false); const [isMainFilterOpen, setIsMainFilterOpen] = useState(false); const [totalContracts, setTotalContracts] = useState(0); - const [selectedEarning, setSelectedEarning] = useState(null); + const [selectedContractAgreement, setSelectedContractAgreement] = useState( + null + ); const [isDetailShown, setDetailShown] = useState(false); async function getContracts(fetchAll = false) { @@ -262,10 +269,10 @@ function ContractAgreementsTable() { openDateFilter={handleOpenDateFilter} handleGetData={getContracts} setSelectedRow={(value) => { - setSelectedEarning(value); + setSelectedContractAgreement(value); setDetailShown(true); }} - selectedRow={selectedEarning} + selectedRow={selectedContractAgreement} tableMetaData={contractsTableMetaData} activeFiltersCount={ Object.keys(filter).filter((key) => { @@ -283,7 +290,7 @@ function ContractAgreementsTable() { @@ -298,15 +305,16 @@ function ContractAgreementsTable() { /> } rowDetails={ - selectedEarning ? ( + selectedContractAgreement ? ( { setDetailShown(false); - setSelectedEarning(null); + setSelectedContractAgreement(null); }} + showLogPaymentForm={false} /> ) : null } diff --git a/src/components/Contracts/ContractsTable.js b/src/components/Contracts/ContractsTable.js index 26f2fcf08..cee6ca24c 100644 --- a/src/components/Contracts/ContractsTable.js +++ b/src/components/Contracts/ContractsTable.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { format } from 'date-fns'; +// import { format } from 'date-fns'; import contractsAPI from '../../api/contracts'; import CustomTable from '../common/CustomTable/CustomTable'; import { @@ -53,11 +53,10 @@ const contractsTableMetaData = [ name: 'organization', sortable: true, showInfoIcon: false, - // align: 'right', }, { description: 'Contractor', - name: 'contractor', + name: 'worker_id', sortable: false, showInfoIcon: false, }, @@ -70,6 +69,12 @@ const contractsTableMetaData = [ // 'The effective data is the date on which captures were consolidated and the contracts record was created', align: 'right', }, + { + description: 'Notes', + name: 'notes', + sortable: true, + showInfoIcon: false, + }, { description: 'Status', name: 'status', @@ -78,7 +83,7 @@ const contractsTableMetaData = [ }, { description: 'Last Modified', - name: 'last_modified', + name: 'updated_at', sortable: true, showInfoIcon: false, }, @@ -95,24 +100,32 @@ const prepareRows = (rows) => rows.map((row) => { return { ...row, - csv_start_date: row.consolidation_period_start, - csv_end_date: row.consolidation_period_end, - consolidation_period_start: convertDateStringToHumanReadableFormat( - row.consolidation_period_start, + created_at: convertDateStringToHumanReadableFormat( + row.created_at, 'yyyy-MM-dd' ), - consolidation_period_end: convertDateStringToHumanReadableFormat( - row.consolidation_period_end, + updated_at: convertDateStringToHumanReadableFormat( + row.updated_at, 'yyyy-MM-dd' ), - calculated_at: convertDateStringToHumanReadableFormat( - row.calculated_at, - 'yyyy-MM-dd' - ), - payment_confirmed_at: convertDateStringToHumanReadableFormat( - row.payment_confirmed_at - ), - paid_at: row.paid_at ? format(new Date(row.paid_at), 'yyyy-MM-dd') : '', + // csv_start_date: row.consolidation_period_start, + // csv_end_date: row.consolidation_period_end, + // consolidation_period_start: convertDateStringToHumanReadableFormat( + // row.consolidation_period_start, + // 'yyyy-MM-dd' + // ), + // consolidation_period_end: convertDateStringToHumanReadableFormat( + // row.consolidation_period_end, + // 'yyyy-MM-dd' + // ), + // calculated_at: convertDateStringToHumanReadableFormat( + // row.calculated_at, + // 'yyyy-MM-dd' + // ), + // payment_confirmed_at: convertDateStringToHumanReadableFormat( + // row.payment_confirmed_at + // ), + // paid_at: row.paid_at ? format(new Date(row.paid_at), 'yyyy-MM-dd') : '', }; }); @@ -139,7 +152,7 @@ function ContractsTable() { const [isDateFilterOpen, setIsDateFilterOpen] = useState(false); const [isMainFilterOpen, setIsMainFilterOpen] = useState(false); const [totalContracts, setTotalContracts] = useState(0); - const [selectedEarning, setSelectedEarning] = useState(null); + const [selectedContract, setSelectedContract] = useState(null); const [isDetailShown, setDetailShown] = useState(false); async function getContracts(fetchAll = false) { @@ -156,7 +169,9 @@ function ContractsTable() { async function getContractsReal(fetchAll = false) { // console.warn('fetchAll:', fetchAll); - const filtersToSubmit = { ...filter }; + const filtersToSubmit = { + ...filter, + }; // filter out keys we don't want to submit Object.keys(filtersToSubmit).forEach((k) => { if (k === 'grower') { @@ -183,9 +198,10 @@ function ContractsTable() { // log.debug('queryParams', queryParams); const response = await contractsAPI.getContracts(queryParams); - console.log('getContracts response: ', response); + // log.debug('getContracts response ---> ', response.contracts); const results = prepareRows(response.contracts); + // log.debug('prepareRows --->', results); return { results, totalCount: response.query.count, @@ -230,10 +246,10 @@ function ContractsTable() { openDateFilter={handleOpenDateFilter} handleGetData={getContracts} setSelectedRow={(value) => { - setSelectedEarning(value); + setSelectedContract(value); setDetailShown(true); }} - selectedRow={selectedEarning} + selectedRow={selectedContract} tableMetaData={contractsTableMetaData} activeFiltersCount={ Object.keys(filter).filter((key) => { @@ -251,7 +267,7 @@ function ContractsTable() { @@ -266,15 +282,16 @@ function ContractsTable() { /> } rowDetails={ - selectedEarning ? ( + selectedContract ? ( { setDetailShown(false); - setSelectedEarning(null); + setSelectedContract(null); }} + showLogPaymentForm={false} /> ) : null } diff --git a/src/components/Contracts/CreateContract.js b/src/components/Contracts/CreateContract.js index f424d0c27..16de04493 100644 --- a/src/components/Contracts/CreateContract.js +++ b/src/components/Contracts/CreateContract.js @@ -119,7 +119,7 @@ const initialState = { export default function CreateContractAgreement() { const classes = useStyles(); const context = useContext(AppContext); - console.log('context', context); + console.log('context.orgId', context.orgId); const [formData, setFormData] = useState(initialState); const [open, setOpen] = useState(false); const [errors, setErrors] = useState({}); diff --git a/src/components/common/CustomTableFilter/CustomTableFilter.js b/src/components/common/CustomTableFilter/CustomTableFilter.js index dccb9eb07..51d619973 100644 --- a/src/components/common/CustomTableFilter/CustomTableFilter.js +++ b/src/components/common/CustomTableFilter/CustomTableFilter.js @@ -16,6 +16,13 @@ import SelectOrg from '../SelectOrg'; import useStyles from './CustomTableFilter.styles'; import { AppContext } from '../../../context/AppContext'; import { ALL_ORGANIZATIONS } from '../../../models/Filter'; +import { + CONTRACT_STATUS, + COORDINATOR_ROLES, + CURRENCY, + AGREEMENT_STATUS, + AGREEMENT_TYPE, +} from 'common/variables'; const PAYMENT_STATUS = ['calculated', 'cancelled', 'paid', 'all']; @@ -35,10 +42,12 @@ const PAYMENT_STATUS = ['calculated', 'cancelled', 'paid', 'all']; function CustomTableFilter(props) { // console.warn('orgList', orgList); const initialFilter = { - // organization_id: '', + organization_id: ALL_ORGANIZATIONS, grower: '', payment_status: 'all', earnings_status: 'all', + contract_status: 'all', + agreement_type: 'all', phone: '', }; const [localFilter, setLocalFilter] = useState(initialFilter); @@ -231,6 +240,108 @@ function CustomTableFilter(props) { ); + const renderContractFilter = () => ( + <> + { + + Contract Status + + + } + + { + + Agreement Type + + + } + + { + + + + } + + + + + + {/* + + */} + + ); + return ( {filterType === 'date' && renderDateFilter()} {filterType === 'main' && renderMainFilter()} + {filterType === 'contract' && renderContractFilter()} {/* add select input */} diff --git a/src/components/common/CustomTableItemDetails/CustomTableItemDetails.js b/src/components/common/CustomTableItemDetails/CustomTableItemDetails.js index ca4c8b425..4e1ab5c68 100644 --- a/src/components/common/CustomTableItemDetails/CustomTableItemDetails.js +++ b/src/components/common/CustomTableItemDetails/CustomTableItemDetails.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; import CloseIcon from '@material-ui/icons/Close'; @@ -163,7 +163,7 @@ function CustomTableItemDetails(props) { const [userName, setUserName] = useState(''); const classes = useStyles(); - React.useEffect(() => { + useEffect(() => { if (selectedItem?.status === 'paid') { treeTrackerApi .getAdminUserById(selectedItem.payment_confirmed_by) @@ -203,43 +203,90 @@ function CustomTableItemDetails(props) { {/* end detail header */} - - Grower - {selectedItem.grower} - - - Funder - {selectedItem.funder} - + {selectedItem.grower && ( + + Grower + {selectedItem.grower} + + )} + {selectedItem.funder && ( + + Funder + {selectedItem.funder} + + )} Organization - {selectedItem.sub_organization_name || '---'} + {selectedItem.sub_organization_name || + selectedItem.organization || + '---'} - - Record ID - {selectedItem.id} - + {selectedItem.id && ( + + Record ID + {selectedItem.id} + + )} + + {(selectedItem.agreement_id || + selectedItem.species_agreement_id) && ( + <> + + {Object.entries(selectedItem).map( + ([property, value]) => + property !== 'created_at' && + property !== 'updated_at' && + property !== 'id' && + property !== 'status' && ( + + + {property.replace(/_/g, ' ').toLocaleUpperCase()} + + {property.includes('id') ? ( + + {value || '---'} + + ) : ( + {value || '---'} + )} + + ) + )} + + )} - + {(selectedItem.currency || selectedItem.captures_count) && ( + <> + - - - Amount - - {selectedItem.amount} {selectedItem.currency}{' '} - - - - - Captures Count - - {selectedItem.captures_count} - - - + + {selectedItem.currency && ( + + Amount + + {selectedItem.amount} {selectedItem.currency}{' '} + + + )} + + {selectedItem.captures_count && ( + + Captures Count + + {selectedItem.captures_count || '---'} + + + )} + + + )} @@ -249,62 +296,72 @@ function CustomTableItemDetails(props) { {selectedItem.status} - - - Effective Date - - - - - {selectedItem.calculated_at} - + {selectedItem.calculated_at && ( + + + Effective Date + + + + + + {selectedItem.calculated_at} + + + )} - - - Payment Date - - - - - {selectedItem.paid_at} - + {selectedItem.paid_at && ( + + + Payment Date + + + + + {selectedItem.paid_at} + + )} - - - - - Consolidation Type - FCC Tiered - + {selectedItem.consolidation_period_start && ( + <> + - - - - Start Date - - {selectedItem.consolidation_period_start} - + + + Consolidation Type + FCC Tiered - - End Date - - {selectedItem.consolidation_period_end} - + + + + Start Date + + {selectedItem.consolidation_period_start} + + + + + End Date + + {selectedItem.consolidation_period_end} + + + - - + + )} {showLogPaymentForm && selectedItem?.status !== 'paid' && ( Date: Wed, 29 Mar 2023 12:08:38 -0700 Subject: [PATCH 5/5] feat: fix filters for agreements and contracts --- .../Contracts/ContractAgreementsTable.js | 42 ++--- src/components/Contracts/ContractsTable.js | 43 +++-- .../CustomTableFilter/CustomTableFilter.js | 170 +++++++++++++----- 3 files changed, 167 insertions(+), 88 deletions(-) diff --git a/src/components/Contracts/ContractAgreementsTable.js b/src/components/Contracts/ContractAgreementsTable.js index a6a0a44a5..4b7a2c3ac 100644 --- a/src/components/Contracts/ContractAgreementsTable.js +++ b/src/components/Contracts/ContractAgreementsTable.js @@ -194,20 +194,22 @@ function ContractAgreementsTable() { } async function getContractAgreements(fetchAll = false) { - // console.warn('fetchAll:', fetchAll); const filtersToSubmit = { ...filter }; + + console.log('getContractAgreements before ', filtersToSubmit); // filter out keys we don't want to submit Object.keys(filtersToSubmit).forEach((k) => { - if (k === 'grower') { - return; - } else { - if ( - filtersToSubmit[k] === 'all' || - filtersToSubmit[k] === '' || - k === 'organization_id' - ) { - delete filtersToSubmit[k]; - } + if (k === 'organization_id' && filtersToSubmit[k].length) { + filtersToSubmit['growing_organization_id'] = filtersToSubmit[k]; + delete filtersToSubmit[k]; + delete filtersToSubmit['sub_organization']; + } + if ( + filtersToSubmit[k] === 'all' || + filtersToSubmit[k] === '' || + filtersToSubmit[k] === undefined + ) { + delete filtersToSubmit[k]; } }); @@ -219,6 +221,8 @@ function ContractAgreementsTable() { ...filtersToSubmit, }; + console.log('getContractAgreements after', queryParams); + // log.debug('queryParams', queryParams); const response = await contractsAPI.getContractAgreements(queryParams); @@ -235,6 +239,7 @@ function ContractAgreementsTable() { const handleOpenDateFilter = () => setIsDateFilterOpen(true); useEffect(() => { + // console.log('contractAgreementsTable usEffect filter', filter); if (filter?.start_date && filter?.end_date) { const dateRangeString = generateActiveDateRangeFilterString( filter?.start_date, @@ -275,22 +280,17 @@ function ContractAgreementsTable() { selectedRow={selectedContractAgreement} tableMetaData={contractsTableMetaData} activeFiltersCount={ - Object.keys(filter).filter((key) => { - return key === 'start_date' || - key === 'end_date' || - key === 'organization_id' || - filter[key] === 'all' || - filter[key] === '' - ? false - : true; - }).length + Object.keys(filter).filter( + (k) => + filter[k] !== 'all' && filter[k] !== '' && filter[k] !== undefined + ).length } headerTitle="Contract Agreements" mainFilterComponent={ diff --git a/src/components/Contracts/ContractsTable.js b/src/components/Contracts/ContractsTable.js index cee6ca24c..e2759d00d 100644 --- a/src/components/Contracts/ContractsTable.js +++ b/src/components/Contracts/ContractsTable.js @@ -42,6 +42,12 @@ const contractsTableMetaData = [ sortable: true, showInfoIcon: false, }, + { + description: 'Agreement ID', + name: 'agreement_id', + sortable: true, + showInfoIcon: false, + }, { description: 'Type', name: 'type', @@ -172,19 +178,20 @@ function ContractsTable() { const filtersToSubmit = { ...filter, }; + console.log('getContractsReal', filtersToSubmit); // filter out keys we don't want to submit Object.keys(filtersToSubmit).forEach((k) => { - if (k === 'grower') { - return; - } else { - if ( - filtersToSubmit[k] === 'all' || - filtersToSubmit[k] === '' || - k === 'organization_id' - ) { - delete filtersToSubmit[k]; - } + // if (k === 'grower') { + // return; + // } else { + if ( + filtersToSubmit[k] === 'all' || + filtersToSubmit[k] === '' || + filtersToSubmit[k] === undefined + ) { + delete filtersToSubmit[k]; } + // } }); const queryParams = { @@ -195,6 +202,8 @@ function ContractsTable() { ...filtersToSubmit, }; + console.log('getContractsReal', queryParams); + // log.debug('queryParams', queryParams); const response = await contractsAPI.getContracts(queryParams); @@ -212,6 +221,7 @@ function ContractsTable() { const handleOpenDateFilter = () => setIsDateFilterOpen(true); useEffect(() => { + console.log('contractsTable usEffect filter', filter); if (filter?.start_date && filter?.end_date) { const dateRangeString = generateActiveDateRangeFilterString( filter?.start_date, @@ -252,15 +262,10 @@ function ContractsTable() { selectedRow={selectedContract} tableMetaData={contractsTableMetaData} activeFiltersCount={ - Object.keys(filter).filter((key) => { - return key === 'start_date' || - key === 'end_date' || - key === 'organization_id' || - filter[key] === 'all' || - filter[key] === '' - ? false - : true; - }).length + Object.keys(filter).filter( + (k) => + filter[k] !== 'all' && filter[k] !== '' && filter[k] !== undefined + ).length } headerTitle="Contracts" mainFilterComponent={ diff --git a/src/components/common/CustomTableFilter/CustomTableFilter.js b/src/components/common/CustomTableFilter/CustomTableFilter.js index 51d619973..7a1bba049 100644 --- a/src/components/common/CustomTableFilter/CustomTableFilter.js +++ b/src/components/common/CustomTableFilter/CustomTableFilter.js @@ -16,13 +16,13 @@ import SelectOrg from '../SelectOrg'; import useStyles from './CustomTableFilter.styles'; import { AppContext } from '../../../context/AppContext'; import { ALL_ORGANIZATIONS } from '../../../models/Filter'; -import { - CONTRACT_STATUS, - COORDINATOR_ROLES, - CURRENCY, - AGREEMENT_STATUS, - AGREEMENT_TYPE, -} from 'common/variables'; +// import { +// CONTRACT_STATUS, +// COORDINATOR_ROLES, +// CURRENCY, +// AGREEMENT_STATUS, +// AGREEMENT_TYPE, +// } from 'common/variables'; const PAYMENT_STATUS = ['calculated', 'cancelled', 'paid', 'all']; @@ -46,9 +46,13 @@ function CustomTableFilter(props) { grower: '', payment_status: 'all', earnings_status: 'all', - contract_status: 'all', - agreement_type: 'all', phone: '', + // status: 'all', // contract or agreement, filter not allowed + // type: 'all', // agreement, filter not allowed + owner_id: '', // agreement + name: '', // agreement + agreement_id: '', // contract + worker_id: '', // contract }; const [localFilter, setLocalFilter] = useState(initialFilter); const { @@ -72,18 +76,19 @@ function CustomTableFilter(props) { } else { updatedFilter = { ...updatedFilter, - organization_id: e?.id || ALL_ORGANIZATIONS, + organization_id: e?.stakeholder_uuid || ALL_ORGANIZATIONS, sub_organization: e?.stakeholder_uuid || ALL_ORGANIZATIONS, }; } - setLocalFilter(updatedFilter); }; const handleOnFilterFormSubmit = (e) => { e.preventDefault(); + // console.log('handleSubmit filter', filter, localFilter); const filtersToSubmit = { ...filter, + ...localFilter, grower: localFilter.grower ? localFilter.grower.trim() : undefined, phone: localFilter.phone ? localFilter.phone.trim() : undefined, payment_status: disablePaymentStatus @@ -97,12 +102,15 @@ function CustomTableFilter(props) { organization_id: '', sub_organization: '', }; + + // console.log('handleSubmit final modified', modifiedFiltersToSubmit); setFilter(modifiedFiltersToSubmit); setIsFilterOpen(false); updateSelectedFilter({ modifiedFiltersToSubmit, }); } else { + // console.log('handleSubmit final', filtersToSubmit); setFilter(filtersToSubmit); setIsFilterOpen(false); updateSelectedFilter(filtersToSubmit); @@ -240,55 +248,58 @@ function CustomTableFilter(props) { ); - const renderContractFilter = () => ( + const renderAgreementFilter = () => ( <> - { + +

Contract Agreements

+ + {/* { - Contract Status + Agreement Type - } + } */} - { + {/* { - Agreement Type + Agreement Status - } + } */} - { + {/* { - } + } */} + + + + + + + + + + ); + + const renderContractFilter = () => ( + <> + +

Contracts

+ + {/* { + + Contract Status + + + } */} - {/* - */} +
); @@ -375,6 +448,7 @@ function CustomTableFilter(props) { {filterType === 'date' && renderDateFilter()} {filterType === 'main' && renderMainFilter()} {filterType === 'contract' && renderContractFilter()} + {filterType === 'agreement' && renderAgreementFilter()} {/* add select input */}