Skip to content

Commit

Permalink
Merge pull request #66 from DSACMS/k8/FFS-1487/expenses-under-jobs-da…
Browse files Browse the repository at this point in the history
…ta-model-change

Job data model change
  • Loading branch information
kategreenUSDS authored Oct 7, 2024
2 parents 264fca9 + 591fe1b commit 915bacf
Show file tree
Hide file tree
Showing 42 changed files with 1,014 additions and 564 deletions.
3 changes: 2 additions & 1 deletion app/[locale]/job/IncomeFormPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import ErrorSummary from "@/app/components/ErrorSummary"
import RequiredFieldDescription from "@/app/components/RequiredFieldDescription"
import TextFieldWithValidation from "@/app/components/TextFieldWithValidation"
import { PaymentItem } from "@/lib/features/job/jobSlice"
import { PaymentItem } from "@/lib/features/job/payment/paymentSlice"
import { Button, DatePicker, Form, FormGroup, Label, RequiredMarker } from "@trussworks/react-uswds"
import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
Expand All @@ -14,6 +14,7 @@ export interface IncomeFormPaymentProps {
}

export type IncomeFormPaymentData = {
job: string
amount: number
date: string
payer: string
Expand Down
134 changes: 134 additions & 0 deletions app/[locale]/job/[idx]/expense/FormExpense.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use client'

import ErrorSummary from "@/app/components/ErrorSummary"
import RequiredFieldDescription from "@/app/components/RequiredFieldDescription"
import TextFieldWithValidation from "@/app/components/TextFieldWithValidation"
import { ExpenseItem } from "@/lib/features/job/expenses/expensesSlice"
import { Button, Form, FormGroup, Checkbox, DatePicker, ComboBox, Label, RequiredMarker } from '@trussworks/react-uswds'
import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"

interface ExpenseFormPaymentProps {
onSubmit: SubmitHandler<ExpenseFormPaymentData>
item?: ExpenseItem
}

export type ExpenseFormPaymentData = {
job: string
name: string
expenseType: string
amount: number
date: string
isMileage: boolean
}

export default function FormExpense(params: ExpenseFormPaymentProps) {
const { t } = useTranslation()

const expenseTypeOptions = [
t('add_expense_materials'),
t('add_expense_travel'),
t('add_expense_equipment'),
t('add_expense_advertising'),
t('add_expense_cost'),
t('add_expense_other')
].map((str) => {
return { value: str, label: str }
})

const {
register,
control,
formState: { errors },
handleSubmit
} = useForm<ExpenseFormPaymentData>()

return (<Form onSubmit={handleSubmit(params.onSubmit)}>
<RequiredFieldDescription />
<ErrorSummary errors={errors} headingText={t('add_income_error_header')} />

<FormGroup>
<TextFieldWithValidation
id="name"
{...register("name", {required:{value: true, message: t('add_expense_name_required')}})}
label={t('add_expense_name_field')}
error={errors.name?.message}
data-testid="name"
requiredMarker
/>
</FormGroup>

<FormGroup>
<Controller
name="isMileage"
control={control}
render={({ field }) =>
<Checkbox
id="isMileage"
{...field}
label={t('add_expense_mileage_field')}
value="true"
data-testid="isMileage"
/>
}
/>

</FormGroup>

<FormGroup>
<Controller
name="date"
control={control}
rules={{ required: {value:true, message: t('add_expense_date_required')} }}
render={({ field }) => (
<>
<Label htmlFor="date">{t('add_expense_date_field')}<RequiredMarker /></Label>
<p className="usa-hint font-body-2xs">{t('add_expense_date_hint')}</p>
<DatePicker
id="date"
data-testid="date"
{...field}
{...(errors.date?.message !== undefined ? {validationStatus: 'error'} : {})}
/>
</>
)
}
/>
</FormGroup>

<FormGroup>
<TextFieldWithValidation
id="amount"
{...register("amount", { valueAsNumber:true, validate: (value) => value > 0, required:{value: true, message: t('add_expense_amount_required')}})}
label={t('add_expense_amount_field')}
error={errors.amount?.message}
data-testid="amount"
requiredMarker
/>
</FormGroup>

<FormGroup>
<Controller
name="expenseType"
control={control}
// Question if this field is optional or required https://confluenceent.cms.gov/pages/viewpage.action?spaceKey=SFIV&title=IRT+Epics+and+Stories
// rules={{ required: {value: true, message: t('add_expense_type_required')}}}
render={({ field }) => (
<>
<Label htmlFor="expenseType">{t('add_expense_type_field')}</Label>
<p className="usa-hint font-body-2xs">{t('add_expense_type_hint')}</p>
<ComboBox
id="expenseType"
options={expenseTypeOptions}
{...field}
data-testid="expenseType"
/>
</>
)
}
/>
</FormGroup>

<Button type="submit" data-testid="continue_button">{t('add_expense_continue_button')}</Button>
</Form>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('Add Income To Ledger Page', async () => {
}))
mockRouter.push('/job/expense/add')
store = makeStore()
render (<Provider store={store}><Page /></Provider>)
render (<Provider store={store}><Page params={{idx: '0'}} /></Provider>)
})
afterEach(cleanup)

Expand Down Expand Up @@ -59,7 +59,7 @@ describe('Add Income To Ledger Page', async () => {

await waitFor(() => {
expect(mockRouter).toMatchObject({
asPath: "/job/expense/list"
asPath: "/job/list"
})
})
})
Expand Down
52 changes: 52 additions & 0 deletions app/[locale]/job/[idx]/expense/add/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client'
import { useTranslation } from 'react-i18next'
import { Grid, GridContainer } from '@trussworks/react-uswds'
import { useAppDispatch } from "@/lib/hooks"
import { SetExpensePayload, addExpense } from "@/lib/features/job/expenses/expensesSlice"
import { useRouter } from "next/navigation"
import VerifyNav from "@/app/components/VerifyNav"
import FormExpense, { ExpenseFormPaymentData } from '../FormExpense'
import { createUuid } from '@/lib/store'


