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 (
+ <>
+
+
+ >
+ );
+}
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 (
+ <>
+
+
+ >
+ );
+}
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;