Untitled

 avatar
unknown
plain_text
2 years ago
460 kB
8
Indexable
// Code from file collective-frontend/src/modules/Hub/index.jsx
import React from 'react';
import { Redirect, Route, useLocation } from 'react-router-dom';

import lazyWithSuspenseAndRetry from 'modules/common/lazyWithSuspenseAndRetry';

const HubHomePage = lazyWithSuspenseAndRetry(() => import('./Home/Page'));

/**
 * This function redirects old urls to the new hub path.
 * Should be removed when everything is migrated to the correct path on SFDC
 */
function RedirectToHub() {
    const location = useLocation();

    return <Redirect to={`/hub${location.pathname}`} />;
}

export default function getHubRoutes() {
    return [
        <Route exact key="hub-home" path="/hub/*" component={HubHomePage} />,
        <Route exact key="client-info" path="/client-info/*" to={RedirectToHub} />,
        <Route exact key="transition-plan" path="/transition-plan/*" component={RedirectToHub} />,
    ];
}
// Code from file collective-frontend/src/modules/Hub/ClientInfo/index.jsx
import React from 'react';
import { Redirect, Route } from 'react-router-dom';

import lazyWithSuspenseAndRetry from 'modules/common/lazyWithSuspenseAndRetry';

export const ClientInfoSearchPage = lazyWithSuspenseAndRetry(() => import('./Search/Page'));
export const ClientInfoDetailsPage = lazyWithSuspenseAndRetry(() => import('./Details/Page'));

export default function getHubClientInfoRoutes() {
    return [
        <Route exact key="client-info-search" path="/hub/client-info/search" component={ClientInfoSearchPage} />,
        <Route exact key="client-info-details" path="/hub/client-info/:id/info" component={ClientInfoDetailsPage} />,
    ];
}
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/Page.test.jsx
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';

import ClientInfoDetails from './Page';

import RootStoreContext from 'modules/common/stores/Root/Context';
import browserHistory from 'modules/common/browserHistory';

const history = browserHistory;

jest.mock('moment', () => {
    return () => jest.requireActual('moment')();
});

class MockLegacyStore {
    getTransactionInfo = () => jest.fn();

    getSplitTreatmentAttributes = () => jest.fn();

    fetchBankStatus = () => jest.fn();

    getAllStates = () => jest.fn();

    startTimeTracker = () => jest.fn();

    isEmployee = true;

    isImpersonator = false;

    transitionDocs = [];

    transitionClientInfo = {
        transition_plan_status: 'scheduled',
    };
}

const mockProps = {
    match: {
        params: {
            action: () => jest.fn(),
        },
    },
};

const renderWithProviders = (legacyStore) => {
    return render(
        <RootStoreContext.Provider value={{ legacyStore }}>
            <MemoryRouter>
                <ClientInfoDetails match={mockProps.match} history={history} />
            </MemoryRouter>
        </RootStoreContext.Provider>
    );
};

describe('ClientInfoDetails Page', () => {
    it('should render established date input when viewing LLC-SC takeover member', async () => {
        const legacyStore = new MockLegacyStore();
        legacyStore.transitionClientInfo.usertype = 'LLC-SC';
        renderWithProviders(legacyStore);
        expect(screen.getByTestId('established-date-input')).toBeInTheDocument();
        expect(screen.getByTestId('established-date-submit-btn')).toBeInTheDocument();
    });

    it('should render established date input when viewing SC-SC takeover member', async () => {
        const legacyStore = new MockLegacyStore();
        legacyStore.transitionClientInfo.usertype = 'SC-SC';
        renderWithProviders(legacyStore);
        expect(screen.getByTestId('established-date-input')).toBeInTheDocument();
        expect(screen.getByTestId('established-date-submit-btn')).toBeInTheDocument();
    });

    it('should NOT render established date input when viewing SP-SC formation member', async () => {
        const legacyStore = new MockLegacyStore();
        legacyStore.transitionClientInfo.usertype = 'SP-SC';
        renderWithProviders(legacyStore);
        expect(screen.queryByTestId('established-date-input')).not.toBeInTheDocument();
        expect(screen.queryByTestId('established-date-submit-btn')).not.toBeInTheDocument();
    });

    it('should render a readonly established date input when transition plan is approved', async () => {
        const legacyStore = new MockLegacyStore();
        legacyStore.transitionClientInfo.usertype = 'LLC-SC';
        legacyStore.transitionClientInfo.transition_plan_status = 'approved';
        renderWithProviders(legacyStore);
        expect(screen.getByTestId('established-date-input')).toBeInTheDocument();
        expect(screen.queryByTestId('established-date-submit-btn')).not.toBeInTheDocument();
    });

    it('should show the impersonator button', async () => {
        const legacyStore = new MockLegacyStore();
        legacyStore.isImpersonator = true;
        renderWithProviders(legacyStore);
        expect(screen.getByRole('button', { name: /impersonate/i })).toBeInTheDocument();
    });

    it('should not show the impersonator button', async () => {
        const legacyStore = new MockLegacyStore();
        renderWithProviders(legacyStore);
        expect(screen.queryByRole('button', { name: /impersonate/i })).not.toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ComplianceRemindersViewer/index.js
export { default } from './ComplianceRemindersViewer';
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ComplianceRemindersViewer/ComplianceRemindersTable.jsx
import * as moment from 'moment';
import React from 'react';
import { Label, Message, Table } from 'semantic-ui-react';

export default class ComplianceRemindersTable extends React.Component {
    renderStatus = (status) => {
        let color;
        switch (status) {
            case 'pending':
                color = 'yellow';
                break;
            case 'completed':
                color = 'green';
                break;
            case 'overdue':
                color = 'red';
                break;
            case 'unnecessary':
                color = 'grey';
                break;
            default:
                break;
        }
        return (
            <Label color={color} horizontal>
                {status}
            </Label>
        );
    };

    renderDate = (dateStr) => {
        const parsedDate = moment(dateStr);
        return parsedDate.isValid() ? parsedDate.format('MMM Do YYYY') : '';
    };

    render() {
        const { reminders } = this.props;

        if (reminders.length === 0) {
            return <Message info>This Member does not have any compliance reminders yet!</Message>;
        }

        return (
            <Table singleLine celled selectable compact="very" size="small">
                <Table.Header>
                    <Table.Row>
                        <Table.HeaderCell>Status</Table.HeaderCell>
                        <Table.HeaderCell>State</Table.HeaderCell>
                        <Table.HeaderCell>Report</Table.HeaderCell>
                        <Table.HeaderCell>Next Reminder</Table.HeaderCell>
                        <Table.HeaderCell>Due Date</Table.HeaderCell>
                    </Table.Row>
                </Table.Header>

                <Table.Body>
                    {reminders.map((reminder) => (
                        <Table.Row key={reminder.uuid}>
                            <Table.Cell>{this.renderStatus(reminder.status)}</Table.Cell>
                            <Table.Cell>{reminder.report.state}</Table.Cell>
                            <Table.Cell>{reminder.report.title}</Table.Cell>
                            <Table.Cell>{this.renderDate(reminder.next_reminder_date)}</Table.Cell>
                            <Table.Cell>{this.renderDate(reminder.due_date)}</Table.Cell>
                        </Table.Row>
                    ))}
                </Table.Body>
            </Table>
        );
    }
}
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ComplianceRemindersViewer/ComplianceRemindersViewer.jsx
import { observer } from 'mobx-react';
import React from 'react';
import { Loader } from 'semantic-ui-react';

import ComplianceRemindersTable from './ComplianceRemindersTable';

import RootStoreContext from 'modules/common/stores/Root/Context';

class ComplianceRemindersViewer extends React.Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
        this.state = {
            loading: true,
            errorLoading: false,
            reminders: [],
        };
    }

    componentDidMount() {
        this.store
            .fetchComplianceReminders(this.props.memberId)
            .then((data) => {
                this.setState({ reminders: data });
            })
            .catch((e) => {
                this.setState({ errorLoading: true });
                console.error(e);
                this.store.globalError = 'Error loading compliance reminders!';
            })
            .finally(() => {
                this.setState({ loading: false });
            });
    }

    render() {
        const { loading, errorLoading, reminders } = this.state;

        if (loading) {
            return <Loader active inline />;
        }

        if (errorLoading) {
            return <p>Something went wrong :(</p>;
        }

        return <ComplianceRemindersTable reminders={reminders} />;
    }
}

export default observer(ComplianceRemindersViewer);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/LoginInfoEditor/index.jsx
import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import { useFormik } from 'formik';
import { Button, FormControlLabel, Checkbox, Alert } from '@mui/material';

import collectiveApi from 'modules/common/collectiveApi';

const LoginInfoEditor = ({ member }) => {
    const [mfaEnabled, setMfaEnabled] = useState(false);
    const [canEditMfa, setEditMfa] = useState(false);
    const [loading, setLoading] = useState(false);
    const [successMessage, setSuccessMessage] = useState('');
    const [errorMessage, setErrorMessage] = useState('');
    const userId = member.user_id;
    const onSubmit = async (values, { resetForm }) => {
        setErrorMessage('');
        setSuccessMessage('');
        try {
            const res = await collectiveApi.put(`rest-auth/mfa/${userId}/`, {
                enable: values.mfaEnabled,
            });
            if (values.mfaEnabled !== res.data?.is_mfa_enabled) {
                setErrorMessage('Failed to update MFA status for this user.');
            } else {
                setMfaEnabled(res.data?.is_mfa_enabled);
                setSuccessMessage('MFA status updated.');
            }
            setEditMfa(res.data?.can_mfa_be_toggled);
        } catch (error) {
            setErrorMessage(error.response?.data?.detail);
        } finally {
            resetForm();
        }
    };

    useEffect(() => {
        setLoading(true);
        const fetchMFAStatus = async () => {
            try {
                const res = await collectiveApi.get(`rest-auth/mfa/${userId}/`);
                setMfaEnabled(res.data?.is_mfa_enabled);
                setEditMfa(res.data?.can_mfa_be_toggled);
            } catch (error) {
                setErrorMessage(error.response?.data?.detail);
            } finally {
                setLoading(false);
            }
        };
        fetchMFAStatus();
    }, [userId]);

    const { values, isSubmitting, handleSubmit, setFieldValue } = useFormik({
        initialValues: { mfaEnabled },
        enableReinitialize: true,
        onSubmit,
    });

    if (loading) {
        return null;
    }

    return (
        <>
            <h2 id="loginInformation">Login information</h2>
            {errorMessage && <Alert severity="error">{errorMessage}</Alert>}
            {successMessage && <Alert severity="success">{successMessage}</Alert>}
            {!canEditMfa && (
                <Alert severity="info">
                    MFA feature is currently inactive for this user either because MFA has been globally disabled or
                    this user has not completed onboarding yet.
                </Alert>
            )}
            <form onSubmit={handleSubmit}>
                <FormControlLabel
                    disabled={!canEditMfa}
                    checked={values.mfaEnabled}
                    onChange={() => setFieldValue('mfaEnabled', !values.mfaEnabled)}
                    control={<Checkbox />}
                    label="Require user to complete a multi-factor authentication (MFA) login."
                />
                {mfaEnabled !== values.mfaEnabled && (
                    <Button type="submit" disabled={isSubmitting}>
                        Update
                    </Button>
                )}
            </form>
        </>
    );
};

export default observer(LoginInfoEditor);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/LoginInfoEditor/index.test.jsx
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { configure } from 'mobx';
import MockAxiosAdapter from 'axios-mock-adapter';

import LoginInfoEditor from './index';

import collectiveApi from 'modules/common/collectiveApi';

configure({ safeDescriptors: false });

const mockCollectiveApi = new MockAxiosAdapter(collectiveApi); // Expected order of requests:

const setup = (enableMfaEditing = true) => {
    mockCollectiveApi
        .onGet('rest-auth/mfa/62/')
        .reply(200, { is_mfa_enabled: true, can_mfa_be_toggled: enableMfaEditing });
    render(<LoginInfoEditor member={{ user_id: 62 }} />);
};

describe('LoginInfoEditor - MFA editing', () => {
    it('Should render correctly', async () => {
        setup();
        const title = await screen.findByText(/Login information/i);
        expect(title).toBeInTheDocument();
    });

    it('should have MFA status as true', async () => {
        setup();
        const checkbox = await screen.findByRole('checkbox');
        expect(checkbox).toBeChecked();
    });

    it('Should update MFA status to false', async () => {
        setup();
        mockCollectiveApi.onPut('rest-auth/mfa/62/').reply(200, { is_mfa_enabled: false, can_mfa_be_toggled: true });
        const user = userEvent.setup();
        const checkbox = await screen.findByRole('checkbox');
        await user.click(checkbox);
        const submitButton = screen.getByRole('button', { name: 'Update' });
        expect(submitButton).toBeInTheDocument();
        await user.click(submitButton);
        await waitFor(() => {
            expect(screen.getByText(/MFA status updated./i)).toBeInTheDocument();
        });
    }, 20000);

    it('Should fail to update MFA status', async () => {
        setup();
        mockCollectiveApi.onPut('rest-auth/mfa/62/').reply(400, { detail: 'backend error' });
        const user = userEvent.setup();
        const checkbox = await screen.findByRole('checkbox');
        await user.click(checkbox);
        const submitButton = screen.getByRole('button', { name: 'Update' });
        expect(submitButton).toBeInTheDocument();
        await user.click(submitButton);
        await waitFor(() => {
            expect(screen.getByText(/backend error/i)).toBeInTheDocument();
        });
    }, 20000);

    it('Should not be able to edit MFA', async () => {
        setup(false);
        await waitFor(() => {
            const checkbox = screen.getByRole('checkbox');
            expect(checkbox).toBeDisabled();
        });
    }, 20000);
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/LLCFilingEditor/FormFileUpload.js
import { useState } from 'react';
import { observer } from 'mobx-react';
import { Button, Icon, Modal, Segment } from 'semantic-ui-react';

import UploadToFilestack from 'modules/common/UploadToFilestack';

const FormFileUpload = ({ btnText = 'Upload document', onUpload, onRemove, name }) => {
    const [showModal, setShowModal] = useState(false);
    const [documentTitle, setDocumentTitle] = useState('');
    const [documentUrl, setDocumentUrl] = useState('');
    const [documents, setDocuments] = useState([]);

    const onSuccess = (result) => {
        setDocuments(result.filesUploaded);
        setDocumentTitle(result.filesUploaded[0].filename);
        setDocumentUrl(result.filesUploaded[0].url);

        if (onUpload) {
            onUpload(name, result.filesUploaded);
        }
    };

    const removeUpload = () => {
        setDocuments([]);

        if (onRemove) {
            onRemove(name);
        }
    };

    const showModalToggle = () => {
        setShowModal(!showModal);
    };

    const MOBILE_DEVICE = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;

    const uploadedRow = documents.map((document) => {
        return (
            <Segment key={document.filename} className="upload-document tw-w-full" clearing textAlign="left">
                {document.filename ? document.filename : document['document-title']}
                {MOBILE_DEVICE ? null : (
                    <Button icon className="tw-mr-0.5" floated="right" size="mini" onClick={removeUpload}>
                        <Icon name="close" />
                    </Button>
                )}
                {MOBILE_DEVICE ? null : (
                    <Button icon className="tw-mr-0.5" floated="right" size="mini" onClick={showModalToggle}>
                        <Icon name="eye" />
                    </Button>
                )}
            </Segment>
        );
    });

    return (
        <>
            {documents.length === 0 && (
                <div className="tw-mb-4">
                    <UploadToFilestack primary buttonText={btnText} onSuccess={onSuccess} displayMode="dropPane" />
                </div>
            )}
            {documents && <div className="tw-mb-4">{uploadedRow}</div>}

            <Modal
                size="large"
                className="hyke-modal collective-modal"
                open={showModal}
                onClose={showModalToggle}
                closeIcon>
                <Modal.Content>
                    <div className="modal-body">
                        <div>
                            <h3>{documentTitle}</h3>
                            <iframe
                                title={documentTitle}
                                src={documentUrl}
                                style={{ height: '80vh' }}
                                width="100%"
                                frameBorder="1"
                            />
                        </div>
                    </div>
                </Modal.Content>
            </Modal>
        </>
    );
};

export default observer(FormFileUpload);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/LLCFilingEditor/index.js
export { default } from './LLCFilingEditor';
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/LLCFilingEditor/LLCFilingEditor.jsx
import * as Sentry from '@sentry/react';
import { useFormik } from 'formik';
import { observer } from 'mobx-react';
import { useState } from 'react';
import MaskedInput from 'react-text-mask';
import { Button, Form } from 'semantic-ui-react';
import { array, object, string } from 'yup';

import FormFileUpload from './FormFileUpload';

import collectiveApi from 'modules/common/collectiveApi';
import states from 'modules/common/constants/states';
import { date } from 'modules/common/masks';
import { dateRegex } from 'modules/common/regexes';

const statesOptions = Object.entries(states).map(([key, value]) => ({ key, text: value, value: key }));

const initialValues = {
    establisheddate: '',
    entitynumber: '',
    state_of_inc: '',
    llcFiling: null,
};

const validationSchema = object({
    establisheddate: string()
        .label('Established date')
        .matches(dateRegex, 'Established date is not in the correct format')
        .required(),
    entitynumber: string().label('Entity number').required(),
    state_of_inc: string().label('State of incorporation').length(2).required(),
    llcFiling: array().min(1),
});

const LLCFilingEditor = ({ client, llcformationstatus }) => {
    const [llcFilingCreated, setLlcFilingCreated] = useState(false);

    const { id: memberId } = client;

    const postMemberLlcFiling = async (memberId, postObject) => {
        try {
            const { data: success } = await collectiveApi.post(`/llc-filing/${memberId}/`, postObject);
            setLlcFilingCreated(success);
        } catch (error) {
            console.error("Error while creating member's llc filing: ", error);
            Sentry.captureException(error);
            throw error;
        }
    };

    const onSubmit = async (values) => {
        const postObject = {
            file_name: values.llcFiling[0].filename,
            file_link: values.llcFiling[0].url,
            establisheddate: values.establisheddate,
            entitynumber: values.entitynumber,
            state_of_inc: values.state_of_inc,
        };
        await postMemberLlcFiling(memberId, postObject);
    };

    const { values, errors, touched, isValid, isSubmitting, handleChange, handleBlur, handleSubmit, setFieldValue } =
        useFormik({
            initialValues,
            validationSchema,
            onSubmit,
        });

    const handleFileUpload = (name, files) => {
        setFieldValue(name, files);
    };

    const handleFileRemove = (name) => {
        setFieldValue(name, []);
    };

    const handleSelectChange = (event, data) => {
        event.target.name = data.name;
        event.target.value = data.value;
        handleChange(event);
    };

    const handleSelectBlur = (event, data) => {
        event.target.name = data.name;
        event.target.value = data.value;
        handleBlur(event);
    };

    if (llcformationstatus === 'completed' || llcFilingCreated) {
        return (
            <div className="ui success message">
                <div className="header">LLC filing has been successfully completed for this member</div>
            </div>
        );
    }

    return (
        <Form className="tw-w-60">
            <Form.Input
                label="When is the established date for this member?"
                error={touched.establisheddate && errors.establisheddate}
                fluid>
                <MaskedInput
                    type="text"
                    name="establisheddate"
                    id="establisheddate"
                    data-testid="establisheddate"
                    value={values.establisheddate}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    mask={date}
                    guide={false}
                />
            </Form.Input>
            <Form.Input
                label="What is the entity number for this member?"
                name="entitynumber"
                id="entitynumber"
                data-testid="entitynumber"
                value={values.entitynumber}
                error={touched.entitynumber && errors.entitynumber}
                onChange={handleChange}
                onBlur={handleBlur}
                fluid
            />
            <Form.Select
                label="What is the state of inc for this member?"
                options={statesOptions}
                data-testid="state_of_inc"
                name="state_of_inc"
                id="state_of_inc"
                value={values.state_of_inc}
                error={touched.state_of_inc && errors.state_of_inc}
                onChange={handleSelectChange}
                onBlur={handleSelectBlur}
                search
                fluid
            />
            <div className="field">
                <label htmlFor="llcFiling">LLC Filing Document Upload</label>
            </div>
            <FormFileUpload
                name="llcFiling"
                onUpload={handleFileUpload}
                onRemove={handleFileRemove}
                type="LLC Filing"
            />
            <br />
            <Button type="submit" disabled={isSubmitting || !isValid} primary size="small" onClick={handleSubmit}>
                {isSubmitting ? 'Saving...' : 'Save'}
            </Button>
        </Form>
    );
};

export default observer(LLCFilingEditor);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/LLCFilingEditor/LLCFilingEditor.test.jsx
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { configure } from 'mobx';

import LLCFilingEditor from '.';

import RootStoreContext from 'modules/common/stores/Root/Context';
import HubStoreContext from 'modules/Hub/common/Store/Context';

configure({ safeDescriptors: false });

class MockLegacyStore {
    fetchFilestackCredentials = () => ({ api_key: 'api_key_test', policy: 'policy_test', signature: 'signature_test' });
}

class MockHubStore {
    // saveTaxPayment = () => jest.fn();
}

class MockClient {
    id = 1;
}

const renderWithProviders = (llcformationstatus) => {
    const legacyStore = new MockLegacyStore();
    const hubStore = new MockHubStore();
    const client = new MockClient();

    return render(
        <RootStoreContext.Provider value={{ legacyStore }}>
            <HubStoreContext.Provider value={{ hubStore }}>
                <LLCFilingEditor client={client} llcformationstatus={llcformationstatus} />
            </HubStoreContext.Provider>
        </RootStoreContext.Provider>
    );
};

beforeEach(cleanup);

describe('LLCFilingEditor', () => {
    it('should render correctly', async () => {
        renderWithProviders('inprogress');

        const elements = await screen.findAllByText(/When is the established date for this member?/i);

        expect(elements[0]).toBeInTheDocument();
    });

    it('should show completed lcc filing message', async () => {
        renderWithProviders('completed');

        const elements = await screen.findAllByText(/LLC filing has been successfully completed for this member/i);

        expect(elements[0]).toBeInTheDocument();
    });

    it('should show lcc filing form', async () => {
        renderWithProviders('inprogress');

        const button = screen.getByRole('button', {
            name: /Save/i,
        });
        const user = userEvent.setup();

        const establisheddateInput = screen.getByTestId('establisheddate');
        const entitynumberInput = screen.getByTestId('entitynumber');
        const stateofincSelect = screen.getByTestId('state_of_inc');

        await user.type(establisheddateInput, '12/12/2021');
        await user.type(entitynumberInput, '123456');
        await user.type(stateofincSelect, 'California');

        expect(button).toBeDisabled();

        const uploadButton = screen.getByRole('button', {
            name: /Upload document/i,
        });

        await user.click(uploadButton);
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/Page.js
import { observer } from 'mobx-react';
import { Component } from 'react';
import { Button, Icon, Input } from 'semantic-ui-react';
import moment from 'moment';

import TimeoutModal from '../../common/TimeoutModal';
import TransitionInput from '../../common/TransitionInput';
import TransitionNavBar from '../../common/TransitionNavBar';
import UploadTransitionDocument from '../../common/UploadDocument';
import UploadParticularDocument from '../../common/UploadParticularDocument';
import ImpersonateButton from '../common/ImpersonateButton';

import BankStatus from './BankStatus';
import ComplianceRemindersViewer from './ComplianceRemindersViewer';
import ForeignRegistrationsEditor from './ForeignRegistrationsEditor';
import TaxPaymentsEditor from './TaxPaymentsEditor';
import LLCFilingEditor from './LLCFilingEditor';
import EINFilingEditor from './EINFilingEditor';
import LoginInfoEditor from './LoginInfoEditor';
import RegenerateFormW9Button from './RegenerateFormW9';

import collectiveApi from 'modules/common/collectiveApi';
import RootStoreContext from 'modules/common/stores/Root/Context';
import AccountablePlan from 'modules/common/AccountablePlan';

class ClientInfoDetailsPage extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.state = {
            client: {},
            'personal-information': true,
            'business-information': true,
            isForm2553Regenerated: false,
            isForm2553RegenerationError: false,
            establishedDate: '',
            savingEstablishedDate: false,
        };
        this.store = context.legacyStore;
    }

    componentDidMount() {
        this.store.redirectedfrompage = `hub/client-info/${this.props.match.params.id}/info`;
        if (!this.store.isLoginSuccess || !this.store.isEmployee) {
            this.props.history.push('/login');
        }

        const sfid = this.props.match.params.id;
        this.fetchClientInfo(sfid);
        // Get list of states from Backend and populate for later use
        this.store.getAllStates();

        if (!this.store.startedTimeTracking) {
            this.store.startTimeTracker();
        }
    }

    fetchClientInfo = async (sfid) => {
        try {
            const {
                data: { data: client, success },
            } = await collectiveApi.get(`getclient/`, { params: { sfid } });

            if (!success) {
                throw new Error();
            }

            this.setState({
                client,
                errorMessage: null,
            });
            await this.store.getTransitionClientInfo(client?.sfaccountid);
            this.setState({
                establishedDate: moment(this.store.transitionClientInfo.date_of_incorporation).format('YYYY-MM-DD'),
            });
            this.store.fetchTransitionUserDocuments(client?.email);
        } catch (error) {
            console.log('Fetch client info error.', error);
            this.setState({
                errorMessage:
                    'Unable to find the specified Collective user - Please try searching again or contact Engineering',
            });
        }
    };

    regenerateForm2553 = async () => {
        try {
            await collectiveApi.patch(`regenerate-form-2553/`, {
                email: this.state.client?.email,
            });

            this.setState({
                isForm2553Regenerated: true,
                isForm2553RegenerationError: false,
            });
        } catch (error) {
            console.log('Regenerate Form 2553 error.', error);
            this.setState({
                isForm2553RegenerationError: true,
            });
        }
    };

    toggleNav = (section) => {
        const obj = {};
        obj[section] = !this.state[section];
        section = !this.state[section];
        this.setState(obj);
    };

    getOtherDocuments = () => {
        const docs = [];
        this.store.transitionDocs.forEach((doc) => {
            if (!doc['document-title'].includes('Tax')) {
                docs.push(doc);
            }
        });

        return (
            <>
                <UploadParticularDocument documentTitle="Annual Report" />
                <UploadParticularDocument documentTitle="IRS EIN" />
                <UploadParticularDocument documentTitle="Operating Agreement" />
                <UploadParticularDocument documentTitle="Business License" />
                <UploadParticularDocument documentTitle="Statement of Information" />
                <UploadParticularDocument documentTitle="Form 2553" />
                <UploadTransitionDocument />
            </>
        );
    };

    getTakeoverDocs = () => {
        return (
            <>
                <UploadParticularDocument documentTitle="W9" />
                <UploadParticularDocument documentTitle="Articles of Organization" />
                <UploadParticularDocument documentTitle="Foreign Registration Document" />
            </>
        );
    };

    getRegenerateForm2553Button = () => {
        if (this.state.isForm2553Regenerated) {
            return (
                <div className="ui success message">
                    <div className="header">Form 2553 was regenerated successfully.</div>
                </div>
            );
        }
        return (
            <>
                <Button primary size="small" content="Regenerate" onClick={this.regenerateForm2553} />
                {this.state.isForm2553RegenerationError ? (
                    <div className="ui negative message">
                        <div className="header">Form 2553 regeneration was unsuccessful.</div>
                    </div>
                ) : null}
            </>
        );
    };

    getTaxDocuments = () => {
        return <UploadParticularDocument documentTitle="Tax Return" />;
    };

    handleEstablishedDateChange = (e) => this.setState({ establishedDate: e.target.value });

    handleEstablishedDateSave = () => {
        this.setState({ savingEstablishedDate: true });
        this.store.transitionClientInfo.establisheddate = moment(this.state.establishedDate).format('MM/DD/YYYY');
        this.store.updateClientInfo('establisheddate').then(() => {
            this.setState({ savingEstablishedDate: false });
        });
    };

    renderEstablishedDateInput = () => {
        if (this.store.transitionClientInfo.usertype === 'SP-SC') {
            // Do not show this input for Formation members
            return null;
        }

        const inputProps = {
            type: 'date',
            value: this.state.establishedDate,
            onChange: this.handleEstablishedDateChange,
            'data-testid': 'established-date-input',
        };

        if (this.store.transitionClientInfo.transition_plan_status === 'approved') {
            // Once the transition plan is approved, this input should be readonly
            inputProps.disabled = true;
        } else {
            inputProps.action = {
                icon: 'save',
                size: 'small',
                onClick: this.handleEstablishedDateSave,
                loading: this.state.savingEstablishedDate,
                'data-testid': 'established-date-submit-btn',
            };
        }

        return (
            <>
                <p>Established Date</p>
                <Input {...inputProps} />
            </>
        );
    };

    render() {
        return this.store.isEmployee ? (
            <>
                <TransitionNavBar
                    clientName={this.store.transitionClientInfo.fullname}
                    sfid={this.store.transitionClientInfo.sf_id}
                    tpid={this.store.transitionClientInfo.transition_plan_id}
                    clientCompany={this.store.transitionClientInfo.business_name}
                />

                <div className="transition-two-column">
                    <div id="personalInformation" className="transition-info">
                        <h1>Personal Information</h1>
                        <p>Full legal name</p>
                        <TransitionInput clientInfo name="fullname" />

                        <p>What's your full legal address?</p>
                        <TransitionInput clientInfo name="home_address" />

                        <p>Home Address Number</p>
                        <TransitionInput clientInfo name="home_number" />

                        <p>Home Address Street</p>
                        <TransitionInput clientInfo name="home_street" />

                        <p>Home Address Apt/Unit</p>
                        <TransitionInput clientInfo name="home_aptunit" />

                        <p>Home Address City</p>
                        <TransitionInput clientInfo name="home_city" />

                        <p>Home Address ZIP Code</p>
                        <TransitionInput clientInfo name="home_zipcode" />

                        <p>State of residency</p>
                        <TransitionInput clientInfo name="home_state" />

                        <p>E-mail address</p>
                        <TransitionInput clientInfo name="email" />

                        <p>What's your phone number?</p>
                        <TransitionInput clientInfo name="phone_number" />

                        <p>When is your birthday?</p>
                        <TransitionInput clientInfo name="dob" />

                        <p>US Citizen or permanent resident?</p>
                        <TransitionInput clientInfo name="citizen_or_resident" />

                        <p>Citizenship</p>
                        <TransitionInput clientInfo name="citizenship" />

                        <p>Self-service Checkout</p>
                        <TransitionInput clientInfo name="is_ssc_eligible" />

                        <h2>Personal documents</h2>
                        {this.getOtherDocuments()}
                        {this.state.client?.id && <LoginInfoEditor member={this.state.client} />}
                        <h1 id="organization" style={{ paddingBottom: '24px' }}>
                            Business Information
                        </h1>
                        <h2>Organization</h2>

                        <p>LLC name</p>
                        <TransitionInput clientInfo name="business_name" />

                        <p>Business name suffix</p>
                        <TransitionInput clientInfo name="inc_suffix" />

                        <p>Organization Type</p>
                        <TransitionInput clientInfo name="incorporation" />

                        <p>Business Address</p>
                        <TransitionInput clientInfo name="business_address" placesInput />

                        <p>Business Address Number</p>
                        <TransitionInput clientInfo name="business_number" />

                        <p>Business Address Street</p>
                        <TransitionInput clientInfo name="business_street" />

                        <p>Business Address Apt/Unit</p>
                        <TransitionInput clientInfo name="business_aptunit" />

                        <p>Business Address City</p>
                        <TransitionInput clientInfo name="business_city" />

                        <p>Business Address State</p>
                        <TransitionInput clientInfo name="business_state" />

                        <p>Business Address ZIP Code</p>
                        <TransitionInput clientInfo name="business_zipcode" />

                        <p>Mail Address</p>
                        <TransitionInput clientInfo name="mail_address" placesInput />

                        <p>Mail Address Number</p>
                        <TransitionInput clientInfo name="mail_number" />

                        <p>Mail Address Street</p>
                        <TransitionInput clientInfo name="mail_street" />

                        <p>Mail Address Apt/Unit</p>
                        <TransitionInput clientInfo name="mail_aptunit" />

                        <p>Mail Address City</p>
                        <TransitionInput clientInfo name="mail_city" />

                        <p>Mail Address State</p>
                        <TransitionInput clientInfo name="mail_state" />

                        <p>Mail Address ZIP Code</p>
                        <TransitionInput clientInfo name="mail_zipcode" />

                        <p>State of residency</p>
                        <TransitionInput clientInfo name="home_state" />

                        <p>Live and work in the same state</p>
                        <TransitionInput clientInfo name="business_home_same_state" />

                        <p>State of business</p>
                        <TransitionInput clientInfo name="state_of_business" />

                        <p>State of operation</p>
                        <TransitionInput clientInfo name="state_of_operation" />

                        <p>State of incorporation</p>
                        <TransitionInput clientInfo name="state_of_incorporation" />

                        <p>LLC Entity number</p>
                        <TransitionInput clientInfo name="entity_number" />

                        <p>Spouse owns portion of LLC</p>
                        <TransitionInput clientInfo name="spouse_owns_portion" />

                        <p>Name of spouse</p>
                        <TransitionInput clientInfo name="spouse_name" />

                        <p>Percentage</p>
                        <TransitionInput clientInfo name="spouse_share" />

                        <p>Spouse lives at the same home address</p>
                        <TransitionInput clientInfo name="spouse_sameaddress" />

                        <p>Registered Agent</p>
                        <TransitionInput clientInfo name="registered_agent" />

                        <p>Regenerate Form W9</p>
                        <RegenerateFormW9Button email={this.state.client.email} />

                        <h2 id="finances">Organization documents</h2>
                        {this.getTakeoverDocs()}

                        <h2>Finances</h2>

                        <p>Base year</p>
                        <TransitionInput clientInfo name="base_year" />

                        <p>Expected Revenue</p>
                        <TransitionInput clientInfo name="expected_revenue" />

                        <p>Expected expenses</p>
                        <TransitionInput clientInfo name="expected_expenses" />

                        <p>Personal & business expenses separated</p>
                        <TransitionInput clientInfo name="expenses_separated" />

                        <p id="services">Uses a bookkeeping tool</p>
                        <TransitionInput clientInfo name="bookkeeping_tool" />

                        <p>Version</p>
                        <TransitionInput clientInfo name="bookkeeping_version" />

                        <h2>Services</h2>

                        <p>Professional license required</p>
                        <TransitionInput clientInfo name="pro_license" />

                        <p>Service offered</p>
                        <TransitionInput clientInfo name="service_offered" />

                        <p>Selling tangible goods</p>
                        <TransitionInput clientInfo name="tangible_goods" />

                        <h2>Tax</h2>

                        <p>EIN</p>
                        <TransitionInput clientInfo name="ein" />

                        {this.renderEstablishedDateInput()}

                        <p>Federal Business Name (from CP-575)</p>
                        <TransitionInput clientInfo name="federal_business_name" />

                        <p>Last time paid CA FTB LLC fee</p>
                        <TransitionInput clientInfo name="ftb_last_paid" />

                        <p>FTB fee amount</p>
                        <TransitionInput clientInfo name="ftb_last_amount" />

                        <p>Regenerate Form 2553</p>
                        {this.getRegenerateForm2553Button()}

                        <h2 id="bankInformation">Tax documents</h2>
                        {this.getTaxDocuments()}

                        {this.store.transitionClientInfo.email && (
                            <>
                                <h2 id="bankInformation">Bank Information</h2>
                                <BankStatus email={this.store.transitionClientInfo.email} />
                            </>
                        )}

                        {this.state.client.id && (
                            <>
                                <h2 id="foreignRegistrations">Foreign Registrations</h2>
                                <ForeignRegistrationsEditor memberId={this.state.client.id} />

                                <h2 id="complianceReminders">Compliance Reminders</h2>
                                <ComplianceRemindersViewer memberId={this.state.client.id} />

                                <h2 id="taxPayments">Tax Payments</h2>
                                <TaxPaymentsEditor memberId={this.state.client.id} />

                                <h2 id="llcFiling">LLC Filing</h2>
                                <LLCFilingEditor
                                    client={this.state.client}
                                    llcformationstatus={this.store.transitionClientInfo?.llcformationstatus}
                                />

                                <h2 id="llcFiling">EIN Filing</h2>
                                <EINFilingEditor
                                    client={this.state.client}
                                    einstatus={this.store.transitionClientInfo?.einstatus}
                                />

                                <h2 id="accountable-plan">Accountable Plan</h2>
                                <AccountablePlan readOnly memberId={this.state.client.id} />
                            </>
                        )}
                    </div>

                    <div className="transition-sticky-navigation">
                        {this.store.isImpersonator && (
                            <div className="tw-mb-2">
                                <ImpersonateButton
                                    userId={this.state.client.id}
                                    displayName={this.state.client.fullname}
                                />
                            </div>
                        )}

                        <div className="sticky-box">
                            <h2 onClick={() => this.toggleNav('personal-information')}>
                                Personal Information
                                <Icon name={this.state['personal-information'] ? 'angle up' : 'angle down'} />
                            </h2>

                            {this.state['personal-information'] && (
                                <a href="#personalInformation">Personal Information</a>
                            )}

                            <h2 onClick={() => this.toggleNav('business-information')}>
                                Business Information{' '}
                                <Icon name={this.state['business-information'] ? 'angle up' : 'angle down'} />
                            </h2>

                            {this.state['business-information'] && (
                                <>
                                    <a href="#organization">Organization</a>
                                    <a href="#finances">Finances</a>
                                    <a href="#services">Services</a>
                                    <a href="#bankInformation">Bank Information</a>
                                    <a href="#foreignRegistrations">Foreign Registrations</a>
                                    <a href="#complianceReminders">Compliance Reminders</a>
                                    <a href="#taxPayments">Tax Payments</a>
                                    <a href="#accountable-plan">Accountable Plan</a>
                                    <a href="#llcFiling">LLC Filing</a>
                                    <a href="#llcFiling">EIN Filing</a>
                                </>
                            )}
                        </div>
                    </div>
                </div>

                {this.store.showTimeoutModal ? <TimeoutModal /> : null}
            </>
        ) : (
            <h2>You are not authorized to view this page</h2>
        );
    }
}

export default observer(ClientInfoDetailsPage);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/BankStatus.js
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import ClickOutHandler from 'react-onclickout';

import RootStoreContext from 'modules/common/stores/Root/Context';

class BankStatus extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
        this.state = { hover: false, showBankModal: false };
    }

    componentDidMount() {
        this.store.fetchBankStatus(this.props.email);
    }

    toggleStateProperty = (e, stateProperty) => {
        e.stopPropagation();
        this.setState((previousState) => ({ [stateProperty]: !previousState[stateProperty] }));
    };

    closeBankModal(e) {
        e.stopPropagation();
        if (this.state.showBankModal) {
            this.setState({ showBankModal: false });
        }
    }

    render() {
        return this.store.bank_account_connected ? (
            <div className="transition-input">
                Connected
                <ClickOutHandler onClickOut={(e) => this.closeBankModal(e)}>
                    <div className="transition-edit" onClick={(e) => this.toggleStateProperty(e, 'showBankModal')}>
                        Disconnect Bank
                        {this.state.showBankModal && (
                            <div>
                                <div
                                    className="confirm-button"
                                    onClick={(e) => this.store.disconnectBank(this.props.email)}>
                                    Yes, disconnect
                                </div>
                                <div
                                    className="confirm-button"
                                    onClick={(e) => this.toggleStateProperty(e, 'showBankModal')}>
                                    No, cancel
                                </div>
                            </div>
                        )}
                    </div>
                    {this.store.bank_account_feedback && <p>{this.store.bank_account_feedback}</p>}
                </ClickOutHandler>
            </div>
        ) : (
            <div>{this.store.bank_account_feedback ? `${this.store.bank_account_feedback}` : 'Disconnected'}</div>
        );
    }
}

export default observer(BankStatus);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/index.js
export { default } from './TaxPaymentsEditor';
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/QTEPreviewEmail.jsx
import Parser from 'html-react-parser';
import React, { Component } from 'react';
import { Modal, Button } from 'semantic-ui-react';
import { observer } from 'mobx-react';

import RootStoreContext from 'modules/common/stores/Root/Context';

class QTEPreviewEmail extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
        this.state = {
            fetchedQte: false,
            showModal: false,
            emailBody: '',
            emailSubject: '',
        };
    }

    loadQTE = () => {
        const { qteId } = this.props;
        if (this.state.fetchedQte) {
            this.setState({ showModal: true });
            return;
        }

        this.store
            .fetchQTEPreviewEmail(qteId)
            .then((response) => {
                this.setState({
                    emailBody: response.body,
                    emailSubject: response.subject,
                    fetchedQte: true,
                    showModal: true,
                });
            })
            .catch((e) => {
                this.store.globalError = 'error preview qte email';
            });
    };

    render() {
        return (
            <>
                <Button primary size="tiny" content="Preview E-mail" onClick={this.loadQTE} />
                <Modal
                    size="large"
                    className="hyke-modal collective-modal"
                    open={this.state.showModal}
                    onClose={() => this.setState({ showModal: false })}
                    closeIcon>
                    <Modal.Content>
                        <div className="modal-body">
                            <h2>QTE Preview E-mail</h2>
                            <h4>{this.state.emailSubject}</h4>
                            {Parser(this.state.emailBody)}
                        </div>
                    </Modal.Content>
                </Modal>
            </>
        );
    }
}

export default observer(QTEPreviewEmail);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/TaxesPaidForm.jsx
import React from 'react';
import { Button } from 'semantic-ui-react';
import { withFormik } from 'formik';
import * as yup from 'yup';

import MoneyInput from 'modules/common/MoneyInput';

const TaxesPaidFormik = ({ values, handleSubmit, handleChange, touched, errors, isSubmitting }) => {
    return (
        <>
            {errors.message && <p className="error">{errors.message}</p>}
            <p>Actual Federal Taxes Paid (When different than the amount in QTE):</p>
            <MoneyInput name="federalTaxesPaid" value={values.federalTaxesPaid} onChange={handleChange} min={0} />
            {touched.federalTaxesPaid && errors.federalTaxesPaid && <p className="error">{errors.federalTaxesPaid}</p>}

            <p>Actual State Taxes Paid (When different than the amount in QTE):</p>
            <MoneyInput name="stateTaxesPaid" value={values.stateTaxesPaid} onChange={handleChange} min={0} />
            {touched.stateTaxesPaid && errors.stateTaxesPaid && <p className="error">{errors.stateTaxesPaid}</p>}

            {(values.federalTaxesPaid || values.stateTaxesPaid) && (
                <>
                    <p>
                        <b>Extra Taxes Paid or previous payments carried over</b>
                    </p>
                    <p>Extra Federal Taxes Paid (If you enter a value here, don't leave the top 2 fields blank):</p>
                    <MoneyInput
                        name="extraFederalPaid"
                        value={values.extraFederalPaid}
                        onChange={handleChange}
                        min={0}
                    />
                    {touched.extraFederalPaid && errors.extraFederalPaid && (
                        <p className="error">{errors.extraFederalPaid}</p>
                    )}

                    <p>Extra State Taxes Paid (If you enter a value here, don't leave the top 2 fields blank):</p>
                    <MoneyInput name="extraStatePaid" value={values.extraStatePaid} onChange={handleChange} min={0} />
                    {touched.extraStatePaid && errors.extraStatePaid && (
                        <p className="error">{errors.extraStatePaid}</p>
                    )}
                </>
            )}
            <p>
                <Button type="submit" disabled={isSubmitting} primary size="small" onClick={handleSubmit}>
                    {isSubmitting ? 'Saving...' : 'Save'}
                </Button>
            </p>
        </>
    );
};

const TaxesPaidForm = withFormik({
    mapPropsToValues: ({ federalTaxesPaid, stateTaxesPaid, extraFederalPaid, extraStatePaid }) => {
        return { federalTaxesPaid, stateTaxesPaid, extraFederalPaid, extraStatePaid };
    },
    validationSchema: yup.object().shape({
        federalTaxesPaid: yup.number().required(),
        stateTaxesPaid: yup.number().required(),
        extraFederalPaid: yup
            .number()
            .notRequired()
            .when(['federalTaxesPaid', 'stateTaxesPaid'], {
                is: (federalTaxesPaid, stateTaxesPaid) => federalTaxesPaid || stateTaxesPaid,
                then: yup.number().required(),
            }),
        extraStatePaid: yup
            .number()
            .notRequired()
            .when(['federalTaxesPaid', 'stateTaxesPaid'], {
                is: (federalTaxesPaid, stateTaxesPaid) => federalTaxesPaid || stateTaxesPaid,
                then: yup.number().required(),
            }),
    }),
    handleSubmit: async (values, { props, setFieldError }) => {
        const response = await props.onSave(values);

        if (response.error && response.error.federal_tax_amount) {
            setFieldError('federalTaxesPaid', response.error.federal_tax_amount[0]);
        }
        if (response.error && response.error.state_tax_amount) {
            setFieldError('stateTaxesPaid', response.error.state_tax_amount[0]);
        }
        if (response.error && response.error.federal_extra_tax_paid) {
            setFieldError('extraFederalPaid', response.error.federal_extra_tax_paid[0]);
        }
        if (response.error && response.error.state_extra_tax_paid) {
            setFieldError('extraStatePaid', response.error.state_extra_tax_paid[0]);
        }

        if (response.error && response.error.non_field_errors) {
            setFieldError('message', response.error.non_field_errors[0]);
        }
        if (response.error && response.error.message) {
            setFieldError('message', response.error.message);
        }
    },
})(TaxesPaidFormik);

export default TaxesPaidForm;
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/QTEDetails.test.jsx
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import QTEDetails from './QTEDetails';

import RootStoreContext from 'modules/common/stores/Root/Context';

const mockQte = {
    uuid: '1234',
    year: 2022,
    state: 'CA',
    estimated_federal_tax: 123,
    estimated_state_tax: 456,
    sent: 'Yes',
};

class MockLegacyStore {
    saveTaxPayment = () => jest.fn();

    updateTaxPayment = () => jest.fn();
}

const renderWithProviders = (legacyStore) => {
    return render(
        <RootStoreContext.Provider value={{ legacyStore }}>
            <QTEDetails qte={mockQte} />
        </RootStoreContext.Provider>
    );
};

describe('TaxPayment Form', () => {
    it('should match snap', async () => {
        const legacyStore = new MockLegacyStore();
        const { asFragment } = renderWithProviders(legacyStore);

        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render correctly', async () => {
        const legacyStore = new MockLegacyStore();
        const { findByText } = renderWithProviders(legacyStore);

        const title = await findByText(/Estimated Federal Tax/i);

        expect(title).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/TaxPaymentsEditor.jsx
import { observer } from 'mobx-react';
import React from 'react';
import { Dropdown, Loader } from 'semantic-ui-react';
import moment from 'moment';

import QTEDetails from './QTEDetails';
import TaxesPaidForm from './TaxesPaidForm';

import RootStoreContext from 'modules/common/stores/Root/Context';

class TaxPaymentsEditor extends React.Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
        this.state = {
            loading: true,
            errorLoading: false,
            selectedYear: moment().year(),
            selectedQuarter: moment().quarter(),
            qte: null,
            taxPaymentUUID: null,
            federalTaxesPaid: '',
            stateTaxesPaid: '',
            extraFederalPaid: '',
            extraStatePaid: '',
        };
    }

    componentDidMount() {
        this.fetchData();
    }

    fetchData = () => {
        const { memberId } = this.props;
        const { selectedYear, selectedQuarter } = this.state;

        const p1 = this.store.fetchQuarterlyTaxEstimate(memberId, selectedYear, selectedQuarter, true);
        const p2 = this.store.fetchTaxPayment(memberId, selectedYear, selectedQuarter);

        this.setState({ loading: true, errorLoading: false });

        Promise.all([p1, p2])
            .then(([taxEstimates, taxPayments]) => {
                const taxEstimate = taxEstimates[0];
                const taxPayment = taxPayments[0];
                this.setState({
                    qte: taxEstimate,
                    taxPaymentUUID: taxPayment ? taxPayment.uuid : null,
                    federalTaxesPaid: taxPayment ? taxPayment.federal_tax_amount : '',
                    stateTaxesPaid: taxPayment ? taxPayment.state_tax_amount : '',
                    extraFederalPaid: taxPayment ? taxPayment.federal_extra_tax_paid : 0,
                    extraStatePaid: taxPayment ? taxPayment.state_extra_tax_paid : 0,
                });
                this.store.globalError = '';
            })
            .catch((e) => {
                this.setState({ errorLoading: true });
                this.store.globalError = 'Error loading tax payments!';
            })
            .finally(() => {
                this.setState({ loading: false });
            });
    };

    handleYearChange = (event, data) => {
        const newValue = data.value;
        // avoid re-fetching if value did not change
        if (newValue !== this.state.selectedYear) {
            this.setState({ selectedYear: newValue }, this.fetchData);
        }
    };

    handleQuarterChange = (event, data) => {
        const newValue = data.value;
        // avoid re-fetching if value did not change
        if (newValue !== this.state.selectedQuarter) {
            this.setState({ selectedQuarter: newValue }, this.fetchData);
        }
    };

    handleSave = async (values) => {
        const { memberId } = this.props;
        const { federalTaxesPaid, stateTaxesPaid, extraFederalPaid, extraStatePaid } = values;
        const { taxPaymentUUID, selectedYear, selectedQuarter } = this.state;

        if (taxPaymentUUID === null) {
            // create new record
            const response = await this.store.saveTaxPayment(
                memberId,
                selectedYear,
                selectedQuarter,
                federalTaxesPaid,
                stateTaxesPaid,
                extraFederalPaid,
                extraStatePaid
            );

            if (response.uuid) {
                // save the uuid in state to use it for updating the newly created record on subsequent saves
                this.setState({ taxPaymentUUID: response.uuid });
            }

            return response;
        }
        // update existing record
        const response = await this.store.updateTaxPayment(
            taxPaymentUUID,
            federalTaxesPaid,
            stateTaxesPaid,
            extraFederalPaid,
            extraStatePaid
        );
        return response;
    };

    renderQuarterPicker() {
        const currentYear = moment().year();
        const { selectedYear, selectedQuarter } = this.state;

        const yearOptions = [];
        for (let i = 2020; i <= currentYear; i++) {
            yearOptions.push({ key: i, text: i, value: i });
        }

        const quarterOptions = [
            { key: 1, text: 1, value: 1 },
            { key: 2, text: 2, value: 2 },
            { key: 3, text: 3, value: 3 },
            { key: 4, text: 4, value: 4 },
        ];

        return (
            <div id="hub-qte-quarter-picker">
                <span>
                    Year:
                    <Dropdown
                        inline
                        selection
                        options={yearOptions}
                        value={selectedYear}
                        onChange={this.handleYearChange}
                    />
                </span>
                <span>
                    Quarter:
                    <Dropdown
                        inline
                        selection
                        options={quarterOptions}
                        value={selectedQuarter}
                        onChange={this.handleQuarterChange}
                    />
                </span>
            </div>
        );
    }

    render() {
        const { loading, errorLoading, qte, federalTaxesPaid, stateTaxesPaid, extraFederalPaid, extraStatePaid } =
            this.state;
        let content = null;
        if (loading) {
            content = <Loader active inline />;
        } else if (errorLoading) {
            content = <p>Something went wrong :(</p>;
        } else {
            content = (
                <>
                    {qte && <QTEDetails qte={qte} />}

                    <h4>
                        This form is used when the member pays an amount different than directed in QTEs.
                        <br />
                        By default we assume they paid what they were directed to pay.
                    </h4>
                    <TaxesPaidForm
                        federalTaxesPaid={federalTaxesPaid}
                        stateTaxesPaid={stateTaxesPaid}
                        extraFederalPaid={extraFederalPaid}
                        extraStatePaid={extraStatePaid}
                        onSave={this.handleSave}
                    />
                </>
            );
        }
        return (
            // we want to always render the quarter picker, but the content below it can vary
            <>
                {this.renderQuarterPicker()}
                {content}
            </>
        );
    }
}

export default observer(TaxPaymentsEditor);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/index.test.jsx
import { render, screen } from '@testing-library/react';

import TaxPaymentsEditor from './TaxPaymentsEditor';

import RootStoreContext from 'modules/common/stores/Root/Context';

jest.mock('moment', () => {
    const moment = jest.requireActual('moment');
    moment.now = () => +new Date('2022-01-18T12:33:37.000Z');
    return moment;
});

class MockLegacyStore {
    saveTaxPayment = () => jest.fn();

    fetchQuarterlyTaxEstimate = () => jest.fn();

    fetchTaxPayment = () => jest.fn();

    updateTaxPayment = () => jest.fn();
}

const renderWithProviders = (legacyStore) => {
    return render(
        <RootStoreContext.Provider value={{ legacyStore }}>
            <TaxPaymentsEditor />
        </RootStoreContext.Provider>
    );
};

describe('TaxPayment Form', () => {
    it('should match snap', async () => {
        const legacyStore = new MockLegacyStore();
        const { asFragment } = renderWithProviders(legacyStore);

        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render correctly', async () => {
        const legacyStore = new MockLegacyStore();
        const { findByText } = renderWithProviders(legacyStore);

        const text = await findByText(
            /This form is used when the member pays an amount different than directed in QTEs/i
        );

        expect(text).toBeInTheDocument();
    });

    it('extra fields not there on render', async () => {
        const legacyStore = new MockLegacyStore();

        const { queryByLabelText } = renderWithProviders(legacyStore);

        expect(queryByLabelText(/Extra Federal Taxes Paid/i)).not.toBeInTheDocument();
    });

    it('extra fields render at the time that they should render', async () => {
        const legacyStore = new MockLegacyStore();
        legacyStore.fetchQuarterlyTaxEstimate = jest.fn().mockReturnValue([{ estimated_federal_tax: 102030 }]);
        legacyStore.fetchTaxPayment = jest.fn().mockReturnValue([{ federal_tax_amount: 123, state_tax_amount: 456 }]);
        const { container } = renderWithProviders(legacyStore);

        const field = await screen.findByText('Actual State Taxes Paid (When different than the amount in QTE):');

        expect(field).toBeInTheDocument();

        const inputField = container.querySelector('[name="federalTaxesPaid"]');
        expect(inputField.value).toMatch('123');

        const inputFieldState = container.querySelector('[name="stateTaxesPaid"]');
        expect(inputFieldState.value).toMatch('456');
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/TaxesPaidForm.test.jsx
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import TaxesPaidForm from './TaxesPaidForm';

import RootStoreContext from 'modules/common/stores/Root/Context';

class MockLegacyStore {
    saveTaxPayment = () => jest.fn();

    updateTaxPayment = () => jest.fn();
}

const renderWithProviders = (legacyStore) => {
    return render(
        <RootStoreContext.Provider value={{ legacyStore }}>
            <TaxesPaidForm />
        </RootStoreContext.Provider>
    );
};

describe('TaxPayment Form', () => {
    it('should match snap', async () => {
        const legacyStore = new MockLegacyStore();
        const { asFragment } = renderWithProviders(legacyStore);

        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render correctly', async () => {
        const legacyStore = new MockLegacyStore();
        const { findByText } = renderWithProviders(legacyStore);

        const title = await findByText(/Actual Federal Taxes Paid/i);

        expect(title).toBeInTheDocument();
    });

    it('extra fields not there on render', async () => {
        const legacyStore = new MockLegacyStore();

        const { queryByLabelText } = renderWithProviders(legacyStore);

        const user = userEvent.setup();

        expect(queryByLabelText(/Extra Federal Taxes Paid/i)).not.toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/TaxPaymentsEditor/QTEDetails.jsx
import React from 'react';
import { List } from 'semantic-ui-react';

import QTEPreviewEmail from './QTEPreviewEmail';

const QTEDetails = ({ qte }) => (
    <div id="hub-qte-details">
        <List divided size="big">
            <List.Item>
                <List.Description>Period</List.Description>
                <List.Header>
                    Q{qte.quarter}-{qte.year}
                </List.Header>
            </List.Item>

            <List.Item>
                <List.Description>State</List.Description>
                <List.Header>{qte.state}</List.Header>
            </List.Item>

            <List.Item>
                <List.Description>Estimated Federal Tax</List.Description>
                <List.Header>${qte.estimated_federal_tax}</List.Header>
            </List.Item>

            <List.Item>
                <List.Description>Estimated State Tax</List.Description>
                <List.Header>${qte.estimated_state_tax}</List.Header>
            </List.Item>

            <List.Item>
                <List.Description>Email Has Been Sent?</List.Description>
                <List.Header>{qte.sent ? 'Yes' : 'No'}</List.Header>
            </List.Item>

            <List.Item>
                <QTEPreviewEmail qteId={qte.uuid} />
            </List.Item>
        </List>
    </div>
);

export default QTEDetails;
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ForeignRegistrationsEditor/ForeignRegistrationsEditor.jsx
import { observer } from 'mobx-react';
import React from 'react';
import { Loader } from 'semantic-ui-react';

import ForegTable from './ForegTable';

import RootStoreContext from 'modules/common/stores/Root/Context';

class ForeignRegistrationsEditor extends React.Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
        this.state = {
            loading: true,
            errorLoading: false,
            sourceRecords: [], // the source of truth, straight from the DB, never changes
            formRecords: [], // whatever data is currently being displayed in the form, can change
        };
    }

    componentDidMount() {
        this.store
            .fetchForeignRegistrations(this.props.memberId)
            .then((data) => {
                this.setState({ sourceRecords: data, formRecords: data });
            })
            .catch((e) => {
                this.setState({ errorLoading: true });
                console.error(e);
                this.store.globalError = 'Error loading foreign registrations!';
            })
            .finally(() => {
                this.setState({ loading: false });
            });
    }

    addRecord = () => {
        // add a new blank record
        this.setState((previousState) => ({
            formRecords: previousState.formRecords.concat({
                uuid: new Date().getTime(), // random uuid
                state: '',
                entity_id: '',
                entity_name: '',
                effective_registration_date: '',
            }),
        }));
    };

    removeRecord = (uuid) => {
        // remove record by uuid
        this.setState((previousState) => ({
            formRecords: previousState.formRecords.filter((record) => record.uuid !== uuid),
        }));
    };

    resetRecord = (uuid) => {
        // reset a record to its original state (make it match its corresponding source record)
        const { formRecords, sourceRecords } = this.state;
        const sourceRecord = sourceRecords.find((srcRecord) => srcRecord.uuid === uuid);
        this.setState({
            formRecords: formRecords.map((formRecord) => {
                if (formRecord.uuid === sourceRecord.uuid) {
                    return { ...sourceRecord };
                }
                return formRecord;
            }),
        });
    };

    editRecord = (uuid, changes) => {
        // modify a record
        this.setState((previousState) => ({
            formRecords: previousState.formRecords.map((record) => {
                if (record.uuid === uuid) {
                    record = { ...record, ...changes };
                }
                return record;
            }),
        }));
    };

    saveRecord = (uuid) => {
        // persist a record
        const formIndex = this.state.formRecords.findIndex((record) => record.uuid === uuid);
        const formRecord = this.state.formRecords[formIndex];
        const isNew = this.state.sourceRecords.find((record) => record.uuid === uuid) === undefined;
        return this.store
            .saveForeignRegistration(this.props.memberId, formRecord, isNew)
            .then((savedRecord) => {
                if (isNew) {
                    // when a new record is saved, we need to:
                    // * add it to sourceRecords
                    // * replace the corresponding formRecord with the newly saved record
                    this.setState((previousState) => ({
                        sourceRecords: previousState.sourceRecords.concat(savedRecord),
                        formRecords: previousState.formRecords.map((fr, index) =>
                            index === formIndex ? savedRecord : fr
                        ),
                    }));
                } else {
                    // when an existing record is saved, we need to:
                    // * replace the corresponding sourceRecord with the newly saved record
                    this.setState((previousState) => ({
                        sourceRecords: previousState.sourceRecords.map((sr) =>
                            sr.uuid === savedRecord.uuid ? savedRecord : sr
                        ),
                    }));
                }
            })
            .catch((e) => {
                console.error(e);
                this.store.globalError = 'Error saving foreign registration!';
            });
    };

    render() {
        const { loading, errorLoading, sourceRecords, formRecords } = this.state;

        if (loading) {
            return <Loader active inline />;
        }

        if (errorLoading) {
            return <p>Something went wrong :(</p>;
        }

        return (
            <ForegTable
                sourceRecords={sourceRecords}
                formRecords={formRecords}
                addRecord={this.addRecord}
                removeRecord={this.removeRecord}
                resetRecord={this.resetRecord}
                editRecord={this.editRecord}
                saveRecord={this.saveRecord}
            />
        );
    }
}

export default observer(ForeignRegistrationsEditor);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ForeignRegistrationsEditor/ForegTable.jsx
import React from 'react';
import { Button, Message, Table } from 'semantic-ui-react';
import { isEqual } from 'lodash';

import ForegRow from './ForegRow';

const ForegTable = ({ sourceRecords, formRecords, addRecord, removeRecord, resetRecord, editRecord, saveRecord }) => {
    if (formRecords.length === 0) {
        return (
            <>
                <Message info>This Member does not have any foreign registrations yet!</Message>
                <Button primary size="tiny" content="Add +" onClick={addRecord} />
            </>
        );
    }
    return (
        <Table singleLine celled selectable compact="very" size="small">
            <Table.Header>
                <Table.Row>
                    <Table.HeaderCell>State</Table.HeaderCell>
                    <Table.HeaderCell>Entity Name</Table.HeaderCell>
                    <Table.HeaderCell>Entity ID</Table.HeaderCell>
                    <Table.HeaderCell>Reg. Date</Table.HeaderCell>
                    <Table.HeaderCell>Actions</Table.HeaderCell>
                </Table.Row>
            </Table.Header>

            <Table.Body>
                {formRecords.map((formRecord) => {
                    const sourceRecord = sourceRecords.find((srcRecord) => srcRecord.uuid === formRecord.uuid);
                    const isNew = sourceRecord === undefined; // if there is no corresponding source record, then this is a new record
                    const isDirty = !isNew && !isEqual(formRecord, sourceRecord);

                    return (
                        <ForegRow
                            key={formRecord.uuid}
                            record={formRecord}
                            source={sourceRecord}
                            isNew={isNew}
                            isDirty={isDirty}
                            removeRecord={removeRecord}
                            resetRecord={resetRecord}
                            editRecord={editRecord}
                            saveRecord={saveRecord}
                        />
                    );
                })}
            </Table.Body>

            <Table.Footer>
                <Table.Row>
                    <Table.HeaderCell colSpan="5">
                        <Button primary size="tiny" content="Add +" onClick={addRecord} />
                    </Table.HeaderCell>
                </Table.Row>
            </Table.Footer>
        </Table>
    );
};

export default ForegTable;
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ForeignRegistrationsEditor/index.js
export { default } from './ForeignRegistrationsEditor';
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ForeignRegistrationsEditor/ForegRow.jsx
import React from 'react';
import { Icon, Loader, Table } from 'semantic-ui-react';

import StatePickerCell from './StatePickerCell';
import InputCell from './InputCell';

export default class ForegRow extends React.Component {
    constructor(props, context) {
        super(props, context);
        this.state = { saving: false };
    }

    handleSave = () => {
        this.setState({ saving: true });
        this.props.saveRecord(this.props.record.uuid).finally(() => {
            this.setState({ saving: false });
        });
    };

    handleRemove = () => this.props.removeRecord(this.props.record.uuid);

    handleReset = () => this.props.resetRecord(this.props.record.uuid);

    handleChange = (key, value) => this.props.editRecord(this.props.record.uuid, { [key]: value });

    isStateValid = () => this.props.record.state.length > 0;

    isEntityNameValid = () => this.props.record.entity_name.length > 0;

    isEntityIdValid = () => this.props.record.entity_id.length > 0;

    isEffectiveRegistrationDateValid = () => {
        const { effective_registration_date } = this.props.record;
        return effective_registration_date && effective_registration_date.length > 0;
    };

    renderActions = () => {
        if (this.state.saving) {
            return <Loader active inline="centered" size="mini" />;
        }

        const { isNew, isDirty } = this.props;
        const actionButtons = [];

        if (
            (isNew || isDirty) &&
            this.isStateValid() &&
            this.isEntityNameValid() &&
            this.isEntityIdValid() &&
            this.isEffectiveRegistrationDateValid()
        ) {
            actionButtons.push(
                <Icon
                    key="save-button"
                    link
                    title="Save"
                    name="save"
                    color="teal"
                    size="large"
                    onClick={this.handleSave}
                />
            );
        }

        if (isNew) {
            actionButtons.push(
                <Icon
                    key="remove-button"
                    link
                    title="Remove"
                    name="remove"
                    color="grey"
                    size="large"
                    onClick={this.handleRemove}
                />
            );
        }

        if (isDirty) {
            actionButtons.push(
                <Icon
                    key="undo-button"
                    link
                    title="Undo"
                    name="undo"
                    color="grey"
                    size="large"
                    onClick={this.handleReset}
                />
            );
        }

        return actionButtons;
    };

    render() {
        const { record, isNew, isDirty } = this.props;
        return (
            <Table.Row warning={isNew || isDirty}>
                <StatePickerCell
                    fieldName="state"
                    value={record.state}
                    onChange={this.handleChange}
                    hasError={!this.isStateValid()}
                />

                <InputCell
                    fieldName="entity_name"
                    type="text"
                    value={record.entity_name}
                    onChange={this.handleChange}
                    hasError={!this.isEntityNameValid()}
                />

                <InputCell
                    fieldName="entity_id"
                    type="text"
                    value={record.entity_id}
                    onChange={this.handleChange}
                    hasError={!this.isEntityIdValid()}
                />

                <InputCell
                    fieldName="effective_registration_date"
                    type="date"
                    value={record.effective_registration_date || ''}
                    onChange={this.handleChange}
                    hasError={!this.isEffectiveRegistrationDateValid()}
                />

                <Table.Cell>{this.renderActions()}</Table.Cell>
            </Table.Row>
        );
    }
}
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ForeignRegistrationsEditor/StatePickerCell.jsx
import React from 'react';
import { Dropdown, Icon, Table } from 'semantic-ui-react';

import RootStoreContext from 'modules/common/stores/Root/Context';

export default class StatePickerCell extends React.Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;

        const stateOptions = [{ text: '---', value: '' }].concat(
            this.store.allStates.map((state) => ({ text: state.name, value: state.id }))
        );

        this.state = { stateOptions };
    }

    handleChange = (event, data) => this.props.onChange(this.props.fieldName, data.value);

    render() {
        const { value, hasError } = this.props;

        return (
            <Table.Cell className="foreg-state-picker" error={hasError}>
                {hasError && <Icon name="attention" />}
                <Dropdown
                    search
                    selection
                    placeholder="State"
                    options={this.state.stateOptions}
                    onChange={this.handleChange}
                    value={value}
                />
            </Table.Cell>
        );
    }
}
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/ForeignRegistrationsEditor/InputCell.jsx
import React from 'react';
import { Icon, Input, Table } from 'semantic-ui-react';

export default class InputCell extends React.Component {
    handleChange = (event, data) => this.props.onChange(this.props.fieldName, data.value);

    render() {
        const { type, value, hasError } = this.props;

        return (
            <Table.Cell error={hasError}>
                {hasError && <Icon name="attention" />}
                <Input transparent size="large" type={type} title={value} value={value} onChange={this.handleChange} />
            </Table.Cell>
        );
    }
}
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/RegenerateFormW9/RegenerateFormW9Button.test.jsx
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MockAxiosAdapter from 'axios-mock-adapter';

import RegenerateFormW9Button from '.';

import collectiveApi from 'modules/common/collectiveApi';

const mockCollectiveApi = new MockAxiosAdapter(collectiveApi);

beforeEach(cleanup);

describe('RegenerateFormW9Button', () => {
    it('should match snapshot', async () => {
        const { asFragment } = render(<RegenerateFormW9Button email="test@email.com" />);
        expect(asFragment()).toMatchSnapshot();
    });

    it('should call w9 api', async () => {
        mockCollectiveApi.onPatch('regenerate-form-w9/').reply(200, {});
        render(<RegenerateFormW9Button email="test@email.com" />);

        const user = userEvent.setup();
        const button = screen.getByRole('button', {
            name: /Regenerate/i,
        });
        await user.click(button);
        await waitFor(() => {
            expect(screen.getByText(/Form W9 was regenerated successfully/i)).toBeInTheDocument();
        });
    });

    it('should call w9 api with error', async () => {
        mockCollectiveApi.onPatch('regenerate-form-w9/').reply(500, {});
        render(<RegenerateFormW9Button email="test@email.com" />);

        const user = userEvent.setup();
        const button = screen.getByRole('button', {
            name: /Regenerate/i,
        });
        await user.click(button);
        await waitFor(() => {
            expect(screen.getByText(/Form W9 regeneration was unsuccessful/i)).toBeInTheDocument();
        });
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/RegenerateFormW9/RegenerateFormW9Button.jsx
import { Button } from '@mui/material';
import { useState } from 'react';

import collectiveApi from 'modules/common/collectiveApi';

const RegenerateFormW9Button = ({ email }) => {
    const [isFormW9Regenerated, setIsFormW9Regenerated] = useState(false);
    const [isFormW9RegenerationError, setIsFormW9RegenerationError] = useState(false);

    const regenerateFormW9 = async () => {
        try {
            await collectiveApi.patch(`regenerate-form-w9/`, {
                email,
            });

            setIsFormW9Regenerated(true);
        } catch (error) {
            setIsFormW9RegenerationError(true);
        }
    };

    if (isFormW9Regenerated) {
        return (
            <div className="ui success message">
                <div className="header">Form W9 was regenerated successfully.</div>
            </div>
        );
    }
    return (
        <>
            <Button onClick={regenerateFormW9}> Regenerate </Button>
            {isFormW9RegenerationError && (
                <div className="ui negative message">
                    <div className="header">Form W9 regeneration was unsuccessful.</div>
                </div>
            )}
        </>
    );
};

export default RegenerateFormW9Button;
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/RegenerateFormW9/index.js
export { default } from './RegenerateFormW9Button';
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/EINFilingEditor/index.js
export { default } from './EINFilingEditor';
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/EINFilingEditor/EINFilingEditor.jsx
import * as Sentry from '@sentry/react';
import { useFormik } from 'formik';
import { observer } from 'mobx-react';
import { useState } from 'react';
import { Button, Form } from 'semantic-ui-react';
import { array, object, string } from 'yup';

import FormFileUpload from '../LLCFilingEditor/FormFileUpload';

import collectiveApi from 'modules/common/collectiveApi';

const initialValues = {
    ein: '',
    federal_business_name: '',
    einFiling: null,
};

const validationSchema = object({
    ein: string().label('Employer Identification Number').required(),
    federal_business_name: string().label('Federal Business Name').required(),
    einFiling: array().min(1),
});

const EINFilingEditor = ({ client, einstatus }) => {
    const [einFilingCreated, setEinFilingCreated] = useState(false);

    const { id: memberId } = client;

    const postMemberEinFiling = async (memberId, postObject) => {
        try {
            const { data: success } = await collectiveApi.post(`/ein-filing/${memberId}/`, postObject);
            setEinFilingCreated(success);
        } catch (error) {
            console.error("Error while creating member's ein filing: ", error);
            Sentry.captureException(error);
            throw error;
        }
    };

    const onSubmit = async (values) => {
        const postObject = {
            file_name: values.einFiling[0].filename,
            file_link: values.einFiling[0].url,
            ein: values.ein,
            federal_business_name: values.federal_business_name,
        };
        await postMemberEinFiling(memberId, postObject);
    };

    const { values, errors, touched, isValid, isSubmitting, handleChange, handleBlur, handleSubmit, setFieldValue } =
        useFormik({
            initialValues,
            validationSchema,
            onSubmit,
        });

    const handleFileUpload = (name, files) => {
        setFieldValue(name, files);
    };

    const handleFileRemove = (name) => {
        setFieldValue(name, []);
    };

    if (einstatus === 'completed' || einFilingCreated) {
        return (
            <div className="ui success message">
                <div className="header">EIN filing has been successfully completed for this member</div>
            </div>
        );
    }

    return (
        <Form className="tw-w-60">
            <Form.Input
                label="What is the Employer Identification Number for this member?"
                name="ein"
                id="ein"
                data-testid="ein"
                value={values.ein}
                error={touched.ein && errors.ein}
                onChange={handleChange}
                onBlur={handleBlur}
                fluid
            />
            <Form.Input
                label="What is the Federal Business Name for this member?"
                name="federal_business_name"
                id="federal_business_name"
                data-testid="federal_business_name"
                value={values.federal_business_name}
                error={touched.federal_business_name && errors.federal_business_name}
                onChange={handleChange}
                onBlur={handleBlur}
                fluid
            />
            <div className="field">
                <label htmlFor="einFiling">EIN Filing Document Upload</label>
            </div>
            <FormFileUpload
                name="einFiling"
                onUpload={handleFileUpload}
                onRemove={handleFileRemove}
                type="EIN Filing"
            />
            <br />
            <Button type="submit" disabled={isSubmitting || !isValid} primary size="small" onClick={handleSubmit}>
                {isSubmitting ? 'Saving...' : 'Save'}
            </Button>
        </Form>
    );
};

export default observer(EINFilingEditor);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Details/EINFilingEditor/EINFilingEditor.test.jsx
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { configure } from 'mobx';

import EINFilingEditor from '.';

import RootStoreContext from 'modules/common/stores/Root/Context';
import HubStoreContext from 'modules/Hub/common/Store/Context';

configure({ safeDescriptors: false });

class MockLegacyStore {
    fetchFilestackCredentials = () => ({ api_key: 'api_key_test', policy: 'policy_test', signature: 'signature_test' });
}

class MockHubStore {
    // saveTaxPayment = () => jest.fn();
}

class MockClient {
    id = 1;
}

const renderWithProviders = (einstatus) => {
    const legacyStore = new MockLegacyStore();
    const hubStore = new MockHubStore();
    const client = new MockClient();

    return render(
        <RootStoreContext.Provider value={{ legacyStore }}>
            <HubStoreContext.Provider value={{ hubStore }}>
                <EINFilingEditor client={client} einstatus={einstatus} />
            </HubStoreContext.Provider>
        </RootStoreContext.Provider>
    );
};

beforeEach(cleanup);

describe('EINFilingEditor', () => {
    it('should render correctly', async () => {
        renderWithProviders('inprogress');

        const elements = await screen.findAllByText(/What is the Employer Identification Number for this member?/i);

        expect(elements[0]).toBeInTheDocument();
    });

    it('should show completed ein filing message', async () => {
        renderWithProviders('completed');

        const elements = await screen.findAllByText(/EIN filing has been successfully completed for this member/i);

        expect(elements[0]).toBeInTheDocument();
    });

    it('should show ein filing form', async () => {
        renderWithProviders('inprogress');

        const user = userEvent.setup();

        const entityNumberInput = screen.getByTestId('ein');
        const federalBusinessName = screen.getByTestId('federal_business_name');

        await user.type(entityNumberInput, '123456');
        await user.type(federalBusinessName, 'Big Company LLC');

        const uploadButton = screen.getByRole('button', {
            name: /Upload document/i,
        });

        await user.click(uploadButton);
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientInfo/Search/Page.js
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { Input, Message, Button } from 'semantic-ui-react';

import TimeoutModal from '../../common/TimeoutModal';

import collectiveApi from 'modules/common/collectiveApi';
import RootStoreContext from 'modules/common/stores/Root/Context';

class ClientSearchPage extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.state = {
            clients: [],
            sentBackClients: [],
            errorMessage: null,
            searchString: '',
            typing: false,
            typingTimer: null,
            loadingClients: false,
            loadingSentBackClients: false,
        };
        this.store = context.legacyStore;
    }

    componentDidMount() {
        this.store.redirectedfrompage = 'hub/client-info/search';
        if (!this.store.isLoginSuccess || !this.store.isEmployee) {
            this.props.history.push('/login');
        } else {
            this.fetchSentBackClients();

            if (!this.store.startedTimeTracking) {
                this.store.startTimeTracker();
            }
        }
    }

    fetchClients = async () => {
        try {
            const { searchString: q } = this.state;
            if (!q.length) {
                return;
            }

            this.setState({ loadingClients: true });
            const {
                data: { data: clients },
            } = await collectiveApi.get('searchclients/', { params: { q } });
            this.setState({
                clients,
                loadingClients: false,
            });
        } catch (error) {
            console.log('Fetch searched clients error.', error);
            this.setState({
                errorMessage: 'Something went wrong. Please contact Engineering',
            });
        }
    };

    fetchSentBackClients = async () => {
        try {
            this.setState({ loadingSentBackClients: true });
            const {
                data: { data: sentBackClients },
            } = await collectiveApi.get('sentbackclients/');
            this.setState({
                sentBackClients,
                loadingSentBackClients: false,
            });
        } catch (error) {
            console.log('Fetch sent back clients error.', error);
            this.setState({
                errorMessage: 'Something went wrong. Please contact Engineering',
            });
        }
    };

    goToClientInfo = (client) => {
        if (client.sfaccountid) {
            this.store.clientInQuestion = client;
            this.props.history.push(`/hub/client-info/${client.sfaccountid}/info`);
        }
    };

    handleChange = (event) => {
        clearTimeout(this.state.typingTimer);

        const newSearchString = event.target.value;
        const newTyping = newSearchString.length > 0;
        const newTypingTimer = setTimeout(() => {
            this.setState({ typing: false });
            this.fetchClients();
        }, 1000);

        this.setState({
            searchString: newSearchString,
            typing: newTyping,
            typingTimer: newTypingTimer,
        });
    };

    renderClients = () => {
        const { clients, loadingClients } = this.state;

        if (clients.length === 0 || loadingClients) {
            return null;
        }

        return (
            <div className="client-names-box">
                <div className="space-between box-fixed">
                    <div className="inner-box">Name:</div>
                    <div className="inner-box">Email:</div>
                </div>
                {clients.map((client, index) => (
                    <div
                        onClick={() => this.goToClientInfo(client)}
                        className={`space-between client-row ${index === clients.length - 1 ? 'last-client' : ''} ${
                            client.sfaccountid !== '---' ? '' : 'disable'
                        }`}
                        key={client.email}>
                        <div className="client-row-inner">{client.fullname}</div>
                        <div className="client-row-inner">{client.email}</div>{' '}
                    </div>
                ))}
            </div>
        );
    };

    renderSentBackClients = () => {
        const { loadingSentBackClients, sentBackClients } = this.state;
        let content;

        if (loadingSentBackClients) {
            content = <Message color="green">Loading Sent Back Plans...</Message>;
        } else if (sentBackClients.length === 0) {
            content = <Message>No sent back plans found</Message>;
        } else {
            content = (
                <>
                    <h1>Sent Back Plans</h1>
                    {sentBackClients.map((client) => (
                        <div onClick={() => this.goToClientInfo(client)} className="sentback-client" key={client.email}>
                            <div>{client.email}</div>
                        </div>
                    ))}
                </>
            );
        }

        return <div className="sentback">{content}</div>;
    };

    render() {
        const { errorMessage, loadingClients, searchString, typing } = this.state;
        return (
            <div className="ui text container multistep-container pt85">
                {this.store.isEmployee ? (
                    <>
                        {errorMessage && (
                            <div>
                                <Message color="red">{errorMessage}</Message>
                            </div>
                        )}

                        <h1>Client information</h1>
                        <p>
                            Search for clients that are signed up for Collective. You can see and edit the information
                            they already provided or fill out information for them they haven’t provided yet.
                        </p>
                        <Input
                            style={{ width: '100%', marginBottom: 64 }}
                            value={searchString}
                            onChange={this.handleChange}
                            loading={loadingClients || typing}
                            icon="search"
                            iconPosition="left"
                            placeholder="Client name"
                        />

                        {this.renderClients()}
                        {this.renderSentBackClients()}
                        {this.store.showTimeoutModal ? <TimeoutModal /> : null}
                        <div className="sentback">
                            <Button
                                color="pink"
                                as="a"
                                href={`${process.env.REACT_APP_RETOOL_URL}?token=${encodeURIComponent(
                                    window.localStorage.getItem('token')
                                )}&api_url=${encodeURIComponent(process.env.REACT_APP_DJANGO_URL)}`}>
                                Bookkeeping Migrations
                            </Button>
                        </div>
                    </>
                ) : (
                    <h1>You are not authorized to view this page.</h1>
                )}
            </div>
        );
    }
}

export default observer(ClientSearchPage);
// Code from file collective-frontend/src/modules/Hub/ClientInfo/common/ImpersonateButton.js
import React from 'react';
import { Button, Dialog, DialogTitle, DialogActions, DialogContentText, DialogContent } from '@mui/material';
import VisibilityIcon from '@mui/icons-material/Visibility';

import useRootStore from 'modules/common/stores/Root/useRootStore';

const ImpersonateButton = ({ userId, displayName }) => {
    const [isConfirming, setIsConfirming] = React.useState(false);
    const { legacyStore } = useRootStore();
    const openModal = () => {
        setIsConfirming(true);
    };

    const closeModal = () => {
        setIsConfirming(false);
    };

    const impersonateMember = () => {
        // TODO: Implement/trigger impersonation logic here...
        legacyStore.impersonateStart(userId);
        setIsConfirming(false);
    };

    return (
        <>
            <Button
                type="button"
                variant="contained"
                onClick={openModal}
                className="tw-w-full"
                startIcon={<VisibilityIcon />}>
                Impersonate Member
            </Button>

            <Dialog open={isConfirming} onClose={closeModal}>
                <DialogTitle>Impersonate member?</DialogTitle>
                <DialogContent>
                    <DialogContentText>
                        You will leave this page and be logged in as member {displayName}.
                    </DialogContentText>
                </DialogContent>

                <DialogActions>
                    <Button onClick={closeModal}>Cancel</Button>

                    <Button data-testid="confirmButton" onClick={impersonateMember}>
                        Impersonate member
                    </Button>
                </DialogActions>
            </Dialog>
        </>
    );
};

export default ImpersonateButton;
// Code from file collective-frontend/src/modules/Hub/ClientInfo/common/ImpersonateButton.test.js
import { render, screen, fireEvent } from '@testing-library/react';

import ImpersonateButton from './ImpersonateButton';

import RootStoreContext from 'modules/common/stores/Root/Context';

const legacyStore = {
    impersonateStart: jest.fn(),
};

describe('ImpersonationButton', () => {
    it('should trigger an impersonation', async () => {
        render(
            <RootStoreContext.Provider value={{ legacyStore }}>
                <ImpersonateButton userId={1985} displayName="Marty Mcfly" />
            </RootStoreContext.Provider>
        );

        await fireEvent.click(await screen.findByRole('button', { name: /Impersonate Member/i }));
        await fireEvent.click(await screen.findByTestId('confirmButton'));

        expect(legacyStore.impersonateStart).toHaveBeenCalled();
    });

    it('should bail out of the model on cancel', async () => {
        render(
            <RootStoreContext.Provider value={{ legacyStore }}>
                <ImpersonateButton userId={1985} displayName="Marty Mcfly" />
            </RootStoreContext.Provider>
        );

        await fireEvent.click(await screen.findByRole('button', { name: /Impersonate Member/i }));
        await fireEvent.click(await screen.findByRole('button', { name: /Cancel/i }));

        expect(legacyStore.impersonateStart).not.toHaveBeenCalled();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/index.jsx
import React from 'react';
import { Route } from 'react-router-dom';

import lazyWithSuspenseAndRetry from 'modules/common/lazyWithSuspenseAndRetry';

export const QTEHomePage = lazyWithSuspenseAndRetry(() => import('./Home/Page'));
export const QTEDetailsPage = lazyWithSuspenseAndRetry(() => import('./Details/Page'));

export default function getHubQTERoutes() {
    return [
        <Route exact key="qte-home" path="/hub/qte/" component={QTEHomePage} />,
        <Route exact key="qte-details" path="/hub/qte/:workflowId" component={QTEDetailsPage} />,
    ];
}
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Tags/Tags.test.jsx
import { render } from '@testing-library/react';

import Tags from './Tags';

describe('TrackerTool/Tags', () => {
    it('should render correctly', () => {
        const { asFragment } = render(
            <>
                <Tags text={10} status="completed" />
                <Tags text={10} status="incomplete" />
                <Tags text={10} />
                <Tags text="Hello World" />
            </>
        );
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render completed tag', () => {
        const { getByText } = render(<Tags text="10 days" status="completed" />);
        expect(getByText(/completed/i)).toBeInTheDocument();
    });

    it('should render incomplete tag', () => {
        const { getByText } = render(<Tags text="10 days" status="incomplete" />);
        expect(getByText(/incomplete/i)).toBeInTheDocument();
    });
    it('should render text tag', () => {
        const { getByText } = render(<Tags text="Hello world" />);
        expect(getByText(/hello world/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Tags/index.js
export { default } from './Tags';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Tags/Tags.module.less
@color-green: #008866;
@color-red: #fa5a60;
@color-olive: #b5cc18;

.tags {
    display: flex;
}

.tag {
    margin-left: 10px;
    font-size: 12px;
    padding: 3px 8px;
    border-radius: 20px;
    background: rgba(0, 0, 0, 0.1);
    text-transform: capitalize;
    display: flex;
    justify-content: center;
    align-items: center;

    &[class~='text'] {
        text-transform: none;
    }

    &[class~='single'] {
        font-size: 1em;
        margin: 0;
        border-radius: 20px;
        padding: 4px 10px 5px 10px;
    }

    &[class~='completed'],
    &[class~='review'] {
        background: @color-green;
        color: #ffffff !important;
    }

    &[class~='incomplete'] {
        background: @color-red;
        color: #ffffff !important;
    }

    &[class~='flag'] {
        background: @color-olive;
        color: #ffffff !important;
    }

    &[class~='days'] {
        color: #000000;
    }
}
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Tags/Tags.jsx
import StatusIcon from '../StatusIcon';

import classes from './Tags.module.less';

const Tags = ({ status, text, single }) => {
    return (
        <div className={classes.tags}>
            {text && <div className={`${classes.tag} days text`}>{text}</div>}
            {status && (
                <div className={`${classes.tag} ${status} ${single && 'single'}`}>
                    {single && <StatusIcon status={status} />}
                    {status || 'Upcoming'}
                </div>
            )}
        </div>
    );
};

export default Tags;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/EmailPreviewModal.jsx
import Parser from 'html-react-parser';
import { cloneElement, useState } from 'react';
import { Modal, Button } from 'semantic-ui-react';
import { observer } from 'mobx-react';

import RootStoreContext from 'modules/common/stores/Root/Context';

const EmailPreviewModal = ({ subject, body, button }) => {
    const [open, setOpen] = useState(false);

    return (
        <>
            {cloneElement(button, { onClick: () => setOpen(true) })}
            {subject && body && (
                <Modal
                    size="large"
                    className="hyke-modal collective-modal"
                    open={open}
                    onClose={() => setOpen(false)}
                    closeIcon>
                    <Modal.Content>
                        <div className="modal-body">
                            <h2>{subject}</h2>
                            {Parser(body)}
                        </div>
                    </Modal.Content>
                </Modal>
            )}
        </>
    );
};

export default EmailPreviewModal;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/queries.js
import collectiveApi from 'modules/common/collectiveApi';

export const getWorkflowDetail = (workflowId) => collectiveApi.get(`/workflows/qte/${workflowId}/`);
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/Page.jsx
import { useParams } from 'react-router-dom';
import { Loader } from 'semantic-ui-react';
import { useQuery } from '@tanstack/react-query';

import { getWorkflowDetail } from './queries';
import { BasicTable, CommentTable, QTETable, OauthTable, WorkflowTable } from './Tables';

const QTEDetailsPage = () => {
    const { workflowId } = useParams();
    const query = useQuery(['workflowDetails', workflowId], () => getWorkflowDetail(workflowId));

    const { isLoading, isError, data: resp, error } = query;

    if (isLoading) {
        return (
            <Loader inline="centered" active>
                Loading
            </Loader>
        );
    }

    if (isError) {
        return error.message;
    }

    const { states, oauth, qte, workflow, hub_link, ...basics } = resp.data;

    return (
        <div>
            <h2 className="tw-float-right">
                <a href={hub_link}>Member Profile</a>
            </h2>
            <BasicTable title="Basic Info" items={basics} />
            <BasicTable title="State Info" items={states} />
            <QTETable qte={qte} />
            <OauthTable oauth={oauth} />
            <WorkflowTable workflow={workflow} />
            <CommentTable qte={qte} />
        </div>
    );
};

export default QTEDetailsPage;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/CommentThread/queries.js
import collectiveApi from 'modules/common/collectiveApi';

export const getComments = (contentType, objectId) =>
    collectiveApi.get(`/comments/?content_type=${contentType}&object_id=${objectId}`);
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/CommentThread/CommentThread.jsx
import { useState, useRef, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
    Card,
    CardHeader,
    CardContent,
    CardActions,
    Typography,
    CircularProgress,
    Button,
    TextField,
    Snackbar,
} from '@mui/material';

import { getComments } from './queries';
import Comment from './Comment';

import collectiveApi from 'modules/common/collectiveApi';
import useRootStore from 'modules/common/stores/Root/useRootStore';

const CommentThread = ({ contentType, objectId }) => {
    const [formShowing, setFormShowing] = useState(false);
    const [openSnack, setOpenSnack] = useState(false);
    const [snackMessage, setSnackMessage] = useState('');
    const [newCommentContent, setNewCommentContent] = useState('');
    const query = useQuery(['getComments'], () => getComments(contentType, objectId));
    const { legacyStore } = useRootStore();
    const bottomRef = useRef(null);
    useEffect(() => {
        if (!bottomRef.current) {
            return;
        }
        bottomRef.current.scrollIntoView({ behavior: 'smooth' });
    }, [formShowing]);

    const { refetch, isLoading, isError, data: resp, error } = query;

    if (isLoading) {
        return (
            <div className="tw-ml-3 tw-opacity-100">
                <CircularProgress />
            </div>
        );
    }

    if (isError) {
        return error.message;
    }

    const comments = resp.data;

    const handleNewCommentContentChange = (event) => {
        setNewCommentContent(event.target.value);
    };

    const handleSubmit = async () => {
        const result = await collectiveApi.post(`/comments/`, {
            content_type: contentType,
            object_id: objectId,
            content: newCommentContent,
        });
        setSnackMessage('Comment sent');
        setOpenSnack(true);
        setFormShowing(false);
        setNewCommentContent('');
        if (result.status === 201) {
            comments.push(result.data);
        }
    };
    const handleSnackClose = () => {
        setOpenSnack(false);
    };

    const handleDeleteComment = (uuid) => {
        setSnackMessage('Comment deleted');
        setOpenSnack(true);
        refetch();
    };

    return (
        <div className="tw-flex tw-flex-col tw-grid-cols-1 tw-gap-2">
            {comments.map((comment) => (
                <Comment
                    key={comment.uuid}
                    data={comment}
                    isOwner={legacyStore.email === comment.user.email}
                    deleteCallback={handleDeleteComment}
                />
            ))}
            {formShowing && (
                <Card raised>
                    <CardHeader title="Add new comment" />
                    <CardContent>
                        <TextField
                            id="outlined-multiline-flexible"
                            label="---"
                            multiline
                            fullWidth
                            minRows={4}
                            maxRows={8}
                            value={newCommentContent}
                            onChange={handleNewCommentContentChange}
                        />
                    </CardContent>
                    <CardActions disableSpacing>
                        <div className="tw-flex tw-w-full tw-justify-end tw-gap-2">
                            <Button variant="outlined" color="secondary" onClick={() => setFormShowing(false)}>
                                Cancel
                            </Button>
                            <Button color="primary" onClick={handleSubmit}>
                                Submit
                            </Button>
                        </div>
                    </CardActions>
                </Card>
            )}
            {!formShowing && (
                <div className="tw-flex tw-justify-end">
                    <Button onClick={() => setFormShowing(true)} variant="contained" size="large">
                        Add Comment
                    </Button>
                </div>
            )}
            <div ref={bottomRef} />
            <Snackbar open={openSnack} autoHideDuration={1500} message={snackMessage} onClose={handleSnackClose} />
        </div>
    );
};

export default CommentThread;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/CommentThread/Comment.test.jsx
import { fireEvent, render, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import moment from 'moment';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';

import Comment from './Comment';

import collectiveApi from 'modules/common/collectiveApi';

const mock = new MockAdapter(collectiveApi);
const comment = {
    uuid: '15c3b737-9bd8-4d41-8b01-441240c0dcba',
    user: { email: 'jwon@slava.com', first_name: 'Jwon', last_name: 'Slaav' },
    created_at: moment().subtract(7, 'days').format(),
    updated_at: moment().subtract(7, 'days').format(),
    content: 'A third comment.',
};

const WrappedComponent = ({ isOwner, callback }) => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });
    return (
        <QueryClientProvider client={queryClient}>
            <Comment data={comment} isOwner={isOwner} deleteCallback={callback} />
        </QueryClientProvider>
    );
};

describe('Comment', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<WrappedComponent isOwner={false} callback={() => {}} />);
        expect(asFragment()).toMatchSnapshot();
    });
    it('should render delete button and submit correctly', async () => {
        const mockback = jest.fn();
        mock.onDelete('/comments/15c3b737-9bd8-4d41-8b01-441240c0dcba/').replyOnce(204, {});
        const { getByTestId } = render(<WrappedComponent isOwner callback={mockback} />);
        expect(getByTestId('comment-delete-button')).toBeTruthy();
        fireEvent.click(getByTestId('comment-delete-button'));
        await waitFor(() => expect(mockback.mock.calls.length).toBe(1));
        expect(mock.history.delete.length).toBe(1);
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/CommentThread/index.js
export { default } from './CommentThread';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/CommentThread/Comment.jsx
import { Card, CardHeader, CardContent, Typography } from '@mui/material';
import { LoadingButton } from '@mui/lab';
import { useMutation } from '@tanstack/react-query';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import moment from 'moment';

import collectiveApi from 'modules/common/collectiveApi';

const Comment = ({ data, isOwner, deleteCallback }) => {
    const deleteComment = useMutation(
        (id) => {
            return collectiveApi.delete(`/comments/${id}/`);
        },
        {
            onSuccess: () => deleteCallback(data.uuid),
        }
    );

    const getCommentAction = () => {
        return isOwner ? (
            <LoadingButton
                loading={deleteComment.isLoading}
                variant="contained"
                data-testid="comment-delete-button"
                onClick={() => deleteComment.mutate(data.uuid)}>
                <DeleteForeverIcon />
            </LoadingButton>
        ) : null;
    };
    return (
        <Card raised aria-label={`comment-${data.uuid}`}>
            <CardHeader
                title={`${data.user.first_name} ${data.user.last_name} <${data.user.email}>`}
                subheader={`commented ${moment(data.created_at).fromNow()}`}
                action={getCommentAction()}
            />
            <CardContent>
                <Typography variant="body1" color="text.secondary">
                    {data.content}
                </Typography>
            </CardContent>
        </Card>
    );
};

export default Comment;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/CommentThread/CommentThread.test.jsx
import { fireEvent, render, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';

import CommentThread from './CommentThread';

import constants from 'modules/common/constants/constants';
import RootStoreContext from 'modules/common/stores/Root/Context';
import collectiveApi from 'modules/common/collectiveApi';

const commentsData = [
    {
        uuid: '15c3b737-9bd8-4d41-8b01-440e40c0dcba',
        user: { email: 'flluch@collective.com', first_name: 'Francisco', last_name: 'Tester' },
        created_at: '2022-08-24T06:54:59.857152-07:00',
        updated_at: '2022-08-24T06:54:59.857168-07:00',
        content: 'A simple comment.',
    },
    {
        uuid: '15c3b737-9bd8-4d41-8b01-441240c0dcba',
        user: { email: 'jwon@slava.com', first_name: 'Jwon', last_name: 'Slaav' },
        created_at: '2022-08-24T06:54:59.857152-07:00',
        updated_at: '2022-08-24T06:54:59.857168-07:00',
        content: 'Another comment.',
    },
];

const WrappedComponent = () => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });
    return (
        <RootStoreContext.Provider value={{ legacyStore: { email: 'jwon@slava.com' } }}>
            <QueryClientProvider client={queryClient}>
                <CommentThread
                    contentType={constants.contentTypes.QUARTERLY_TAX_ESTIMATE}
                    objectId="be20f314-042f-42c7-8fae-9c42618b9b07"
                />
            </QueryClientProvider>
        </RootStoreContext.Provider>
    );
};

describe('CommentThread', () => {
    let mock;

    beforeEach(() => {
        mock = new MockAdapter(collectiveApi);
        mock.onGet('/comments/?content_type=quarterlytaxestimate&object_id=be20f314-042f-42c7-8fae-9c42618b9b07').reply(
            200,
            commentsData
        );
    });

    it('should render correctly', async () => {
        const { getAllByTestId, getByText, findByText } = render(<WrappedComponent />);
        await findByText('Add Comment');
        expect(getByText('A simple comment.')).toBeTruthy();
        expect(getByText('Another comment.')).toBeTruthy();
        expect(getAllByTestId('comment-delete-button').length).toBe(1);
    });

    it('should render comment form and submit successfully', async () => {
        window.HTMLElement.prototype.scrollIntoView = jest.fn();
        mock.onPost('/comments/').replyOnce(201, []);
        const { getByText, findByText } = render(<WrappedComponent />);
        // button is being shown
        await findByText('Add Comment');
        fireEvent.click(getByText('Add Comment'));

        // form is being shown
        expect(getByText('Add new comment')).toBeTruthy();
        fireEvent.click(getByText('Submit'));

        // form is sent
        await waitFor(() => expect(mock.history.post.length).toBe(1));

        // button is shown again
        expect(getByText('Add Comment')).toBeTruthy();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/Page.module.less
.tableSection {
    margin: 14px auto;
}

.workflowTaskInset {
    padding-left: 32px;
    border-left: 2px solid lightgray;
}
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/Tables.jsx
import { Label, Table, Button, Icon } from 'semantic-ui-react';
import moment from 'moment';

import styles from './Page.module.less';
import EmailPreviewModal from './EmailPreviewModal';
import CommentThread from './CommentThread';

import constants from 'modules/common/constants/constants';
import { moneyFormat } from 'modules/common/display';

const depythonKey = (value) => value.split('_').join(' ');

export const BasicTable = ({ items, title }) => (
    <div className={styles.tableSection}>
        <h2>{title}</h2>
        <Table celled>
            <Table.Header>
                <Table.Row>
                    {Object.keys(items).map((columnName) => (
                        <Table.HeaderCell key={columnName}>{depythonKey(columnName)}</Table.HeaderCell>
                    ))}
                </Table.Row>
            </Table.Header>

            <Table.Body>
                <Table.Row>
                    {Object.values(items).map((value, index) => {
                        return <Table.Cell key={index}>{value}</Table.Cell>;
                    })}
                </Table.Row>
            </Table.Body>
        </Table>
    </div>
);

export const QTETable = ({ qte }) => {
    const moneyColumns = [
        'net_operating_income',
        'payroll_federal_tax',
        'payroll_state_tax',
        'payroll_wages',
        'estimated_federal_tax',
        'estimated_state_tax',
    ];

    const { subject, body, uuid: _, ...qteInfo } = qte;
    const emailPreviewable = Boolean(subject) && Boolean(body);

    return (
        <div className={styles.tableSection}>
            <h2>QTE</h2>
            <Table celled>
                <Table.Header>
                    <Table.Row>
                        {Object.keys(qteInfo).map((columnName) => (
                            <Table.HeaderCell key={columnName}>{depythonKey(columnName)}</Table.HeaderCell>
                        ))}
                    </Table.Row>
                </Table.Header>

                <Table.Body>
                    <Table.Row>
                        {Object.entries(qteInfo).map(([key, value], index) => {
                            return (
                                <Table.Cell key={index}>
                                    {moneyColumns.includes(key) ? moneyFormat(value) : value}
                                </Table.Cell>
                            );
                        })}
                    </Table.Row>
                </Table.Body>
                <Table.Footer fullWidth>
                    <Table.Row>
                        <Table.HeaderCell colSpan="15">
                            <EmailPreviewModal
                                subject={subject}
                                body={body}
                                button={
                                    <Button disabled={!emailPreviewable} icon labelPosition="left" primary size="small">
                                        <Icon name="mail" />
                                        Preview Email
                                    </Button>
                                }
                            />
                            {!emailPreviewable && (
                                <Label pointing="left" color="red">
                                    QTE not generated, no email to preview
                                </Label>
                            )}
                        </Table.HeaderCell>
                    </Table.Row>
                </Table.Footer>
            </Table>
        </div>
    );
};

export const OauthTable = ({ oauth }) => {
    const dateFields = ['access_expiration_date', 'created', 'updated'];

    if (!oauth.length) {
        return null;
    }
    return (
        <div className={styles.tableSection}>
            <h2>Connected Services</h2>
            <Table celled>
                <Table.Header>
                    <Table.Row>
                        {Object.keys(oauth[0]).map((columnName) => (
                            <Table.HeaderCell key={columnName}>{depythonKey(columnName)}</Table.HeaderCell>
                        ))}
                    </Table.Row>
                </Table.Header>

                <Table.Body>
                    {oauth
                        .sort((a, b) => a.id - b.id) // never change, js
                        .map((row, index) => (
                            <Table.Row key={index}>
                                {Object.entries(row).map(([columnName, value]) => (
                                    <Table.Cell key={columnName}>
                                        {value && dateFields.includes(columnName)
                                            ? moment(value).format('MMMM Do YYYY, h:mm:ss a')
                                            : value}
                                    </Table.Cell>
                                ))}
                            </Table.Row>
                        ))}
                </Table.Body>
            </Table>
        </div>
    );
};

export const WorkflowTable = ({ workflow }) => {
    const { tasks, input: workflowInput, ...workflowStats } = workflow;

    return (
        <div className={styles.tableSection}>
            <h2>Workflow</h2>
            <Table celled>
                <Table.Header>
                    <Table.Row>
                        {Object.keys(workflowStats).map((key) => (
                            <Table.HeaderCell key={key}>{depythonKey(key)}</Table.HeaderCell>
                        ))}
                    </Table.Row>
                </Table.Header>

                <Table.Body>
                    <Table.Row>
                        {Object.values(workflowStats).map((value, index) => {
                            return <Table.Cell key={index}>{value}</Table.Cell>;
                        })}
                    </Table.Row>
                </Table.Body>
            </Table>

            {tasks.length > 0 && (
                <div className={styles.workflowTaskInset}>
                    <h3>Workflow Tasks</h3>
                    <Table celled striped>
                        <Table.Header>
                            <Table.Row>
                                {Object.keys(tasks[0]).map((columnName) => (
                                    <Table.HeaderCell key={columnName}>{depythonKey(columnName)}</Table.HeaderCell>
                                ))}
                            </Table.Row>
                        </Table.Header>

                        <Table.Body>
                            {tasks.map((row, index) => (
                                <Table.Row key={index}>
                                    {Object.values(row).map((value, i) => (
                                        <Table.Cell key={i}>{value}</Table.Cell>
                                    ))}
                                </Table.Row>
                            ))}
                        </Table.Body>
                    </Table>
                </div>
            )}
        </div>
    );
};

export const CommentTable = ({ qte }) => {
    return (
        <div className={styles.tableSection}>
            <h2>Comments</h2>
            <CommentThread contentType={constants.contentTypes.QUARTERLY_TAX_ESTIMATE} objectId={qte.uuid} />
        </div>
    );
};
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Details/Page.test.js
import { render } from '@testing-library/react';
import { Route, MemoryRouter } from 'react-router-dom';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter';

import QTEDetailsPage from './Page';

import RootStoreContext from 'modules/common/stores/Root/Context';
import collectiveApi from 'modules/common/collectiveApi';

const commentsData = [
    {
        uuid: '15c3b737-9bd8-4d41-8b01-440e40c0dcba',
        user: { email: 'flluch@collective.com', first_name: 'Francisco', last_name: 'Tester' },
        created_at: '2022-08-24T06:54:59.857152-07:00',
        updated_at: '2022-08-24T06:54:59.857168-07:00',
        content: 'A simple comment.',
    },
    {
        uuid: '15c3b737-9bd8-4d41-8b01-441240c0dcba',
        user: { email: 'jwon@slava.com', first_name: 'Jwon', last_name: 'Slaav' },
        created_at: '2022-08-24T06:54:59.857152-07:00',
        updated_at: '2022-08-24T06:54:59.857168-07:00',
        content: 'Another comment.',
    },
];

const workflowData = {
    id: 1,
    status: 'OK',
    fullname: 'Fourty Two',
    businessname: 'Forty Two',
    email: 'localtest1@collective.com',
    taxpro: 'Landon Brantley',
    bookkeeper: 'Prentiss Johnson',
    pops: 'Jacob Frediani',
    member_relationship_manager: 'Hector Rodriguez',
    usertype: 'LLC-SC',
    workflow: {
        status: 'COMPLETED',
        workflow_name: 'qte_process',
        workflow_id: '6d4eab49-0902-11ed-8fc0-aa06f1e740a0',
        input: {
            member_id: 3,
            year: 2022,
            due_date: 1234567890,
            email: 'joe5@collective.com',
            tags: 'asd1 asd2 asd5 asd4',
            quarter: 1,
        },
        tasks: [
            {
                reference_task_name: 'qte_initial_data_validation_loop_ref',
                status: 'COMPLETED',
                task_def_name: 'Data Validation',
                task_id: '6d4f207a-0902-11ed-8fc0-aa06f1e740a0',
                iteration: 1,
            },
            {
                reference_task_name: 'qte_fetch_data__1',
                status: 'COMPLETED',
                task_def_name: 'QTE Fetch Data',
                task_id: '6d4f6e9b-0902-11ed-8fc0-aa06f1e740a0',
                iteration: 1,
            },
            {
                reference_task_name: 'qte_manual_data_review_ref__1',
                status: 'COMPLETED',
                task_def_name: 'qte_manual_data_review',
                task_id: '61de8ca5-0903-11ed-983f-ca93eb8d3a16',
                iteration: 1,
            },
            {
                reference_task_name: 'qte_is_record_valid__1',
                status: 'COMPLETED',
                task_def_name: 'SWITCH',
                task_id: '77e6f5f7-0903-11ed-983f-ca93eb8d3a16',
                iteration: 1,
            },
            {
                reference_task_name: 'set_approved__1',
                status: 'COMPLETED',
                task_def_name: 'Approve Workflow',
                task_id: '77e6f5f8-0903-11ed-983f-ca93eb8d3a16',
                iteration: 1,
            },
            {
                reference_task_name: 'qte_send_email',
                status: 'COMPLETED',
                task_def_name: 'qte_send_email',
                task_id: '77ed5e99-0903-11ed-983f-ca93eb8d3a16',
                iteration: 0,
            },
        ],
    },
    qte: {
        uuid: 'be20f314-042f-42c7-8fae-9c42618b9b07',
        subject: '[ACTION REQUIRED] Your 2022 Q1 Estimated Tax Payments -- Forty Two LLC',
        body: "\n<p>Hi Fourty,</p>\n\n\n<p>\n  We've updated our forecast for your Q1 estimated\n  <i>quarterly</i>\n  tax payments.\n</p>\n<p>\n  Our number is based on the year-to-last month bookkeeping activity in your accounting software\n  and Gusto accounts. Based on that information, we're able to estimate what we think your total\n  tax liability will be for the year – and how much you should pay each quarter.\n</p>\n<p>\n  For more information on how we calculated the amount, check out our <a href=\"https://help.collective.com/en/articles/5482079-quarterly-tax-estimates\">Help Article</a>.\n</p>\n<p><b>IRS Estimated tax payment: $1,000</b></p>\n\n<p><b>CA estimated tax payment:  $2,000</b></p>\n\n<p>\n  This quarter's payment is due by April 18, 2022.\n</p>\n\n<h2> What if my 2021 Tax Returns haven't been completed yet? </h2>\n<p>\n  No worries, this is very common! You should make the estimated payment as an\n  Extension payment for 2021 and upload the confirmation\n  page with the amount paid in your TaxCaddy account. Our tax team will notate\n  that on the return and apply any of the excess to 2022.\n</p>\n\n\n<h2>Which bank account should I use?</h2>\n<p>\n  Your estimated tax payment should only come from your personal bank account,\n  not your business bank account. If you need funds from your business account\n  to cover this payment, transfer the amount needed from your business bank account\n  to your personal account and categorize this transaction as a\n  'shareholder distribution' in your accounting software.\n</p>\n\n<h2>What's the easiest way to pay the IRS?</h2>\n<p>\n  To make your Federal estimated quarterly tax payment please <a\n    href=\"https://help.collective.com/en/articles/4564769-irs-estimated-payment-instructions\">visit our Help Center</a>\n  to view instructions.\n</p>\n\n\n\n<p>\n  Questions? We have answers! Feel free to email us at <a href=\"mailto:hello@collective.com\">hello@collective.com</a>! We're\n  always here to help.\n  <br /><br />\n</p>\n\n<p>Best,</p>\n<p>The Collective Team</p>",
        reason: '',
        net_operating_income: '123.00',
        payroll_federal_tax: '567.00',
        payroll_state_tax: '890.00',
        payroll_wages: '1011.00',
        quarter: 1,
        year: 2022,
        estimated_federal_tax: '1000.00',
        estimated_state_tax: '2000.00',
        state: 'CA',
        sent: false,
        scheduled_date: null,
        state_exception: true,
        user: 1,
    },
    states: {
        Operation: '---',
        Incorporation: 'Alaska',
        Residency: '---',
        'Business Address': '---',
        'Mail Address': '---',
        Shareholder: '---',
    },
    hub_link: '/hub/client-info/None/info',
    oauth: [
        {
            id: 2,
            service: 'qbo',
            service_id: '9130350073293736',
            service_name: '',
            access_expiration_date: null,
            created: '2022-06-16T19:33:18.097528-07:00',
            updated: '2022-06-27T12:44:39.985768-07:00',
            status: 'valid',
            user: 1,
        },
        {
            id: 1,
            service: 'gusto',
            service_id: '9130350679423356',
            service_name: '',
            access_expiration_date: '2022-07-13T10:45:37.050603-07:00',
            created: '2022-06-15T14:03:18.987813-07:00',
            updated: '2022-07-13T08:45:38.051019-07:00',
            status: 'valid',
            user: 1,
        },
    ],
};

const Wrapper = ({ children, routeDefinition = '', initialRoute = '' }) => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });
    return (
        <RootStoreContext.Provider value={{ legacyStore: {} }}>
            <MemoryRouter initialEntries={[initialRoute]}>
                <QueryClientProvider client={queryClient}>
                    <Route exact path={routeDefinition || initialRoute}>
                        {children}
                    </Route>
                </QueryClientProvider>
            </MemoryRouter>
        </RootStoreContext.Provider>
    );
};

describe('QTEDetail', () => {
    let mock;
    beforeEach(() => {
        mock = new MockAdapter(collectiveApi);
        mock.onGet('workflows/qte/abc-123/').reply(200, workflowData);
    });

    it('should render loading state', () => {
        const { asFragment } = render(
            <Wrapper routeDefinition="/hub/QTE/:workflowId" initialRoute="/hub/QTE/abc-123">
                <QTEDetailsPage />
            </Wrapper>
        );

        expect(asFragment()).toMatchSnapshot();
    });

    it('should render loaded state', async () => {
        mock.onGet(
            '/comments/?content_type=quarterlytaxestimate&object_id=be20f314-042f-42c7-8fae-9c42618b9b07'
        ).replyOnce(200, commentsData);

        const { asFragment, getAllByRole, getByText, findByText } = render(
            <Wrapper routeDefinition="/hub/QTE/:workflowId" initialRoute="/hub/QTE/abc-123">
                <QTEDetailsPage />
            </Wrapper>
        );

        await findByText('Basic Info');

        expect(getByText('Basic Info')).toBeTruthy();
        expect(getByText('Connected Services')).toBeTruthy();
        expect(getByText('Workflow')).toBeTruthy();
        expect(getByText('Workflow Tasks')).toBeTruthy();
        expect(getByText('QTE')).toBeTruthy();
        expect(getByText('Comments')).toBeTruthy();

        expect(getAllByRole('table').length).toBe(6);
    });

    xit('should render error state', async () => {
        /**
         * TODO: Disabling this test until @Joeseph Rodrigues can fix and re-enable it.
         * Reason: `react-query` is not handling axios exceptions properly. And that is causing our CI to fail with a
         * `UnhandledPromiseRejectionWarning` error.
         */
        mock.onGet(
            '/comments/?content_type=quarterlytaxestimate&object_id=be20f314-042f-42c7-8fae-9c42618b9b07'
        ).replyOnce(403);

        const { findByText, asFragment } = render(
            <Wrapper routeDefinition="/hub/QTE/:workflowId" initialRoute="/hub/QTE/abc-123">
                <QTEDetailsPage />
            </Wrapper>
        );

        await findByText('Request failed with status code 403');

        expect(asFragment()).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/StatusIcon/StatusIcon.test.jsx
import { render } from '@testing-library/react';

import StatusIcon from './StatusIcon';

describe('TrackerTool/StatusIcon', () => {
    it('should render correctly', () => {
        const { asFragment } = render(
            <>
                <StatusIcon status="completed" />
                <StatusIcon status="incomplete" />
                <StatusIcon status="not applicable" />
                <StatusIcon />
            </>
        );
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/StatusIcon/index.js
export { default } from './StatusIcon';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/StatusIcon/StatusIcon.jsx
import { Icon } from 'semantic-ui-react';

const StatusIcon = ({ status }) => {
    switch (status) {
        case 'completed':
            return <Icon name="check circle outline" />;
        case 'incomplete':
            return <Icon name="hourglass one" />;
        case 'not applicable':
            return <Icon name="window close outline" />;
        case 'not started':
            return <Icon name="tasks" />;
        case 'review':
            return <Icon name="hourglass one" />;
        case 'flag':
            return <Icon name="flag" />;
        case 'fetch':
            return <Icon name="sync" />;
        default:
            return <Icon name="circle outline" />;
    }
};

export default StatusIcon;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/Page.test.jsx
import { makeObservable, observable, action } from 'mobx';
import { act, render, waitFor, screen, fireEvent } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';

import HubStoreContext from '../../common/Store/Context';

import QTEHomePage from './Page';

import RootStoreContext from 'modules/common/stores/Root/Context';

class MockHubStore {
    filter = {};

    milestones = [];

    members = [];

    constructor() {
        makeObservable(this, {
            filter: observable,
            milestones: observable,
            members: observable,
            setFilter: action,
            setMembers: action,
            setMilestones: action,
            fetchMembersQTE: action,
        });
    }

    fetchMembersQTE = () => jest.fn();

    fetchMilestones = () => jest.fn();

    setFilter = () => jest.fn();

    setMembers = () => jest.fn();

    setMilestones = (milestones) => {
        this.milestones = milestones;
    };
}

class MockLegacyStore {
    allStates = [];

    constructor() {
        makeObservable(this, {
            allStates: observable,
            getAllStates: action,
        });
    }

    getAllStates = () => {
        this.allStates = [
            { id: 'CA', name: 'California' },
            { id: 'AZ', name: 'Arizona' },
            { id: 'TX', name: 'Texas' },
        ];
    };
}

const renderWithProviders = (hubStore, legacyStore) => {
    const history = createMemoryHistory();
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });

    return render(
        <RootStoreContext.Provider value={{ legacyStore }}>
            <HubStoreContext.Provider value={hubStore}>
                <Router history={history}>
                    <QueryClientProvider client={queryClient}>
                        <QTEHomePage />
                    </QueryClientProvider>
                </Router>
            </HubStoreContext.Provider>
        </RootStoreContext.Provider>
    );
};

describe('QTE Tracker', () => {
    it('should render correctly', () => {
        const hubStore = new MockHubStore();
        const legacyStore = new MockLegacyStore();
        const { asFragment } = renderWithProviders(hubStore, legacyStore);

        act(() => {
            legacyStore.getAllStates();
        });

        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('State options', async () => {
        const hubStore = new MockHubStore();
        const legacyStore = new MockLegacyStore();
        renderWithProviders(hubStore, legacyStore);

        await waitFor(() => {
            legacyStore.getAllStates();
        });

        expect(screen.getByText(/california/i)).toBeInTheDocument();
        expect(screen.getByText(/arizona/i)).toBeInTheDocument();
        expect(screen.getByText(/texas/i)).toBeInTheDocument();
    });

    it('QTE Member Row Actions', async () => {
        const hubStore = new MockHubStore();
        const legacyStore = new MockLegacyStore();
        hubStore.members = [{ id: 1, fullname: 'Test Tester', current_step: 'review' }];
        hubStore.membersPagination = { total_record_count: 1, pages: 1 };
        renderWithProviders(hubStore, legacyStore);

        await waitFor(() => {
            legacyStore.getAllStates();
        });
        // the icons for approve, flag, detail view
        expect(screen.getByLabelText('Approve QTE')).toBeInTheDocument();
        expect(screen.getByLabelText('Flag QTE')).toBeInTheDocument();
        expect(screen.getByLabelText('View Details')).toBeInTheDocument();

        hubStore.members[0].current_step = 'flag';

        expect(screen.queryByLabelText('Approve QTE')).not.toBeInTheDocument();

        // the icons for restart and cancel
        expect(screen.getByLabelText('Restart QTE')).toBeInTheDocument();
        expect(screen.getByLabelText('Cancel QTE')).toBeInTheDocument();

        // clicking on cancel button reveals Reason Select field
        fireEvent.click(screen.getByLabelText('Cancel QTE'));
        expect(screen.getByLabelText('Reason')).toBeInTheDocument();
    });

    it('Confirm Action buttons', async () => {
        const hubStore = new MockHubStore();
        const legacyStore = new MockLegacyStore();
        hubStore.members = [{ id: 1, fullname: 'Test Tester', current_step: 'review' }];
        hubStore.membersPagination = { total_record_count: 1, pages: 1 };
        renderWithProviders(hubStore, legacyStore);

        await waitFor(() => {
            legacyStore.getAllStates();
        });

        const approveButton = screen.getByTestId('ThumbUpIcon');
        fireEvent.click(approveButton);

        expect(screen.getByTestId('CheckIcon')).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/Page.jsx
import { useState } from 'react';
import { Divider, Header, Icon } from 'semantic-ui-react';
import Button from '@mui/material/Button';

import TrackingTool from '../TrackingTool';

import ExtraFilterFields from './FilterFormFields';
import MemberListActions from './MemberListActions';
import Milestones from './Milestones';
import AddMemberModal from './AddMemberModal';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

function QTEHomePage() {
    const hubStore = useHubStore();
    const [showAddMemberModal, setShowAddMemberModal] = useState(false);
    const today = new Date();
    const currentQuarterQTE = {
        qte_quarter: Math.floor((today.getMonth() + 3) / 3),
        qte_year: today.getFullYear(),
        qte_step: 'review',
        activity_status: 'incomplete',
    };
    const memberTableCells = [
        { data: 'fullname', label: 'Full Name' },
        { data: 'email', label: 'Email Address' },
        { data: 'noi', label: 'NOI' },
        { data: 'gross_wages', label: 'Gross Wages' },
        { data: 'state_tax', label: 'State Tax' },
        { data: 'federal_tax', label: 'Federal Tax' },
        { data: 'state', label: 'State' },
        { data: 'current_step', label: 'Current Step' },
    ];

    return (
        <>
            <div className="tw-flex tw-items-start tw-justify-between">
                <Header as="h2">
                    <Icon name="calculator" />
                    QTE (Quarterly Tax Estimates)
                </Header>
                <Button onClick={() => setShowAddMemberModal(true)}>Add Member</Button>
            </div>
            <Divider />
            <TrackingTool
                memberTableCells={memberTableCells}
                ExtraFilterFields={ExtraFilterFields}
                accordianOpen={false}
                MemberListActions={MemberListActions}
                Milestones={Milestones}
                fetchMemberFunc={hubStore.fetchMembersQTE}
                initialValues={currentQuarterQTE}
            />
            <AddMemberModal open={showAddMemberModal} onClose={() => setShowAddMemberModal(false)} />
        </>
    );
}

export default QTEHomePage;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/Milestones/Milestones.jsx
import { useEffect } from 'react';
import { observer } from 'mobx-react';
import { Menu } from 'semantic-ui-react';
import { CircularProgress } from '@mui/material';

import Tags from '../../Tags';

import classes from './Milestones.module.less';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const Milestones = () => {
    const hubStore = useHubStore();

    useEffect(() => {
        const fetchData = async () => {
            await hubStore.fetchMilestones('/workflows/qte/statistics/');
        };
        fetchData();
    }, []);

    if (!hubStore.milestones.length) {
        return (
            <div className="tw-ml-3 tw-opacity-100">
                <CircularProgress />
            </div>
        );
    }

    return (
        <Menu compact className={classes.milestones}>
            <Menu.Item header>QTE Numbers: </Menu.Item>
            {hubStore?.milestones?.map((item) => (
                <Menu.Item key={item.name}>
                    {item.name.toUpperCase()}
                    <Tags text={`${item.member_count} QTEs`} />
                </Menu.Item>
            ))}
        </Menu>
    );
};

export default observer(Milestones);
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/Milestones/Milestones.test.jsx
import { makeObservable, configure, observable, action } from 'mobx';
import { render } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import HubStoreContext from '../../../common/Store/Context';

import Milestones from './Milestones';

/**
 * MobX makes some fields non-configurable or non-writable to disabling spying/mocking/stubbing in tests
 * @link https://mobx.js.org/configuration.html#safedescriptors-boolean
 */
configure({ safeDescriptors: false });

class MockHubStore {
    milestones = [
        { name: 'fetch', member_count: 1 },
        { name: 'review', member_count: 2 },
        { name: 'cancel', member_count: 144 },
        { name: 'flag', member_count: 44 },
    ];

    constructor() {
        makeObservable(this, {
            milestones: observable,
            setFilter: action,
            fetchMembers: action,
            setMilestones: action,
        });
    }

    setFilter = () => jest.fn();

    setMilestones = () => jest.fn();

    fetchMembers = () => jest.fn();

    fetchMilestones = () => jest.fn();
}

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <Milestones />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('Tracker QTE/Milestones', () => {
    it('should render correctly', () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/Milestones/index.js
export { default } from './Milestones';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/Milestones/Milestones.module.less
@color-green: #008866;
@color-red: #FA5A60;

[class~=ui][class~=menu].milestones {
    border-radius: 40px;
    margin-bottom: 20px;
    box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1);
    border: none;

    [class~="item"] {
        padding: 0 40px;
        cursor: pointer;
        border-right: 1px solid rgba(0, 0, 0, 0.1);

        &:hover {
            color: #000;
        }

        &:last-child {
            border-right: none;
        }

        &[class~="active"] {
            border-radius: 40px;
            background-color: #faf9f6;
        }

        &[class~="completed"] {
            [class~="icon"] {
                color: @color-green;
            }
        }

        &[class~="incompleted"] {
            [class~="icon"] {
                color: @color-red;
            }
        }

        &::before {
            background: none;
        }
    }
}// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/FilterFormFields.jsx
import { range } from 'lodash';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { useEffect } from 'react';
import { Form } from 'semantic-ui-react';

import useRootStore from 'modules/common/stores/Root/useRootStore';

function ExtraFilterFields({ values, handleSelectChange }) {
    const { legacyStore } = useRootStore();
    const currentYear = new Date().getFullYear();
    const options = {
        // Year choices for dropdown filter from 2019 to current year
        years: range(2019, currentYear + 1).map((year) => ({ key: year, text: year, value: year })),
        // These values will never change
        quarters: [
            { key: 1, text: 1, value: 1 },
            { key: 2, text: 2, value: 2 },
            { key: 3, text: 3, value: 3 },
            { key: 4, text: 4, value: 4 },
        ],
        // Sourced from backend pre-filled region model
        states: toJS(legacyStore.allStates).map((state) => ({ key: state.id, text: state.name, value: state.id })),
        steps: [
            { key: 'review', text: 'In Review', value: 'review' },
            { key: 'flag', text: 'Flagged', value: 'flag' },
            { key: 'cancel', text: 'Cancelled', value: 'cancel' },
            { key: 'fetch', text: 'Fetching Data', value: 'fetch' },
            { key: 'send', text: 'Sending Email', value: 'send' },
        ],
    };

    useEffect(() => {
        if (legacyStore.allStates.length === 0) {
            legacyStore.getAllStates();
        }
    }, []);

    return (
        <>
            <Form.Select
                fluid
                label="Year"
                name="qte_year"
                id="qte_year"
                value={values.qte_year || ''}
                options={options.years}
                onChange={handleSelectChange}
                placeholder="Year"
                search
            />
            <Form.Select
                fluid
                label="Quarter"
                name="qte_quarter"
                id="qte_quarter"
                value={values.qte_quarter || ''}
                onChange={handleSelectChange}
                options={options.quarters}
                placeholder="Quarter"
                search
            />
            <Form.Select
                fluid
                label="State"
                name="qte_state"
                id="qte_state"
                value={values.qte_state || ''}
                onChange={handleSelectChange}
                options={options.states}
                placeholder="State"
                search
            />
            <Form.Select
                fluid
                label="QTE Step"
                name="qte_step"
                id="qte_step"
                value={values.qte_step || ''}
                onChange={handleSelectChange}
                options={options.steps}
                placeholder="QTE step"
                search
            />
        </>
    );
}

export default observer(ExtraFilterFields);
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/MemberListActions.jsx
import { Link } from 'react-router-dom';
import { useState, useRef } from 'react';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormHelperText from '@mui/material/FormHelperText';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Flag from '@mui/icons-material/Flag';
import ThumbUp from '@mui/icons-material/ThumbUp';
import Cancel from '@mui/icons-material/Cancel';
import Preview from '@mui/icons-material/Preview';
import Sync from '@mui/icons-material/Sync';
import Check from '@mui/icons-material/Check';
import Undo from '@mui/icons-material/Undo';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const ToolTipIconButton = ({ title, iconComponent, toLink = null, onClick = null }) => (
    <Tooltip title={title} arrow>
        {toLink ? (
            <Link target="_blank" to={toLink}>
                <IconButton>{iconComponent}</IconButton>
            </Link>
        ) : (
            <IconButton onClick={onClick}>{iconComponent}</IconButton>
        )}
    </Tooltip>
);

const ReasonSelector = ({ choiceType, onChange, value }) => {
    const reasonChoices = {
        Cancel: [
            { value: 'offboarded', label: 'Member Off-Boarded' },
            { value: 'manual_qte', label: 'Requires Manual' },
            { value: 'not_required', label: 'Not Required' },
            { value: 'other', label: 'Other' },
        ],
    };
    return (
        <FormControl variant="outlined" size="small">
            <InputLabel id="reason-helper-label">Reason</InputLabel>
            <Select labelId="reason-helper-label" id="reason-helper" value={value} label="Reason" onChange={onChange}>
                {reasonChoices[choiceType].map((item) => (
                    <MenuItem value={item.value} key={item.value}>
                        {item.label}
                    </MenuItem>
                ))}
            </Select>
            <FormHelperText>Select a reason</FormHelperText>
        </FormControl>
    );
};

function MemberListActions({ member }) {
    const hubStore = useHubStore();
    const [confirmation, setConfirmation] = useState('');

    const reasonRef = useRef('');
    const handleConfirm = async () => {
        await hubStore.patchWorkflowStatusQTE(
            member.workflow_id,
            member.task_id,
            member.current_step,
            confirmation.toLowerCase(),
            reasonRef.current.toLowerCase()
        );
        setConfirmation('');
        reasonRef.current = '';
        hubStore.popMemberByID(member.id);
    };

    const actionButtonDetails = {
        approve: {
            title: 'Approve QTE',
            iconComponent: <ThumbUp fontSize="large" color="success" />,
            onClick: () => setConfirmation('Approve'),
        },
        flag: {
            title: 'Flag QTE',
            iconComponent: <Flag fontSize="large" color="warning" />,
            onClick: () => setConfirmation('Flag'),
        },
        restart: {
            title: 'Restart QTE',
            iconComponent: <Sync fontSize="large" color="info" />,
            onClick: () => setConfirmation('Restart'),
        },
        cancel: {
            title: 'Cancel QTE',
            iconComponent: <Cancel fontSize="large" color="error" />,
            onClick: () => setConfirmation('Cancel'),
        },
        detail: {
            title: 'View Details',
            iconComponent: <Preview fontSize="large" />,
            toLink: `/hub/qte/${member.workflow_id}`,
        },
        goBack: {
            title: 'Go Back',
            iconComponent: <Undo fontSize="Large" />,
            onClick: () => setConfirmation(''),
        },
        confirm: {
            title: `Confirm ${confirmation} QTE`,
            iconComponent: <Check fontSize="Large" color="success" />,
            onClick: handleConfirm,
        },
    };

    const handleReasonSelection = (e) => {
        reasonRef.current = e.target.value;
        handleConfirm();
    };
    const actionButtonGroup = {
        review: (
            <>
                <ToolTipIconButton {...actionButtonDetails.approve} />
                <ToolTipIconButton {...actionButtonDetails.flag} />
            </>
        ),
        flag: (
            <>
                <ToolTipIconButton {...actionButtonDetails.restart} />
                <ToolTipIconButton {...actionButtonDetails.cancel} />
            </>
        ),
    };

    if (!confirmation) {
        return (
            <>
                {actionButtonGroup[member.current_step]}
                <ToolTipIconButton {...actionButtonDetails.detail} />
            </>
        );
    }

    return (
        <>
            {confirmation === 'Cancel' ? (
                <ReasonSelector choiceType={confirmation} onChange={handleReasonSelection} value={reasonRef.current} />
            ) : (
                <>
                    {`${confirmation} QTE? `}
                    <ToolTipIconButton {...actionButtonDetails.confirm} />
                </>
            )}
            <ToolTipIconButton {...actionButtonDetails.goBack} />
        </>
    );
}

export default MemberListActions;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/AddMemberModal/index.js
export { default } from './AddMemberModal';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/AddMemberModal/AddMemberModal.jsx
import { useEffect, useRef, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import Paper from '@mui/material/Paper';
import Modal from '@mui/material/Modal';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
import LoadingButton from '@mui/lab/LoadingButton';

import useDebounceValue from 'modules/common/useDebounceValue';
import collectiveApi from 'modules/common/collectiveApi';

const searchUsers = async (term) => {
    const {
        // our api has a great interface
        data: { data },
    } = await collectiveApi.get(`searchclients/?q=${term}`);
    return data;
};

const AddMemberModal = ({ open, onClose }) => {
    const [searchTerm, setSearchTerm] = useState('');
    const [selection, setSelection] = useState('');
    const debouncedSearchTerm = useDebounceValue(searchTerm);
    const buttonRef = useRef();

    const queryClient = useQueryClient();
    const { refetch, isFetching, data } = useQuery(['memberSearch'], () => searchUsers(debouncedSearchTerm), {
        enabled: false, // should only fetch on search input change, not on mount
    });

    const submitUser = useMutation(async ({ email, id }) => {
        const results = await collectiveApi.post('/workflows/qte/', { id });
        return results?.data;
    });

    useEffect(() => {
        // debounce the search request
        if (debouncedSearchTerm.length) {
            refetch();
        }
    }, [debouncedSearchTerm]);

    useEffect(() => {
        // enables focusing submit button until after selection is set, wherebyunto, the button will be undisabled
        // the ref kinda freaks out if you focus a disabled button, wont trigger the button onClick
        if (buttonRef.current) {
            buttonRef.current.focusVisible();
        }
    }, [selection]);

    const handleClose = () => {
        onClose();

        // cleanup for repeatedly opening modal and adding users
        submitUser.reset();
        setSelection('');
        queryClient.resetQueries('memberSearch');
    };

    useEffect(handleClose, [submitUser.isSuccess]);

    return (
        <Modal open={open} onClose={handleClose}>
            <Paper className="tw-absolute tw-p-4 -tw-translate-x-1/2 -tw-translate-y-1/2 tw-w-80 tw-left-1/2 tw-top-1/2">
                <div className="tw-flex tw-flex-row tw-items-center tw-justify-between tw-mb-3">
                    <Autocomplete
                        fullWidth
                        loading={isFetching}
                        options={
                            data?.map(({ fullname, email, id }) => ({
                                label: fullname,
                                email,
                                id,
                            })) || []
                        }
                        getOptionLabel={(option) => `${option.label} - ${option.email}`}
                        filterOptions={(o) => o}
                        blurOnSelect
                        autoHighlight
                        renderInput={(params) => <TextField {...params} color="info" autoFocus label="Member Search" />}
                        onInputChange={(_, value, reason) => {
                            if (reason === 'input') {
                                setSearchTerm(value);
                            }
                        }}
                        onChange={(_, value) => setSelection(value)}
                        isOptionEqualToValue={(a, b) => a.id === b.id}
                    />
                    <div className={`tw-ml-3 ${isFetching ? 'tw-opacity-100' : 'tw-opacity-0'}`}>
                        <CircularProgress />
                    </div>
                </div>
                <LoadingButton
                    fullWidth
                    disabled={!selection}
                    loading={submitUser.isLoading}
                    variant="contained"
                    action={buttonRef}
                    size="large"
                    onClick={() => submitUser.mutate(selection)}>
                    Add Member
                </LoadingButton>
            </Paper>
        </Modal>
    );
};

export default AddMemberModal;
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/Home/AddMemberModal/AddMemberModal.test.js
import { render } from '@testing-library/react';
import { Route, MemoryRouter } from 'react-router-dom';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';

import AddMemberModal from './AddMemberModal';

const Wrapper = ({ children, routeDefinition = '', initialRoute = '' }) => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });
    return (
        <MemoryRouter initialEntries={[initialRoute]}>
            <QueryClientProvider client={queryClient}>
                <Route exact path={routeDefinition || initialRoute}>
                    {children}
                </Route>
            </QueryClientProvider>
        </MemoryRouter>
    );
};

describe('AddMemberModal', () => {
    it('should render successfully', () => {
        const { baseElement } = render(
            <Wrapper routeDefinition="/hub/QTE/:workflowId" initialRoute="/hub/QTE/abc-123">
                <AddMemberModal open onClose={jest.fn()} />
            </Wrapper>
        );

        expect(baseElement).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/TrackingTool.jsx
import { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Loader, Divider, Accordion, Icon } from 'semantic-ui-react';
import { isEmpty } from 'lodash';

import classes from './TrackingTool.module.less';
import FilterForm from './FilterForm';
import MemberList from './List';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

function TrackingTool({
    accordianOpen = true,
    memberTableCells,
    ExtraFilterFields,
    MemberListActions,
    Milestones,
    fetchMemberFunc,
    initialValues = {},
}) {
    const hubStore = useHubStore();
    const [loading, setLoading] = useState(false);
    const [showAccordian, setShowAccordian] = useState(accordianOpen);

    const setAccordian = () => {
        setShowAccordian(!showAccordian);
    };

    useEffect(() => {
        hubStore.setMembers([]);

        const fetchOnPageLoad = async () => {
            await handleSubmit(initialValues);
            setLoading(false);
        };

        if (!isEmpty(initialValues)) {
            setLoading(true);
            fetchOnPageLoad();
        }
    }, [initialValues]);

    const handleReset = async () => {
        hubStore.setFilter({});
        hubStore.setMembers([]);
        await hubStore.fetchMembersOnboarding();
    };

    const handleSubmit = async (values) => {
        hubStore.setFilter(values);
        await fetchMemberFunc(values);
    };

    if (loading) {
        return (
            <Loader inline="centered" active>
                Loading
            </Loader>
        );
    }

    return (
        <div className={classes.trackerTool}>
            {Milestones && <Milestones />}
            <Divider />
            <Accordion fluid>
                <Accordion.Title
                    active={showAccordian === true}
                    onClick={setAccordian}
                    as="h3"
                    className="accordianTitle">
                    <Icon name="dropdown" />
                    Search Members
                </Accordion.Title>
                <Accordion.Content active={showAccordian === true}>
                    <FilterForm onSubmit={handleSubmit} onReset={handleReset} ExtraFilterFields={ExtraFilterFields} />
                </Accordion.Content>
            </Accordion>
            <Divider />
            <MemberList
                tableCells={memberTableCells}
                MemberListActions={MemberListActions}
                fetchMemberFunc={fetchMemberFunc}
            />
        </div>
    );
}

export default observer(TrackingTool);
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/List/List.jsx
import { observer } from 'mobx-react';
import { useEffect, useState } from 'react';
import { Icon, Loader, Menu, Segment, Table } from 'semantic-ui-react';

import Tags from '../../Tags';

import classes from './List.module.less';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const MemberList = ({ tableCells, MemberListActions, fetchMemberFunc }) => {
    const hubStore = useHubStore();
    const [hasData, setHasData] = useState(false);
    const [page, setPage] = useState(1);
    const [sortColumn, setSortColumn] = useState('businessname');
    const [sortDirection, setSortDirection] = useState('ascending');

    useEffect(() => {
        setHasData(hubStore.members.length > 0);
    }, [hubStore, hubStore.members]);

    useEffect(() => {
        const { filter } = hubStore;
        fetchMemberFunc(filter, page, sortColumn, sortDirection);
    }, [page, sortColumn, sortDirection]);

    const changeSort = (column, direction) => {
        let updatedSortDirection = direction === 'ascending' ? 'descending' : 'ascending';
        if (column !== sortColumn) {
            updatedSortDirection = 'ascending';
        }
        setSortColumn(column);
        setSortDirection(updatedSortDirection);
    };

    const isSortableColumn = (currentColumn) => {
        return ['businessname', 'fullname', 'email', 'status', 'usertype'].includes(currentColumn);
    };

    if (hubStore.isMembersLoading) {
        return (
            <Segment padded="very" textAlign="center" className={classes.empty}>
                <div className="content">
                    <Loader active>Loading</Loader>
                </div>
            </Segment>
        );
    }

    if (!hasData) {
        return (
            <Segment padded="very" textAlign="center" className={classes.empty}>
                <div className="content">No data available.</div>
            </Segment>
        );
    }

    return (
        <Table sortable striped celled basic="very" className={classes.list}>
            <Table.Header>
                <Table.Row>
                    {tableCells.map((cell) => (
                        <Table.HeaderCell
                            key={cell.data}
                            sorted={isSortableColumn(cell.data) && cell.data === sortColumn ? sortDirection : null}
                            onClick={() => isSortableColumn(cell.data) && changeSort(cell.data, sortDirection)}>
                            {cell.label}
                        </Table.HeaderCell>
                    ))}
                    <Table.HeaderCell textAlign="center" width={2}>
                        Actions
                    </Table.HeaderCell>
                </Table.Row>
            </Table.Header>
            <Table.Body>
                {hubStore.members.map((member) => (
                    <Table.Row key={member.id}>
                        {tableCells.map((cell) => {
                            return (
                                <Table.Cell key={`${member.id || member.email}__${cell.data}`}>
                                    {['Completed', 'Incomplete', 'Not Started', 'review', 'flag', 'fetch'].includes(
                                        member[cell.data]
                                    ) ? (
                                        <Tags status={member[cell.data].toLowerCase()} single>
                                            {member[cell.data]}
                                        </Tags>
                                    ) : (
                                        member[cell.data]
                                    )}
                                </Table.Cell>
                            );
                        })}
                        <Table.Cell collapsing textAlign="center" key={`${member.id}__actions`}>
                            {MemberListActions && <MemberListActions member={member} />}
                        </Table.Cell>
                    </Table.Row>
                ))}
            </Table.Body>
            <Table.Footer>
                <Table.Row>
                    <Table.HeaderCell colSpan={tableCells.length + 1}>
                        <Menu pagination>
                            <Menu.Item as="span" icon>
                                Total number of members: {hubStore.membersPagination.total_record_count}
                            </Menu.Item>
                        </Menu>
                        <Menu floated="right" pagination>
                            <Menu.Item as="span" icon>
                                {hubStore.membersPagination.number_of_pages}
                            </Menu.Item>
                            <Menu.Item
                                as="a"
                                icon
                                disabled={!hubStore.membersPagination.previous_page}
                                onClick={() => setPage(hubStore.membersPagination.previous_page)}>
                                <Icon name="chevron left" />
                            </Menu.Item>
                            <Menu.Item
                                as="a"
                                icon
                                disabled={!hubStore.membersPagination.next_page}
                                onClick={() => setPage(hubStore.membersPagination.next_page)}>
                                <Icon name="chevron right" />
                            </Menu.Item>
                        </Menu>
                    </Table.HeaderCell>
                </Table.Row>
            </Table.Footer>
        </Table>
    );
};

export default observer(MemberList);
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/List/index.js
export { default } from './List';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/List/List.module.less
[class~='ui'][class~='table'].list {
    background: #faf9f6;
    padding: 20px;

    thead {
        font-size: 10px;
        text-transform: uppercase;

        tr {
            th {
                border: 0;
                border-radius: 0;
                box-shadow: none;
                text-transform: uppercase;
                color: rgba(0, 0, 0, 0.87);
                opacity: 0.5;
                border-bottom: 1px solid rgba(34, 36, 38, 0.1);
                border-left: transparent !important;
            }
        }
    }

    tbody {
        tr {
            background-color: #ffffff;

            td {
                a {
                    color: rgba(0, 0, 0, 0.57);

                    text-decoration: none;
                    transition: all 0.2s linear;

                    &:hover,
                    &:active,
                    &:focus {
                        color: rgba(0, 0, 0, 0.87);
                    }
                }

                [class~='ui'][class~='button'] {
                    width: 54px;
                    // color: rgba(34, 36, 38, 0.8);
                    // border: 1px solid rgba(34, 36, 38, 0.2);
                    // background-color: #ffffff;
                    transition: all 0.2s linear;

                    &:hover,
                    &:active,
                    &:focus {
                        color: rgba(34, 36, 38, 0.9);
                        background-color: #faf9f6;
                    }
                }
            }
        }
    }

    tfoot {
        tr {
            th {
                padding: 10px 0;

                [class~='ui'][class~='pagination'][class~='menu'] {
                    &:first-child {
                        border: 0;
                    }

                    a:hover,
                    a:active,
                    a:focus {
                        color: #000000;
                    }
                }
            }
        }
    }
}

[class~='ui'][class~='segment'].empty {
    background: #faf9f6;

    [class~='content'] {
        padding: 40px;
        color: rgba(34, 36, 38, 0.8);
        opacity: 0.5;
    }
}
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/List/List.test.jsx
import { act, render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { action, makeObservable, observable } from 'mobx';
import { Router } from 'react-router-dom';

import HubStoreContext from '../../../common/Store/Context';

import List from './List';

class MockHubStore {
    members = [];

    membersPagination = {};

    constructor() {
        makeObservable(this, {
            members: observable,
            membersPagination: observable,
            setMembers: action,
            setMembersPagination: action,
        });
    }

    setMembers = (members) => {
        this.members = members;
    };

    setMembersPagination = (membersPagination) => {
        this.membersPagination = membersPagination;
    };
}

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();
    const tableCells = [
        { data: 'businessname', label: 'Business Name' },
        { data: 'fullname', label: 'Full Name' },
        { data: 'email', label: 'Email Address' },
        { data: 'onboarding_duration', label: 'Onboarding Duration' },
        { data: 'legalSetup', label: 'Legal Setup' },
        { data: 'financialSetup', label: 'Financial Setup' },
        { data: 'technologySetup', label: 'Technology Setup' },
    ];

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <List tableCells={tableCells} fetchMemberFunc={() => {}} />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('TrackerTool/List', () => {
    it('should render correctly', () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);
        act(() => {
            hubStore.setMembers([
                {
                    email: 'test@test.com',
                    days: 13,
                    legalSetup: 'incomplete',
                    financialSetup: 'incomplete',
                    technologySetup: 'incomplete',
                },
            ]);
            hubStore.setMembersPagination({
                total_record_count: 0,
                number_of_pages: '',
                next_page: null,
                previous_page: null,
            });
        });
        expect(asFragment().firstChild).toMatchSnapshot();
    });
    it('Should display empty data table', () => {
        const hubStore = new MockHubStore();
        const { getByText } = renderWithProviders(hubStore);
        expect(getByText(/No data available/i)).toBeInTheDocument();
    });

    it('Should display populated data table', () => {
        const hubStore = new MockHubStore();
        const { getByText } = renderWithProviders(hubStore);
        expect(getByText(/No data available/i)).toBeInTheDocument();

        act(() => {
            hubStore.setMembers([
                {
                    businessname: 'Test LLC',
                    fullname: 'Test test',
                    email: 'test@test.com',
                    onboarding_duration: 13,
                    legalSetup: 'incomplete',
                    financialSetup: 'incomplete',
                    technologySetup: 'incomplete',
                },
            ]);
        });

        act(() => {
            hubStore.setMembersPagination({
                total_record_count: 0,
                number_of_pages: '',
                next_page: null,
                previous_page: null,
            });
        });

        expect(getByText(/test@test.com/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/index.js
export { default } from './TrackingTool';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/TrackingTool.module.less
.trackerTool {
    h3 {
        font-family: 'recoletasemibold' !important;
        font-size: initial !important;
    }
}
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/FilterForm/index.js
export { default } from './FilterForm';
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/FilterForm/FilterForm.module.less
.filter {
    [class~='ui'][class~='form'] {
        [class~='ui'][class~='button'] {
            font-size: 16px;
            border: 1px solid !important;
            padding: 18px !important;
            border-radius: 5px !important;
            width: 140px;
            z-index: 0;

            &[class~='secondary'] {
                color: #fa5a60;
                border-color: lighten(#fa5a60, 0.3);

                &:hover {
                    background-color: #ffffff;
                }

                &[class~='disabled'] {
                    opacity: 0.3 !important;
                    color: rgba(0, 0, 0, 0.5) !important;
                    border-color: rgba(0, 0, 0, 0.2) !important;
                }
            }

            &[class~='disabled'] {
                opacity: 0.3 !important;
            }
        }

        [class~='fields'] {
            margin: 0em -0.5em 1em;

            [class~='dropdown'] {
                padding: 8px 6px;

                [class~='text'] {
                    font-size: 1.286em;
                    line-height: 1.30769231em;
                }

                [class~='item'] {
                    font-size: 1em;
                    line-height: 1em;
                }
            }

            [class~='field'] {
                [class~='text'] {
                    color: #000;
                    font-weight: normal;
                }

                [class~='default'] {
                    opacity: 0.3;
                }

                label {
                    font-family: 'mier_bregular';
                    font-weight: normal;
                    opacity: 0.7;
                }

                input {
                    font-size: 1.286em;
                    line-height: 1.30769231em;
                    padding: 8px 6px;
                }

                input::placeholder {
                    opacity: 0.2;
                }

                input,
                input::placeholder {
                    font-family: 'mier_bregular';
                    box-shadow: none;
                    color: #000;
                    font-weight: normal;
                }
            }
        }
    }
}
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/FilterForm/FilterForm.test.jsx
import { cleanup, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMemoryHistory } from 'history';
import { makeObservable, observable } from 'mobx';
import { Router } from 'react-router-dom';

import HubStoreContext from '../../../common/Store/Context';

import Filter from './FilterForm';

class MockHubStore {
    filter = {};

    memberRelationshipManagers = [{ key: 1, text: 'test', value: 'test' }];

    onboardingAccountants = [{ key: 1, text: 'test', value: 'test' }];

    taxpro = [{ key: 1, text: 'test', value: 'test' }];

    pops = [{ key: 1, text: 'test', value: 'test' }];

    constructor() {
        makeObservable(this, {
            filter: observable,
            memberRelationshipManagers: observable,
            onboardingAccountants: observable,
            taxpro: observable,
            pops: observable,
        });
    }
}

const handleSubmit = jest.fn();
const handleReset = jest.fn();

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <Filter filter={{}} onSubmit={handleSubmit} onReset={handleReset} />
            </Router>
        </HubStoreContext.Provider>
    );
};

beforeEach(cleanup);

describe('TrackerTool/Filter', () => {
    it('should render correctly', () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('Should call the function handleSubmit once', async () => {
        const hubStore = new MockHubStore();
        const user = userEvent.setup();
        const { getByLabelText, getByRole } = renderWithProviders(hubStore);
        await user.type(getByLabelText(/Email Address/i), 'test@test.com');
        await user.click(getByRole('button', { name: /Search/i }));

        await waitFor(() => expect(handleSubmit).toBeCalledTimes(1));
    });

    it('Should call the function handleReset once', async () => {
        const hubStore = new MockHubStore();
        const user = userEvent.setup();
        const { getByLabelText, getByRole } = renderWithProviders(hubStore);
        await user.type(getByLabelText(/Email Address/i), 'test@test.com');
        await user.click(getByRole('button', { name: /Clear/i }));

        await waitFor(() => expect(handleSubmit).toBeCalledTimes(0));
        await waitFor(() => expect(handleReset).toBeCalledTimes(1));
        await waitFor(() => expect(getByLabelText(/Email Address/i)).not.toHaveValue('test@test.com'));
    });
});
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/FilterForm/FilterForm.jsx
import { useFormik } from 'formik';
import { Form, Icon } from 'semantic-ui-react';
import { observer } from 'mobx-react';
import { isEmpty } from 'lodash';
import { useEffect } from 'react';
import { LoadingButton as Button } from '@mui/lab';

import classes from './FilterForm.module.less';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const FilterForm = ({ onReset, onSubmit, ExtraFilterFields }) => {
    const hubStore = useHubStore();

    const options = [
        { key: 'completed', text: 'Completed', value: 'completed' },
        { key: 'incomplete', text: 'Incomplete', value: 'incomplete' },
    ];

    const { values, dirty, isSubmitting, handleSubmit, handleChange, resetForm, setValues, setFieldValue } = useFormik({
        initialValues: { ...hubStore.filter },
        onSubmit,
        onReset,
    });

    useEffect(() => {
        setValues(hubStore.filter);
    }, [hubStore, hubStore.filter, setValues]);

    const handleSelectChange = (event, { value, name }) => {
        setFieldValue(name, value);
    };

    const handleReset = () => {
        resetForm({ values: {} });
    };

    return (
        <div className={classes.filter}>
            <Form>
                {ExtraFilterFields && (
                    <Form.Group widths="equal">
                        <ExtraFilterFields values={values} handleSelectChange={handleSelectChange} />
                    </Form.Group>
                )}
                <Form.Group widths="equal">
                    <Form.Input
                        fluid
                        name="member_businessname"
                        id="member_businessname"
                        value={values.member_businessname || ''}
                        onChange={handleChange}
                        label="Business Name"
                        placeholder="Business Name"
                    />
                    <Form.Input
                        fluid
                        name="member_fullname"
                        id="member_fullname"
                        value={values.member_fullname || ''}
                        onChange={handleChange}
                        label="Full Name"
                        placeholder="Full Name"
                    />
                    <Form.Input
                        fluid
                        name="member_email"
                        id="member_email"
                        value={values.member_email || ''}
                        onChange={handleChange}
                        label="Email Address"
                        placeholder="Email Address"
                    />
                    <Form.Select
                        fluid
                        label="Activity Status"
                        name="activity_status"
                        id="activity_status"
                        value={values.activity_status || ''}
                        options={options}
                        onChange={handleSelectChange}
                        placeholder="Activity Status"
                        search
                    />
                </Form.Group>
                <div className="tw-flex tw-items-center">
                    <Button
                        variant="contained"
                        type="submit"
                        className="tw-mr-2"
                        onClick={handleSubmit}
                        disabled={isSubmitting || (!dirty && isEmpty(hubStore.filter))}
                        loading={isSubmitting}>
                        <Icon name="search" />
                        Search
                    </Button>
                    <Button
                        variant="outlined"
                        type="reset"
                        onClick={handleReset}
                        disabled={isSubmitting || (!dirty && isEmpty(hubStore.filter))}>
                        <Icon name="eraser" />
                        Clear
                    </Button>
                </div>
            </Form>
        </div>
    );
};

export default observer(FilterForm);
// Code from file collective-frontend/src/modules/Hub/QuarterlyTaxEstimates/TrackingTool/TrackingTool.test.jsx
import { makeObservable, observable, action } from 'mobx';
import { act, render, waitFor } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import HubStoreContext from '../../common/Store/Context';

import TrackingTool from './TrackingTool';

class MockHubStore {
    filter = {};

    milestones = [];

    memberRelationshipManagers = [];

    onboardingAccountants = [];

    members = [];

    constructor() {
        makeObservable(this, {
            filter: observable,
            milestones: observable,
            memberRelationshipManagers: observable,
            onboardingAccountants: observable,
            members: observable,
            fetchMilestones: action,
            fetchMrmsAndOnbs: action,
            setFilter: action,
            setMembers: action,
            setMilestones: action,
            setMemberRelationshipManagers: action,
            setOnboardingAccountants: action,
        });
    }

    fetchMilestones = () => jest.fn();

    fetchMrmsAndOnbs = () => jest.fn();

    setFilter = () => jest.fn();

    setMembers = () => jest.fn();

    setMilestones = (milestones) => {
        this.milestones = milestones;
    };

    setMemberRelationshipManagers = (memberRelationshipManagers) => {
        this.memberRelationshipManagers = memberRelationshipManagers;
    };

    setOnboardingAccountants = (onboardingAccountants) => {
        this.onboardingAccountants = onboardingAccountants;
    };
}

const milestones = [
    {
        name: 'Legal Setup',
        status: 'incompleted',
        members: 144,
        days: 44.5,
    },
];

const mrmsAndOnbs = [{ key: 1, text: 'test', value: 'test' }];

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <TrackingTool fetchMemberFunc={() => {}} />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('TrackerTool', () => {
    it('should render correctly', async () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);

        await waitFor(() => hubStore.fetchMilestones().mockResolvedValue(null));
        await waitFor(() => hubStore.fetchMrmsAndOnbs().mockResolvedValue(null));
        act(() => {
            hubStore.setMilestones(milestones);
            hubStore.setMemberRelationshipManagers(mrmsAndOnbs);
            hubStore.setOnboardingAccountants(mrmsAndOnbs);
        });

        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render the component', async () => {
        const hubStore = new MockHubStore();
        const { getByText } = renderWithProviders(hubStore);

        await waitFor(() => hubStore.fetchMilestones().mockResolvedValue(null));
        await waitFor(() => hubStore.fetchMrmsAndOnbs().mockResolvedValue(null));
        act(() => {
            hubStore.setMilestones(milestones);
            hubStore.setMemberRelationshipManagers(mrmsAndOnbs);
            hubStore.setOnboardingAccountants(mrmsAndOnbs);
        });

        expect(getByText(/Search Members/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/TaxReturnSignatureRequests/requests.js
import collectiveApi from 'modules/common/collectiveApi';

export const getSignatureRequestByMemberId = async ({ signal, queryKey: [_key, { memberId: member_id }] }) => {
    const resp = await collectiveApi.get('tax_organizer/signature-requests/', {
        signal,
        params: { member_id },
    });
    return resp.data;
};

export const getSignatureRequest = async ({ signal, queryKey: [_key, { requestId }] }) => {
    const resp = await collectiveApi.get(`tax_organizer/signature-requests/${requestId}/`, { signal });
    return resp.data;
};

export const getMember = async ({ signal, queryKey: [_key, { memberId }] }) => {
    const resp = await collectiveApi.get(`members/${memberId}/`, { signal });
    return resp.data;
};

export const downloadFile = ({ signal, queryKey: [_key, { fragment }] }) => {
    return collectiveApi.get(new URL(fragment, process.env.REACT_APP_DJANGO_URL), {
        signal,
        responseType: 'blob',
    });
};
// Code from file collective-frontend/src/modules/Hub/TaxReturnSignatureRequests/TaxReturnSignatureRequestDetail.jsx
import { IconButton, Typography, Skeleton, Paper, Chip, Avatar } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'react-router-dom';
import { useState } from 'react';
import { LoadingButton } from '@mui/lab';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';

import { getSignatureRequest, getMember, downloadFile } from './requests';

import states from 'modules/common/constants/states';

const taxTypeMapping = {
    ITR: 'Individual Tax Return',
    BTR: 'Business Tax Return',
};

const TaxReturnSignatureRequestDetail = () => {
    const { requestId } = useParams();
    const [downloadUrl, setDownloadUrl] = useState(null);

    const { data: signatureRequest } = useQuery(['signatureRequests', { requestId }], getSignatureRequest);

    const { isLoading: memberLoading, data: member } = useQuery(
        ['signatureRequests', { memberId: signatureRequest?.member_id }],
        getMember,
        { enabled: Boolean(signatureRequest?.member_id) }
    );

    const { isFetching: documentFetching } = useQuery(
        [
            'downloadFile',
            {
                fragment: downloadUrl,
            },
        ],
        downloadFile,
        {
            enabled: Boolean(downloadUrl),
            onSettled: () => setDownloadUrl(),
            onSuccess: (data) => {
                // use browser api to host data blob as if it was a hosted resource
                // open tab with resource url
                // kill resource url, we dont want to leave resources in memory after the fact
                const href = URL.createObjectURL(data.data);
                const openReference = window.open();
                openReference.location.href = href;
                setTimeout(() => URL.revokeObjectURL(href), 1000);
            },
        }
    );

    return (
        <div className="tw-max-w-full tw-min-w-min tw-m-auto">
            <div className="tw-mb-4">
                <Typography variant="h1">{memberLoading ? <Skeleton width="200px" /> : member?.fullname}</Typography>
                <Typography>{memberLoading ? <Skeleton width="200px" /> : member?.businessname}</Typography>
                <div className="tw-flex tw-gap-0.5 tw-items-center">
                    <Typography>{memberLoading ? <Skeleton width="200px" /> : member?.email}</Typography>{' '}
                    <IconButton
                        onClick={() => navigator.clipboard.writeText(member?.email)}
                        size="small"
                        aria-label="copy email"
                        color="black">
                        <ContentCopyIcon color="black" fontSize="inherit" />
                    </IconButton>
                </div>
            </div>
            <Paper elevation={3} className="tw-p-4 child:tw-mb-2">
                <Typography>
                    {signatureRequest?.tax_year} {taxTypeMapping[signatureRequest?.tax_return_type]}
                </Typography>
                <div className="tw-flex tw-gap-x-1">
                    {signatureRequest?.states.map((s) =>
                        s === 'US' ? (
                            <Chip key={s} avatar={<Avatar>{s}</Avatar>} label="Federal" />
                        ) : (
                            <Chip key={s} avatar={<Avatar>{s}</Avatar>} label={states[s]} />
                        )
                    )}
                </div>
                <div className="tw-flex tw-gap-2 child:tw-basis-1/2">
                    <LoadingButton
                        size="large"
                        loading={documentFetching && downloadUrl === signatureRequest?.original_document_url}
                        type="button"
                        onClick={() => setDownloadUrl(signatureRequest.original_document_url)}>
                        View original document
                    </LoadingButton>
                    <LoadingButton
                        size="large"
                        color="success"
                        disabled={!signatureRequest?.signed_document_url}
                        loading={documentFetching && downloadUrl === signatureRequest?.signed_document_url}
                        type="button"
                        onClick={() => setDownloadUrl(signatureRequest.signed_document_url)}>
                        View signed document
                    </LoadingButton>
                </div>
            </Paper>
        </div>
    );
};

export default TaxReturnSignatureRequestDetail;
// Code from file collective-frontend/src/modules/Hub/TaxReturnSignatureRequests/TaxReturnSignatureRequestDetail.test.js
import { render, screen } from '@testing-library/react';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter';
import { Route, MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@mui/material';

import TaxReturnSignatureRequestDetail from './TaxReturnSignatureRequestDetail';

import theme from 'theme';
import collectiveApi from 'modules/common/collectiveApi';

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            retry: false,
        },
    },
});

it('should render successfully', async () => {
    const sigReqId = 'abc-123';
    const httpMock = new MockAdapter(collectiveApi);
    httpMock.onGet(`tax_organizer/signature-requests/${sigReqId}/`).reply(200, {
        member_id: 1,
        tax_year: 2022,
        tax_return_type: 'BTR',
        states: ['US', 'CA'],
        original_document_url: '/incompleteDocLink',
        signed_document_url: '/completeDocLink',
    });
    httpMock
        .onGet(`members/1/`)
        .reply(200, { fullname: 'Bob Johansson', email: 'bob@bobsfishmart.com', businessname: 'Bobs Fish Mart' });

    const { asFragment } = render(
        <MemoryRouter initialEntries={[`/hub/tax-return-signature-requests/${sigReqId}`]}>
            <QueryClientProvider client={queryClient}>
                <ThemeProvider theme={theme}>
                    <Route
                        exact
                        path="/hub/tax-return-signature-requests/:requestId"
                        component={TaxReturnSignatureRequestDetail}
                    />
                </ThemeProvider>
            </QueryClientProvider>
        </MemoryRouter>
    );

    expect(await screen.findByText('Bob Johansson')).toBeInTheDocument();
    expect(httpMock.history.get.length).toBe(2);
    expect(asFragment()).toMatchSnapshot();
});
// Code from file collective-frontend/src/modules/Hub/TaxReturnSignatureRequests/TaxReturnSignatureRequestSearch.test.js
import { render, screen } from '@testing-library/react';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { Route, MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '@mui/material';

import TaxReturnSignatureRequestSearch from './TaxReturnSignatureRequestSearch';

import theme from 'theme';

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            retry: false,
        },
    },
});

describe('TaxReturnSignatureRequestSearch', () => {
    it('should render successfully', async () => {
        const sigReqId = 'abc-123';

        const { asFragment } = render(
            <MemoryRouter initialEntries={[`/hub/tax-return-signature-requests/`]}>
                <QueryClientProvider client={queryClient}>
                    <ThemeProvider theme={theme}>
                        <Route
                            exact
                            path="/hub/tax-return-signature-requests/"
                            component={TaxReturnSignatureRequestSearch}
                        />
                    </ThemeProvider>
                </QueryClientProvider>
            </MemoryRouter>
        );

        expect(asFragment()).toMatchSnapshot();
    });

    it('should render successfully with a search query', async () => {
        const { asFragment } = render(
            <MemoryRouter initialEntries={[`/hub/tax-return-signature-requests`]}>
                <QueryClientProvider client={queryClient}>
                    <ThemeProvider theme={theme}>
                        <Route exact path="/hub/tax-return-signature-requests">
                            <TaxReturnSignatureRequestSearch memberId={1} haveSignatureRequest={false} />
                        </Route>
                    </ThemeProvider>
                </QueryClientProvider>
            </MemoryRouter>
        );

        expect(asFragment()).toMatchSnapshot();
        expect(await screen.findByText('No signature request found for this member')).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/TaxReturnSignatureRequests/TaxReturnSignatureRequestSearch.jsx
import PropTypes from 'prop-types';
import { Button, Typography, Alert } from '@mui/material';
import { Formik, Form } from 'formik';
import { useQuery } from '@tanstack/react-query';
import { useHistory } from 'react-router-dom';
import { useState } from 'react';

import MemberSearch from '../TaxReturnUploader/MemberSearch';

import { getSignatureRequestByMemberId } from './requests';

const TaxReturnSignatureRequestSearch = (props) => {
    const history = useHistory();
    const [memberId, setMemberId] = useState(props.memberId || null);
    const [haveSignatureRequest, setHaveSignatureRequest] = useState(props.haveSignatureRequest);

    useQuery(['signatureRequests', { memberId }], getSignatureRequestByMemberId, {
        enabled: Boolean(memberId),
        onSuccess: (data) => {
            if (data?.results?.at(0)?.uuid) {
                history.push(`/hub/tax-return-signature-requests/${data?.results[0].uuid}`);
                return;
            }
            setHaveSignatureRequest(false);
        },
    });

    return (
        <div className="tw-max-w-full tw-min-w-min tw-m-auto">
            <div className="tw-flex tw-justify-between tw-items-start">
                <Typography className="tw-mb-4" variant="h1">
                    Tax Return Signature Requests
                </Typography>
                <Button onClick={() => history.push('/hub/tax-return-signature-requests/create')}>Create</Button>
            </div>

            {/* I know I'm not using the form, MemberSearch is wired for formik, just go with it */}
            <Formik initialValues={{ member: null }}>
                {() => (
                    <Form className="tw-flex tw-flex-col child:tw-mb-2">
                        <MemberSearch
                            data-testid="member-search-input"
                            name="member"
                            onChange={(value) => setMemberId(value?.id)}
                        />
                        {haveSignatureRequest === false && memberId && (
                            <Alert severity="info">No signature request found for this member</Alert>
                        )}
                    </Form>
                )}
            </Formik>
        </div>
    );
};

TaxReturnSignatureRequestSearch.propTypes = {
    memberId: PropTypes.number,
    haveSignatureRequest: PropTypes.bool,
};

export default TaxReturnSignatureRequestSearch;
// Code from file collective-frontend/src/modules/Hub/PayrollSetup/Page.js
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';

import TransitionNavBar from '../common/TransitionNavBar';

import useRootStore from 'modules/common/stores/Root/useRootStore';
import PayrollSetup from 'modules/Dashboard/Payroll/Setup/Page';

const PayrollSetupPage = () => {
    const { legacyStore } = useRootStore();
    const { id } = useParams();

    const [memberId, setMemberId] = useState(null);

    const fetchClientInfo = useCallback(async (sfaccountid) => {
        try {
            await legacyStore.getTransitionClientInfo(sfaccountid);
            setMemberId(legacyStore.transitionClientInfo.member_id);
        } catch (error) {
            console.error(error);
        }
    }, []);

    useEffect(() => {
        fetchClientInfo(id);
    }, []);

    if (!memberId) {
        return null;
    }

    return (
        <>
            <TransitionNavBar
                clientName={legacyStore.transitionClientInfo.fullname}
                sfid={legacyStore.transitionClientInfo.sf_id}
                tpid={legacyStore.transitionClientInfo.transition_plan_id}
                clientCompany={legacyStore.transitionClientInfo.business_name}
                payrollSetup
            />
            <PayrollSetup memberId={memberId} />
        </>
    );
};

export default PayrollSetupPage;
// Code from file collective-frontend/src/modules/Hub/Payments/Page.test.jsx
import { render } from '@testing-library/react';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { Router } from 'react-router-dom';
import MockAxiosAdapter from 'axios-mock-adapter';

import PaymentsPage from './Page';

import browserHistory from 'modules/common/browserHistory';
import RootStoreContext from 'modules/common/stores/Root/Context';
import collectiveApi from 'modules/common/collectiveApi';

const history = browserHistory;
const mockCollectiveApi = new MockAxiosAdapter(collectiveApi);

describe('Payments Home', () => {
    it('should render correctly', () => {
        const legacyStore = {
            transitionClientInfo: {
                fullname: '',
                sf_id: '',
                transition_plan_id: '',
                business_name: '',
            },
            transactionInfo: {
                entity_name: '',
            },
            getSplitTreatmentAttributes: jest.fn(),
        };
        const queryClient = new QueryClient({
            defaultOptions: {
                queries: {
                    retry: false,
                },
            },
        });
        const match = {
            params: {
                stripeCustomerId: 1,
            },
        };
        const { asFragment } = render(
            <Router history={history}>
                <RootStoreContext.Provider value={{ legacyStore }}>
                    <QueryClientProvider client={queryClient}>
                        <PaymentsPage match={match} />
                    </QueryClientProvider>
                </RootStoreContext.Provider>
            </Router>
        );
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/Payments/InvoicesTableRow.jsx
import { useEffect, useMemo, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import moment from 'moment';
import { CircularProgress, Typography } from '@mui/material';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableHead from '@mui/material/TableHead';
import TableCell from '@mui/material/TableCell';
import IconButton from '@mui/material/IconButton';
import TableRow from '@mui/material/TableRow';
import Box from '@mui/material/Box';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Collapse from '@mui/material/Collapse';
import LinearProgress from '@mui/material/LinearProgress';
import DownloadForOfflineIcon from '@mui/icons-material/DownloadForOffline';
import Link from '@mui/material/Link';

import InvoicesTableRowDetail from './InvoicesTableRowDetail';
import { useCreditNotes } from './query-hooks';

import { moneyWithCommas } from 'modules/common/display';

const InvoicesTableRow = ({ rowData, isStaleCreditNotes, setIsStaleCreditNotes }) => {
    const [open, setOpen] = useState(false);

    const {
        isLoading: creditNotesIsLoading,
        data: creditNotesData,
        refetch: refetchCreditNotes,
        isFetching: creditNotesIsFetching,
    } = useCreditNotes(rowData.invoice);

    useEffect(() => {
        if (rowData.invoice && isStaleCreditNotes) {
            refetchCreditNotes();
            setIsStaleCreditNotes(false);
        }
    }, [rowData.invoice, isStaleCreditNotes]);

    const toggleDetails = () => {
        if (!creditNotesIsLoading) {
            setOpen(!open);
        }
    };

    const lineItems = useMemo(() => {
        if (Boolean(rowData) && Boolean(creditNotesData)) {
            return rowData.lines.map((line) => {
                const creditNote = creditNotesData.find((note) => note.invoice_line_item_unique_id === line.unique_id);
                return {
                    ...line,
                    ...creditNote,
                };
            });
        }
        return [];
    }, [rowData, creditNotesData]);

    const amountCredited = useMemo(() => {
        if (creditNotesData) {
            return creditNotesData.reduce((sum, creditNote) => {
                const updatedSum = sum + creditNote.amount;
                return updatedSum;
            }, 0);
        }
        return 0;
    }, [creditNotesData]);

    return (
        <>
            <TableRow sx={{ '& > *': { borderBottom: 'unset' } }} onClick={toggleDetails}>
                <TableCell>
                    <IconButton aria-label="expand row" size="large" disabled={creditNotesIsLoading}>
                        {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
                    </IconButton>
                </TableCell>
                <TableCell component="th" scope="row">
                    <Typography>{moneyWithCommas(rowData.amount / 100)}</Typography>
                </TableCell>
                <TableCell align="right">
                    <Typography>
                        &minus; {rowData.charges ? moneyWithCommas(rowData.charges[0].amount_refunded / 100) : 0}
                    </Typography>
                </TableCell>
                <TableCell align="right">
                    <Typography>&minus; {moneyWithCommas(amountCredited / 100)}</Typography>
                </TableCell>
                <TableCell align="right">
                    <Typography>{rowData.description}</Typography>
                </TableCell>
                <TableCell align="right">
                    <Typography>{moment.unix(rowData.created).format('YYYY-MM-DD')}</Typography>
                </TableCell>
                <TableCell align="right">
                    <Typography>{rowData.status === 'succeeded' ? 'Paid' : 'Not Paid'}</Typography>
                </TableCell>
            </TableRow>
            <TableRow>
                <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={8}>
                    {creditNotesIsLoading ? (
                        <LinearProgress />
                    ) : (
                        <Collapse in={open} timeout="auto" unmountOnExit>
                            <Box className="tw-mt-5">
                                <Typography className="tw-mb-3" variant="h6" gutterBottom>
                                    <div className="tw-flex tw-items-baseline tw-justify-start">
                                        <Typography fontWeight="bold" fontSize={20} className="tw-ml-2">
                                            Invoice #{rowData.number}
                                        </Typography>
                                        <div className="tw-self-end">
                                            <Link
                                                href={rowData.hosted_invoice_url}
                                                rel="noopener noreferrer"
                                                target="_blank"
                                                className="tw-ml-2">
                                                <DownloadForOfflineIcon fontSize="large" />
                                            </Link>
                                        </div>
                                    </div>
                                </Typography>

                                <Table size="medium" aria-label="purchases">
                                    <TableHead>
                                        <TableRow>
                                            <TableCell>
                                                <Typography fontWeight="bold">Amount</Typography>
                                            </TableCell>
                                            <TableCell>
                                                <Typography fontWeight="bold">Description</Typography>
                                            </TableCell>
                                            <TableCell align="right">
                                                <Typography fontWeight="bold">Quantity</Typography>
                                            </TableCell>
                                            <TableCell align="right">
                                                <Typography fontWeight="bold">Status</Typography>
                                            </TableCell>
                                        </TableRow>
                                    </TableHead>
                                    <TableBody>
                                        {lineItems.map((lineDetail) => (
                                            <InvoicesTableRowDetail key={lineDetail.id} detailData={lineDetail} />
                                        ))}
                                    </TableBody>
                                </Table>
                            </Box>
                        </Collapse>
                    )}
                </TableCell>
            </TableRow>
        </>
    );
};
export default InvoicesTableRow;
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useCreateRefundRequest.js
import { useMutation, useQueryClient } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const postRefundRequest = (postObject) =>
    collectiveApi.post(`/stripe/refund_requests/`, postObject).then((response) => response.data);

export default function useCreateRefundRequest(postObject) {
    const queryClient = useQueryClient();

    return useMutation((postObject) => postRefundRequest(postObject), {
        onSuccess: (data) => queryClient.setQueryData(['refundRequest', data], data),
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/usePayments.js
import { useQuery } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const fetchPayments = (stripeCustomerId) =>
    collectiveApi
        .get(`/stripe/payment_intents/?customer=${stripeCustomerId}&limit=100`)
        .then((response) => response.data);

export default function usePayments(stripeCustomerId) {
    return useQuery(['payments', stripeCustomerId], () => fetchPayments(stripeCustomerId), {
        enabled: false,
        staleTime: Infinity,
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useCustomer.js
import { useQuery } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const fetchCustomer = (stripeCustomerId) =>
    collectiveApi.get(`/stripe/customers/${stripeCustomerId}/`).then((response) => response.data);

export default function useCustomer(stripeCustomerId) {
    return useQuery(['customer', stripeCustomerId], () => fetchCustomer(stripeCustomerId), {
        enabled: false,
        staleTime: Infinity,
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useCreateCreditRequest.js
import { useMutation, useQueryClient } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const postCreditRequest = (postObject) =>
    collectiveApi.post(`/stripe/credit_requests/`, postObject).then((response) => response.data);

export default function useCreateCreditRequest() {
    const queryClient = useQueryClient();

    return useMutation((postObject) => postCreditRequest(postObject), {
        onSuccess: (data) => queryClient.setQueryData(['creditRequest', data], data),
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useSubscriptions.js
import { useQuery } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const fetchSubscriptions = (stripeCustomerId) =>
    collectiveApi.get(`/stripe/subscriptions/?customer=${stripeCustomerId}`).then((response) => response.data);

export default function useSubscriptions(stripeCustomerId) {
    return useQuery(['subscriptions', stripeCustomerId], () => fetchSubscriptions(stripeCustomerId), {
        enabled: false,
        staleTime: Infinity,
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useCreateCreditNoteRequest.js
import { useMutation, useQueryClient } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const postCreditNoteRequest = (postObject) =>
    collectiveApi.post(`/stripe/credit_note_requests/`, postObject).then((response) => response.data);

export default function useCreateCreditRequest(postObject) {
    const queryClient = useQueryClient();

    return useMutation((postObject) => postCreditNoteRequest(postObject), {
        onSuccess: (data) => queryClient.setQueryData(['creditNoteRequest', data], data),
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useCreditNotes.js
import { useQuery } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const fetchCreditNotes = (invoiceId) =>
    collectiveApi.get(`/stripe/credit_notes/?invoice=${invoiceId}`).then((response) => response.data.data);

export default function useCreditNotes(invoiceId) {
    return useQuery(['creditNotes', invoiceId], () => fetchCreditNotes(invoiceId), {
        enabled: Boolean(invoiceId),
        staleTime: Infinity,
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/index.js
export { default as useCreateCreditNoteRequest } from './useCreateCreditNoteRequest';
export { default as useCreateCreditRequest } from './useCreateCreditRequest';
export { default as useCreateRefundRequest } from './useCreateRefundRequest';
export { default as useCreditNotes } from './useCreditNotes';
export { default as useCustomer } from './useCustomer';
export { default as useInvoice } from './useInvoice';
export { default as useInvoices } from './useInvoices';
export { default as usePayments } from './usePayments';
export { default as useSubscriptions } from './useSubscriptions';
export { default as useToggleSubscription } from './useToggleSubscription';
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useInvoice.js
import { useQuery } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const fetchInvoice = (invoiceId) =>
    collectiveApi.get(`/stripe/invoices/${invoiceId}/`).then((response) => response.data);

export default function useInvoice(invoiceId) {
    return useQuery(['invoice', invoiceId], () => fetchInvoice(invoiceId), {
        enabled: false,
        staleTime: Infinity,
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useInvoices.js
import { useQuery } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const fetchInvoices = (stripeCustomerId) =>
    collectiveApi.get(`/stripe/invoices/?customer=${stripeCustomerId}&limit=100`).then((response) => response.data);

export default function useInvoices(stripeCustomerId) {
    return useQuery(['invoices', stripeCustomerId], () => fetchInvoices(stripeCustomerId), {
        enabled: false,
        staleTime: Infinity,
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/query-hooks/useToggleSubscription.js
import { useMutation, useQueryClient } from '@tanstack/react-query';

import collectiveApi from 'modules/common/collectiveApi';

const toggleSubscription = (putObject) =>
    collectiveApi
        .put(`/stripe/subscriptions/${putObject.subscriptionId}/${putObject.verb}/`)
        .then((response) => response.data);

export default function useToggleSubscription(putObject) {
    const queryClient = useQueryClient();

    return useMutation((putObject) => toggleSubscription(putObject), {
        onSuccess: (data) => queryClient.setQueryData(['toggleSubscription', data], data),
    });
}
// Code from file collective-frontend/src/modules/Hub/Payments/Page.jsx
import { useEffect, useState } from 'react';
import { Typography, LinearProgress, Alert } from '@mui/material';
import { useQuery } from '@tanstack/react-query';

import TransitionNavBar from '../common/TransitionNavBar';

import CustomerPanel from './CustomerPanel';
import InvoicesTable from './InvoicesTable';
import ActionsPanel from './ActionsPanel';
import { useCustomer, useInvoices, usePayments } from './query-hooks';

import useRootStore from 'modules/common/stores/Root/useRootStore';

const PaymentsPage = (props) => {
    const { legacyStore } = useRootStore();
    const { stripeCustomerId } = props.match.params;
    const [update, setUpdate] = useState(false);
    const [isStaleCreditNotes, setIsStaleCreditNotes] = useState(false);
    const [isStaleCreditNoteAction, setIsStaleCreditNoteAction] = useState(false);

    const {
        isError: customerIsError,
        isLoading: customerIsLoading,
        data: customerData,
        refetch: refetchCustomer,
        isFetching: customerIsFetching,
    } = useCustomer(stripeCustomerId);
    const {
        isError: paymentsIsError,
        isLoading: paymentsIsLoading,
        data: paymentData,
        refetch: refetchPayments,
        isFetching: paymentsIsFetching,
    } = usePayments(stripeCustomerId);
    const {
        isError: invoicesIsError,
        isLoading: invoicesIsLoading,
        data: invoicesData,
        refetch: refetchInvoices,
        isFetching: invoicesIsFetching,
    } = useInvoices(stripeCustomerId);

    const toggleUpdate = () => {
        setUpdate(!update);
        refetchCustomer();
        refetchPayments();
        refetchInvoices();
        setIsStaleCreditNotes(true);
        setIsStaleCreditNoteAction(true);
    };

    useEffect(() => {
        if (stripeCustomerId !== '---') {
            toggleUpdate();
        }
    }, []);

    if (stripeCustomerId === '---' || customerIsError || paymentsIsError || invoicesIsError) {
        return (
            <div className="tw-flex tw-flex-col">
                <TransitionNavBar
                    clientName={legacyStore.transitionClientInfo.fullname}
                    payments
                    sfid={legacyStore.transitionClientInfo.sf_id}
                    tpid={legacyStore.transitionClientInfo.transition_plan_id}
                    clientCompany={legacyStore.transactionInfo.entity_name}
                />
                <div className="tw-w-full tw-mt-4">
                    <Alert severity="error">Failed to load member's data from Stripe</Alert>
                </div>
            </div>
        );
    }

    const customer = customerIsLoading
        ? { subscriptions: [{ plan: { nickname: '' }, start_date: '1900-01-01' }], balance: 0, email: '' }
        : customerData;
    const payments = paymentsIsLoading ? [] : paymentData.data;
    const invoices = invoicesIsLoading ? [] : invoicesData.data;

    return (
        <div className="tw-flex tw-flex-col">
            {(customerIsLoading || customerIsFetching || paymentsIsFetching || paymentsIsLoading) && <LinearProgress />}
            <TransitionNavBar
                clientName={legacyStore.transitionClientInfo.fullname}
                payments
                sfid={legacyStore.transitionClientInfo.sf_id}
                tpid={legacyStore.transitionClientInfo.transition_plan_id}
                clientCompany={legacyStore.transactionInfo.entity_name}
            />
            <div className="tw-w-full tw-mt-4">
                <div className="tw-grid tw-grid-flow-col tw-auto-cols-2">
                    <div className="">
                        <Typography variant="h2">Subscription</Typography>
                        <CustomerPanel customer={customer} />
                    </div>
                    <div className="tw-space-y-2">
                        <Typography variant="h2">Actions</Typography>
                        <ActionsPanel
                            update={update}
                            triggerUpdate={toggleUpdate}
                            customer={customer}
                            payments={payments}
                            invoices={invoices}
                            isStaleCreditNoteAction={isStaleCreditNoteAction}
                            setIsStaleCreditNoteAction={setIsStaleCreditNoteAction}
                        />
                    </div>
                </div>
            </div>
            <div className="tw-flex tw-space-y-2 tw-mt-2 tw-flex-col">
                <Typography variant="h2">Invoices</Typography>
                {payments && invoices && (
                    <InvoicesTable
                        invoices={invoices}
                        payments={payments}
                        stripeCustomerId={stripeCustomerId}
                        isStaleCreditNotes={isStaleCreditNotes}
                        setIsStaleCreditNotes={setIsStaleCreditNotes}
                    />
                )}
            </div>
        </div>
    );
};

export default PaymentsPage;
// Code from file collective-frontend/src/modules/Hub/Payments/CustomerPanel.jsx
import { Typography, Paper } from '@mui/material';
import moment from 'moment';

import { moneyWithCommas } from 'modules/common/display';

const CustomerPanel = ({ customer }) => {
    const subscription = customer.subscriptions[0];
    const display_balance =
        customer.balance < 0
            ? `(${moneyWithCommas((customer.balance * -1) / 100)})`
            : moneyWithCommas(customer.balance / 100);
    const status = subscription.pause_collection ? `paused (${subscription.pause_collection.behavior})` : 'active';
    return (
        <div className="tw-mt-2 tw-mb-2 tw-pr-4">
            <Paper elevation={2}>
                <div className="tw-p-2 tw-flex-row">
                    <Typography variant="body1">Balance: {display_balance}</Typography>
                    <Typography variant="body1">Type: {subscription.plan.nickname}</Typography>
                    <Typography variant="body1">Status: {status}</Typography>
                    <Typography variant="body1">Email: {customer.email}</Typography>
                    <Typography variant="body1">
                        Started: {moment.unix(subscription.start_date).format('YYYY-MM-DD')}
                    </Typography>
                </div>
            </Paper>
        </div>
    );
};
export default CustomerPanel;
// Code from file collective-frontend/src/modules/Hub/Payments/Action.jsx
import { useState } from 'react';
import { Button, Typography } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';

const Action = ({ verb, title, children, formik, disabled = false }) => {
    const [dialogOpen, setDialogOpen] = useState(false);

    const handleSubmit = (e) => {
        e.preventDefault();
        formik.handleSubmit();
        if (formik.isValid) {
            setDialogOpen(false);
        }
    };
    return (
        <>
            <Button size="large" onClick={() => setDialogOpen(true)}>
                <Typography>{verb}</Typography>
            </Button>
            <Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} fullWidth>
                <form onSubmit={handleSubmit}>
                    <DialogTitle>{title}</DialogTitle>
                    <DialogContent>{children}</DialogContent>
                    <DialogActions className="tw-p-0 tw-pb-3 tw-pr-3">
                        <Button type="submit" disabled={disabled || formik.isSubmitting}>
                            <Typography>Submit</Typography>
                        </Button>
                        <Button onClick={() => setDialogOpen(false)} variant="outlined">
                            <Typography>Close</Typography>
                        </Button>
                    </DialogActions>
                </form>
            </Dialog>
        </>
    );
};

export default Action;
// Code from file collective-frontend/src/modules/Hub/Payments/InvoicesTableRowDetail.jsx
import { Typography } from '@mui/material';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';

import { moneyWithCommas } from 'modules/common/display';

const InvoicesTableRowDetail = ({ detailData }) => {
    const status = detailData.status === 'issued' ? 'Credited' : 'Paid';
    return (
        <TableRow key={detailData.id}>
            <TableCell component="th" scope="row">
                <Typography>{detailData ? moneyWithCommas(detailData.amount / 100) : '$0'}</Typography>
            </TableCell>
            <TableCell>
                <Typography>{detailData.description}</Typography>
            </TableCell>
            <TableCell align="right">
                <Typography>{detailData.quantity}</Typography>
            </TableCell>
            <TableCell align="right">
                <Typography>{status}</Typography>
            </TableCell>
        </TableRow>
    );
};

export default InvoicesTableRowDetail;
// Code from file collective-frontend/src/modules/Hub/Payments/SubscriptionAction.jsx
import DialogContentText from '@mui/material/DialogContentText';
import { useFormik } from 'formik';

import Action from './Action';
import { useToggleSubscription } from './query-hooks';

const SubscriptionAction = ({ triggerUpdate, customer }) => {
    const subscription = customer.subscriptions[0];
    const verb = subscription.pause_collection ? 'Resume' : 'Pause';

    const { mutateAsync: toggleSubscription } = useToggleSubscription();

    const onSubmit = async (values) => {
        const putObject = {
            subscriptionId: subscription.id,
            verb: verb.toLowerCase(),
        };
        await toggleSubscription(putObject);
        triggerUpdate();
    };
    const formik = useFormik({
        initialValues: {},
        onSubmit,
    });
    return (
        <Action title={`${verb} a Subscription`} verb={verb} formik={formik}>
            <DialogContentText>Are you sure you want to change the subscription?</DialogContentText>
        </Action>
    );
};

export default SubscriptionAction;
// Code from file collective-frontend/src/modules/Hub/Payments/CreditAction.jsx
import { DialogContentText, TextField, Box } from '@mui/material';
import { useFormik } from 'formik';
import { number, object, string } from 'yup';

import Action from './Action';
import { useCreateCreditRequest } from './query-hooks';

const validationSchema = object({
    creditAmount: number().min(1).required('must credit at least 1 dollar'),
    reason: string().required('Please enter a reason'),
});

const CreditAction = ({ triggerUpdate, stripeCustomerId }) => {
    const { mutateAsync: createCreditRequest } = useCreateCreditRequest();

    const onSubmit = async (values) => {
        const postObject = {
            customer: stripeCustomerId,
            currency: 'usd',
            description: values.reason,
            amount: -1 * (values.creditAmount * 100),
        };
        await createCreditRequest(postObject);
        resetForm();
        triggerUpdate();
    };
    const formik = useFormik({
        initialValues: {
            creditAmount: 0,
            reason: '',
        },
        validationSchema,
        onSubmit,
        validateOnMount: true,
    });
    const { values, errors, touched, handleChange, resetForm } = formik;
    return (
        <Action title="Credit a Member" verb="Credit" formik={formik}>
            <form>
                <DialogContentText>Set the credit amount in dollars.</DialogContentText>
                <Box noValidate className="tw-mb-2">
                    <TextField
                        error={touched.creditAmount && Boolean(errors.creditAmount)}
                        type="number"
                        id="creditAmount"
                        name="creditAmount"
                        value={values.creditAmount}
                        onChange={handleChange}
                        fullWidth
                        helperText={touched.creditAmount && errors.creditAmount}
                    />
                </Box>
                <DialogContentText>Reason for credit</DialogContentText>
                <Box noValidate className="tw-mb-2">
                    <TextField
                        error={touched.reason && Boolean(errors.reason)}
                        multiline
                        rows={5}
                        id="reason"
                        name="reason"
                        type="text"
                        value={values.reason}
                        onChange={handleChange}
                        fullWidth
                        helperText={touched.reason && errors.reason}
                    />
                </Box>
            </form>
        </Action>
    );
};

export default CreditAction;
// Code from file collective-frontend/src/modules/Hub/Payments/InvoicesTable.jsx
import { useMemo } from 'react';
import LinearProgress from '@mui/material/LinearProgress';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';

import InvoicesTableRow from './InvoicesTableRow';

const InvoicesTable = ({ invoices, payments, isStaleCreditNotes, setIsStaleCreditNotes }) => {
    const rowDataArray = useMemo(() => {
        if (invoices.length !== 0 && payments.length !== 0) {
            return invoices
                .map((invoice) => {
                    const payment = payments.find((payment) => payment.invoice === invoice.id);
                    if (payment) {
                        return {
                            ...invoice,
                            ...payment,
                        };
                    }
                    return undefined;
                })
                .filter((element) => element !== undefined);
        }
    }, [invoices, payments]);

    console.log('rowDataArray', rowDataArray);

    if (invoices.length === 0 && payments.length === 0) {
        return <LinearProgress />;
    }

    return (
        <TableContainer component={Paper} className="tw-ml-0 tw-pl-3 tw-pr-3">
            <Table aria-label="collapsible table">
                <TableHead>
                    <TableRow>
                        <TableCell />
                        <TableCell>
                            <Typography fontWeight="bold">Amount</Typography>
                        </TableCell>
                        <TableCell align="right">
                            <Typography fontWeight="bold">Amount Refunded</Typography>
                        </TableCell>
                        <TableCell align="right">
                            <Typography fontWeight="bold">Amount Credited</Typography>
                        </TableCell>
                        <TableCell align="right">
                            <Typography fontWeight="bold">Description</Typography>
                        </TableCell>
                        <TableCell align="right">
                            <Typography fontWeight="bold">Date</Typography>
                        </TableCell>
                        <TableCell align="right">
                            <Typography fontWeight="bold">Status</Typography>
                        </TableCell>
                    </TableRow>
                </TableHead>
                <TableBody>
                    {rowDataArray &&
                        rowDataArray.map((rowData) => (
                            <InvoicesTableRow
                                key={rowData.created}
                                rowData={rowData}
                                isStaleCreditNotes={isStaleCreditNotes}
                                setIsStaleCreditNotes={setIsStaleCreditNotes}
                            />
                        ))}
                </TableBody>
            </Table>
        </TableContainer>
    );
};
export default InvoicesTable;
// Code from file collective-frontend/src/modules/Hub/Payments/CreditNoteAction.jsx
import { DialogContentText, TextField, Box, Select, MenuItem, Alert, Typography, FormHelperText } from '@mui/material';
import { useFormik } from 'formik';
import { useEffect, useMemo, useState } from 'react';
import { object, string } from 'yup';
import moment from 'moment';

import Action from './Action';
import { useCreateCreditNoteRequest, useCreditNotes } from './query-hooks';

import { moneyWithCommas } from 'modules/common/display';

const validationSchema = object({
    selectedInvoice: string().required('Please select an invoice'),
    selectedInvoiceItem: string().required('Please select an item to refund'),
    reasonStripe: string().required('Please select a reason'),
    memo: string().required('Please add a memo for the customer'),
});

const CreditNoteAction = ({
    triggerUpdate,
    stripeCustomerId,
    invoices,
    isStaleCreditNoteAction,
    setIsStaleCreditNoteAction,
}) => {
    const [invoiceId, setInvoiceId] = useState(null);

    const { mutateAsync: createCreditNoteRequest } = useCreateCreditNoteRequest();

    const invoice = useMemo(() => invoices.find((invoice) => invoice.id === invoiceId), [invoiceId]);

    const {
        isLoading: creditNotesIsLoading,
        data: creditNotesData,
        refetch: refetchCreditNotes,
        isFetching: creditNotesIsFetching,
    } = useCreditNotes(invoiceId);

    useEffect(() => {
        if (invoiceId && isStaleCreditNoteAction) {
            refetchCreditNotes();
            setIsStaleCreditNoteAction(false);
        }
    }, [invoiceId, isStaleCreditNoteAction]);

    const lineItems = useMemo(() => {
        if (invoice && creditNotesData) {
            return invoice.lines.map((line) => {
                const creditNote = creditNotesData.find((note) => note.invoice_line_item_unique_id === line.unique_id);

                return {
                    ...line,
                    ...creditNote,
                };
            });
        }
    }, [invoice, creditNotesData]);

    const onSubmit = async (values) => {
        const invoiceItem = invoice.lines.find((item) => item.unique_id === values.selectedInvoiceItem);
        const postObject = {
            amount: -1 * (invoiceItem.amount / 100),
            currency: invoiceItem.currency,
            customer: stripeCustomerId,
            description: invoiceItem.description,
            invoice: values.selectedInvoice,
            invoice_line_item: values.selectedInvoiceItem,
            reason_stripe: values.reasonStripe,
            memo: values.memo,
        };

        await createCreditNoteRequest(postObject);
        resetForm();
        triggerUpdate();
    };
    const formik = useFormik({
        initialValues: {
            selectedInvoice: '',
            selectedInvoiceItem: '',
            reasonStripe: '',
            memo: '',
        },
        validationSchema,
        onSubmit,
    });
    const { values, errors, touched, handleChange, setFieldValue, resetForm } = formik;
    const handleSelectChange = (event) => {
        setFieldValue(event.target.name, event.target.value);
    };

    const filteredLineItems = lineItems ? lineItems.filter((item) => item.status !== 'issued') : [];
    const noMoreCreditNotesAvailable = filteredLineItems ? filteredLineItems.length === 0 : false;

    if (!invoices) {
        return null;
    }

    return (
        <Action title="Create a credit note" verb="Credit note" formik={formik}>
            <DialogContentText>Select the invoice to refund</DialogContentText>
            <Box noValidate className="tw-mb-2">
                <Select
                    autoFocus
                    fullWidth
                    value={values.selectedInvoice}
                    error={touched.selectedInvoice && Boolean(errors.selectedInvoice)}
                    helperText={<p className="error">{errors.selectedInvoice}</p>}
                    onChange={(event) => {
                        handleSelectChange(event);
                        setInvoiceId(event.target.value);
                    }}
                    inputProps={{
                        name: 'selectedInvoice',
                        id: 'selectedInvoice',
                    }}>
                    {invoices.map((invoice) => (
                        <MenuItem value={invoice.id} key={invoice.id}>
                            Invoice #{invoice.number} - {moneyWithCommas(invoice.amount_paid / 100)} -{' '}
                            {moment.unix(invoice.created).format('MM-DD-YYYY')}
                        </MenuItem>
                    ))}
                </Select>
            </Box>
            {creditNotesData && (
                <>
                    {noMoreCreditNotesAvailable && (
                        <Alert className="tw-mb-2" severity="warning">
                            <Typography>There are no more credit notes left to create from this invoice</Typography>
                        </Alert>
                    )}
                    <DialogContentText>
                        Select the invoice item you'd like to credit back to the member
                    </DialogContentText>
                    <Box noValidate className="tw-mb-2">
                        <Select
                            autoFocus
                            fullWidth
                            disabled={noMoreCreditNotesAvailable}
                            value={values.selectedInvoiceItem}
                            error={touched.selectedInvoiceItem && Boolean(errors.selectedInvoiceItem)}
                            helperText={<p className="error">{errors.selectedInvoiceItem}</p>}
                            onChange={handleSelectChange}
                            inputProps={{
                                name: 'selectedInvoiceItem',
                                id: 'selectedInvoiceItem',
                            }}>
                            {filteredLineItems.map((item) => (
                                <MenuItem value={item.unique_id} key={item.unique_id}>
                                    {item.description} - {moneyWithCommas(item.amount / 100)}
                                </MenuItem>
                            ))}
                        </Select>
                    </Box>
                    <DialogContentText>Reason for issuing this credit note</DialogContentText>
                    <Box noValidate className="tw-mb-2">
                        <Select
                            autoFocus
                            fullWidth
                            disabled={noMoreCreditNotesAvailable}
                            value={values.reasonStripe}
                            error={touched.reasonStripe && Boolean(errors.reasonStripe)}
                            helperText={<p className="error">{errors.reasonStripe}</p>}
                            onChange={handleSelectChange}
                            inputProps={{
                                name: 'reasonStripe',
                                id: 'reasonStripe',
                            }}>
                            <MenuItem value="duplicate" key="duplicate">
                                Duplicate
                            </MenuItem>
                            <MenuItem value="fraudulent" key="fraudulent">
                                Fraudulent
                            </MenuItem>
                            <MenuItem value="order_change" key="order_change">
                                Order Change
                            </MenuItem>
                            <MenuItem value="product_unsatisfactory" key="product_unsatisfactory">
                                Product Unsatisfactory
                            </MenuItem>
                        </Select>
                    </Box>
                    <DialogContentText>The credit note's memo appears on the credit note PDF</DialogContentText>
                    <Box noValidate className="tw-mb-2">
                        <TextField
                            disabled={noMoreCreditNotesAvailable}
                            error={touched.memo && Boolean(errors.memo)}
                            helperText={<p className="error">{errors.memo}</p>}
                            value={values.memo}
                            multiline
                            id="memo"
                            name="memo"
                            rows={5}
                            type="text"
                            onChange={handleChange}
                            fullWidth
                        />
                    </Box>
                </>
            )}
        </Action>
    );
};

export default CreditNoteAction;
// Code from file collective-frontend/src/modules/Hub/Payments/RefundAction.jsx
import { DialogContentText, TextField, Box, Select, MenuItem } from '@mui/material';
import { useFormik } from 'formik';
import { number, object, string } from 'yup';
import moment from 'moment';

import { useCreateRefundRequest } from './query-hooks';
import Action from './Action';

import { moneyWithCommas } from 'modules/common/display';

const validationSchema = object({
    refundAmount: number().min(1).required('must refund at least 1 dollar'),
    reasonInternal: string().required('Please select a reason'),
    reasonStripe: string().required('Please select a reason'),
    selectedPayment: string().required('Please select a payment'),
});

const RefundAction = ({ triggerUpdate, stripeCustomerId, payments }) => {
    const { mutateAsync: createRefundRequest } = useCreateRefundRequest();

    const onSubmit = async (values) => {
        const postObject = {
            customer: stripeCustomerId,
            reason_stripe: values.reasonStripe,
            reason_internal: values.reasonInternal,
            amount: values.refundAmount * 100,
            payment_intent_id: values.selectedPayment,
        };
        await createRefundRequest(postObject);
        resetForm();
        triggerUpdate();
    };
    const formik = useFormik({
        initialValues: {
            refundAmount: 0,
            reasonStripe: '',
            reasonInternal: '',
            selectedPayment: '',
        },
        validationSchema,
        onSubmit,
    });
    const { values, errors, touched, handleChange, setFieldValue, resetForm } = formik;
    const handleSelectChange = (event) => {
        setFieldValue(event.target.name, event.target.value);
    };

    if (!payments) {
        return null;
    }

    return (
        <Action title="Refund a Payment" verb="Refund" formik={formik}>
            <DialogContentText>Select the payment to refund</DialogContentText>
            <Box noValidate className="tw-mb-2">
                <Select
                    autoFocus
                    fullWidth
                    value={values.selectedPayment}
                    error={touched.selectedPayment && Boolean(errors.selectedPayment)}
                    helperText={errors.selectedPayment}
                    onChange={handleSelectChange}
                    inputProps={{
                        name: 'selectedPayment',
                        id: 'selectedPayment',
                    }}>
                    {payments.map((payment) => (
                        <MenuItem value={payment.id} key={payment.id}>
                            {moment.unix(payment.created).format('MM-DD-YYYY')} {moneyWithCommas(payment.amount / 100)}
                        </MenuItem>
                    ))}
                </Select>
            </Box>
            <DialogContentText>Set the refund amount in dollars.</DialogContentText>
            <Box noValidate className="tw-mb-2">
                <TextField
                    type="number"
                    id="refundAmount"
                    name="refundAmount"
                    error={touched.refundAmount && Boolean(errors.refundAmount)}
                    helperText={errors.refundAmount}
                    value={values.refundAmount}
                    onChange={handleChange}
                    fullWidth
                />
            </Box>
            <DialogContentText>Reason for refund</DialogContentText>
            <Box noValidate className="tw-mb-2">
                <Select
                    autoFocus
                    fullWidth
                    value={values.reasonStripe}
                    error={touched.reasonStripe && Boolean(errors.reasonStripe)}
                    helperText={errors.reasonStripe}
                    onChange={handleSelectChange}
                    inputProps={{
                        name: 'reasonStripe',
                        id: 'reasonStripe',
                    }}>
                    <MenuItem value="duplicate" key="duplicate">
                        Duplicate
                    </MenuItem>
                    <MenuItem value="fraudulent" key="fraudulent">
                        Fraudulent
                    </MenuItem>
                    <MenuItem value="requested_by_customer" key="requested_by_customer">
                        Requested by Customer
                    </MenuItem>
                </Select>
            </Box>
            <DialogContentText>Additional details for refund</DialogContentText>
            <Box noValidate className="tw-mb-2">
                <TextField
                    error={touched.reasonInternal && Boolean(errors.reasonInternal)}
                    helperText={errors.reasonInternal}
                    value={values.reasonInternal}
                    multiline
                    id="reasonInternal"
                    name="reasonInternal"
                    rows={5}
                    type="text"
                    onChange={handleChange}
                    fullWidth
                />
            </Box>
        </Action>
    );
};

export default RefundAction;
// Code from file collective-frontend/src/modules/Hub/Payments/ActionsPanel.jsx
import CreditAction from './CreditAction';
import CreditNoteAction from './CreditNoteAction';
import RefundAction from './RefundAction';
import SubscriptionAction from './SubscriptionAction';

const ActionsPanel = ({
    triggerUpdate,
    customer,
    payments,
    invoices,
    isStaleCreditNoteAction,
    setIsStaleCreditNoteAction,
}) => {
    return (
        <div className="tw-flex tw-cols-2 tw-space-x-1">
            <CreditAction triggerUpdate={triggerUpdate} stripeCustomerId={customer.id} />
            <RefundAction triggerUpdate={triggerUpdate} stripeCustomerId={customer.id} payments={payments} />
            <CreditNoteAction
                triggerUpdate={triggerUpdate}
                stripeCustomerId={customer.id}
                invoices={invoices}
                isStaleCreditNoteAction={isStaleCreditNoteAction}
                setIsStaleCreditNoteAction={setIsStaleCreditNoteAction}
            />
            <SubscriptionAction triggerUpdate={triggerUpdate} customer={customer} />
        </div>
    );
};
export default ActionsPanel;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/index.jsx
import React from 'react';
import { Route } from 'react-router-dom';

import lazyWithSuspenseAndRetry from 'modules/common/lazyWithSuspenseAndRetry';

export const ClientAppSyncHomePage = lazyWithSuspenseAndRetry(() => import('./Home/Page'));

export default function getHubClientAppSyncRoutes() {
    return [<Route exact key="client-app-sync-home" path="/hub/client-app-sync" component={ClientAppSyncHomePage} />];
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/services.test.js
import { taxReturnCsvHistoryAdapter } from './adapters';

import collectiveApi from 'modules/common/collectiveApi';
import {
    commitChanges,
    fetchTaxActivityCsvCompare,
    fetchTaxReturnCSVHistory,
    hubClientAppSyncUploadFile,
} from 'modules/Hub/ClientAppSync/services';

jest.mock('modules/common/collectiveApi');
jest.mock('./adapters');

describe('Client App Sync services', () => {
    const client = {
        post: jest.fn(),
        patch: jest.fn(),
    };
    collectiveApi.mockImplementation(() => client);

    describe('hubClientAppSyncUploadFile', () => {
        it('should upload file', async () => {
            // Given
            const data = { message: 'success' };
            const expectedResponse = { data, status: 201 };
            collectiveApi.post.mockResolvedValueOnce(expectedResponse);
            const formData = new FormData();
            formData.append('file', 'file content');

            // When
            const response = await hubClientAppSyncUploadFile(formData);

            // Then
            expect(collectiveApi.post).toHaveBeenCalledWith('/taxtracker/csv/', formData, {
                headers: { 'Content-Type': 'multipart/form-data' },
            });
            expect(response).toEqual(expectedResponse);
        });
    });

    describe('commitChanges', () => {
        it('should commit changes', async () => {
            // Given
            const data = { message: 'success' };
            const expectedResponse = { data, status: 200 };
            collectiveApi.patch.mockResolvedValueOnce(expectedResponse);

            // When
            const response = await commitChanges(1);

            // Then
            expect(collectiveApi.patch).toHaveBeenCalledWith('taxtracker/csv-compare/1/');
            expect(response).toEqual(data);
        });
    });

    describe('fetchTaxActivityCsvCompare', () => {
        it('should fetch tax activity csv compare', async () => {
            // Given
            const data = { message: 'success' };
            const expectedResponse = { data, status: 200 };
            collectiveApi.get.mockResolvedValueOnce(expectedResponse);

            // When
            const response = await fetchTaxActivityCsvCompare(1);

            // Then
            expect(collectiveApi.get).toHaveBeenCalledWith('taxtracker/csv-compare/1/');
            expect(response).toEqual(data);
        });
    });

    describe('fetchTaxReturnCSVHistory', () => {
        beforeEach(() => {
            taxReturnCsvHistoryAdapter.mockResolvedValueOnce({
                message: 'success',
            });
        });

        it('should fetch tax activity csv history', async () => {
            // Given
            const data = { message: 'success' };
            const expectedResponse = { data, status: 200 };
            collectiveApi.get.mockResolvedValueOnce(expectedResponse);

            // When
            const response = await fetchTaxReturnCSVHistory(1);

            // Then
            expect(collectiveApi.get).toHaveBeenCalledWith('taxtracker/tax-return-csv-history/');
            expect(response).toEqual(data);
        });

        it('should fetch tax activity csv history if giving a page number', async () => {
            // Given
            const data = { message: 'success' };
            const expectedResponse = { data, status: 200 };
            collectiveApi.get.mockResolvedValueOnce(expectedResponse);

            // When
            const response = await fetchTaxReturnCSVHistory(2);

            // Then
            expect(collectiveApi.get).toHaveBeenCalledWith('taxtracker/tax-return-csv-history/?page=2');
            expect(response).toEqual(data);
        });
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/services.js
import { taxReturnCsvHistoryAdapter } from './adapters';

import collectiveApi from 'modules/common/collectiveApi';

// TaxTracker: Client App Sync
export const hubClientAppSyncUploadFile = (formData) =>
    collectiveApi.post('/taxtracker/csv/', formData, {
        headers: {
            'Content-Type': 'multipart/form-data',
        },
    });

// Fetch tax activity csv compare tool
export const fetchTaxActivityCsvCompare = async (file_id) => {
    const { data } = await collectiveApi.get(`taxtracker/csv-compare/${file_id}/`);
    return data;
};

// Apply the changes to the tax activity
export const commitChanges = async (file_id) => {
    const { data } = await collectiveApi.patch(`taxtracker/csv-compare/${file_id}/`);
    return data;
};

// Fetch the list of Tax Return CSV History paginated
export const fetchTaxReturnCSVHistory = async (page) => {
    const next_page = page > 1 ? `?page=${page}` : '';
    const { data } = await collectiveApi.get(`taxtracker/tax-return-csv-history/${next_page}`);
    return taxReturnCsvHistoryAdapter(data);
};

export const fetchDownloadCsvFile = async (uuid) => {
    const { data } = await collectiveApi.get(`taxtracker/csv-download/${uuid}/`);
    const url = window.URL.createObjectURL(new Blob([data]));
    const link = document.createElement('a');
    link.href = url;
    link.setAttribute('download', `${uuid}.csv`);
    document.body.appendChild(link);
    link.click();
};
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/adapters.js
import moment from 'moment';

export const dateFormat = 'MMM DD, YYYY H:mm:ss';

const taxReturnCsvHistoryAdapter = (data) => {
    return {
        next_page: data.next_page,
        number_of_pages: data.number_of_pages,
        previous_page: data.previous_page,
        total_records_count: data.total_records_count,
        results: data.results.map((res) => {
            return {
                is_active: res.is_active,
                uuid: res.uuid,
                updated_at: moment.utc(res.updated_at).format(dateFormat),
                created_at: moment.utc(res.created_at).format(dateFormat),
                uploaded_by: res.uploaded_by,
            };
        }),
    };
};

export { taxReturnCsvHistoryAdapter };
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/Page.test.jsx
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { ThemeProvider } from '@mui/material';
import { render, act } from '@testing-library/react';

import ClientAppSyncHomePage, { UploadActions, DataDiffInfo } from './Page';

import theme from 'theme';

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            retry: false,
        },
    },
});

const setup = (customProps) => {
    const props = {
        ...customProps,
    };

    return render(
        <ThemeProvider theme={theme}>
            <QueryClientProvider client={queryClient}>
                <ClientAppSyncHomePage {...props} />
            </QueryClientProvider>
        </ThemeProvider>
    );
};

describe('ClientAppSyncHomePage', () => {
    it('should render correctly', () => {
        const { asFragment } = setup();
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should show the upload form when click on first tab', () => {
        const { getByRole } = setup();

        const tab = getByRole('tab', { name: 'Upload Data' });

        expect(tab).toBeInTheDocument();
    });

    it('should show the commit history when click on second tab', () => {
        const { getByRole } = setup();

        const tab = getByRole('tab', { name: 'History' });

        expect(tab).toBeInTheDocument();
    });

    it('should show the confirmation dialog modal when click on commit changes', async () => {
        const { debug, getByTestId, asFragment, getByText } = setup({
            uploadedFileMetadata: {
                file: 'test.csv',
                data: null,
            },
            changesCount: 1,
        });

        await act(() => {
            const button = getByTestId('commit-changes-button');
            button.click();
        });

        expect(getByText('Do you wan to commit the changes?')).toBeInTheDocument();

        expect(asFragment()).toMatchSnapshot();
    });

    describe('UploadActions', () => {
        it('should render correctly when changesCount is 0', () => {
            const { asFragment } = render(<UploadActions changesCount={0} setDialogOpen={jest.fn()} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });

        it('should render correctly when changesCount is greater than 0', () => {
            const { asFragment } = render(<UploadActions changesCount={1} setDialogOpen={jest.fn()} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });
    });

    describe('DataDiffInfo', () => {
        it('should render correctly when changesCount is 0', () => {
            const { asFragment } = render(<DataDiffInfo changesCount={0} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });

        it('should render correctly when changesCount is greater than 0', () => {
            const { asFragment } = render(<DataDiffInfo changesCount={1} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });

        it('should render correctly when changesCount is greater than 1', () => {
            const { asFragment } = render(<DataDiffInfo changesCount={2} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/Page.jsx
import { useState } from 'react';
import {
    Alert,
    Box,
    Button,
    Tab,
    Tabs,
    Dialog,
    DialogActions,
    DialogContent,
    DialogContentText,
    DialogTitle,
} from '@mui/material';

import Header from './Header';
import UploadData from './UploadData';
import FileInfo from './FileInfo';
import DataDiffTable from './DataDiffTable';
import UploadHistory from './UploadHistory';
import MissingMembersTable from './MissingMembersTable';

import { fetchTaxActivityCsvCompare, commitChanges } from 'modules/Hub/ClientAppSync/services';
import CommitChangesSuccessModal from 'modules/Hub/ClientAppSync/Home/CommitChangesSuccessModal';

export function UploadActions({ changesCount, setDialogOpen }) {
    if (changesCount === 0) {
        return null;
    }

    return (
        <div className="tw-flex tw-flex-row tw-justify-end">
            <Button
                variant="contained"
                data-testid="commit-changes-button"
                color="primary"
                size="large"
                style={{ marginTop: '20px' }}
                onClick={() => setDialogOpen(true)}>
                Commit Changes
            </Button>
        </div>
    );
}

export function DataDiffInfo({ changesCount }) {
    if (changesCount === 0) {
        return null;
    }

    return (
        <div className="info">
            <strong>{`${changesCount} change${changesCount > 1 ? 's' : ''} found. `}</strong>
            Review anomalies and commit change
        </div>
    );
}

function TabPanel(props) {
    const { children, value, index, ...other } = props;

    return (
        <div
            role="tabpanel"
            hidden={value !== index}
            id={`simple-tabpanel-${index}`}
            aria-labelledby={`simple-tab-${index}`}
            {...other}>
            {value === index && (
                <Box sx={{ p: 3 }}>
                    <div>{children}</div>
                </Box>
            )}
        </div>
    );
}

function a11yProps(index) {
    return {
        id: `simple-tab-${index}`,
        'aria-controls': `simple-tabpanel-${index}`,
    };
}

export default function ClientAppSyncHomePage(props) {
    const [value, setValue] = useState(0);
    const [tabDataValue, setTabDataValue] = useState(0);
    const [uploadedFileMetadata, setUploadedFileMetadata] = useState(props.uploadedFileMetadata || null);
    const [dialogOpen, setDialogOpen] = useState(false);
    const [loadingDiff, setLoadingDiff] = useState(false);
    const [applyingChanges, setApplyingChanges] = useState(false);
    const [changesApplied, setChangesApplied] = useState(false);
    const [diffData, setDiffData] = useState(null);
    const [changesCount, setChangesCount] = useState(props.changesCount || 0);

    const onUploadedFile = async (file, data) => {
        setUploadedFileMetadata({ file, data });
        setLoadingDiff(true);
        const diffResponse = await fetchTaxActivityCsvCompare(data.uuid);
        setDiffData(diffResponse);
        setLoadingDiff(false);
        setChangesCount(diffResponse.added.length + diffResponse.modified.length + diffResponse.deleted.length);
    };

    const handleChange = (event, newValue) => {
        setValue(newValue);
    };

    const handleTabDataChange = (event, newValue) => {
        setTabDataValue(newValue);
    };

    const handleOnClear = () => {
        setUploadedFileMetadata(null);
        setDiffData(null);
        setChangesCount(0);
        setDialogOpen(false);
    };

    const handleCommitChanges = async () => {
        setApplyingChanges(true);
        await commitChanges(uploadedFileMetadata.data.uuid);
        setApplyingChanges(false);
        setDialogOpen(false);
        setChangesApplied(true);
        handleOnClear();
    };

    return (
        <div>
            <Header title="Client App Sync" />
            <Box sx={{ borderBottom: 1, borderColor: 'divider', marginTop: '20px' }}>
                <Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
                    <Tab label="Upload Data" {...a11yProps(0)} />
                    <Tab label="History" {...a11yProps(1)} />
                </Tabs>
            </Box>
            <TabPanel value={value} index={0}>
                {uploadedFileMetadata === null ? (
                    <UploadData onUploadedFile={onUploadedFile} />
                ) : (
                    <FileInfo onClear={handleOnClear} metadata={uploadedFileMetadata} />
                )}
                {changesApplied && (
                    <CommitChangesSuccessModal isOpen={changesApplied} onClose={() => setChangesApplied(false)} />
                )}
                {uploadedFileMetadata && (
                    <div className="tw-mt-4">
                        <Dialog
                            open={dialogOpen}
                            onClose={() => handleOnClear()}
                            aria-labelledby="alert-dialog-title"
                            aria-describedby="alert-dialog-description">
                            <DialogTitle id="alert-dialog-title">Confirmation Dialog</DialogTitle>
                            <DialogContent>
                                <DialogContentText id="alert-dialog-description">
                                    {applyingChanges ? (
                                        <Alert severity="info">Applying changes...</Alert>
                                    ) : (
                                        'Do you wan to commit the changes?'
                                    )}
                                </DialogContentText>
                            </DialogContent>
                            <DialogActions>
                                <Button onClick={() => setDialogOpen(false)} disabled={applyingChanges}>
                                    Cancel
                                </Button>
                                <Button onClick={handleCommitChanges} disabled={applyingChanges} autoFocus>
                                    {applyingChanges ? 'Applying changes...' : 'Agree'}
                                </Button>
                            </DialogActions>
                        </Dialog>
                        <Header title="Prepare to commit" />
                        <Box sx={{ borderBottom: 1, borderColor: 'divider', marginTop: '20px' }}>
                            <Tabs value={tabDataValue} onChange={handleTabDataChange}>
                                <Tab label="Data Diff" id="tab-data-tab-0" aria-controls="tab-data-tabpanel-0" />
                                <Tab label="Missing Members" id="tab-data-tab-1" aria-controls="tab-data-tabpanel-1" />
                            </Tabs>
                        </Box>
                        <TabPanel value={tabDataValue} index={0}>
                            <DataDiffInfo changesCount={changesCount} />
                            <DataDiffTable diffData={diffData} loading={loadingDiff} data={uploadedFileMetadata.data} />
                            <UploadActions changesCount={changesCount} setDialogOpen={setDialogOpen} />
                        </TabPanel>
                        <TabPanel value={tabDataValue} index={1}>
                            {diffData && <MissingMembersTable data={diffData?.missing} />}
                        </TabPanel>
                    </div>
                )}
            </TabPanel>
            <TabPanel value={value} index={1}>
                <UploadHistory />
            </TabPanel>
        </div>
    );
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/index.js
export { default } from './Page';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DownloadFile/index.js
export { default } from './DownloadFile';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DownloadFile/DownloadFile.jsx
import PropTypes from 'prop-types';
import { useState } from 'react';
import { LoadingButton } from '@mui/lab';
import FileDownloadIcon from '@mui/icons-material/FileDownload';

import { fetchDownloadCsvFile } from 'modules/Hub/ClientAppSync/services';

const DownloadFile = ({ uuid, setError }) => {
    const [isDownloading, setIsDownloading] = useState(false);

    const download = async (uuid) => {
        try {
            setIsDownloading(true);
            await fetchDownloadCsvFile(uuid);
        } catch (error) {
            setError(error.message);
        } finally {
            setIsDownloading(false);
        }
    };

    return (
        <LoadingButton
            color="success"
            loading={isDownloading}
            variant="contained"
            data-testid="csv-download-button"
            onClick={() => download(uuid)}>
            <FileDownloadIcon color="white" />
        </LoadingButton>
    );
};

DownloadFile.propTypes = {
    uuid: PropTypes.string.isRequired,
    setError: PropTypes.func.isRequired,
};

export default DownloadFile;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DownloadFile/DownloadFile.test.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter';
import { act, fireEvent, render } from '@testing-library/react';

import DownloadFile from './DownloadFile';

import collectiveApi from 'modules/common/collectiveApi';

const mockCollectiveApi = new MockAdapter(collectiveApi);

const WrappedComponent = ({ uuid, setError }) => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });
    return (
        <QueryClientProvider client={queryClient}>
            <DownloadFile uuid={uuid} setError={setError} />
        </QueryClientProvider>
    );
};

describe('DownloadFile', () => {
    const fakeUUID = '15c3b737-9bd8-4d41-8b01-441240c0dcba';

    it('should render correctly', () => {
        const { asFragment } = render(<WrappedComponent uuid={fakeUUID} setError={() => {}} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render download when the button is clicked', async () => {
        mockCollectiveApi.onGet(`taxtracker/csv-download/${fakeUUID}/`).replyOnce(204, {});
        const { getByTestId } = render(<WrappedComponent uuid={fakeUUID} setError={() => {}} />);
        expect(getByTestId('csv-download-button')).toBeTruthy();
        await act(async () => {
            await fireEvent.click(getByTestId('csv-download-button'));
        });
        expect(mockCollectiveApi.history.get.length).toBe(1);
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DataDiffTable/DataDiffTable.module.less
.headCell {
    font-weight: bold;
}

.cellChanges {
    background-color: #fff5ef;
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DataDiffTable/index.js
export { default } from './DataDiffTable';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DataDiffTable/DataDiffTable.jsx
import { Table, TableBody, TableCell, TableHead, TableRow } from '@mui/material';

import classes from './DataDiffTable.module.less';

/*
 * Shows the diff rows between the csv and database data
 */
const DataDiffTable = ({ loading, diffData }) => {
    const fields = [
        'client_id',
        'member_name',
        'project_type',
        'task_activity',
        'assigned',
        'task_status',
        'task_end_by',
    ];
    const custom_style = { '&:last-child td, &:last-child th': { border: 0 } };

    const diffAttribute = (row, field) => {
        return row.current_state[field] !== row.new_state[field];
    };

    if (diffData === null) {
        return null;
    }

    return (
        <Table aria-label="task activity table">
            <TableHead>
                <TableRow>
                    <TableCell className={classes.headCell}>Operation</TableCell>
                    <TableCell className={classes.headCell}>Client ID</TableCell>
                    <TableCell className={classes.headCell}>Project</TableCell>
                    <TableCell className={classes.headCell}>Type</TableCell>
                    <TableCell className={classes.headCell}>Task</TableCell>
                    <TableCell className={classes.headCell}>Assigned</TableCell>
                    <TableCell className={classes.headCell}>Status</TableCell>
                    <TableCell className={classes.headCell}>End By</TableCell>
                </TableRow>
            </TableHead>
            <TableBody>
                {/* Modified */}
                {!loading &&
                    diffData.modified.map((row, row_idx) => (
                        <TableRow key={`${row_idx}`} sx={custom_style}>
                            <TableCell>Modified</TableCell>
                            {fields.map((field, field_idx) => (
                                <TableCell
                                    key={`${field_idx}`}
                                    className={diffAttribute(row, field) ? classes.cellChanges : ''}>
                                    {row.current_state[field]}

                                    {diffAttribute(row, field) && (
                                        <>
                                            <br />
                                            <b>{row.new_state[field]}</b>
                                        </>
                                    )}
                                </TableCell>
                            ))}
                        </TableRow>
                    ))}
                {/* Added */}
                {!loading &&
                    diffData.added.map((row, row_idx) => (
                        <TableRow key={`${row_idx}`} sx={custom_style}>
                            <TableCell>Added</TableCell>
                            {fields.map((field, field_idx) => (
                                <TableCell key={`${field_idx}`}>{row[field]}</TableCell>
                            ))}
                        </TableRow>
                    ))}
                {/* Deleted */}
                {!loading &&
                    diffData.deleted.map((row, row_idx) => (
                        <TableRow key={`${row_idx}`} sx={custom_style}>
                            <TableCell>Deleted</TableCell>
                            {fields.map((field, field_idx) => (
                                <TableCell key={`${field_idx}`} className={classes.cellChanges}>
                                    {row[field]}
                                </TableCell>
                            ))}
                        </TableRow>
                    ))}
            </TableBody>
        </Table>
    );
};

export default DataDiffTable;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DataDiffTable/DataDiffTable.test.jsx
import { render } from '@testing-library/react';

import DataDiffTable from './DataDiffTable';

const data = {
    modified: [
        {
            current_state: {
                id: 1,
                client_id: '27ENFDAA00',
                project_name: 'George Washington',
                project_id: '2020 UltraTax CS',
                extended: false,
                project_tracking: 'Started',
                project_type: 'BTR',
                task_activity: 'Review',
                assigned: 'AMEND',
                task_status: 'PRESCREEN',
                task_end_by: '10/12/22 18.34',
            },
            new_state: {
                id: 1,
                client_id: '27ENFDAA00',
                project_name: 'George Washington',
                project_id: '2020 UltraTax CS',
                extended: true,
                project_tracking: 'In Progress',
                project_type: 'BTR',
                task_activity: 'Review',
                assigned: 'AMEND NEW ASSIGN',
                task_status: 'PRESCREEN',
                task_end_by: '10/12/22 18.34',
            },
        },
        {
            current_state: {
                id: 2,
                client_id: '27ENFDAA01',
                extended: false,
                project_tracking: 'Started',
                project_type: 'ITR',
                project_name: 'Tom',
                project_id: '2020 UltraTax CS',
                task_activity: 'Review',
                assigned: 'AMEND',
                task_status: 'PRESCREEN',
                task_end_by: '10/12/22 18.34',
            },
            new_state: {
                id: 2,
                client_id: '27ENFDAA01',
                extended: false,
                project_tracking: 'Completed',
                project_type: 'ITR',
                project_name: 'Thomas Jefferson',
                project_id: '2020 UltraTax CS',
                task_activity: 'Review',
                assigned: 'AMEND',
                task_status: 'PRESCREEN',
                task_end_by: '10/12/22 11.34',
            },
        },
    ],
    added: [
        {
            id: 3,
            client_id: '27ENFDAA02',

            extended: false,
            project_tracking: 'Started',
            project_type: 'BTR',

            project_name: 'John Adams',
            project_id: '2020 UltraTax CS',
            task_activity: 'Review',
            assigned: 'AMEND',
            task_status: 'COMPLETED',
            task_end_by: '10/12/22 13.34',
        },
    ],
    deleted: [
        {
            id: 4,
            client_id: '27ENFDAA03',
            extended: false,
            project_tracking: 'Started',
            project_type: 'ITR',
            project_name: 'Benjamin Franklin',
            project_id: '2020 UltraTax CS',
            task_activity: 'PRESCREEN',
            assigned: 'AMEND',
            task_status: 'COMPLETED',
            task_end_by: '10/12/22 12.34',
        },
    ],
};

describe('DataDiffTable', () => {
    it('should render correctly', async () => {
        // Given
        const diffData = { ...data };
        const loading = false;

        // When
        const { asFragment } = render(<DataDiffTable diffData={diffData} loading={loading} />);

        // Then
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render correctly when the data is loading', async () => {
        // Given
        const loading = true;
        const diffData = null;

        // When
        const { asFragment } = render(<DataDiffTable diffData={diffData} loading={loading} />);

        // Then
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadHistoryList/UploadHistoryList.module.less
.headCell {
    font-weight: bold;
}

.actions {
    display: flex;
    gap: 4px;
    justify-items: flex-start;
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadHistoryList/UploadHistoryList.jsx
import PropTypes from 'prop-types';
import { Table, TableBody, TableCell, TableHead, TableRow, TableFooter, Checkbox } from '@mui/material';
import { Menu, Icon } from 'semantic-ui-react';

import DeleteFile from '../DeleteFile';
import DownloadFile from '../DownloadFile';

import classes from './UploadHistoryList.module.less';

/*
 * Shows history of the CSVs uploaded
 */
export default function UploadHistoryList({ csvData, csvDataPagination, handlePageChange, setError, fetchData }) {
    const fields = ['created_at', 'updated_at', 'uploaded_by'];

    const custom_style = { '&:last-child td, &:last-child th': { border: 0 } };

    return (
        <div>
            <Table aria-label="upload history table">
                <TableHead>
                    <TableRow className="tw-grid tw-grid-cols-5">
                        <TableCell className={classes.headCell}>Created</TableCell>
                        <TableCell className={classes.headCell}>Updated</TableCell>
                        <TableCell className={classes.headCell}>Uploaded By</TableCell>
                        <TableCell className={classes.headCell}>Is Active</TableCell>
                        <TableCell className={classes.headCell}>Actions</TableCell>
                    </TableRow>
                </TableHead>
                <TableBody>
                    {csvData.map((row, row_idx) => (
                        <TableRow className="tw-grid tw-grid-cols-5" key={`${row_idx}`} sx={custom_style}>
                            {fields.map((field, field_idx) => (
                                <TableCell key={`${field_idx}`}>{row[field]}</TableCell>
                            ))}
                            <TableCell>
                                <Checkbox
                                    data-testid="is-active-checkbox"
                                    disableRipple
                                    disabled
                                    color="secondary"
                                    checked={row.is_active}
                                />
                            </TableCell>
                            <TableCell key={row.uuid}>
                                <span className={classes.actions}>
                                    <DownloadFile key={`${row.uuid}-download`} uuid={row.uuid} setError={setError} />
                                    {!row.is_active && (
                                        <DeleteFile
                                            key={`${row.uuid}-delete`}
                                            uuid={row.uuid}
                                            setError={setError}
                                            fetchData={fetchData}
                                        />
                                    )}
                                </span>
                            </TableCell>
                        </TableRow>
                    ))}
                </TableBody>
                <TableFooter>
                    <TableRow>
                        <TableHead colSpan={fields.length + 1}>
                            <Menu pagination>
                                <Menu.Item as="span" icon>
                                    Total of CSV files: {csvDataPagination?.total_record_count}
                                </Menu.Item>
                            </Menu>
                            <Menu floated="right" pagination>
                                <Menu.Item as="span" icon>
                                    {csvDataPagination?.number_of_pages}
                                </Menu.Item>
                                <Menu.Item
                                    as="a"
                                    icon
                                    disabled={!csvDataPagination?.previous_page}
                                    onClick={() => handlePageChange(csvDataPagination?.previous_page)}>
                                    <Icon name="chevron left" />
                                </Menu.Item>
                                <Menu.Item
                                    as="a"
                                    icon
                                    disabled={!csvDataPagination?.next_page}
                                    onClick={() => handlePageChange(csvDataPagination?.next_page)}>
                                    <Icon name="chevron right" />
                                </Menu.Item>
                            </Menu>
                        </TableHead>
                    </TableRow>
                </TableFooter>
            </Table>
        </div>
    );
}

UploadHistoryList.propTypes = {
    csvData: PropTypes.arrayOf(
        PropTypes.shape({
            is_active: PropTypes.bool,
            uuid: PropTypes.string,
            updated_at: PropTypes.string,
            created_at: PropTypes.string,
        }).isRequired
    ),
    csvDataPagination: PropTypes.shape({
        total_record_count: PropTypes.number,
        number_of_pages: PropTypes.string,
        next_page: PropTypes.number,
        previous_page: PropTypes.number,
    }).isRequired,
    handlePageChange: PropTypes.func.isRequired,
    setError: PropTypes.func.isRequired,
    fetchData: PropTypes.func.isRequired,
};
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadHistoryList/index.js
export { default } from './UploadHistoryList';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadHistoryList/UploadHistoryList.test.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cleanup, render } from '@testing-library/react';

import UploadHistoryList from './UploadHistoryList';

const data = [
    {
        commit_owner: 3,
        commit_owner_user: 'user@email.com',
        is_active: false,
        uuid: 'b111f67d-59fe-4017-a55a-5d860b4c66a2',
        updated_at: 'Nov 18, 2022 18:11:78',
        created_at: 'Nov 18, 2022 18:11:78',
    },
    {
        commit_owner: 1,
        commit_owner_user: 'user@email.com',
        is_active: true,
        uuid: '5d5975b7-6180-4567-992c-f5282a0f7c87',
        updated_at: 'Nov 18, 2022 18:11:34',
        created_at: 'Nov 18, 2022 13:11:90',
    },
];

const pagination = {
    total_record_count: 2,
    number_of_pages: 'Page 1 of 1',
    next_page: null,
    previous_page: null,
};

const WrappedComponent = ({ csvData, csvPagination, handlePageChange, setError, fetchData }) => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });
    return (
        <QueryClientProvider client={queryClient}>
            <UploadHistoryList
                csvData={csvData}
                csvDataPagination={csvPagination}
                handlePageChange={handlePageChange}
                setError={setError}
                fetchData={fetchData}
            />
        </QueryClientProvider>
    );
};

afterAll(cleanup);

describe('UploadHistoryList', () => {
    it('should render correctly', () => {
        // Given
        const csvData = data;
        const csvPagination = pagination;

        // When
        const { asFragment } = render(
            <WrappedComponent
                csvData={csvData}
                csvDataPagination={csvPagination}
                handlePageChange={jest.fn()}
                setError={jest.fn()}
                fetchData={jest.fn()}
            />
        );

        // Then
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render correctly when data is empty', () => {
        // Given
        const csvData = [];
        const csvPagination = {};

        // When
        const { asFragment } = render(
            <UploadHistoryList
                csvData={csvData}
                csvDataPagination={csvPagination}
                handlePageChange={jest.fn()}
                setError={jest.fn()}
                fetchData={jest.fn()}
            />
        );

        // Then
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/FileInfo/FileInfo.jsx
import classes from './FileInfo.module.less';

import csvFileUploadedIcon from 'assets/img/icons/csv-file-uploaded-icon.svg';
import arrowUpperRightIcon from 'assets/img/icons/arrow-upper-right-icon.svg';
import closeButtonIcon from 'assets/img/icons/close-circle-icon.svg';

const FileInfo = ({ metadata, onClear }) => {
    return (
        <div className={classes.fileInfo}>
            <div>
                <img src={csvFileUploadedIcon} alt="csv file uploaded icon" />
            </div>
            <div className={classes.fileData}>
                <div className={classes.fileTitle}>
                    {metadata.file.name}
                    <a href="#">
                        <img src={arrowUpperRightIcon} alt="arrow upper right" />
                    </a>
                </div>
                <div className={classes.fileDate}>Today at 12.00pm</div>
            </div>
            <div className={classes.closeButton}>
                <a href="#" onClick={onClear}>
                    <img src={closeButtonIcon} alt="close button" />
                </a>
            </div>
        </div>
    );
};

export default FileInfo;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/FileInfo/FileInfo.module.less
.fileInfo {
    border: 1px solid #a3998f;
    padding: 20px;
    border-radius: 8px;
    color: #333;
    display: flex;
    justify-content: space-between;
}

.fileData {
    padding-left: 5px;
    width: 100%;
}

.fileTitle {
    font-size: 17px;
    font-weight: 600;
    line-height: 20px;

    img {
        margin-left: 5px;
    }
}

.fileDate {
    font-size: 12px;
    line-height: 14px;
    font-weight: 400;
    color: #333;
}

.closeButton {
    display: flex;
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/FileInfo/index.js
export { default } from './FileInfo';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/FileInfo/FileInfo.test.jsx
import { render } from '@testing-library/react';

import FileInfo from './FileInfo';

describe('FileInfo', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<FileInfo onClear={jest.fn()} metadata={{ file: { name: 'filename.pdf' } }} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/Header/Header.test.jsx
import { render } from '@testing-library/react';

import Header from './Header';

describe('Header', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<Header />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/Header/index.js
export { default } from './Header';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/Header/Header.jsx
import classes from './Header.module.less';

export default function Header({ title, subtitle }) {
    return (
        <div className={classes.header}>
            <h1 className={classes.title}>{title}</h1>
            {subtitle && <div className={classes.subtitle}>{subtitle}</div>}
        </div>
    );
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/Header/Header.module.less
.header {
    font-family: recoletasemibold;
    line-height: 32px;

    .subtitle,
    .subtitle {
        letter-spacing: 0.15px;
    }

    .title {
        font-size: 20px;
        line-height: 32px;
        font-weight: 500;
        color: #1c1c1a;
    }

    .subtitle {
        font-family: mier_bregular;
        font-style: normal;
        font-weight: 400;
        font-size: 12px;
        line-height: 12px;
        color: #989794;
    }
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DeleteFile/DeleteFile.test.jsx
import { fireEvent, render, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';

import DeleteFile from './DeleteFile';

import collectiveApi from 'modules/common/collectiveApi';

const mock = new MockAdapter(collectiveApi);

const WrappedComponent = ({ uuid, setError, fetchData }) => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                retry: false,
            },
        },
    });
    return (
        <QueryClientProvider client={queryClient}>
            <DeleteFile uuid={uuid} setError={setError} fetchData={fetchData} />
        </QueryClientProvider>
    );
};

describe('DeleteFile', () => {
    const fakeUUID = '15c3b737-9bd8-4d41-8b01-441240c0dcba';
    it('should render correctly', () => {
        const { asFragment } = render(<WrappedComponent uuid={fakeUUID} fetchData={() => {}} setError={() => {}} />);
        expect(asFragment()).toMatchSnapshot();
    });

    it('should render delete button and submit correctly when action is confirmed', async () => {
        const mockFetchData = jest.fn();
        mock.onDelete(`/taxtracker/tax-return-csv-history/${fakeUUID}/`).replyOnce(204, {});
        const { getByTestId } = render(
            <WrappedComponent uuid={fakeUUID} fetchData={mockFetchData} setError={() => {}} />
        );
        expect(getByTestId('csv-delete-button')).toBeTruthy();
        fireEvent.click(getByTestId('csv-delete-button'));
        expect(getByTestId('yes-button')).toBeInTheDocument();
        fireEvent.click(getByTestId('yes-button'));
        await waitFor(() => expect(mockFetchData).toHaveBeenCalledTimes(1));
        expect(mock.history.delete.length).toBe(1);
    });

    it('should render delete button and close Confirmation Modal when action is canceled', async () => {
        const { getByTestId, queryByTestId } = render(
            <WrappedComponent uuid={fakeUUID} fetchData={jest.fn()} setError={() => {}} />
        );
        expect(getByTestId('csv-delete-button')).toBeTruthy();
        fireEvent.click(getByTestId('csv-delete-button'));
        expect(getByTestId('no-button')).toBeInTheDocument();
        fireEvent.click(getByTestId('no-button'));
        expect(queryByTestId('no-button')).not.toBeInTheDocument();
    });

    it('should set an error when API throws', async () => {
        const mockFetchData = jest.fn();
        const mockSetError = jest.fn();
        mock.onDelete(`/taxtracker/tax-return-csv-history/${fakeUUID}/`).replyOnce(500, {
            detail: 'An error occured.',
        });
        const { getByTestId } = render(
            <WrappedComponent uuid={fakeUUID} fetchData={mockFetchData} setError={mockSetError} />
        );
        expect(getByTestId('csv-delete-button')).toBeTruthy();
        fireEvent.click(getByTestId('csv-delete-button'));
        fireEvent.click(getByTestId('yes-button'));
        await waitFor(() => expect(mockSetError).toHaveBeenCalledTimes(1));
        expect(mockSetError).toHaveBeenCalledWith('An error occured.');
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DeleteFile/DeleteFile.jsx
import PropTypes from 'prop-types';
import { useMutation } from '@tanstack/react-query';
import { LoadingButton } from '@mui/lab';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';

import ConfirmationModal from 'modules/Dashboard/common/ConfirmationModal';
import collectiveApi from 'modules/common/collectiveApi';

const DeleteFile = ({ uuid, setError, fetchData }) => {
    const deleteCsvHistory = useMutation(
        (id) => {
            return collectiveApi.delete(`/taxtracker/tax-return-csv-history/${id}/`);
        },
        {
            onSuccess: () => fetchData(1),
            onError: (error) => {
                setError(error.response.data.detail || error.message);
            },
        }
    );

    return (
        <ConfirmationModal
            trigger={
                <LoadingButton loading={deleteCsvHistory.isLoading} variant="contained" data-testid="csv-delete-button">
                    <DeleteForeverIcon />
                </LoadingButton>
            }
            title="Delete file"
            message="Do you want to delete this file permanently?"
            yesText="Delete"
            noText="Cancel"
            callback={() => deleteCsvHistory.mutate(uuid)}
        />
    );
};

DeleteFile.propTypes = {
    uuid: PropTypes.string.isRequired,
    setError: PropTypes.func.isRequired,
    fetchData: PropTypes.func.isRequired,
};

export default DeleteFile;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/DeleteFile/index.js
export { default } from './DeleteFile';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/CommitChangesSuccessModal/CommitChangesSuccessModal.test.jsx
import { render } from '@testing-library/react';

import CommitChangesSuccessModal from './CommitChangesSuccessModal';

describe('CommitChangesSuccessModal', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<CommitChangesSuccessModal isOpen onClose={() => {}} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render correctly when isOpen is false', () => {
        const { asFragment } = render(<CommitChangesSuccessModal isOpen={false} onClose={() => {}} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/CommitChangesSuccessModal/index.js
export { default } from './CommitChangesSuccessModal';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/CommitChangesSuccessModal/CommitChangesSuccessModal.module.less
.description {
    width: 250px;
    margin: 0 auto;
    font-size: 17px;
    line-height: 25px;
    font-weight: 400;
    padding: 20px 0;
    text-align: center;
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/CommitChangesSuccessModal/CommitChangesSuccessModal.jsx
import { Button, Paper, Typography, Modal } from '@mui/material';

import classes from './CommitChangesSuccessModal.module.less';

import csvUploadSuccess from 'assets/img/pcs-csv-upload-success.svg';

export default function CommitChangesSuccessModal({ isOpen, onClose }) {
    return (
        <Modal
            open={isOpen}
            onClose={onClose}
            aria-labelledby="modal-modal-title"
            aria-describedby="modal-modal-description">
            <Paper className="tw-absolute tw-p-4 -tw-translate-x-1/2 -tw-translate-y-1/2 tw-w-80 tw-left-1/2 tw-top-1/2">
                <div className="tw-pt-12 tw-pb-12">
                    <Typography id="modal-modal-title" className="tw-text-center" variant="h5" component="h2">
                        Member Data updated
                    </Typography>
                    <p id="modal-modal-description" className={classes.description}>
                        Updates will now be reflected on the client dashboard
                    </p>
                    <div className="tw-flex tw-justify-center">
                        <img src={csvUploadSuccess} alt="csv upload success" />
                    </div>
                    <div className="tw-mt-5 tw-justify-center tw-flex">
                        <Button variant="contained" onClick={onClose}>
                            Close
                        </Button>
                    </div>
                </div>
            </Paper>
        </Modal>
    );
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadData/index.js
export { default } from './UploadData';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadData/UploadData.test.jsx
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { render } from '@testing-library/react';

import UploadData, { ShowParsingErrors, UploadingFile, ShowUploadError } from './UploadData';

describe('UploadData', () => {
    const queryClient = new QueryClient();

    it('should render correctly', () => {
        const { asFragment } = render(
            <QueryClientProvider client={queryClient}>
                <UploadData />
            </QueryClientProvider>
        );
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    describe('ShowUploadError', () => {
        it('should render correctly', () => {
            const errors = [];
            const postFileUploadError = null;
            const isPostFileUploadError = false;

            const { asFragment } = render(
                <ShowUploadError
                    postFileUploadError={postFileUploadError}
                    errors={errors}
                    isPostFileUploadError={isPostFileUploadError}
                />
            );
            expect(asFragment().firstChild).toMatchSnapshot();
        });

        it('should render correctly with parsing errors defined', () => {
            const errors = [
                {
                    type: 'error',
                    message: 'Error message',
                },
            ];
            const postFileUploadError = null;
            const isPostFileUploadError = true;

            const { asFragment } = render(
                <ShowUploadError
                    postFileUploadError={postFileUploadError}
                    errors={errors}
                    isPostFileUploadError={isPostFileUploadError}
                />
            );
            expect(asFragment().firstChild).toMatchSnapshot();
        });

        it('should render correctly showing error message', () => {
            const errors = [];
            const postFileUploadError = {
                message: 'Error message',
            };
            const isPostFileUploadError = true;

            const { asFragment } = render(
                <ShowUploadError
                    postFileUploadError={postFileUploadError}
                    errors={errors}
                    isPostFileUploadError={isPostFileUploadError}
                />
            );
            expect(asFragment().firstChild).toMatchSnapshot();
        });
    });

    describe('UploadingFile', () => {
        it('should render correctly', () => {
            const isPostFileUploadLoading = false;

            const { asFragment } = render(<UploadingFile isPostFileUploadLoading={isPostFileUploadLoading} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });

        it('should render correctly with loading', () => {
            const isPostFileUploadLoading = true;

            const { asFragment } = render(<UploadingFile isPostFileUploadLoading={isPostFileUploadLoading} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });
    });

    describe('ShowParsingErrors', () => {
        it('should render correctly', () => {
            const errors = [];

            const { asFragment } = render(<ShowParsingErrors errors={errors} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });

        it('should render correctly with errors', () => {
            const errors = [
                {
                    type: 'error',
                    message: 'Error message',
                },
            ];

            const { asFragment } = render(<ShowParsingErrors errors={errors} />);
            expect(asFragment().firstChild).toMatchSnapshot();
        });
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadData/UploadData.jsx
import { useState, useEffect } from 'react';
import { DropzoneArea } from 'material-ui-dropzone';
import { useMutation, QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { Alert, Stack } from '@mui/material';

import classes from './UploadData.module.less';

import { hubClientAppSyncUploadFile } from 'modules/Hub/ClientAppSync/services';

const queryClient = new QueryClient();

export const ShowParsingErrors = ({ errors }) => {
    if (errors.length === 0) {
        return null;
    }

    return (
        <Stack className={classes.alertStack} sx={{ width: '100%' }} spacing={2}>
            {errors.map((error) => (
                <Alert key={error.message} severity={error.type}>
                    {error.message}
                </Alert>
            ))}
        </Stack>
    );
};

export const UploadingFile = ({ isPostFileUploadLoading }) => {
    if (!isPostFileUploadLoading) {
        return null;
    }

    return (
        <Stack className={classes.alertStack} sx={{ width: '100%' }} spacing={2}>
            <Alert severity="info">Uploading file...</Alert>
        </Stack>
    );
};

export const ShowUploadError = ({ errors, isPostFileUploadError, postFileUploadError }) => {
    if (errors.length > 0 || !isPostFileUploadError) {
        return null;
    }

    return (
        <Stack className={classes.alertStack} sx={{ width: '100%' }} spacing={2}>
            <Alert severity="error">{postFileUploadError.message}</Alert>
        </Stack>
    );
};

const UploadData = ({ onUploadedFile }) => {
    const [file, setFile] = useState(null);
    const [errors, setErrors] = useState([]);

    const {
        error: postFileUploadError,
        isError: isPostFileUploadError,
        isLoading: isPostFileUploadLoading,
        mutate,
    } = useMutation(hubClientAppSyncUploadFile, {
        onSuccess: ({ data }) => {
            onUploadedFile(file, data);
        },
        onError: ({ response: { data } }) => {
            setErrors(data.messages);
        },
    });

    const saveFile = () => {
        const formData = new FormData();

        formData.append('file', file);

        mutate(formData);
    };

    useEffect(() => {
        if (file !== null) {
            saveFile();
        }
    }, [file]);

    return (
        <QueryClientProvider client={queryClient}>
            <ShowUploadError
                errors={errors}
                isPostFileUploadError={isPostFileUploadError}
                postFileUploadError={postFileUploadError}
            />
            <UploadingFile isPostFileUploadLoading={isPostFileUploadLoading} />
            <ShowParsingErrors errors={errors} />
            <DropzoneArea
                clearOnUnmount={false}
                acceptedFiles={['text/csv']}
                dropzoneText="Upload a file or drag and drop .CSV"
                dropzoneClass={classes.dropzone}
                previewText="Selected file"
                filesLimit={1}
                showFileNames
                showFileNamesInPreview
                useChipsForPreview
                showPreviewsInDropzone={false}
                showPreviews
                showAlerts={['error']}
                previewGridClasses={{ container: classes.previewGridContainer, item: classes.previewGridItem }}
                onDrop={(files) => setFile(files[0])}
            />
        </QueryClientProvider>
    );
};

export default UploadData;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadData/UploadData.module.less
.dropzone {
    border: 2px dashed #d1d5dc;
    border-radius: 4px;
    background: #ffffff;
    padding: 20px;
    text-align: center;
    margin-bottom: 20px;
    min-height: 150px !important;

    div {
        display: flex;
        flex-direction: column-reverse;
        justify-content: center;
        align-items: center;
        p {
            color: #1c1c1a;
        }
    }
}

.previewGridContainer {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
}

.previewGridItem {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    align-content: center;
    margin: 10px;
    width: 200px;
    border: 1px solid #000;
    border-radius: 5px;
    background: #fff;
    cursor: pointer;
    min-height: 132px;
}

.alertStack {
    margin-bottom: 20px;
}
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/MissingMembersTable/MissingMembersTable.jsx
import { useState, useEffect } from 'react';
import {
    Table,
    TableBody,
    Divider,
    Typography,
    TableCell,
    TableHead,
    TableRow,
    TextField,
    FormControl,
} from '@mui/material';
import Pagination from '@mui/material/Pagination';

let delayTimer = null;

const MissingMembersTable = ({ data, maxRowsPerPage = 25 }) => {
    const [originalMissingMembers, setOriginalMissingMembers] = useState(null);
    const [missingMembers, setMissingMembers] = useState(null);
    const [selectedMembers, setSelectedMembers] = useState([]);
    const [searchTerm, setSearchTerm] = useState('');
    const [page, setPage] = useState(1);

    const buildRecordData = () => {
        const members = data.map((row) => {
            row.searchableText = `${row.fullname} ${row.email} ${row.sfaccountid}`;
            return row;
        });
        setMissingMembers(members);
        setOriginalMissingMembers(members);
        setSelectedMembers([...members.slice(0, maxRowsPerPage)]);
    };

    useEffect(() => {
        if (missingMembers === null) {
            buildRecordData();
        }
    }, [data]);

    const handleSearch = (e) => {
        const search = e.target.value;
        clearTimeout(delayTimer);
        delayTimer = setTimeout(() => {
            setSearchTerm(search);
        }, 500);
    };

    useEffect(() => {
        if (searchTerm !== '') {
            const filteredMembers = originalMissingMembers.filter((member) => {
                const terms = searchTerm.split(' ');
                // search using AND logic
                const returnMatches = terms.map((term) => {
                    return member.searchableText.toLowerCase().includes(term.toLowerCase());
                });
                return returnMatches.every((match) => match === true);
            });
            setMissingMembers(filteredMembers);
            setSelectedMembers([...filteredMembers.slice(0, maxRowsPerPage)]);
        }

        if (searchTerm === '') {
            buildRecordData();
        }
    }, [searchTerm]);

    const handleSetPage = (e, value) => {
        const start = (value - 1) * maxRowsPerPage;
        const end = start + maxRowsPerPage;
        const newMembers = [...missingMembers.slice(start, end)];
        setSelectedMembers(newMembers);
    };

    if (missingMembers === null) {
        return null;
    }

    return (
        <>
            <FormControl>
                <TextField
                    id="search"
                    label="Member search"
                    type="search"
                    variant="outlined"
                    size="small"
                    inputProps={{ 'data-testid': 'search-input' }}
                    onChange={handleSearch}
                />
            </FormControl>
            <Divider className="tw-mt-2 tw-mb-2" />
            <Typography variant="h6" gutterBottom component="div">
                Showing <strong>{missingMembers.length}</strong> of <strong>{data.length}</strong> missing members
            </Typography>
            <Pagination
                className="tw-mb-2"
                size="small"
                onChange={handleSetPage}
                count={Math.ceil(missingMembers.length / maxRowsPerPage)}
                variant="outlined"
                shape="rounded"
                color="primary"
                data-testid="pagination-top"
                showFirstButton
                showLastButton
                defaultPage={1}
            />
            <Table>
                <TableHead>
                    <TableRow>
                        <TableCell align="center" className="tw-font-black">
                            ID
                        </TableCell>
                        <TableCell align="center" className="tw-font-black">
                            SF Account ID
                        </TableCell>
                        <TableCell className="tw-font-black">Member Name</TableCell>
                        <TableCell className="tw-font-black">Member Email</TableCell>
                    </TableRow>
                </TableHead>
                <TableBody>
                    {selectedMembers.map((row) => (
                        <TableRow key={row.sfaccountid}>
                            <TableCell align="center" component="th" scope="row">
                                {row.sfaccountid_short}
                            </TableCell>
                            <TableCell align="center">{row.sfaccountid}</TableCell>
                            <TableCell>{row.fullname}</TableCell>
                            <TableCell>{row.email}</TableCell>
                        </TableRow>
                    ))}
                </TableBody>
            </Table>
            <Divider className="tw-mt-2 tw-mb-2" />
            <Pagination
                size="small"
                onChange={handleSetPage}
                count={Math.ceil(missingMembers.length / maxRowsPerPage)}
                variant="outlined"
                shape="rounded"
                color="primary"
                showFirstButton
                showLastButton
                data-testid="pagination-bottom"
                defaultPage={1}
            />
        </>
    );
};

export default MissingMembersTable;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/MissingMembersTable/index.js
export { default } from './MissingMembersTable';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/MissingMembersTable/MissingMembersTable.test.js
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { ThemeProvider } from '@mui/material';

import MissingMembersTable from './MissingMembersTable';

import theme from 'theme';

const missingMembersData = [
    {
        sfaccountid: 'JOHN456789ABCDEFGH',
        sfaccountid_short: '9ABCDEFGH',
        fullname: 'John Doe',
        email: 'johndoe@domain.com',
    },
    {
        sfaccountid: 'JANE456789ABCDEFGH',
        sfaccountid_short: '9ABCDEFGH',
        fullname: 'Jane Doe',
        email: 'janedoe@domain.com',
    },
];

const setup = (customProps) => {
    const props = {
        data: [],
        ...customProps,
    };
    return render(
        <ThemeProvider theme={theme}>
            <MissingMembersTable {...props} />
        </ThemeProvider>
    );
};

describe('MissingMembersTable', () => {
    it('should render successfully with empty missing members list', async () => {
        const { asFragment } = setup();
        expect(asFragment()).toMatchSnapshot();
    });

    it('should render successfully with missing members list', async () => {
        const { asFragment } = setup({ data: missingMembersData });
        expect(asFragment()).toMatchSnapshot();
    });

    it('should render after click to go to next page', async () => {
        const { asFragment, container, getByText } = setup({ data: missingMembersData, maxRowsPerPage: 1 });

        container.querySelector('button[aria-label="Go to page 2"]').click();

        await waitFor(() => {
            expect(getByText('Jane Doe')).toBeInTheDocument();
        });
        expect(asFragment()).toMatchSnapshot();
    });

    it('should filter members after type on search input', async () => {
        const { asFragment, container, getByText, getByTestId } = setup({
            data: missingMembersData,
            maxRowsPerPage: 1,
        });

        const searchInput = getByTestId('search-input');
        fireEvent.change(searchInput, { target: { value: 'JANE456789ABCDEFGH' } });

        await waitFor(() => {
            expect(getByText('Jane Doe')).toBeInTheDocument();
        });
    });
});
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadHistory/UploadHistory.jsx
import { useState, useEffect } from 'react';
import { Alert } from '@mui/lab';
import { Typography } from '@mui/material';
import queryString from 'query-string';

import UploadHistoryList from '../UploadHistoryList';

import { fetchTaxReturnCSVHistory } from 'modules/Hub/ClientAppSync/services';

const UploadHistory = () => {
    const [isLoadingUploadHistory, setIsLoadingUploadHistory] = useState(false);
    const [uploadHistory, setUploadHistory] = useState(null);
    const [uploadHistoryPagination, setUploadHistoryPagination] = useState(null);
    const [error, setError] = useState(null);

    const fetchData = async (page) => {
        setIsLoadingUploadHistory(true);
        const uploadHistoryResponse = await fetchTaxReturnCSVHistory(page);
        setUploadHistory(uploadHistoryResponse);
        setIsLoadingUploadHistory(false);
    };

    const handlePageChange = (page) => {
        fetchData(page);
    };

    useEffect(() => {
        fetchData(1);
    }, []);

    useEffect(() => {
        const next_page = uploadHistory?.next_page ? queryString.parseUrl(uploadHistory?.next_page).query.page : null;
        const previous_page = uploadHistory?.previous_page
            ? queryString.parseUrl(uploadHistory?.previous_page).query.page || 1
            : null;
        setUploadHistoryPagination({
            total_record_count: uploadHistory?.total_record_count || uploadHistory?.results.length || 0,
            number_of_pages: uploadHistory?.number_of_pages,
            next_page: parseInt(next_page, 10),
            previous_page: parseInt(previous_page, 10),
        });
    }, [uploadHistory]);

    return (
        <>
            {error && (
                <Alert
                    onClose={() => {
                        setError(null);
                    }}
                    className="tw-mb-2"
                    severity="error">
                    <Typography>{error}</Typography>
                </Alert>
            )}
            {!isLoadingUploadHistory && (
                <UploadHistoryList
                    csvData={uploadHistory?.results || []}
                    csvDataPagination={uploadHistoryPagination}
                    handlePageChange={handlePageChange}
                    setError={setError}
                    fetchData={fetchData}
                />
            )}
        </>
    );
};

export default UploadHistory;
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadHistory/index.js
export { default } from './UploadHistory';
// Code from file collective-frontend/src/modules/Hub/ClientAppSync/Home/UploadHistory/UploadHistory.test.jsx
import { render, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';

import UploadHistory from './UploadHistory';

import collectiveApi from 'modules/common/collectiveApi';

const mockCollectiveApi = new MockAdapter(collectiveApi);

describe('UploadHistory', () => {
    it('should render correctly', async () => {
        mockCollectiveApi.onGet('taxtracker/tax-return-csv-history/').replyOnce(204, {
            total_record_count: 2,
            number_of_pages: 'Page 1 of 1',
            next_page: null,
            previous_page: null,
            results: [],
        });

        const { asFragment, getByText } = render(<UploadHistory />);
        await waitFor(() => {
            expect(getByText('Created')).toBeInTheDocument();
        });
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/common/TransitionInput.js
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import ClickOutHandler from 'react-onclickout';

import RootStoreContext from 'modules/common/stores/Root/Context';

class TransitionInput extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
        this.state = { edit: false, hover: false, oldValue: '' };
        this.clientInfoEditableFields = [
            'home_aptunit',
            'home_street',
            'home_city',
            'home_state',
            'home_zipcode',
            'home_number',
            'business_street',
            'business_number',
            'business_city',
            'business_state',
            'business_zipcode',
            'business_aptunit',
            'mail_street',
            'mail_number',
            'mail_city',
            'mail_state',
            'mail_zipcode',
            'mail_aptunit',
            'business_name',
            'entity_number',
            'ein',
            'federal_business_name',
            'registered_agent',
            'state_of_incorporation',
            'state_of_operation',
        ];
        this.editableClientInfoField = this.props.clientInfo && this.clientInfoEditableFields.includes(this.props.name);
        this.addDollar = false;
    }

    enterEditMode = () => {
        if (!(this.props.clientInfo || this.store.transactionInfo.approved) || this.editableClientInfoField) {
            this.setState({
                oldValue: this.props.index
                    ? this.store.transactionInfo[this.props.name][this.props.index - 1]
                    : this.store.transactionInfo[this.props.name],
                edit: true,
            });
        }
    };

    cancelEdit = () => {
        if (!this.store.keepEmployeeInputState) {
            if (this.props.index) {
                this.store.transactionInfo[this.props.name][this.props.index - 1] = this.state.oldValue;
            } else {
                this.store.transactionInfo[this.props.name] = this.state.oldValue;
            }
            this.setState({ edit: false });
        }
    };

    saveEdit = async (removeBtn) => {
        if (this.editableClientInfoField) {
            this.store.updateClientInfo(this.props.name);
        } else if (!this.store.transactionInfo.approved) {
            if (this.props.name.includes('advised_salary')) {
                this.store.transactionInfo[this.props.name] =
                    Math.round(this.store.transactionInfo[this.props.name] / 1000) * 1000;
                await this.store.getTransitionPlanPotentialSavings();
            }
            const data = { [this.props.name]: this.store.transactionInfo[this.props.name] };
            await this.store.patchTransitionPlanInfo(data);
            this.store.keepEmployeeInputState = false;
        }

        if (removeBtn) {
            this.setState({
                edit: false,
                hover: false,
            });
        } else {
            this.setState({
                edit: false,
            });
        }
    };

    convertToBoolean = (e) => {
        let value = false;
        if (e.target.value === 'Yes') {
            value = true;
        }

        if (e.target.value === 'Select an option') {
            value = null;
        }

        this.store.transactionInfo[this.props.name] = value;
    };

    setHoverTrue = () => {
        if (!(this.props.clientInfo || this.store.transactionInfo.approved) || this.editableClientInfoField) {
            this.setState({ hover: true });
        }
    };

    handleInputChange = (e) => {
        if (!this.store.transactionInfo.approved) {
            if (this.props.index) {
                this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][this.props.name][
                    this.props.index - 1
                ] = e.target.value;
            } else {
                this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][this.props.name] =
                    e.target.value;
            }
        }
    };

    render() {
        this.dollarValue = this.props.dollar ? `${this.store.transactionInfo[this.props.name]}` : '';
        if (this.dollarValue) {
            this.addDollar = !this.dollarValue.includes('$');
        }

        return this.props.notes ? (
            <div
                onMouseEnter={this.setHoverTrue}
                onMouseLeave={() => this.setState({ hover: false })}
                className="transition-input transition-note">
                <textarea
                    placeholder="Add a note about this section"
                    value={this.store.transactionInfo[this.props.name]}
                    onChange={(e) => this.handleInputChange(e)}
                />

                {this.state.hover && (
                    <div onClick={() => this.saveEdit(true)} className="transition-edit">
                        Save
                    </div>
                )}
            </div>
        ) : this.props.choices ? (
            <div
                onMouseEnter={this.setHoverTrue}
                onMouseLeave={() => this.setState({ hover: false })}
                className="transition-input">
                <select onChange={(e) => this.handleInputChange(e)} value={this.store.transactionInfo[this.props.name]}>
                    <option value="">Select an option</option>
                    {this.props.choices.map((choice) => (
                        <option value={choice.value}>{choice.name}</option>
                    ))}
                </select>
                {this.state.hover && (
                    <div onClick={() => this.saveEdit(true)} className="transition-edit">
                        Save
                    </div>
                )}
            </div>
        ) : this.props.link ? (
            <div
                onMouseEnter={this.setHoverTrue}
                onMouseLeave={() => this.setState({ hover: false })}
                className="transition-input transition-note">
                <a href={this.store.transactionInfo[this.props.name]} target="_blank" rel="noreferrer">
                    {this.store.transactionInfo[this.props.name]}
                </a>
            </div>
        ) : (
            <div
                className="transition-input"
                onMouseEnter={this.setHoverTrue}
                onMouseLeave={() => this.setState({ hover: false })}>
                {this.state.edit && (
                    <ClickOutHandler onClickOut={this.cancelEdit}>
                        {this.props.boolean ? (
                            <select
                                onChange={(e) => this.convertToBoolean(e)}
                                value={
                                    this.props.value ||
                                    (this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][
                                        this.props.name
                                    ] &&
                                    this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][
                                        this.props.name
                                    ] === true
                                        ? 'Yes'
                                        : this.store[
                                              this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'
                                          ][this.props.name] === false
                                        ? 'No'
                                        : 'Select an option')
                                }>
                                <option>Select an option</option>
                                <option>Yes</option>
                                <option>No</option>
                            </select>
                        ) : (
                            <input
                                type={this.props.dollar ? 'number' : 'text'}
                                placeholder={this.props.placeholder}
                                value={
                                    this.props.index
                                        ? this.store[
                                              this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'
                                          ][this.props.name][this.props.index - 1]
                                        : this.store[
                                              this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'
                                          ][this.props.name]
                                }
                                onChange={(e) => this.handleInputChange(e)}
                            />
                        )}

                        {this.state.edit && (
                            <div onClick={this.saveEdit} className="transition-edit">
                                Save
                            </div>
                        )}
                    </ClickOutHandler>
                )}

                {!this.state.edit && (
                    <>
                        <span onClick={this.enterEditMode}>
                            {this.props.boolean
                                ? this.props.value ||
                                  (this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][
                                      this.props.name
                                  ] &&
                                  this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][
                                      this.props.name
                                  ] === true
                                      ? 'Yes'
                                      : this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][
                                            this.props.name
                                        ] === false
                                      ? 'No'
                                      : 'Select an option')
                                : this.props.index
                                ? this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][
                                      this.props.name
                                  ][this.props.index - 1]
                                : `${this.props.dollar ? (this.addDollar ? '$' : '') : ''}${
                                      this.store[this.props.clientInfo ? 'transitionClientInfo' : 'transactionInfo'][
                                          this.props.name
                                      ]
                                  }`}
                        </span>

                        {this.state.hover && (
                            <div onClick={this.enterEditMode} className="transition-edit">
                                Edit
                            </div>
                        )}
                    </>
                )}
            </div>
        );
    }
}

export default observer(TransitionInput);
// Code from file collective-frontend/src/modules/Hub/common/TransitionNavBar.test.jsx
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { action, observable, makeObservable } from 'mobx';
import { screen, render, cleanup } from '@testing-library/react';

import TransitionNavBar from './TransitionNavBar';

import RootStoreContext from 'modules/common/stores/Root/Context';

class MockLegacyStore {
    email = 'elon@tesla.com';

    fullname = 'Elon Musk';

    sfaccountid = '0000000000000000';

    bookkeeperTrainingStatus = 'call';

    bookkeepingCallReady = false;

    bookkeeperTrainingTaker = '';

    bookkeeperTrainingImage = '';

    bookkeeperTrainingTime = '';

    bookkeeperTrainingLink = '';

    usertype = 'SC-SC';

    transitionClientInfo = {
        member_id: 123,
        fullname: '',
        sf_id: '',
        transition_plan_id: '',
        business_name: '',
    };

    fetchTrainingCallPrep = () => jest.fn();

    fetchCallScheduler = () => jest.fn();

    onboardingReadyToSchedule = jest.fn().mockImplementation((checked) => {
        this.bookkeepingCallReady = checked;
    });

    constructor() {
        makeObservable(this, {
            bookkeepingCallReady: observable,
            bookkeeperTrainingStatus: observable,
            bookkeeperTrainingTaker: observable,
            bookkeeperTrainingImage: observable,
            bookkeeperTrainingTime: observable,
            bookkeeperTrainingLink: observable,
            fetchTrainingCallPrep: action,
            fetchCallScheduler: action,
            onboardingReadyToSchedule: action,
        });
    }
}

class MockGustoStore {
    fetchCompanyOnboardingStatus = jest.fn().mockImplementation(() => ({
        onboarding_qualified: true,
        onboarding_completed: false,
        onboarding_steps: [
            { title: "Add Your Company's Addresses", id: 'add_addresses', completed: false },
            { title: 'Enter Your Federal Tax Information', id: 'federal_tax_setup', completed: false },
            { title: 'Select Industry', id: 'select_industry', completed: false },
            { title: 'Add Your Bank Account', id: 'add_bank_info', completed: false },
            { title: 'Add Your Employees', id: 'add_employees', completed: false },
            { title: 'Enter Your State Tax Information', id: 'state_setup', completed: false },
            { title: 'Select a Pay Schedule', id: 'payroll_schedule', completed: false },
            { title: 'Sign Documents', id: 'sign_all_forms', completed: false },
            { title: 'Verify Your Bank Account', id: 'verify_bank_info', completed: false },
        ],
    }));

    constructor() {
        makeObservable(this, {
            fetchCompanyOnboardingStatus: action,
        });
    }
}

const renderWithStores = (legacyStore, gustoStore) =>
    render(
        <RootStoreContext.Provider value={{ legacyStore, gustoStore }}>
            <MemoryRouter>
                <TransitionNavBar />
            </MemoryRouter>
        </RootStoreContext.Provider>
    );

beforeEach(cleanup);

describe('TransitionNavBar', () => {
    it('should match gusto onboarding on', async () => {
        const legacyStore = new MockLegacyStore();
        const gustoStore = new MockGustoStore();
        renderWithStores(legacyStore, gustoStore);

        const element = await screen.findByText(/Payroll Setup/i);
        expect(element).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/common/UploadDocument.js
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { Modal, Button } from 'semantic-ui-react';

import RootStoreContext from 'modules/common/stores/Root/Context';
import UploadToFilestack from 'modules/common/UploadToFilestack';

class UploadTransitionDocument extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;

        this.state = { showModal: false, documentName: '' };
    }

    componentDidMount() {
        if (this.props.requiredDoc) {
            this.setState({
                documentName: this.props.documentName,
            });
        }
    }

    uploadDocModal = () => {
        if (this.props.requiredDoc) {
            return;
        }

        this.setState({
            showModal: true,
        });
    };

    onSuccess = (result) => {
        this.store.setUploadedDocuments(this.state.documentName, result.filesUploaded, 1);
        this.store.uploadUserDocuments(this.state.documentName, this.store.transactionInfo.email);
        this.setState({ showModal: false }, () => {
            this.store.getTransactionInfo(this.store.transactionClientId);

            if (this.props.requiredDoc) {
                window.setTimeout(() => {
                    this.props.refresh();
                }, 1500);
            } else {
                window.setTimeout(() => {
                    this.store.fetchTransitionUserDocuments(this.store.transactionInfo.email);
                    this.props.update();
                    // window.location.reload(false);
                }, 1500);
            }
        });
    };

    render() {
        return (
            <>
                <div className="transition-document dashed">
                    <div className="doc-container">
                        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                            <path
                                d="M18.414 6.414L15.586 3.586C15.211 3.211 14.702 3 14.172 3H7C5.895 3 5 3.895 5 5V19C5 20.105 5.895 21 7 21H17C18.105 21 19 20.105 19 19V7.828C19 7.298 18.789 6.789 18.414 6.414V6.414Z"
                                stroke="#A3998F"
                                strokeWidth="1.5"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                            <path
                                d="M19 8H15C14.448 8 14 7.552 14 7V3"
                                stroke="#A3998F"
                                strokeWidth="1.5"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                        </svg>
                        <div>
                            {this.props.requiredDoc ? (
                                <span>
                                    {this.props.documentName}{' '}
                                    <span style={{ color: '#CECABD', marginLeft: '12px' }}>Not uploaded by client</span>
                                </span>
                            ) : (
                                'Missing a file?'
                            )}
                        </div>
                    </div>

                    <div className="doc-container">
                        <div>
                            {this.props.requiredDoc ? (
                                <UploadToFilestack primary size="mini" buttonText="Upload" onSuccess={this.onSuccess} />
                            ) : (
                                <Button primary size="mini" onClick={this.uploadDocModal}>
                                    Upload
                                </Button>
                            )}
                        </div>
                    </div>
                </div>

                <Modal
                    size="large"
                    className={`hyke-modal ${
                        process.env.REACT_APP_COMPANY_NAME === 'Collective' ? 'collective-modal' : ''
                    }`}
                    open={this.state.showModal}
                    onClose={() => this.setState({ showModal: false })}
                    closeIcon>
                    <Modal.Content>
                        <div className="modal-body">
                            <h1>Upload document</h1>
                            <div>
                                <div>
                                    <div>Document Name:</div>
                                    <select
                                        value={this.state.documentName}
                                        onChange={(e) => this.setState({ documentName: e.target.value })}>
                                        <option>Articles of Organization</option>
                                        <option>IRS EIN</option>
                                        <option>Operating Agreement</option>
                                        <option>Business License</option>
                                        <option>Tax Return 2019</option>
                                        <option>Tax Return 2020</option>
                                        <option>Statement of Information</option>
                                        <option>Annual Report</option>
                                        <option>Form 2553</option>
                                        <option>Business Tax Return</option>
                                    </select>
                                </div>

                                <UploadToFilestack primary buttonText="Upload document(s)" onSuccess={this.onSuccess} />
                            </div>
                        </div>
                    </Modal.Content>
                </Modal>
            </>
        );
    }
}

export default observer(UploadTransitionDocument);
// Code from file collective-frontend/src/modules/Hub/common/TimeoutModal.js
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { Button } from 'semantic-ui-react';

import modalIllustration from 'assets/img/icons/dashboard/small-modal.svg';
import RootStoreContext from 'modules/common/stores/Root/Context';

class TimeoutModal extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
    }

    componentDidMount() {
        this.store.keepEmployeeInputState = true;
    }

    submit = () => {
        this.store.resetTimer();
    };

    render() {
        return (
            <div className="modal-backdrop">
                <div style={{ height: '380px' }} className="annual-profit-modal">
                    <div className="annual-profit-top">
                        <img alt="modal-illustration" src={modalIllustration} />
                    </div>

                    <div className="annual-profit-content">
                        <h1>Still using this page?</h1>

                        <Button primary onClick={this.submit}>
                            Click here to continue with your session
                        </Button>
                    </div>

                    <div onClick={this.submit} className="exit-annual-profit">
                        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                            <path
                                d="M6 6L18 18"
                                stroke="#A3998F"
                                strokeWidth="2"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                            <path
                                d="M18 6L6 18"
                                stroke="#A3998F"
                                strokeWidth="2"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                        </svg>
                    </div>
                </div>
            </div>
        );
    }
}

export default observer(TimeoutModal);
// Code from file collective-frontend/src/modules/Hub/common/TransitionNavBar.jsx
import { useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import SVG from 'react-inlinesvg';
import { withRouter } from 'react-router-dom';
import MonetizationOnIcon from '@mui/icons-material/MonetizationOn';
import Payments from '@mui/icons-material/Payments';

import useRootStore from 'modules/common/stores/Root/useRootStore';
import useTreatment from 'modules/common/useTreatment';
import config from 'modules/common/config';

const TransitionNavBar = (props) => {
    const { gustoStore, legacyStore } = useRootStore();

    const [showPayrollSetup, setShowPayrollSetup] = useState(false);

    useEffect(() => {
        const getPayrollOnboardingStatus = async () => {
            try {
                const data = await gustoStore.fetchCompanyOnboardingStatus(legacyStore.transitionClientInfo.member_id);

                const onboardingQualified = data.onboarding_qualified;
                setShowPayrollSetup(onboardingQualified);
            } catch (error) {
                // do nothing, note the calling function handles error logging
                setShowPayrollSetup(false);
            }
        };
        if (legacyStore.transitionClientInfo.member_id) {
            getPayrollOnboardingStatus();
        }
    }, [legacyStore.transitionClientInfo.member_id]);

    const getInitials = () => {
        let name = legacyStore.transitionClientInfo.fullname || '';
        if (!name) {
            return '';
        }

        let initials = '';
        name = name.split(' ');
        if (name) {
            name.forEach((word) => {
                if (word) {
                    initials += word[0].toUpperCase();
                }
            });
        }

        return initials;
    };

    const goToTransitionPlan = () => {
        if (legacyStore.transitionClientInfo.transition_plan_id) {
            props.history.push(`/hub/transition-plan/${legacyStore.transitionClientInfo.transition_plan_id}/edit`);
        } else {
            legacyStore.globalError = 'This client does not have a transition plan.';
        }
    };

    const goToPayments = () => {
        props.history.push(`/hub/client-info/${props.sfid}/payments/${legacyStore.clientInQuestion.stripecustomerid}`);
    };

    return (
        <>
            <div className="transition-top-nav">
                <a href="/hub/client-info/search">
                    <img
                        src="https://collective.com/images/collective__logo.svg"
                        alt="Collective."
                        className="header__logo"
                    />
                </a>
            </div>

            <div className="transition-plan-nav-bar">
                <div className="trans-info-row">
                    <div className="client-initials">{getInitials()}</div>
                    <div className="client-name">
                        <h3>{legacyStore.transitionClientInfo.fullname}</h3>
                        <p>{legacyStore.transitionClientInfo.business_name}</p>
                    </div>
                </div>

                <div className="transition-nav-links">
                    <div
                        onClick={goToTransitionPlan}
                        className={`trans-plan-btn ${props.edit ? 'active' : ''} ${
                            !legacyStore.transitionClientInfo.transition_plan_id ? 'disabled' : ''
                        }`}>
                        <div className="btn-svg">
                            <svg
                                width="40"
                                height="40"
                                viewBox="0 0 40 40"
                                fill="none"
                                xmlns="http://www.w3.org/2000/svg">
                                <circle cx="20" cy="20" r="20" fill="#5700CC" />
                                <path
                                    d="M30 21.8759V21.8748C30 20.2862 28.9975 18.8705 27.4989 18.343V17.7092C27.4989 13.5671 24.1411 10.2092 19.999 10.2092C15.8568 10.2092 12.499 13.5671 12.499 17.7092V18.3434C10.5458 19.0323 9.5209 21.174 10.2098 23.1272C10.5873 24.1976 11.4294 25.0395 12.5 25.4167C13.1509 25.6468 13.865 25.3058 14.0952 24.6549C14.143 24.5195 14.1672 24.377 14.1667 24.2334V17.7092V17.7092C14.1667 14.4876 16.7783 11.8759 20 11.8759C23.2216 11.8759 25.8333 14.4876 25.8333 17.7092V24.2334V24.2334C25.8369 24.584 25.9882 24.9168 26.25 25.1501V25.6234C26.25 26.8517 25.1741 27.2901 24.1666 27.2901H22.685C22.2247 26.4929 21.2054 26.2198 20.4083 26.68C19.6111 27.1402 19.338 28.1596 19.7982 28.9567C20.2585 29.7539 21.2778 30.027 22.0749 29.5668C22.3283 29.4205 22.5387 29.2101 22.685 28.9567H24.1666C26.3741 28.9567 27.9166 27.5859 27.9166 25.6234V25.2251C29.1895 24.5935 29.9961 23.2968 30 21.8759H30Z"
                                    fill="white"
                                />
                            </svg>
                        </div>
                        <h3>Transition Plan</h3>
                    </div>
                    <div
                        onClick={() =>
                            props.history.push(`/hub/client-info/${legacyStore.transitionClientInfo.sf_id}/info`)
                        }
                        className={`trans-plan-btn ${
                            !props.edit && !props.payments && !props.payrollSetup ? 'active' : ''
                        }`}>
                        <div className="btn-svg">
                            <svg
                                width="40"
                                height="40"
                                viewBox="0 0 40 40"
                                fill="none"
                                xmlns="http://www.w3.org/2000/svg">
                                <circle cx="20" cy="20" r="20" fill="#5700CC" />
                                <path
                                    d="M20 21.9165C21.3807 21.9165 22.5 20.7972 22.5 19.4165C22.5 18.0358 21.3807 16.9165 20 16.9165C18.6193 16.9165 17.5 18.0358 17.5 19.4165C17.5 20.7972 18.6193 21.9165 20 21.9165Z"
                                    fill="white"
                                />
                                <path
                                    d="M19.9998 22.75C18.3106 22.7489 16.746 23.6386 15.8831 25.0908C15.8124 25.2091 15.8124 25.3567 15.8831 25.475C15.9581 25.5959 16.0909 25.6687 16.2331 25.6667H23.7648C23.9054 25.6674 24.0363 25.5952 24.1106 25.4758C24.1823 25.3578 24.1823 25.2097 24.1106 25.0917C23.2507 23.6393 21.6877 22.749 19.9998 22.75Z"
                                    fill="white"
                                />
                                <path
                                    d="M26.2497 11.5H24.1663C23.9362 11.5 23.7497 11.6865 23.7497 11.9167V12.75C23.7497 12.9801 23.9362 13.1667 24.1663 13.1667H25.833C26.0631 13.1667 26.2497 13.3532 26.2497 13.5833V26.9167C26.2497 27.1468 26.0631 27.3333 25.833 27.3333H14.1663C13.9362 27.3333 13.7497 27.1468 13.7497 26.9167V13.5833C13.7497 13.3532 13.9362 13.1667 14.1663 13.1667H15.833C16.0631 13.1667 16.2497 12.9801 16.2497 12.75V11.9167V11.9167C16.2497 11.6865 16.0631 11.5 15.833 11.5H13.7497C12.8292 11.5 12.083 12.2462 12.083 13.1667V27.3333C12.083 28.2538 12.8292 29 13.7497 29H26.2497C27.1701 29 27.9163 28.2538 27.9163 27.3333V13.1667C27.9163 12.2462 27.1701 11.5 26.2497 11.5V11.5Z"
                                    fill="white"
                                />
                                <path
                                    d="M17.5 14C17.5 14.6904 18.0596 15.25 18.75 15.25H21.25C21.9404 15.25 22.5 14.6904 22.5 14V12.5C22.5 11.1193 21.3807 10 20 10C18.6193 10 17.5 11.1193 17.5 12.5V14ZM19.1667 12.0833C19.1667 11.6231 19.5398 11.25 20 11.25C20.4602 11.25 20.8333 11.6231 20.8333 12.0833C20.8333 12.5436 20.4602 12.9167 20 12.9167C19.5398 12.9167 19.1667 12.5436 19.1667 12.0833Z"
                                    fill="white"
                                />
                            </svg>
                        </div>
                        <h3>Client Information</h3>
                    </div>
                    <div onClick={goToPayments} className={`trans-plan-btn ${props.payments ? 'active' : ''}`}>
                        <div className="btn-svg">
                            <MonetizationOnIcon fontSize="large" />
                        </div>
                        <h3>Payments</h3>
                    </div>
                    {showPayrollSetup && (
                        <div
                            onClick={() =>
                                props.history.push(
                                    `/hub/client-info/${legacyStore.transitionClientInfo.member_id}/payroll-setup`
                                )
                            }
                            className={`trans-plan-btn ${props.payrollSetup ? 'active' : ''}`}>
                            <div className="btn-svg">
                                <Payments fontSize="large" />
                            </div>
                            <h3>Payroll Setup</h3>
                        </div>
                    )}
                </div>

                <div className="client-type">
                    <div className="client-type-content">
                        <p>Client Type</p>
                        <p>
                            <strong>
                                {legacyStore.transitionClientInfo.usertype ? (
                                    <>
                                        {legacyStore.transitionClientInfo.usertype !== 'SP-SC'
                                            ? 'Takeover'
                                            : 'Formation'}{' '}
                                        ({legacyStore.transitionClientInfo.usertype})
                                    </>
                                ) : (
                                    ''
                                )}
                            </strong>
                        </p>
                    </div>
                    <div className="client-type-content">
                        {/* <p>In Progress</p>
                            <p>
                                <strong>EIN, Banking</strong>
                            </p> */}
                    </div>
                </div>
            </div>
        </>
    );
};

export default withRouter(observer(TransitionNavBar));
// Code from file collective-frontend/src/modules/Hub/common/UploadParticularDocument/index.js
export { default } from './UploadParticularDocument';
// Code from file collective-frontend/src/modules/Hub/common/UploadParticularDocument/TransitionDocument.js
import React, { Component } from 'react';
import { Modal, Button } from 'semantic-ui-react';
import { observer } from 'mobx-react';

import RootStoreContext from 'modules/common/stores/Root/Context';

class TransitionDocument extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);

        this.store = context.legacyStore;
        this.state = { showDeleteModal: false };
    }

    toggleDeleteModal = () => {
        this.setState((previousState) => ({
            showDeleteModal: !previousState.showDeleteModal,
        }));
    };

    showDocument = (e, url) => {
        e.preventDefault();
        e.stopPropagation();
        this.store.fetchDocumentTemporaryLink(url, true);
    };

    removeUpload = (e) => {
        this.store.removeUploadedDocument(
            this.props.documentName,
            this.props.document,
            this.store.transactionInfo.email
        );
        this.store.getTransactionInfo(this.store.transactionClientId);
        window.setTimeout(() => {
            this.props.refresh();
        }, 1000);
        this.toggleDeleteModal();
    };

    render() {
        return (
            <>
                <div key={this.props.documentUrl} className="transition-document">
                    <div className="doc-container">
                        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                            <path
                                d="M18.414 6.414L15.586 3.586C15.211 3.211 14.702 3 14.172 3H7C5.895 3 5 3.895 5 5V19C5 20.105 5.895 21 7 21H17C18.105 21 19 20.105 19 19V7.828C19 7.298 18.789 6.789 18.414 6.414V6.414Z"
                                stroke="#A3998F"
                                strokeWidth="1.5"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                            <path
                                d="M19 8H15C14.448 8 14 7.552 14 7V3"
                                stroke="#A3998F"
                                strokeWidth="1.5"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                        </svg>
                        <div>{this.props.documentName}</div>
                    </div>

                    <div className="doc-container">
                        <a
                            href={this.props.documentUrl}
                            onClick={(e) => this.showDocument(e, this.props.documentUrl)}
                            target="_blank"
                            className="transition-document-icon"
                            rel="noreferrer">
                            <svg
                                className="no-margin"
                                width="24"
                                height="24"
                                viewBox="0 0 24 24"
                                fill="none"
                                xmlns="http://www.w3.org/2000/svg">
                                <path
                                    d="M3.11824 12.467C2.96124 12.176 2.96124 11.823 3.11824 11.532C5.01024 8.033 8.50524 5 12.0002 5C15.4952 5 18.9902 8.033 20.8822 11.533C21.0392 11.824 21.0392 12.177 20.8822 12.468C18.9902 15.967 15.4952 19 12.0002 19C8.50524 19 5.01024 15.967 3.11824 12.467V12.467Z"
                                    stroke="#A3998F"
                                    strokeWidth="1.5"
                                    strokeLinecap="round"
                                    strokeLinejoin="round"
                                />
                                <path
                                    d="M14.1213 9.87868C15.2929 11.0502 15.2929 12.9497 14.1213 14.1213C12.9497 15.2929 11.0502 15.2929 9.87868 14.1213C8.70711 12.9497 8.70711 11.0502 9.87868 9.87868C11.0502 8.70711 12.9497 8.70711 14.1213 9.87868Z"
                                    stroke="#A3998F"
                                    strokeWidth="1.4286"
                                    strokeLinecap="round"
                                    strokeLinejoin="round"
                                />
                            </svg>
                        </a>

                        <div className="transition-document-icon">
                            <svg
                                onClick={this.toggleDeleteModal}
                                style={{ cursor: 'pointer' }}
                                width="12"
                                height="12"
                                viewBox="0 0 12 12"
                                fill="none"
                                xmlns="http://www.w3.org/2000/svg">
                                <path
                                    d="M2 2L10 10"
                                    stroke="#A3998F"
                                    strokeWidth="1.5"
                                    strokeLinecap="round"
                                    strokeLinejoin="round"
                                />
                                <path
                                    d="M10 2L2 10"
                                    stroke="#A3998F"
                                    strokeWidth="1.5"
                                    strokeLinecap="round"
                                    strokeLinejoin="round"
                                />
                            </svg>
                        </div>
                    </div>
                </div>

                <Modal
                    size="small"
                    className={`hyke-modal ${
                        process.env.REACT_APP_COMPANY_NAME === 'Collective' ? 'collective-modal' : ''
                    }`}
                    open={this.state.showDeleteModal}
                    onClose={() => this.setState({ showDeleteModal: false })}
                    closeIcon>
                    <Modal.Content>
                        <div className="modal-body center">
                            <h1>
                                You're about to delete: <br />
                                {this.props.documentName}
                            </h1>
                        </div>
                    </Modal.Content>

                    <Modal.Actions>
                        <div className="aligned">
                            <Button
                                secondary
                                className="back-button"
                                size="huge"
                                content="Keep this file"
                                onClick={this.toggleDeleteModal}
                            />

                            <Button primary size="huge" content="Yes, delete" onClick={(e) => this.removeUpload(e)} />
                        </div>
                    </Modal.Actions>
                </Modal>
            </>
        );
    }
}

export default observer(TransitionDocument);
// Code from file collective-frontend/src/modules/Hub/common/UploadParticularDocument/UploadParticularDocument.js
import React, { Component } from 'react';
import { observer } from 'mobx-react';

import UploadDocument from '../UploadDocument';

import TransitionDocument from './TransitionDocument';

import RootStoreContext from 'modules/common/stores/Root/Context';

class UploadParticularDocument extends Component {
    static contextType = RootStoreContext;

    constructor(props, context) {
        super(props, context);
        this.store = context.legacyStore;
        this.state = { docs: [] };
    }

    componentDidMount() {
        this.refresh();
    }

    refresh = () => {
        const docs = [];
        this.store.transitionDocs.forEach((doc) => {
            if (doc['document-title'].includes(this.props.documentTitle)) {
                docs.push(doc);
            }
        });

        this.setState({
            docs,
        });
    };

    render() {
        return this.state.docs.length ? (
            this.state.docs.map((doc, index) => (
                <TransitionDocument
                    key={doc['document-url']}
                    documentName={doc['document-title']}
                    document={doc}
                    documentUrl={doc['document-url']}
                    refresh={this.refresh}
                />
            ))
        ) : (
            <UploadDocument requiredDoc documentName={this.props.documentTitle} refresh={this.refresh} />
        );
    }
}

export default observer(UploadParticularDocument);
// Code from file collective-frontend/src/modules/Hub/common/Store/Store.js
import { makeAutoObservable } from 'mobx';
import { camelCase } from 'lodash';
import * as Sentry from '@sentry/react';
import queryString from 'query-string';

import collectiveApi from 'modules/common/collectiveApi';

export default class HubStore {
    milestones = [];

    memberRelationshipManagers = [];

    onboardingAccountants = [];

    taxpro = [];

    pops = [];

    filter = {};

    members = [];

    membersPagination = {
        total_record_count: 0,
        number_of_pages: '',
        next_page: null,
        previous_page: null,
    };

    isMembersLoading = false;

    memberInfo = {
        name: null,
        businessName: null,
        type: null,
    };

    upcomingOutputs = [];

    memberMilestones = [];

    memberOutputs = [];

    constructor() {
        makeAutoObservable(this);
    }

    setMembers = (members) => {
        this.members = members;
    };

    setMembersPagination = (membersPagination) => {
        this.membersPagination = membersPagination;
    };

    setIsMembersLoading = (isMembersLoading) => {
        this.isMembersLoading = isMembersLoading;
    };

    setMemberInfo = (memberInfo) => {
        this.memberInfo = memberInfo;
    };

    setMemberMilestones = (memberMilestones) => {
        this.memberMilestones = memberMilestones;
    };

    setMemberOutputs = (memberOutputs) => {
        this.memberOutputs = memberOutputs;
    };

    setMilestones = (milestones) => {
        this.milestones = milestones;
    };

    setFilter = (filter) => {
        this.filter = filter;
    };

    setMemberRelationshipManagers = (memberRelationshipManagers) => {
        this.memberRelationshipManagers = memberRelationshipManagers;
    };

    setOnboardingAccountants = (onboardingAccountants) => {
        this.onboardingAccountants = onboardingAccountants;
    };

    setTaxpro = (taxpro) => {
        this.taxpro = taxpro;
    };

    setPops = (pops) => {
        this.pops = pops;
    };

    popMemberByID = (memberID) => {
        this.members = this.members.filter((member) => member?.id !== memberID);
    };

    makeParams = (filter, page, sortColumn, sortDirection) => {
        let params = {};
        if (filter) {
            params = { ...params, ...filter };
        }
        if (page) {
            params = { ...params, page };
        }
        if (sortColumn && sortDirection) {
            params = {
                ...params,
                sort_column: sortColumn,
                sort_direction: sortDirection,
            };
        }

        return Object.entries({ ...params })
            .map((e) => e.join('='))
            .join('&');
    };

    fetchMilestones = async (milestoneURL) => {
        if (!milestoneURL) {
            return;
        }
        try {
            const { data: milestones } = await collectiveApi.get(milestoneURL);
            this.setMilestones(milestones);
        } catch (error) {
            console.error('Error while fetching milestones data: ', error);
            Sentry.captureException(error);
            throw error;
        }
    };

    fetchMrmsAndOnbs = async () => {
        try {
            const { data: teams } = await collectiveApi.get('/tracker/teams/');

            const mrms = teams.member_relationship_manager
                .filter((name) => name !== '---')
                .map((name) => ({ key: name, text: name, value: name }));
            const onbs = teams.bookkeeper
                .filter((name) => name !== '---')
                .map((name) => ({ key: name, text: name, value: name }));
            const pops = teams.pops
                .filter((name) => name !== '---')
                .map((name) => ({ key: name, text: name, value: name }));
            const taxpro = teams.taxpro
                .filter((name) => name !== '---')
                .map((name) => ({ key: name, text: name, value: name }));

            this.setMemberRelationshipManagers(mrms);
            this.setOnboardingAccountants(onbs);
            this.setPops(pops);
            this.setTaxpro(taxpro);
        } catch (error) {
            console.error('Error while fetching mrms and onbs data: ', error);
            Sentry.captureException(error);
            throw error;
        }
    };

    setFetchedMembers = (members, newMembers) => {
        /**
         * Django's `rest_framework.pagination` returns this response as default pagination.
         * A URL with the next and the previous page.
         * In a single specific case, when there is no parameter `?page=` in the previous_page it means the framework is return 1.
         */
        const next_page = members.next_page ? queryString.parseUrl(members.next_page).query.page : null;
        const previous_page = members.previous_page
            ? queryString.parseUrl(members.previous_page).query.page || 1
            : null;

        this.setMembers(newMembers);
        this.setMembersPagination({
            total_record_count: members.total_record_count || newMembers.length,
            number_of_pages: members.number_of_pages,
            next_page,
            previous_page,
        });
    };

    fetchMembersQTE = async (filter, page = 1) => {
        try {
            this.setIsMembersLoading(true);
            const params = this.makeParams(filter, page);
            const { data: members } = await collectiveApi.get(`/workflows/qte/?${params}`);
            const newMembers = members.results;
            this.setFetchedMembers(members, newMembers);
        } catch (error) {
            console.error('Error while fetching members data: ', error);
            Sentry.captureException(error);
            throw error;
        } finally {
            this.setIsMembersLoading(false);
        }
    };

    fetchMembersOnboarding = async (filter, page = 1, sortColumn = 'businessname', sortDirection = 'ascending') => {
        try {
            this.setIsMembersLoading(true);
            const params = this.makeParams(filter, page, sortColumn, sortDirection);
            const { data: members } = await collectiveApi.get(`/tracker/members/?${params}`);
            const newMembers = members.results.map((member) => {
                return member.tracking.milestones?.reduce(
                    (acc, cur) => {
                        acc[camelCase(cur.name)] = cur.status;
                        return acc;
                    },
                    { ...member }
                );
            });
            this.setFetchedMembers(members, newMembers);
        } catch (error) {
            console.error('Error while fetching members data: ', error);
            Sentry.captureException(error);
            throw error;
        } finally {
            this.setIsMembersLoading(false);
        }
    };

    fetchMemberInfo = async (id) => {
        try {
            const { data: memberInfo } = await collectiveApi.get(`/tracker/members/${id}/`);
            this.setMemberInfo(memberInfo[0]);
            this.setMemberMilestones(memberInfo[0].tracking.milestones);
            this.fetchMemberOutputs(memberInfo[0].tracking.milestones[0].name);
        } catch (error) {
            console.error('Error while fetching members data: ', error);
            Sentry.captureException(error);
            throw error;
        }
    };

    fetchMemberOutputs = (milestoneName) => {
        const milestone = this.memberMilestones?.find((m) => m.name === milestoneName) || [];
        this.setMemberOutputs(milestone.outputs);
    };

    patchWorkflowStatusQTE = async (workflowID, taskID, step, action, reason) => {
        const payload = {
            step, // review | flag
            action, // flag | approve | restart | cancel
            task_id: taskID,
            reason,
        };
        await collectiveApi.patch(`/workflows/qte/${workflowID}/`, payload);
    };
}
// Code from file collective-frontend/src/modules/Hub/common/Store/Provider.js
import React from 'react';

import HubStore from './Store';
import HubStoreContext from './Context';

export default function HubStoreProvider(props) {
    return <HubStoreContext.Provider value={new HubStore()} {...props} />;
}
// Code from file collective-frontend/src/modules/Hub/common/Store/Store.test.js
import HubStore from './Store';

import collectiveApi from 'modules/common/collectiveApi';

jest.mock('modules/common/collectiveApi');

describe('hubStore', () => {
    let hubStore;

    beforeEach(() => {
        hubStore = new HubStore();
    });

    it('should fetchMilestones and update milestones', async () => {
        const milestones = [{ name: 'test' }];

        collectiveApi.get.mockReturnValue({ data: milestones });
        await hubStore.fetchMilestones('fake-url');
        expect(hubStore.milestones).toEqual(milestones);
    });

    it('should fetchMrmsAndOnbs to update memberRelationshipManagers and onboardingAccountants', async () => {
        const mrmsAndOnbs = {
            member_relationship_manager: ['test'],
            bookkeeper: ['test'],
            pops: ['test'],
            taxpro: ['test'],
        };

        collectiveApi.get.mockReturnValue({ data: mrmsAndOnbs });
        await hubStore.fetchMrmsAndOnbs();
        expect(hubStore.memberRelationshipManagers).toHaveLength(1);
        expect(hubStore.onboardingAccountants).toHaveLength(1);
    });

    it('should fetchMembers to update members', async () => {
        const members = {
            total_record_count: 0,
            number_of_pages: '',
            next_page: null,
            previous_page: null,
            results: [
                {
                    id: 1,
                    status: '---',
                    fullname: 'Test Test',
                    businessname: 'Test in a Million LLC',
                    email: 'Test.pinheiro@collective.com',
                    taxpro: 'Prentiss Johnson',
                    bookkeeper: 'Prentiss Johnson',
                    pops: 'Jacob Frediani',
                    member_relationship_manager: 'Hector Rodriguez',
                    usertype: 'SP-SC',
                    onboarding_duration: '2 hours, 14 minutes',
                    tracking: {
                        milestones: [
                            {
                                name: 'Financial Setup',
                                status: 'Incomplete',
                            },
                            {
                                name: 'Legal Setup',
                                status: 'Completed',
                            },
                            {
                                name: 'Technology Setup',
                                status: 'Incomplete',
                            },
                        ],
                    },
                },
            ],
        };

        collectiveApi.get.mockReturnValue({ data: members });
        hubStore.fetchMembers = hubStore.fetchMembersOnboarding;
        await hubStore.fetchMembers({});

        expect(hubStore.members[0].financialSetup).toBe('Incomplete');
        expect(hubStore.members[0].legalSetup).toBe('Completed');
        expect(hubStore.members[0].technologySetup).toBe('Incomplete');
    });

    it('should fetchMemberInfo to update memberInfo', async () => {
        const memberInfo = [
            {
                fullname: 'Test',
                businessname: 'Test LLC',
                usertype: 'Takeover (LLC > SC)',
                tracking: {
                    milestones: [{ name: 'Test Setup' }],
                },
            },
        ];

        collectiveApi.get.mockReturnValue({ data: memberInfo });
        await hubStore.fetchMemberInfo('test@test.com');
        expect(hubStore.memberInfo).toEqual(memberInfo[0]);
        expect(hubStore.memberMilestones).toEqual([{ name: 'Test Setup' }]);
    });
});
// Code from file collective-frontend/src/modules/Hub/common/Store/Context.js
import { createContext } from 'react';

const HubStoreContext = createContext();

export default HubStoreContext;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/index.jsx
import React from 'react';
import { Route } from 'react-router-dom';

import lazyWithSuspenseAndRetry from 'modules/common/lazyWithSuspenseAndRetry';

export const OnboardingTrackerHomePage = lazyWithSuspenseAndRetry(() => import('./Home/Page'));
export const MemberInfoPage = lazyWithSuspenseAndRetry(() => import('./MemberInfo/Page'));

export default function getHubOnboardingTrackerRoutes() {
    return [
        <Route
            exact
            key="onboarding-tracker-home"
            path="/hub/onboarding-tracker/"
            component={OnboardingTrackerHomePage}
        />,
        <Route exact key="member-info" path="/hub/tracker-tool/member-info/:email" component={MemberInfoPage} />,
    ];
}
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Tags/Tags.test.jsx
import { render } from '@testing-library/react';

import Tags from './Tags';

describe('TrackerTool/Tags', () => {
    it('should render correctly', () => {
        const { asFragment } = render(
            <>
                <Tags text={10} status="completed" />
                <Tags text={10} status="incomplete" />
                <Tags text={10} />
                <Tags text="Hello World" />
            </>
        );
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render completed tag', () => {
        const { getByText } = render(<Tags text="10 days" status="completed" />);
        expect(getByText(/completed/i)).toBeInTheDocument();
    });

    it('should render incomplete tag', () => {
        const { getByText } = render(<Tags text="10 days" status="incomplete" />);
        expect(getByText(/incomplete/i)).toBeInTheDocument();
    });
    it('should render text tag', () => {
        const { getByText } = render(<Tags text="Hello world" />);
        expect(getByText(/hello world/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Tags/index.js
export { default } from './Tags';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Tags/Tags.module.less
@color-green: #008866;
@color-red: #fa5a60;
@color-olive: #b5cc18;

.tags {
    display: flex;
}

.tag {
    margin-left: 10px;
    font-size: 12px;
    padding: 3px 8px;
    border-radius: 20px;
    background: rgba(0, 0, 0, 0.1);
    text-transform: capitalize;
    display: flex;
    justify-content: center;
    align-items: center;

    &[class~='text'] {
        text-transform: none;
    }

    &[class~='single'] {
        font-size: 1em;
        margin: 0;
        border-radius: 20px;
        padding: 4px 10px 5px 10px;
    }

    &[class~='completed'],
    &[class~='review'] {
        background: @color-green;
        color: #ffffff !important;
    }

    &[class~='incomplete'] {
        background: @color-red;
        color: #ffffff !important;
    }

    &[class~='flag'] {
        background: @color-olive;
        color: #ffffff !important;
    }

    &[class~='days'] {
        color: #000000;
    }
}
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Tags/Tags.jsx
import StatusIcon from '../StatusIcon';

import classes from './Tags.module.less';

const Tags = ({ status, text, single }) => {
    return (
        <div className={classes.tags}>
            {text && <div className={`${classes.tag} days text`}>{text}</div>}
            {status && (
                <div className={`${classes.tag} ${status} ${single && 'single'}`}>
                    {single && <StatusIcon status={status} />}
                    {status || 'Upcoming'}
                </div>
            )}
        </div>
    );
};

export default Tags;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/StatusIcon/StatusIcon.test.jsx
import { render } from '@testing-library/react';

import StatusIcon from './StatusIcon';

describe('TrackerTool/StatusIcon', () => {
    it('should render correctly', () => {
        const { asFragment } = render(
            <>
                <StatusIcon status="completed" />
                <StatusIcon status="incomplete" />
                <StatusIcon status="not applicable" />
                <StatusIcon />
            </>
        );
        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/StatusIcon/index.js
export { default } from './StatusIcon';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/StatusIcon/StatusIcon.jsx
import { Icon } from 'semantic-ui-react';

const StatusIcon = ({ status }) => {
    switch (status) {
        case 'completed':
            return <Icon name="check circle outline" />;
        case 'incomplete':
            return <Icon name="hourglass one" />;
        case 'not applicable':
            return <Icon name="window close outline" />;
        case 'not started':
            return <Icon name="tasks" />;
        case 'review':
            return <Icon name="hourglass one" />;
        case 'flag':
            return <Icon name="flag" />;
        case 'fetch':
            return <Icon name="sync" />;
        default:
            return <Icon name="circle outline" />;
    }
};

export default StatusIcon;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Page.test.jsx
import { render, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { action, makeObservable, observable } from 'mobx';
import { Router } from 'react-router-dom';

import HubStoreContext from '../../common/Store/Context';

import MemberInfoPage from './Page';

class MockHubStore {
    memberInfo = {
        email: 'test@test.com',
        fullname: 'Test',
        businessname: 'Test LLC',
        usertype: 'LLC > SC',
    };

    memberMilestones = [];

    memberOutputs = [];

    upcomingOutputs = [];

    setMemberOutputs = () => jest.fn();

    fetchMemberInfo = () => jest.fn();

    fetchMemberOutputs = () => jest.fn();

    fetchMemberUpcomingOutputs = () => jest.fn();

    constructor() {
        makeObservable(this, {
            memberInfo: observable,
            memberMilestones: observable,
            memberOutputs: observable,
            upcomingOutputs: observable,
            setMemberOutputs: action,
            fetchMemberInfo: action,
            fetchMemberOutputs: action,
            fetchMemberUpcomingOutputs: action,
        });
    }
}

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <MemberInfoPage />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('TrackerTool/MemberInfo', () => {
    it('should render correctly', async () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);
        await waitFor(() => hubStore.fetchMemberInfo().mockResolvedValue({}));
        await waitFor(() => hubStore.fetchMemberUpcomingOutputs().mockResolvedValue([]));
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render member informaton', async () => {
        const hubStore = new MockHubStore();
        const { getAllByText } = renderWithProviders(hubStore);
        await waitFor(() => hubStore.fetchMemberInfo().mockResolvedValue({}));
        await waitFor(() => hubStore.fetchMemberUpcomingOutputs().mockResolvedValue([]));
        const element = getAllByText(/Test LLC/i);
        expect(element[0]).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Page.jsx
import { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { useParams, Link } from 'react-router-dom';
import { Breadcrumb, Loader, Button, Header, Icon, Divider } from 'semantic-ui-react';

import Milestones from './Milestones';
import Outputs from './Outputs';
import MemberStatus from './Status';
import classes from './Page.module.less';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const MemberInfoPage = () => {
    const { email } = useParams();
    const hubStore = useHubStore();

    const [loading, setLoading] = useState(false);

    useEffect(() => {
        setLoading(true);

        const fetchData = async () => {
            await hubStore.fetchMemberInfo(email);
            // await hubStore.fetchMemberUpcomingOutputs(email);
            setLoading(false);
        };

        fetchData();
    }, [hubStore, email]);

    const handleClick = (name) => {
        hubStore.setMemberOutputs([]);
        hubStore.fetchMemberOutputs(name);
    };

    if (loading) {
        return (
            <Loader inline="centered" active>
                Loading
            </Loader>
        );
    }

    return (
        <>
            <Header as="h2">
                <Icon name="compass" />
                Onboarding Tracker
            </Header>
            <Divider />
            <div className={classes.memberInfo}>
                <Breadcrumb>
                    <Breadcrumb.Section>
                        <Link to="/hub/onboarding-tracker">Onboarding Tracker</Link>
                    </Breadcrumb.Section>
                    <Breadcrumb.Divider />
                    <Breadcrumb.Section active>{hubStore.memberInfo.businessname}</Breadcrumb.Section>
                </Breadcrumb>
                <MemberStatus memberInfo={hubStore.memberInfo} upcomingOutputs={hubStore.upcomingOutputs} />
                <Divider />
                <Milestones milestones={hubStore.memberMilestones} onClick={handleClick} />
                <Outputs outputs={hubStore.memberOutputs} />
                <Divider />
                <Link to="/hub/onboarding-tracker">
                    <Button primary type="button">
                        <Icon name="chevron left" />
                        Back
                    </Button>
                </Link>
            </div>
        </>
    );
};

export default observer(MemberInfoPage);
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Status/Status.test.jsx
import { render } from '@testing-library/react';

import MemberStatus from './Status';

const memberInfo = {
    fullname: 'Tester',
    status: 'Member Active',
    businessname: 'Test LLC',
    email: 'test@test.com',
    usertype: 'SP-SC',
};

describe('TrackerTool/MemberStatus', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<MemberStatus memberInfo={memberInfo} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('Should render member information', () => {
        const { getByText } = render(<MemberStatus memberInfo={memberInfo} />);
        expect(getByText(/Tester/i)).toBeInTheDocument();
        expect(getByText(/Member Active/i)).toBeInTheDocument();
        expect(getByText(/Test LLC/i)).toBeInTheDocument();
        expect(getByText(/test@test.com/i)).toBeInTheDocument();
        expect(getByText(/SP-SC/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Status/index.js
export { default } from './Status';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Status/Status.module.less
[class~='ui'][class~='cards'].status {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;

    [class~='card'] {
        box-shadow: none;
        width: auto;

        [class~='content'] {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            justify-content: center;

            [class~='header'] {
                font-family: 'recoletasemibold';
                font-weight: normal;
            }

            [class~='meta'] {
                margin-bottom: 5px;
            }
        }
    }
}
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Status/Status.jsx
import { Card, Header } from 'semantic-ui-react';

import classes from './Status.module.less';

const MemberStatus = ({ memberInfo }) => {
    return (
        <>
            <Header as="h2">{memberInfo.businessname}</Header>
            <Card.Group className={classes.status}>
                <Card>
                    <Card.Content>
                        <Card.Description>
                            <strong>Full Name:</strong> {memberInfo.fullname}
                        </Card.Description>
                        <Card.Description>
                            <strong>Email Address:</strong> {memberInfo.email}
                        </Card.Description>
                        <Card.Description>
                            <strong>Status:</strong> {memberInfo.status}
                        </Card.Description>
                        <Card.Description>
                            <strong>User Type:</strong> {memberInfo.usertype}
                        </Card.Description>
                    </Card.Content>
                </Card>
                <Card>
                    <Card.Content>
                        <Card.Description>
                            <strong>Member Relationship Manager:</strong> {memberInfo.member_relationship_manager}
                        </Card.Description>
                        <Card.Description>
                            <strong>Onboarding Accountant:</strong> {memberInfo.bookkeeper}
                        </Card.Description>
                        <Card.Description>
                            <strong>Taxpro:</strong> {memberInfo.taxpro}
                        </Card.Description>
                        <Card.Description>
                            <strong>Pops:</strong> {memberInfo.pops}
                        </Card.Description>
                    </Card.Content>
                </Card>
            </Card.Group>
        </>
    );
};

export default MemberStatus;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Milestones/Milestones.jsx
import { useState } from 'react';
import { Header, Menu, Divider } from 'semantic-ui-react';

import Tags from '../../Tags';
import StatusIcon from '../../StatusIcon';

import classes from './Milestones.module.less';

const Milestones = ({ onClick, milestones }) => {
    const [activeIndex, setActiveIndex] = useState(0);
    const handleClick = (index) => {
        setActiveIndex(index);
        onClick(milestones[index].name);
    };

    return (
        <>
            <Header as="h3">Milestone Summary</Header>
            <Menu fluid widths={3} className={classes.milestones}>
                {milestones.map((milestone, index) => (
                    <Menu.Item
                        key={milestone.name}
                        active={index === activeIndex}
                        className={milestone.status?.toLowerCase()}
                        onClick={() => handleClick(index)}>
                        <StatusIcon status={milestone.status?.toLowerCase()} /> {milestone.name}
                        <Tags
                            status={milestone.status?.toLowerCase()}
                            text={milestone.status !== milestone.duration && `Duration: ${milestone.duration}`}
                        />
                    </Menu.Item>
                ))}
            </Menu>
            <Divider />
        </>
    );
};

export default Milestones;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Milestones/Milestones.test.jsx
import { render } from '@testing-library/react';

import MemberMilestones from './Milestones';

const milestones = [
    {
        name: 'Legal Setup',
        status: 'incomplete',
        members: 144,
        days: 44.5,
    },
    {
        name: 'Financial Setup',
        status: 'completed',
        members: 55,
        days: 18,
    },
    {
        name: 'Technology Setup',
        status: 'completed',
        members: 11,
        days: 4.5,
    },
];

describe('TrackerTool/MemberInfo/MemberMilestones', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<MemberMilestones milestones={milestones} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('Should render member milestones data', () => {
        const { getByText } = render(<MemberMilestones milestones={milestones} />);
        expect(getByText(/Legal Setup/i)).toBeInTheDocument();
        expect(getByText(/Financial Setup/i)).toBeInTheDocument();
        expect(getByText(/Technology/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Milestones/index.js
export { default } from './Milestones';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Milestones/Milestones.module.less
@color-green: #008866;
@color-red: #FA5A60;

[class~=ui][class~=menu].milestones {
    margin-bottom: 20px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    border: none;

    [class~="item"] {
        padding: 0 40px;
        cursor: pointer;
        border-right: 1px solid rgba(0, 0, 0, 0.1);
        color: rgba(0, 0, 0, 0.6);

        &[class~="active"] {
            color: rgba(0, 0, 0, 0.6);
            background-color: #faf9f6;
        }

        &:hover,
        &:active,
        &:focus {
            color: rgba(0, 0, 0, 0.6) !important;
            background: darken(#faf9f6, 1.5%);

        }

        &[class~="completed"] {
            [class~="icon"] {
                color: @color-green;
            }
        }

        &[class~="incomplete"] {
            [class~="icon"] {
                color: @color-red;
            }
        }

        &::before {
            background: none;
        }
    }
}// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Page.module.less
.memberInfo {
    i[class~="green"][class~="icon"] {
        color: #008866 !important;
    }

    [class~="ui"][class~="breadcrumb"] {
        margin-bottom: 30px;
    }

    [class~="member"] {
        margin-bottom: 40px;

        [class~="member-name"] {
            margin-bottom: 0;
            font-family: 'recoletasemibold';
        }

        [class~="member-description"] {
            opacity: 0.75;
        }
    }
}// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/index.js
export { default } from './Outputs';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/Outputs.test.jsx
import { render } from '@testing-library/react';

import MemberOutputs from './Outputs';

const outputs = [
    {
        name: 'Output',
        status: 'complete',
        days: 10,
        tasks: [
            {
                name: 'Task?',
                activity: {
                    status: 'Completed',
                    completed_by: 'System Event',
                    completed_on: '2022-05-06T08:19:01.034761-07:00',
                },
            },
        ],
    },
];
describe('TrackerTool/MemberInfo/MemberOutputs', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<MemberOutputs outputs={outputs} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('Should render member outputs data', () => {
        const { getByText } = render(<MemberOutputs outputs={outputs} />);
        expect(getByText(/Output/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/Outputs.jsx
import { useState } from 'react';
import { Accordion, Icon, Segment } from 'semantic-ui-react';

import Tags from '../../Tags';
import StatusIcon from '../../StatusIcon';

import Tasks from './Tasks';
import classes from './Outputs.module.less';

const Output = (output) => {
    const [active, setActive] = useState(false);

    const showDuration = () => {
        const completedTasks = output.tasks.filter((task) => task.activity?.status === 'Completed');
        return completedTasks.length > 0;
    };

    const getTaskProgress = () => {
        const statusCompleted = output.tasks?.filter((task) => task.activity?.status === 'Completed') || [];
        const statusNotApplicable = output.tasks?.filter((task) => task.activity?.status === 'Not Applicable') || [];
        const total = output.tasks || [];
        return `${statusCompleted.length + statusNotApplicable.length} /${total.length}`;
    };

    return (
        <Accordion className={classes.output}>
            <Accordion.Title
                className={output.status?.toLowerCase()}
                active={active}
                index={0}
                onClick={() => setActive(!active)}>
                <div className="left-content">
                    <Icon name="dropdown" /> <StatusIcon status={output.status?.toLowerCase()} /> {output.name}
                </div>
                <div className="right-content">
                    {showDuration() && <Tags text={`Duration: ${output.duration}`} />}
                    <Tags text={getTaskProgress()} status={output.status?.toLowerCase()} />
                </div>
            </Accordion.Title>
            <Accordion.Content active={active}>
                <Tasks tasks={output.tasks} />
            </Accordion.Content>
        </Accordion>
    );
};

const Outputs = ({ outputs }) => {
    if (outputs.length === 0) {
        return (
            <Segment padded="very" textAlign="center" className={classes.empty}>
                <div className="content">No data available.</div>
            </Segment>
        );
    }

    return (
        <>
            {outputs.map((output) => (
                <Output {...output} key={output.name} />
            ))}
        </>
    );
};

export default Outputs;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/Outputs.module.less
@color-green: #008866;
@color-red: #FA5A60;

[class~="ui"][class~="accordion"].output {
    background: white;
    border-radius: 5px;
    padding: 5px 20px;
    background: #faf9f6;
    margin-bottom: 20px;
    box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1);

    [class~=right-content] {
        display: flex;
    }
    
    [class~="title"] {
        color: #000000;
        display: flex;
        justify-content: space-between;

        [class~="dropdown"] {
            color: #000000 !important;
        }

        &[class~="completed"] {
            color: @color-green;

            [class~="icon"] {
                color: @color-green;
            }
        }

        &[class~="incomplete"] {
            color: @color-red;

            [class~="icon"] {
                color: @color-red;
            }
        }
    }
}

[class~=ui][class~=segment].empty {
    background: #faf9f6;

    [class~=content] {
        padding: 40px;
        color: rgba(34, 36, 38, 0.8);
        opacity: 0.5;
    }

}// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/Tasks/Tasks.test.jsx
import { render } from '@testing-library/react';

import Tasks from './Tasks';

const tasks = [
    {
        name: 'Did something great',
        activity: {
            status: 'Completed',
            completed_by: 'System Event',
            completed_on: '2022-05-06T08:19:01.034761-07:00',
        },
    },
];

describe('Tasks', () => {
    it('should render correctly', () => {
        const { asFragment } = render(<Tasks tasks={tasks} />);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('Should render tasks data', () => {
        const { getByText } = render(<Tasks tasks={tasks} />);
        expect(getByText(/Did something great/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/Tasks/Tasks.jsx
import { Table } from 'semantic-ui-react';

import Tags from '../../../Tags';

import classes from './Tasks.module.less';

const Tasks = ({ tasks }) => {
    return (
        <Table striped celled basic="very" className={classes.tasks}>
            <Table.Header>
                <Table.Row>
                    <Table.HeaderCell width={6}>Task Description</Table.HeaderCell>
                    <Table.HeaderCell width={2}>Task Status</Table.HeaderCell>
                    <Table.HeaderCell width={2}>Completed By</Table.HeaderCell>
                    <Table.HeaderCell width={2}>Completed On</Table.HeaderCell>
                </Table.Row>
            </Table.Header>

            <Table.Body>
                {tasks?.map((task, index) => (
                    <Table.Row key={`${task.name}${index}`}>
                        <Table.Cell>{task.name}</Table.Cell>
                        <Table.Cell collapsing>
                            <Tags status={task.activity?.status?.toLowerCase()} single />
                        </Table.Cell>
                        <Table.Cell collapsing>
                            <strong>{task.activity?.status === 'Completed' && task.activity?.completed_by}</strong>
                        </Table.Cell>
                        <Table.Cell collapsing>
                            {task.activity?.status === 'Completed' && task.activity?.completed_on}
                        </Table.Cell>
                    </Table.Row>
                ))}
            </Table.Body>
        </Table>
    );
};

export default Tasks;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/Tasks/index.js
export { default } from './Tasks';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/MemberInfo/Outputs/Tasks/Tasks.module.less
[class~="ui"][class~="table"].tasks {
    margin-bottom: 10px;

    thead {
        font-size: 10px;

        tr {
            th {
                opacity: 0.5;
                text-transform: uppercase;
                padding: 10px 20px !important;
            }
        }
    }

    tbody {
        background-color: #ffffff;

        tr {
            td {
                padding: 10px 20px !important;
                border-bottom: none;
            }
        }
    }
}// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/Page.test.jsx
import { makeObservable, observable, action } from 'mobx';
import { act, render, waitFor } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import HubStoreContext from '../../common/Store/Context';

import OnboardingTrackerHomePage from './Page';

class MockHubStore {
    filter = {};

    milestones = [];

    memberRelationshipManagers = [];

    onboardingAccountants = [];

    members = [];

    constructor() {
        makeObservable(this, {
            filter: observable,
            milestones: observable,
            memberRelationshipManagers: observable,
            onboardingAccountants: observable,
            members: observable,
            fetchMrmsAndOnbs: action,
            fetchMembersOnboarding: action,
            setFilter: action,
            setMembers: action,
            setMilestones: action,
            setMemberRelationshipManagers: action,
            setOnboardingAccountants: action,
        });
    }

    fetchMembersOnboarding = () => jest.fn();

    fetchMilestones = () => jest.fn();

    fetchMrmsAndOnbs = () => jest.fn();

    setFilter = () => jest.fn();

    setMembers = () => jest.fn();

    setMilestones = (milestones) => {
        this.milestones = milestones;
    };

    setMemberRelationshipManagers = (memberRelationshipManagers) => {
        this.memberRelationshipManagers = memberRelationshipManagers;
    };

    setOnboardingAccountants = (onboardingAccountants) => {
        this.onboardingAccountants = onboardingAccountants;
    };
}

const milestones = [
    {
        name: 'Legal Setup',
        status: 'incompleted',
        members: 144,
        days: 44.5,
    },
];

const mrmsAndOnbs = [{ key: 1, text: 'test', value: 'test' }];

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <OnboardingTrackerHomePage />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('Onboarding Tracker', () => {
    it('should render correctly', async () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);

        await waitFor(() => hubStore.fetchMrmsAndOnbs().mockResolvedValue(null));
        act(() => {
            hubStore.setMilestones(milestones);
            hubStore.setMemberRelationshipManagers(mrmsAndOnbs);
            hubStore.setOnboardingAccountants(mrmsAndOnbs);
        });

        expect(asFragment().firstChild).toMatchSnapshot();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/Page.jsx
import { Divider, Header, Icon } from 'semantic-ui-react';

import TrackingTool from '../TrackingTool';

import ExtraFilterFields from './FilterFormFields';
import MemberListActions from './MemberListActions';
import Milestones from './Milestones';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

export default function OnboardingTrackerHomePage() {
    const hubStore = useHubStore();
    const memberTableCells = [
        { data: 'businessname', label: 'Business Name' },
        { data: 'fullname', label: 'Full Name' },
        { data: 'email', label: 'Email Address' },
        { data: 'status', label: 'Status' },
        { data: 'usertype', label: 'User Type' },
        { data: 'onboarding_duration', label: 'Onboarding Duration' },
        { data: 'legalSetup', label: 'Legal Setup' },
        { data: 'financialSetup', label: 'Financial Setup' },
        { data: 'technologySetup', label: 'Technology Setup' },
    ];

    return (
        <>
            <Header as="h2">
                <Icon name="compass" />
                Onboarding Tracker
            </Header>
            <Divider />
            <TrackingTool
                memberTableCells={memberTableCells}
                ExtraFilterFields={ExtraFilterFields}
                MemberListActions={MemberListActions}
                Milestones={Milestones}
                fetchMemberFunc={hubStore.fetchMembersOnboarding}
            />
        </>
    );
}
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/Milestones/Milestones.jsx
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import { Header, Statistic, Icon } from 'semantic-ui-react';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const Milestones = () => {
    const hubStore = useHubStore();

    useEffect(() => {
        const fetchData = async () => {
            await hubStore.fetchMilestones('/tracker/milestones/');
        };

        if (hubStore.milestones.length === 0 || hubStore.milestones.length > 4) {
            fetchData();
        }
    }, []);

    return (
        <>
            <Header as="h3">Milestone Summary</Header>
            <Statistic.Group widths={3}>
                {hubStore?.milestones?.map((item) => (
                    <Statistic key={item.id}>
                        <Statistic.Value text>
                            <Icon name="clock outline" />
                            &nbsp;~&nbsp;{item.average_duration}
                        </Statistic.Value>
                        <Statistic.Label>{item.name}</Statistic.Label>
                    </Statistic>
                ))}
            </Statistic.Group>
        </>
    );
};

export default observer(Milestones);
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/Milestones/Milestones.test.jsx
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { action, configure, makeObservable, observable } from 'mobx';
import { Router } from 'react-router-dom';

import HubStoreContext from '../../../common/Store/Context';

import Milestones from './Milestones';

/**
 * MobX makes some fields non-configurable or non-writable to disabling spying/mocking/stubbing in tests
 * @link https://mobx.js.org/configuration.html#safedescriptors-boolean
 */
configure({ safeDescriptors: false });

class MockHubStore {
    milestones = [
        {
            name: 'Legal Setup',
            status: 'incompleted',
            member_count: 144,
            average_duration: 44.5,
        },
    ];

    constructor() {
        makeObservable(this, {
            milestones: observable,
            setFilter: action,
            fetchMembersOnboarding: action,
            setMilestones: action,
        });
    }

    setFilter = () => jest.fn();

    setMilestones = () => jest.fn();

    fetchMembersOnboarding = () => jest.fn();
}

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <Milestones />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('TrackerTool/Milestones', () => {
    it('should render correctly', () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should load milestone title', () => {
        const hubStore = new MockHubStore();
        const { getByText } = renderWithProviders(hubStore);
        expect(getByText(/Milestone Summary/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/Milestones/index.js
export { default } from './Milestones';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/Milestones/Milestones.module.less
@color-green: #008866;
@color-red: #FA5A60;

[class~=ui][class~=menu].milestones {
    margin-bottom: 20px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    border: none;

    [class~="item"] {
        padding: 0 40px;
        cursor: pointer;
        border-right: 1px solid rgba(0, 0, 0, 0.1);

        &:hover {
            color: #000;
        }

        &:last-child {
            border-right: none;
        }

        &[class~="active"] {
            background-color: #faf9f6;
        }

        &[class~="completed"] {
            [class~="icon"] {
                color: @color-green;
            }
        }

        &[class~="incompleted"] {
            [class~="icon"] {
                color: @color-red;
            }
        }

        &::before {
            background: none;
        }
    }
}// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/FilterFormFields.jsx
import { useEffect } from 'react';
import { Form } from 'semantic-ui-react';
import { observer } from 'mobx-react';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

function ExtraFilterFields({ values, handleSelectChange }) {
    const hubStore = useHubStore();

    useEffect(() => {
        if (hubStore.onboardingAccountants.length === 0 || hubStore.memberRelationshipManagers.length === 0) {
            hubStore.fetchMrmsAndOnbs();
        }
    }, [hubStore.onboardingAccountants, hubStore.memberRelationshipManagers]);

    return (
        <>
            <Form.Select
                fluid
                label="Member Relationship Manager"
                name="member_relationship_manager"
                id="member_relationship_manager"
                value={values.member_relationship_manager || ''}
                options={hubStore.memberRelationshipManagers}
                onChange={handleSelectChange}
                placeholder="Member Relationship Manager"
                search
            />
            <Form.Select
                fluid
                label="Onboarding Accountant"
                name="member_bookkeeper"
                id="member_bookkeeper"
                value={values.member_bookkeeper || ''}
                onChange={handleSelectChange}
                options={hubStore.onboardingAccountants}
                placeholder="Onboarding Accountant"
                search
            />
            <Form.Select
                fluid
                label="Taxpro"
                name="member_taxpro"
                id="member_taxpro"
                value={values.member_taxpro || ''}
                onChange={handleSelectChange}
                options={hubStore.taxpro}
                placeholder="Taxpro"
                search
            />
            <Form.Select
                fluid
                label="Pops"
                name="member_pops"
                id="member_pops"
                value={values.member_pops || ''}
                options={hubStore.pops}
                onChange={handleSelectChange}
                placeholder="Pops"
                search
            />
        </>
    );
}

export default observer(ExtraFilterFields);
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/Home/MemberListActions.jsx
import React from 'react';
import { Link } from 'react-router-dom';
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import Preview from '@mui/icons-material/Preview';

const MemberListActions = ({ member }) => (
    <Tooltip title="View Details" arrow>
        <Link target="_blank" to={`/hub/tracker-tool/member-info/${member.id}`}>
            <IconButton>
                <Preview fontSize="large" />
            </IconButton>
        </Link>
    </Tooltip>
);

export default MemberListActions;
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/TrackingTool.jsx
import { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Loader, Divider, Accordion, Icon } from 'semantic-ui-react';
import { isEmpty } from 'lodash';

import FilterForm from './FilterForm';
import MemberList from './List';
import classes from './TrackingTool.module.less';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

function TrackingTool({
    accordianOpen = true,
    memberTableCells,
    ExtraFilterFields,
    MemberListActions,
    Milestones,
    fetchMemberFunc,
    initialValues = {},
}) {
    const hubStore = useHubStore();
    const [loading, setLoading] = useState(false);
    const [showAccordian, setShowAccordian] = useState(accordianOpen);

    const setAccordian = () => {
        setShowAccordian(!showAccordian);
    };

    useEffect(() => {
        hubStore.setMembers([]);

        const fetchOnPageLoad = async () => {
            await handleSubmit(initialValues);
            setLoading(false);
        };

        if (!isEmpty(initialValues)) {
            setLoading(true);
            fetchOnPageLoad();
        }
    }, [initialValues]);

    const handleReset = async () => {
        hubStore.setFilter({});
        hubStore.setMembers([]);
        await hubStore.fetchMembersOnboarding();
    };

    const handleSubmit = async (values) => {
        hubStore.setFilter(values);
        await fetchMemberFunc(values);
    };

    if (loading) {
        return (
            <Loader inline="centered" active>
                Loading
            </Loader>
        );
    }

    return (
        <div className={classes.trackerTool}>
            {Milestones && <Milestones />}
            <Divider />
            <Accordion fluid>
                <Accordion.Title
                    active={showAccordian === true}
                    onClick={setAccordian}
                    as="h3"
                    className="accordianTitle">
                    <Icon name="dropdown" />
                    Search Members
                </Accordion.Title>
                <Accordion.Content active={showAccordian === true}>
                    <FilterForm onSubmit={handleSubmit} onReset={handleReset} ExtraFilterFields={ExtraFilterFields} />
                </Accordion.Content>
            </Accordion>
            <Divider />
            <MemberList
                tableCells={memberTableCells}
                MemberListActions={MemberListActions}
                fetchMemberFunc={fetchMemberFunc}
            />
        </div>
    );
}

export default observer(TrackingTool);
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/List/List.jsx
import { observer } from 'mobx-react';
import { useEffect, useState } from 'react';
import { Icon, Loader, Menu, Segment, Table } from 'semantic-ui-react';

import Tags from '../../Tags';

import classes from './List.module.less';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const MemberList = ({ tableCells, MemberListActions, fetchMemberFunc }) => {
    const hubStore = useHubStore();
    const [hasData, setHasData] = useState(false);
    const [page, setPage] = useState(1);
    const [sortColumn, setSortColumn] = useState('businessname');
    const [sortDirection, setSortDirection] = useState('ascending');

    useEffect(() => {
        setHasData(hubStore.members.length > 0);
    }, [hubStore, hubStore.members]);

    useEffect(() => {
        const { filter } = hubStore;
        fetchMemberFunc(filter, page, sortColumn, sortDirection);
    }, [page, sortColumn, sortDirection]);

    const changeSort = (column, direction) => {
        let updatedSortDirection = direction === 'ascending' ? 'descending' : 'ascending';
        if (column !== sortColumn) {
            updatedSortDirection = 'ascending';
        }
        setSortColumn(column);
        setSortDirection(updatedSortDirection);
    };

    const isSortableColumn = (currentColumn) => {
        return ['businessname', 'fullname', 'email', 'status', 'usertype'].includes(currentColumn);
    };

    if (hubStore.isMembersLoading) {
        return (
            <Segment padded="very" textAlign="center" className={classes.empty}>
                <div className="content">
                    <Loader active>Loading</Loader>
                </div>
            </Segment>
        );
    }

    if (!hasData) {
        return (
            <Segment padded="very" textAlign="center" className={classes.empty}>
                <div className="content">No data available.</div>
            </Segment>
        );
    }

    return (
        <Table sortable striped celled basic="very" className={classes.list}>
            <Table.Header>
                <Table.Row>
                    {tableCells.map((cell) => (
                        <Table.HeaderCell
                            key={cell.data}
                            sorted={isSortableColumn(cell.data) && cell.data === sortColumn ? sortDirection : null}
                            onClick={() => isSortableColumn(cell.data) && changeSort(cell.data, sortDirection)}>
                            {cell.label}
                        </Table.HeaderCell>
                    ))}
                    <Table.HeaderCell textAlign="center" width={2}>
                        Actions
                    </Table.HeaderCell>
                </Table.Row>
            </Table.Header>
            <Table.Body>
                {hubStore.members.map((member) => (
                    <Table.Row key={member.id}>
                        {tableCells.map((cell) => {
                            return (
                                <Table.Cell key={`${member.id || member.email}__${cell.data}`}>
                                    {['Completed', 'Incomplete', 'Not Started', 'review', 'flag', 'fetch'].includes(
                                        member[cell.data]
                                    ) ? (
                                        <Tags status={member[cell.data].toLowerCase()} single>
                                            {member[cell.data]}
                                        </Tags>
                                    ) : (
                                        member[cell.data]
                                    )}
                                </Table.Cell>
                            );
                        })}
                        <Table.Cell collapsing textAlign="center" key={`${member.id}__actions`}>
                            {MemberListActions && <MemberListActions member={member} />}
                        </Table.Cell>
                    </Table.Row>
                ))}
            </Table.Body>
            <Table.Footer>
                <Table.Row>
                    <Table.HeaderCell colSpan={tableCells.length + 1}>
                        <Menu pagination>
                            <Menu.Item as="span" icon>
                                Total number of members: {hubStore.membersPagination.total_record_count}
                            </Menu.Item>
                        </Menu>
                        <Menu floated="right" pagination>
                            <Menu.Item as="span" icon>
                                {hubStore.membersPagination.number_of_pages}
                            </Menu.Item>
                            <Menu.Item
                                as="a"
                                icon
                                disabled={!hubStore.membersPagination.previous_page}
                                onClick={() => setPage(hubStore.membersPagination.previous_page)}>
                                <Icon name="chevron left" />
                            </Menu.Item>
                            <Menu.Item
                                as="a"
                                icon
                                disabled={!hubStore.membersPagination.next_page}
                                onClick={() => setPage(hubStore.membersPagination.next_page)}>
                                <Icon name="chevron right" />
                            </Menu.Item>
                        </Menu>
                    </Table.HeaderCell>
                </Table.Row>
            </Table.Footer>
        </Table>
    );
};

export default observer(MemberList);
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/List/index.js
export { default } from './List';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/List/List.module.less
[class~='ui'][class~='table'].list {
    background: #faf9f6;
    padding: 20px;

    thead {
        font-size: 10px;
        text-transform: uppercase;

        tr {
            th {
                border: 0;
                border-radius: 0;
                box-shadow: none;
                text-transform: uppercase;
                color: rgba(0, 0, 0, 0.87);
                opacity: 0.5;
                border-bottom: 1px solid rgba(34, 36, 38, 0.1);
                border-left: transparent !important;
            }
        }
    }

    tbody {
        tr {
            background-color: #ffffff;

            td {
                a {
                    color: rgba(0, 0, 0, 0.57);

                    text-decoration: none;
                    transition: all 0.2s linear;

                    &:hover,
                    &:active,
                    &:focus {
                        color: rgba(0, 0, 0, 0.87);
                    }
                }

                [class~='ui'][class~='button'] {
                    width: 54px;
                    // color: rgba(34, 36, 38, 0.8);
                    // border: 1px solid rgba(34, 36, 38, 0.2);
                    // background-color: #ffffff;
                    transition: all 0.2s linear;

                    &:hover,
                    &:active,
                    &:focus {
                        color: rgba(34, 36, 38, 0.9);
                        background-color: #faf9f6;
                    }
                }
            }
        }
    }

    tfoot {
        tr {
            th {
                padding: 10px 0;

                [class~='ui'][class~='pagination'][class~='menu'] {
                    &:first-child {
                        border: 0;
                    }

                    a:hover,
                    a:active,
                    a:focus {
                        color: #000000;
                    }
                }
            }
        }
    }
}

[class~='ui'][class~='segment'].empty {
    background: #faf9f6;

    [class~='content'] {
        padding: 40px;
        color: rgba(34, 36, 38, 0.8);
        opacity: 0.5;
    }
}
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/List/List.test.jsx
import { act, render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { action, makeObservable, observable } from 'mobx';
import { Router } from 'react-router-dom';

import HubStoreContext from '../../../common/Store/Context';

import List from './List';

class MockHubStore {
    members = [];

    membersPagination = {};

    constructor() {
        makeObservable(this, {
            members: observable,
            membersPagination: observable,
            setMembers: action,
            setMembersPagination: action,
        });
    }

    setMembers = (members) => {
        this.members = members;
    };

    setMembersPagination = (membersPagination) => {
        this.membersPagination = membersPagination;
    };
}

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();
    const tableCells = [
        { data: 'businessname', label: 'Business Name' },
        { data: 'fullname', label: 'Full Name' },
        { data: 'email', label: 'Email Address' },
        { data: 'onboarding_duration', label: 'Onboarding Duration' },
        { data: 'legalSetup', label: 'Legal Setup' },
        { data: 'financialSetup', label: 'Financial Setup' },
        { data: 'technologySetup', label: 'Technology Setup' },
    ];

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <List tableCells={tableCells} fetchMemberFunc={() => {}} />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('TrackerTool/List', () => {
    it('should render correctly', () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);
        act(() => {
            hubStore.setMembers([
                {
                    email: 'test@test.com',
                    days: 13,
                    legalSetup: 'incomplete',
                    financialSetup: 'incomplete',
                    technologySetup: 'incomplete',
                },
            ]);
            hubStore.setMembersPagination({
                total_record_count: 0,
                number_of_pages: '',
                next_page: null,
                previous_page: null,
            });
        });
        expect(asFragment().firstChild).toMatchSnapshot();
    });
    it('Should display empty data table', () => {
        const hubStore = new MockHubStore();
        const { getByText } = renderWithProviders(hubStore);
        expect(getByText(/No data available/i)).toBeInTheDocument();
    });

    it('Should display populated data table', () => {
        const hubStore = new MockHubStore();
        const { getByText } = renderWithProviders(hubStore);
        expect(getByText(/No data available/i)).toBeInTheDocument();

        act(() => {
            hubStore.setMembers([
                {
                    businessname: 'Test LLC',
                    fullname: 'Test test',
                    email: 'test@test.com',
                    onboarding_duration: 13,
                    legalSetup: 'incomplete',
                    financialSetup: 'incomplete',
                    technologySetup: 'incomplete',
                },
            ]);
        });

        act(() => {
            hubStore.setMembersPagination({
                total_record_count: 0,
                number_of_pages: '',
                next_page: null,
                previous_page: null,
            });
        });

        expect(getByText(/test@test.com/i)).toBeInTheDocument();
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/index.js
export { default } from './TrackingTool';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/TrackingTool.module.less
.trackerTool {
    h3 {
        font-family: 'recoletasemibold' !important;
        font-size: initial !important;
    }
}
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/FilterForm/index.js
export { default } from './FilterForm';
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/FilterForm/FilterForm.module.less
.filter {
    [class~='ui'][class~='form'] {
        [class~='ui'][class~='button'] {
            font-size: 16px;
            border: 1px solid !important;
            padding: 18px !important;
            border-radius: 5px !important;
            width: 140px;
            z-index: 0;

            &[class~='secondary'] {
                color: #fa5a60;
                border-color: lighten(#fa5a60, 0.3);

                &:hover {
                    background-color: #ffffff;
                }

                &[class~='disabled'] {
                    opacity: 0.3 !important;
                    color: rgba(0, 0, 0, 0.5) !important;
                    border-color: rgba(0, 0, 0, 0.2) !important;
                }
            }

            &[class~='disabled'] {
                opacity: 0.3 !important;
            }
        }

        [class~='fields'] {
            margin: 0em -0.5em 1em;

            [class~='dropdown'] {
                padding: 8px 6px;

                [class~='text'] {
                    font-size: 1.286em;
                    line-height: 1.30769231em;
                }

                [class~='item'] {
                    font-size: 1em;
                    line-height: 1em;
                }
            }

            [class~='field'] {
                [class~='text'] {
                    color: #000;
                    font-weight: normal;
                }

                [class~='default'] {
                    opacity: 0.3;
                }

                label {
                    font-family: 'mier_bregular';
                    font-weight: normal;
                    opacity: 0.7;
                }

                input {
                    font-size: 1.286em;
                    line-height: 1.30769231em;
                    padding: 8px 6px;
                }

                input::placeholder {
                    opacity: 0.2;
                }

                input,
                input::placeholder {
                    font-family: 'mier_bregular';
                    box-shadow: none;
                    color: #000;
                    font-weight: normal;
                }
            }
        }
    }
}
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/FilterForm/FilterForm.test.jsx
import { cleanup, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMemoryHistory } from 'history';
import { makeObservable, observable } from 'mobx';
import { Router } from 'react-router-dom';

import HubStoreContext from '../../../common/Store/Context';

import Filter from './FilterForm';

class MockHubStore {
    filter = {};

    memberRelationshipManagers = [{ key: 1, text: 'test', value: 'test' }];

    onboardingAccountants = [{ key: 1, text: 'test', value: 'test' }];

    taxpro = [{ key: 1, text: 'test', value: 'test' }];

    pops = [{ key: 1, text: 'test', value: 'test' }];

    constructor() {
        makeObservable(this, {
            filter: observable,
            memberRelationshipManagers: observable,
            onboardingAccountants: observable,
            taxpro: observable,
            pops: observable,
        });
    }
}

const handleSubmit = jest.fn();
const handleReset = jest.fn();

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <Filter filter={{}} onSubmit={handleSubmit} onReset={handleReset} />
            </Router>
        </HubStoreContext.Provider>
    );
};

beforeEach(cleanup);

describe('TrackerTool/Filter', () => {
    it('should render correctly', () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);
        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('Should call the function handleSubmit once', async () => {
        const hubStore = new MockHubStore();
        const user = userEvent.setup();
        const { getByLabelText, getByRole } = renderWithProviders(hubStore);
        await user.type(getByLabelText(/Email Address/i), 'test@test.com');
        await user.click(getByRole('button', { name: /Search/i }));

        await waitFor(() => expect(handleSubmit).toBeCalledTimes(1));
    });

    it('Should call the function handleReset once', async () => {
        const hubStore = new MockHubStore();
        const user = userEvent.setup();
        const { getByLabelText, getByRole } = renderWithProviders(hubStore);
        await user.type(getByLabelText(/Email Address/i), 'test@test.com');
        await user.click(getByRole('button', { name: /Clear/i }));

        await waitFor(() => expect(handleSubmit).toBeCalledTimes(0));
        await waitFor(() => expect(handleReset).toBeCalledTimes(1));
        await waitFor(() => expect(getByLabelText(/Email Address/i)).not.toHaveValue('test@test.com'));
    });
});
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/FilterForm/FilterForm.jsx
import { useFormik } from 'formik';
import { Form, Icon } from 'semantic-ui-react';
import { observer } from 'mobx-react';
import { isEmpty } from 'lodash';
import { useEffect } from 'react';
import { LoadingButton as Button } from '@mui/lab';

import classes from './FilterForm.module.less';

import useHubStore from 'modules/Hub/common/Store/useHubStore';

const FilterForm = ({ onReset, onSubmit, ExtraFilterFields }) => {
    const hubStore = useHubStore();

    const options = [
        { key: 'completed', text: 'Completed', value: 'completed' },
        { key: 'incomplete', text: 'Incomplete', value: 'incomplete' },
    ];

    const { values, dirty, isSubmitting, handleSubmit, handleChange, resetForm, setValues, setFieldValue } = useFormik({
        initialValues: { ...hubStore.filter },
        onSubmit,
        onReset,
    });

    useEffect(() => {
        setValues(hubStore.filter);
    }, [hubStore, hubStore.filter, setValues]);

    const handleSelectChange = (event, { value, name }) => {
        setFieldValue(name, value);
    };

    const handleReset = () => {
        resetForm({ values: {} });
    };

    return (
        <div className={classes.filter}>
            <Form>
                {ExtraFilterFields && (
                    <Form.Group widths="equal">
                        <ExtraFilterFields values={values} handleSelectChange={handleSelectChange} />
                    </Form.Group>
                )}
                <Form.Group widths="equal">
                    <Form.Input
                        fluid
                        name="member_businessname"
                        id="member_businessname"
                        value={values.member_businessname || ''}
                        onChange={handleChange}
                        label="Business Name"
                        placeholder="Business Name"
                    />
                    <Form.Input
                        fluid
                        name="member_fullname"
                        id="member_fullname"
                        value={values.member_fullname || ''}
                        onChange={handleChange}
                        label="Full Name"
                        placeholder="Full Name"
                    />
                    <Form.Input
                        fluid
                        name="member_email"
                        id="member_email"
                        value={values.member_email || ''}
                        onChange={handleChange}
                        label="Email Address"
                        placeholder="Email Address"
                    />
                    <Form.Select
                        fluid
                        label="Activity Status"
                        name="activity_status"
                        id="activity_status"
                        value={values.activity_status || ''}
                        options={options}
                        onChange={handleSelectChange}
                        placeholder="Activity Status"
                        search
                    />
                </Form.Group>
                <div className="tw-flex tw-items-center">
                    <Button
                        variant="contained"
                        type="submit"
                        className="tw-mr-2"
                        onClick={handleSubmit}
                        disabled={isSubmitting || (!dirty && isEmpty(hubStore.filter))}
                        loading={isSubmitting}>
                        <Icon name="search" />
                        Search
                    </Button>
                    <Button
                        variant="outlined"
                        type="reset"
                        onClick={handleReset}
                        disabled={isSubmitting || (!dirty && isEmpty(hubStore.filter))}>
                        <Icon name="eraser" />
                        Clear
                    </Button>
                </div>
            </Form>
        </div>
    );
};

export default observer(FilterForm);
// Code from file collective-frontend/src/modules/Hub/OnboardingTracker/TrackingTool/TrackingTool.test.jsx
import { makeObservable, observable, action } from 'mobx';
import { act, render, waitFor } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import HubStoreContext from '../../common/Store/Context';

import TrackingTool from './TrackingTool';

class MockHubStore {
    filter = {};

    milestones = [];

    memberRelationshipManagers = [];

    onboardingAccountants = [];

    members = [];

    constructor() {
        makeObservable(this, {
            filter: observable,
            milestones: observable,
            memberRelationshipManagers: observable,
            onboardingAccountants: observable,
            members: observable,
            fetchMilestones: action,
            fetchMrmsAndOnbs: action,
            setFilter: action,
            setMembers: action,
            setMilestones: action,
            setMemberRelationshipManagers: action,
            setOnboardingAccountants: action,
        });
    }

    fetchMilestones = () => jest.fn();

    fetchMrmsAndOnbs = () => jest.fn();

    setFilter = () => jest.fn();

    setMembers = () => jest.fn();

    setMilestones = (milestones) => {
        this.milestones = milestones;
    };

    setMemberRelationshipManagers = (memberRelationshipManagers) => {
        this.memberRelationshipManagers = memberRelationshipManagers;
    };

    setOnboardingAccountants = (onboardingAccountants) => {
        this.onboardingAccountants = onboardingAccountants;
    };
}

const milestones = [
    {
        name: 'Legal Setup',
        status: 'incompleted',
        members: 144,
        days: 44.5,
    },
];

const mrmsAndOnbs = [{ key: 1, text: 'test', value: 'test' }];

const renderWithProviders = (hubStore) => {
    const history = createMemoryHistory();

    return render(
        <HubStoreContext.Provider value={hubStore}>
            <Router history={history}>
                <TrackingTool fetchMemberFunc={() => {}} />
            </Router>
        </HubStoreContext.Provider>
    );
};

describe('TrackerTool', () => {
    it('should render correctly', async () => {
        const hubStore = new MockHubStore();
        const { asFragment } = renderWithProviders(hubStore);

        await waitFor(() => hubStore.fetchMilestones().mockResolvedValue(null));
        await waitFor(() => hubStore.fetchMrmsAndOnbs().mockResolvedValue(null));
        act(() => {
            hubStore.setMilestones(milestones);
            hubStore.setMemberRelationshipManagers(mrmsAndOnbs);
            hubStore.setOnboardingAccountants(mrmsAndOnbs);
        });

        expect(asFragment().firstChild).toMatchSnapshot();
    });

    it('should render the component', async () => {
        const hubStore = new MockHubStore();
        const { getByText } = renderWithProviders(hubStore);

        await waitFor(() => hubStore.fetchMilestones().mockResolvedValue(null));
        await waitFor(() => hubStore.fetchMrmsAndOnbs().mockResolvedValue(null));
        act(() => {
            hubStore.setMilestones(milestones);
            hubStore.setMemberRelationshipManagers(mrmsAndOnbs);
            hubStore.setOnboardingAccountants(mrmsAndOnbs);
        });

        expect(getByText(/Search Members/i)).toBeInTheDocument();
    });
});
Editor is loading...