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..e3835fff3 --- /dev/null +++ b/src/api/contracts.js @@ -0,0 +1,165 @@ +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 }); + +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, + }; + + // 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 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 patchContractAgreement(contract) { + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + return Axios.patch(`/agreement`, contract, { headers }).then( + (res) => res.data + ); + }, + + /** + * @function createConsolidationRule + * @description Get Contracts from the API + * @returns {Promise} + */ + async createConsolidationRule(params) { + const headers = { + 'content-type': 'application/json', + Authorization: session.token, + }; + + // 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/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, diff --git a/src/common/variables.js b/src/common/variables.js index 182e6252d..1c4366860 100644 --- a/src/common/variables.js +++ b/src/common/variables.js @@ -21,6 +21,49 @@ export const verificationStatesArr = [ verificationStates.REJECTED, ]; +export const CONTRACT_STATUS = { + all: 'all', + unsigned: 'unsigned', // db default state + signed: 'signed', + completed: 'completed', + aborted: 'aborted', + cancelled: 'cancelled', +}; + +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 = { + 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', +}; + +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..4b7a2c3ac --- /dev/null +++ b/src/components/Contracts/ContractAgreementsTable.js @@ -0,0 +1,328 @@ +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, + }, + { + description: 'Owner ID', + name: 'owner_id', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Organization', + name: 'growing_organization_id', + sortable: true, + showInfoIcon: false, + }, + { + 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, + created_at: convertDateStringToHumanReadableFormat( + row.created_at, + 'yyyy-MM-dd' + ), + updated_at: convertDateStringToHumanReadableFormat( + row.updated_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') : '', + }; + }); + +/** + * @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 [selectedContractAgreement, setSelectedContractAgreement] = 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) { + const filtersToSubmit = { ...filter }; + + console.log('getContractAgreements before ', filtersToSubmit); + // filter out keys we don't want to submit + Object.keys(filtersToSubmit).forEach((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]; + } + }); + + const queryParams = { + offset: fetchAll ? 0 : page * contractsPerPage, + limit: fetchAll ? 90000 : contractsPerPage, + // sort_by: sortBy?.field, + // order: sortBy?.order, + ...filtersToSubmit, + }; + + console.log('getContractAgreements after', queryParams); + + // 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(() => { + // console.log('contractAgreementsTable usEffect filter', filter); + 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 ( + <> +
+ +
+ + { + setSelectedContractAgreement(value); + setDetailShown(true); + }} + selectedRow={selectedContractAgreement} + tableMetaData={contractsTableMetaData} + activeFiltersCount={ + Object.keys(filter).filter( + (k) => + filter[k] !== 'all' && filter[k] !== '' && filter[k] !== undefined + ).length + } + headerTitle="Contract Agreements" + mainFilterComponent={ + + } + dateFilterComponent={ + + } + rowDetails={ + selectedContractAgreement ? ( + { + setDetailShown(false); + setSelectedContractAgreement(null); + }} + showLogPaymentForm={false} + /> + ) : null + } + actionButtonType="export" + exportDataFetch={getContractAgreements} + /> + + ); +} + +export default ContractAgreementsTable; diff --git a/src/components/Contracts/ContractsTable.js b/src/components/Contracts/ContractsTable.js new file mode 100644 index 000000000..e2759d00d --- /dev/null +++ b/src/components/Contracts/ContractsTable.js @@ -0,0 +1,310 @@ +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: 'Agreement ID', + name: 'agreement_id', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Type', + name: 'type', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Organization', + name: 'organization', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Contractor', + name: 'worker_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: 'Notes', + name: 'notes', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Status', + name: 'status', + sortable: true, + showInfoIcon: false, + }, + { + description: 'Last Modified', + name: 'updated_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, + created_at: convertDateStringToHumanReadableFormat( + row.created_at, + 'yyyy-MM-dd' + ), + updated_at: convertDateStringToHumanReadableFormat( + row.updated_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') : '', + }; + }); + +/** + * @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 [selectedContract, setSelectedContract] = 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, + }; + 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] === '' || + filtersToSubmit[k] === undefined + ) { + delete filtersToSubmit[k]; + } + // } + }); + + const queryParams = { + offset: fetchAll ? 0 : page * contractsPerPage, + limit: fetchAll ? 90000 : contractsPerPage, + // sort_by: sortBy?.field, + // order: sortBy?.order, + ...filtersToSubmit, + }; + + console.log('getContractsReal', queryParams); + + // log.debug('queryParams', queryParams); + + const response = await contractsAPI.getContracts(queryParams); + // log.debug('getContracts response ---> ', response.contracts); + + const results = prepareRows(response.contracts); + // log.debug('prepareRows --->', results); + return { + results, + totalCount: response.query.count, + }; + } + + const handleOpenMainFilter = () => setIsMainFilterOpen(true); + const handleOpenDateFilter = () => setIsDateFilterOpen(true); + + useEffect(() => { + console.log('contractsTable usEffect filter', filter); + 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 ( + <> +
+ +
+ + { + setSelectedContract(value); + setDetailShown(true); + }} + selectedRow={selectedContract} + tableMetaData={contractsTableMetaData} + activeFiltersCount={ + Object.keys(filter).filter( + (k) => + filter[k] !== 'all' && filter[k] !== '' && filter[k] !== undefined + ).length + } + headerTitle="Contracts" + mainFilterComponent={ + + } + dateFilterComponent={ + + } + rowDetails={ + selectedContract ? ( + { + setDetailShown(false); + setSelectedContract(null); + }} + showLogPaymentForm={false} + /> + ) : null + } + actionButtonType="export" + exportDataFetch={getContracts} + /> + + ); +} + +export default ContractsTable; diff --git a/src/components/Contracts/CreateContract.js b/src/components/Contracts/CreateContract.js new file mode 100644 index 000000000..16de04493 --- /dev/null +++ b/src/components/Contracts/CreateContract.js @@ -0,0 +1,400 @@ +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 SelectOrg from '../common/SelectOrg'; +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', + }, +}); + +/* +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: '', + organization: '', + contractor: '', + totalTrees: '', + status: '', + lastModified: '', +}; + +// "id": "", -- from database? +// "agreement_id": "", +// "worker_id": "", -- ?? +// "status": "", -- from database? +// "notes": "", +// // "type": "CBO", +// // "organization": "Freetown", -- from logged in org_id +// // "contractor": "gwynn", +// // "totalTrees": "", +// // "signed_at": "", +// // "closed_at": null, +// // "listed": true + +export default function CreateContractAgreement() { + const classes = useStyles(); + const context = useContext(AppContext); + console.log('context.orgId', context.orgId); + 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 + .createContractAgreement(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/components/Contracts/CreateContractAgreement.js b/src/components/Contracts/CreateContractAgreement.js new file mode 100644 index 000000000..054b176b0 --- /dev/null +++ b/src/components/Contracts/CreateContractAgreement.js @@ -0,0 +1,405 @@ +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, + // Select, + TextField, +} from '@material-ui/core'; +import { AppContext } from '../../context/AppContext'; +import { ALL_ORGANIZATIONS, ORGANIZATION_NOT_SET } from 'models/Filter'; +// import DateFnsUtils from '@date-io/date-fns'; +import SelectOrg from '../common/SelectOrg'; +import contractsAPI from '../../api/contracts'; +import { AGREEMENT_TYPE } from 'common/variables'; +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', + }, +}); + +/* +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 = { + name: '', + type: '', + owner_id: '', + funder_id: '', + consolidation_rule_id: '6ff67c3a-e588-40e3-ba86-0df623ec6435', + species_agreement_id: 'e14b78c8-8f71-4c42-bb86-5a7f71996336', +}; + +export default function CreateContractAgreement() { + const classes = useStyles(); + const { orgId, userHasOrg, orgList } = useContext(AppContext); + console.log( + 'orgId - ', + orgId, + 'userHasOrg - ', + userHasOrg + // 'orgList - ', + // orgList + ); + const [formData, setFormData] = useState(initialState); + const [open, setOpen] = useState(false); + const [errors, setErrors] = useState({}); + + const openModal = () => { + 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/components/common/CustomTableFilter/CustomTableFilter.js b/src/components/common/CustomTableFilter/CustomTableFilter.js index dccb9eb07..7a1bba049 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,11 +42,17 @@ 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', 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 { @@ -63,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 @@ -88,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); @@ -231,6 +248,173 @@ function CustomTableFilter(props) { ); + const renderAgreementFilter = () => ( + <> + +

Contract Agreements

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

Contracts

+ + {/* { + + Contract Status + + + } */} + + + + + + + + + + ); + return ( {filterType === 'date' && renderDateFilter()} {filterType === 'main' && renderMainFilter()} + {filterType === 'contract' && renderContractFilter()} + {filterType === 'agreement' && renderAgreementFilter()} {/* 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' && ( { + 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;