export default function Page({ params }: { params: { idx: string } }) {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const router = useRouter()

function addExpenseClicked ({job=params.idx, name, expenseType, amount, isMileage=false, date }: ExpenseFormPaymentData) {
const id = createUuid()

const expenseItem: SetExpensePayload = {
id,
item: {
job,
name,
expenseType,
amount,
isMileage,
date,
}
}

dispatch(addExpense(expenseItem))
router.push('/job/list')
}

return (
<div>
<VerifyNav title={t('add_expense_title')} />
<div className="usa-section">
<GridContainer>
<Grid row gap>
<main className="usa-layout-docs">
<h3>{t('add_expense_header')}</h3>
<h4 className="margin-top-2">{t('add_expense_subheader', {month_count: '3'})}</h4>
<FormExpense onSubmit={addExpenseClicked} />
</main>
</Grid>
</GridContainer>
</div>
</div>
)
}
64 changes: 64 additions & 0 deletions app/[locale]/job/[idx]/expense/list/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import { Provider } from 'react-redux'
import Page from './page'
import { makeStore, createUuid } from '@/lib/store'
import { vi } from 'vitest'
import { EnhancedStore } from '@reduxjs/toolkit'
import mockRouter from 'next-router-mock'
import { SetExpensePayload, addExpense } from '@/lib/features/job/expenses/expensesSlice'

describe('List Income in Ledger Page', async () => {
let store: EnhancedStore
beforeEach(() => {
vi.mock('next/navigation', () => ({
useRouter: () => mockRouter,
usePathname: () => mockRouter.asPath,
}))
mockRouter.push('/job/expense/add')
store = makeStore()
})
afterEach(cleanup)

it('shows navigation buttons', () => {
render (<Provider store={store}><Page /></Provider>)
expect(screen.getByTestId('add_another_button')).toBeDefined()
expect(screen.getByTestId('continue_button')).toBeDefined()
})

it('shows expenses in a list', () => {
const expense1: SetExpensePayload = {
id: createUuid(),
item: {
job: createUuid(),
name: "Supplies",
date: "2024/11/07",
amount: 20,
isMileage: true,
expenseType: "Other"
}
}

const expense2: SetExpensePayload = {
id: createUuid(),
item: {
job: createUuid(),
name: "Clothing for work",
date: "2024/11/09",
amount: 49,
isMileage: false,
expenseType: "Other"
}
}
const expenses = [expense1, expense2]
for (const expense of expenses) {
store.dispatch(addExpense(expense))
}
render(<Provider store={store}><Page /></Provider>)

for (const expense of expenses) {
expect(screen.findByText(expense.item.name)).toBeDefined()
expect(screen.findByText("$" + expense.item.amount)).toBeDefined()
}
})
})
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeAll, describe, expect, it } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import Page from './page'
import { makeStore } from '@/lib/store'
import { makeStore, createUuid } from '@/lib/store'
import { vi } from 'vitest'
import { EnhancedStore } from '@reduxjs/toolkit'
import mockRouter from 'next-router-mock'
import { BenefitsState, selectBenefits, setBenefits } from '@/lib/features/benefits/benefitsSlice'
import { JobItem, addJob } from '@/lib/features/job/jobSlice'
import { JobItem, SetJobPayload, addJob } from '@/lib/features/job/jobSlice'
import TestWrapper from '@/app/TestWrapper'

describe('SNAP Recommend Deduction Screen', async () => {
let store: EnhancedStore
beforeEach(() => {
beforeAll(() => {
vi.mock('next/navigation', () => ({
useRouter: () => mockRouter,
usePathname: () => mockRouter.asPath,
Expand All @@ -26,23 +26,17 @@ describe('SNAP Recommend Deduction Screen', async () => {
}
store.dispatch(setBenefits(benefits))

const incomeItem: JobItem = {
description: 'A description2',
business: '',
taxesFiled: false,
payments: [
{
idx: 0,
amount: 10,
date: '09/30/2024',
payer: 'Someone'
}
]
const jobItem: SetJobPayload = {
id: createUuid(),
item: {
description: 'A description2',
business: '',
taxesFiled: false
} as JobItem
}
store.dispatch(addJob(incomeItem))
store.dispatch(addJob(jobItem))
render(<TestWrapper store={store}><Page /></TestWrapper>)
})
afterEach(cleanup)

it('shows header', () => {
expect(screen.getByTestId('expenses_snap_recommend_header')).toBeDefined()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"
import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { useAppDispatch, useAppSelector } from "@/lib/hooks"
import { BenefitsState, selectBenefits, setBenefits } from "@/lib/features/benefits/benefitsSlice"
import { selectExpenseTotal } from "@/lib/features/job/expenses/expensesSlice"
import { selectTotalExpensesByAllJobs } from '@/lib/features/job/jobSlice'
import { useEffect } from "react"
import { isStandardDeductionBetter } from "@/lib/store"
import VerifyNav from "@/app/components/VerifyNav"
Expand All @@ -16,7 +16,7 @@ export default function Page() {
const router = useRouter()
const dispatch = useAppDispatch()
const benefits = useAppSelector(state => selectBenefits(state))
const expenseTotal = useAppSelector(state => selectExpenseTotal(state))
const expenseTotal = useAppSelector(state => selectTotalExpensesByAllJobs(state))
const standardDeductionIsBetter = useAppSelector(state => isStandardDeductionBetter(state))

interface FormData {
Expand Down
Loading

0 comments on commit 915bacf

Please sign in to comment.