diff --git a/app/[locale]/job/FormPayment.tsx b/app/[locale]/job/FormPayment.tsx index 7d5f6f3..3c87ac5 100644 --- a/app/[locale]/job/FormPayment.tsx +++ b/app/[locale]/job/FormPayment.tsx @@ -1,13 +1,15 @@ 'use client' -import ErrorSummary from "@/app/components/ErrorSummary" -import RequiredFieldDescription from "@/app/components/RequiredFieldDescription" -import TextFieldWithValidation from "@/app/components/TextFieldWithValidation" -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" +import { PaymentItem } from "@/lib/features/job/payment/paymentSlice" + +import ErrorSummary from "@/app/components/ErrorSummary" +import RequiredFieldDescription from "@/app/components/RequiredFieldDescription" +import TextFieldWithValidation from "@/app/components/TextFieldWithValidation" + export interface FormPaymentProps { onSubmit: SubmitHandler item?: PaymentItem @@ -23,12 +25,34 @@ export type FormPaymentData = { export default function FormPayment(params: FormPaymentProps) { const { t } = useTranslation() + let formatDate = () => { + let formattedDate: string = ''; + if (params.item?.date) { + const paymentDate = new Date(params.item?.date) + + const addLeadingZero = (d: number) => d.toString().length === 1 ? + `0${d}` : d + + formattedDate = `${paymentDate.getFullYear()}-${addLeadingZero(paymentDate.getMonth())}-${addLeadingZero(paymentDate.getDate())}` + } + + return formattedDate + } + const formattedDate = formatDate() + const { register, + getValues, control, formState: { errors }, handleSubmit - } = useForm() + } = useForm({ + defaultValues: { + amount: params.item?.amount, + date: formattedDate, + payer: params.item?.payer + } + }) return (
@@ -52,12 +76,16 @@ export default function FormPayment(params: FormPaymentProps) { name="date" control={control} rules={{ required: {value:true, message: t('add_income_required_field')} }} - render={({ field }) => ( + render={({ + field: { onChange, name }, + }) => ( <> @@ -78,8 +106,12 @@ export default function FormPayment(params: FormPaymentProps) { /> - + + + {/* */} ) } \ No newline at end of file diff --git a/app/[locale]/job/[jobId]/expense/FormExpense.tsx b/app/[locale]/job/[jobId]/expense/FormExpense.tsx index c259f7f..1c04a93 100644 --- a/app/[locale]/job/[jobId]/expense/FormExpense.tsx +++ b/app/[locale]/job/[jobId]/expense/FormExpense.tsx @@ -8,12 +8,12 @@ import { Button, Form, FormGroup, Checkbox, DatePicker, ComboBox, Label, Require import { Controller, SubmitHandler, useForm } from "react-hook-form" import { useTranslation } from "react-i18next" -interface ExpenseFormPaymentProps { - onSubmit: SubmitHandler +interface FormExpenseProps { + onSubmit: SubmitHandler item?: ExpenseItem } -export type ExpenseFormPaymentData = { +export type FormExpenseData = { job: string name: string expenseType: string @@ -22,7 +22,7 @@ export type ExpenseFormPaymentData = { isMileage: boolean } -export default function FormExpense(params: ExpenseFormPaymentProps) { +export default function FormExpense(params: FormExpenseProps) { const { t } = useTranslation() const expenseTypeOptions = [ @@ -41,7 +41,7 @@ export default function FormExpense(params: ExpenseFormPaymentProps) { control, formState: { errors }, handleSubmit - } = useForm() + } = useForm() return (
@@ -54,6 +54,7 @@ export default function FormExpense(params: ExpenseFormPaymentProps) { label={t('add_expense_name_field')} error={errors.name?.message} data-testid="name" + value={params.item?.name?.toString()} requiredMarker /> @@ -104,6 +105,7 @@ export default function FormExpense(params: ExpenseFormPaymentProps) { error={errors.amount?.message} data-testid="amount" requiredMarker + value={params.item?.amount?.toString()} /> diff --git a/app/[locale]/job/[jobId]/expense/[expenseId]/edit/page.test.tsx b/app/[locale]/job/[jobId]/expense/[expenseId]/edit/page.test.tsx new file mode 100644 index 0000000..7d9aa29 --- /dev/null +++ b/app/[locale]/job/[jobId]/expense/[expenseId]/edit/page.test.tsx @@ -0,0 +1,73 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import TestWrapper from '@/app/TestWrapper' +import mockRouter from 'next-router-mock' + +import { generateExpense, generateJob } from '@/test/fixtures/generator' +import { generateFormattedDate, today } from '@/test/fixtures/date' + +import Page from './page' +import { EnhancedStore } from '@reduxjs/toolkit/react' +import { makeStore } from '@/lib/store' + +import { addJob } from '@/lib/features/job/jobSlice' +import { addExpense } from '@/lib/features/job/expenses/expensesSlice' + +describe('Edit expenses', () => { + let store: EnhancedStore + const item1 = generateJob() + const expense1 = generateExpense(item1.id) + + beforeAll(() => { + vi.mock('next/navigation', () => ({ + useRouter: () => mockRouter, + usePathname: () => mockRouter.asPath, + })) + + mockRouter.push('/job/0/payment/0/edit') + store = makeStore() + store.dispatch(addJob(item1)) + store.dispatch(addExpense(expense1)) + render () + }) + + it('should load all existing items ot the page', () => { + expect(screen.getByTestId('amount')).toHaveProperty('value', expense1.item.amount.toString()) + // expect(screen.getByTestId('date-picker-external-input')).toHaveProperty('value', expense1.item.date) + expect(screen.getByTestId('name')).toHaveProperty('value', expense1.item.name) + // expect(screen.getByTestId('expenseType')).toHaveProperty('value', expense1.item.expenseType) + // expect(screen.getByTestId('isMileage')).toHaveProperty('value', expense1.item.isMileage) + + }) + + it('should allow for editing of items on the page', async ()=> { + const expected = { + amount: '11', + date: '11', + fullDate: '', + name: 'My client' + } + expected.fullDate = generateFormattedDate(today(), expected.date) + + fireEvent.click(screen.getByTestId('amount'), { + target: { + value: expected.amount + } + }) + fireEvent.click(screen.getByTestId('date-picker-button')) + // const dateButton = screen.getByText(expected.date) + // fireEvent.click(dateButton) + fireEvent.change(screen.getByTestId("name"), { + target: { + value: expected.name + } + }) + + await waitFor(() => { + expect(screen.getByTestId('amount')).toHaveProperty('value', expected.amount) + // expect(screen.getByTestId('date-picker-external-input')).toHaveProperty('value', expected.fullDate) + expect(screen.getByTestId('name')).toHaveProperty('value', expected.name) + }) + }) + it.skip('when there are existing entries, unchanged fields should not trigger form validation') +}) \ No newline at end of file diff --git a/app/[locale]/job/[jobId]/expense/[expenseId]/edit/page.tsx b/app/[locale]/job/[jobId]/expense/[expenseId]/edit/page.tsx new file mode 100644 index 0000000..6b4b434 --- /dev/null +++ b/app/[locale]/job/[jobId]/expense/[expenseId]/edit/page.tsx @@ -0,0 +1,59 @@ +'use client' + +import { useDispatch } from "react-redux" +import { useRouter } from "next/navigation" +import { Grid, GridContainer } from "@trussworks/react-uswds" +import { useTranslation } from "react-i18next" +import { useAppSelector } from "@/lib/hooks" + +import { SetExpensePayload, selectExpenseItemAt, setExpenseItem } from "@/lib/features/job/expenses/expensesSlice" +import { selectJobItemAt } from "@/lib/features/job/jobSlice" + +import FormExpense, { FormExpenseData } from '@/app/[locale]/job/[jobId]/expense/FormExpense' +import VerifyNav from "@/app/components/VerifyNav" + + +export default function EditExpense({ params }: { params: { expenseId: string, jobId: string } }) { + const { t } = useTranslation() + const dispatch = useDispatch() + const router = useRouter() + const expense = useAppSelector(state => selectExpenseItemAt(state, params.expenseId)) + + const job = useAppSelector(state => selectJobItemAt(state, params.jobId)) + const jobDescription = job ? job.description : 'your job' + + function editExpenseClicked({job=params.jobId, name, expenseType, amount, isMileage=false, date}: FormExpenseData) { + const id = params.expenseId + const expense: SetExpensePayload = { + id, + item: { + job, + name, + expenseType, + amount, + isMileage, + date, + } + } + + dispatch(setExpenseItem(expense)) + + router.push(`/expense/list`) + } + + return ( +
+ +
+ + +
+

{t('add_income_payment_header', {description: jobDescription })}

+ +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/[locale]/job/[jobId]/expense/add/page.test.tsx b/app/[locale]/job/[jobId]/expense/add/page.test.tsx index 890e006..f7fb6e1 100644 --- a/app/[locale]/job/[jobId]/expense/add/page.test.tsx +++ b/app/[locale]/job/[jobId]/expense/add/page.test.tsx @@ -59,7 +59,7 @@ describe('Add Income To Ledger Page', async () => { await waitFor(() => { expect(mockRouter).toMatchObject({ - asPath: "/job/list" + asPath: "/expense/list" }) }) }) diff --git a/app/[locale]/job/[jobId]/expense/add/page.tsx b/app/[locale]/job/[jobId]/expense/add/page.tsx index 288de2a..b3c3855 100644 --- a/app/[locale]/job/[jobId]/expense/add/page.tsx +++ b/app/[locale]/job/[jobId]/expense/add/page.tsx @@ -5,7 +5,7 @@ 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 FormExpense, { FormExpenseData } from '../FormExpense' import { createUuid } from '@/lib/store' @@ -14,7 +14,7 @@ export default function Page({ params }: { params: { jobId: string } }) { const dispatch = useAppDispatch() const router = useRouter() - function addExpenseClicked ({job=params.jobId, name, expenseType, amount, isMileage=false, date }: ExpenseFormPaymentData) { + function addExpenseClicked ({job=params.jobId, name, expenseType, amount, isMileage=false, date }: FormExpenseData) { const id = createUuid() const expenseItem: SetExpensePayload = { @@ -30,7 +30,7 @@ export default function Page({ params }: { params: { jobId: string } }) { } dispatch(addExpense(expenseItem)) - router.push('/job/list') + router.push('/expense/list') } return ( diff --git a/app/[locale]/job/[jobId]/expense/list/page.test.tsx b/app/[locale]/job/[jobId]/expense/list/page.test.tsx index e93891f..ec4d073 100644 --- a/app/[locale]/job/[jobId]/expense/list/page.test.tsx +++ b/app/[locale]/job/[jobId]/expense/list/page.test.tsx @@ -25,7 +25,6 @@ describe('List Income in Ledger Page', async () => { it('shows navigation buttons', () => { render () - expect(screen.getByTestId('add_another_button')).toBeDefined() expect(screen.getByTestId('continue_button')).toBeDefined() }) diff --git a/app/[locale]/job/[jobId]/expense/list/page.tsx b/app/[locale]/job/[jobId]/expense/list/page.tsx index 1585a69..ba8432a 100644 --- a/app/[locale]/job/[jobId]/expense/list/page.tsx +++ b/app/[locale]/job/[jobId]/expense/list/page.tsx @@ -1,17 +1,26 @@ 'use client' -import { Button, Grid, GridContainer } from '@trussworks/react-uswds' import { useTranslation } from 'react-i18next' import { useRouter } from "next/navigation" -import ExpenseList from "@/app/components/ExpenseList" -import { selectBenefits } from '@/lib/features/benefits/benefitsSlice' import { useAppSelector } from "@/lib/hooks" -import VerifyNav from "@/app/components/VerifyNav" + +import { selectBenefits } from '@/lib/features/benefits/benefitsSlice' +import { selectJobItems } from '@/lib/features/job/jobSlice' import { isStandardDeductionBetter } from '@/lib/store' +import { Button, Grid, GridContainer } from '@trussworks/react-uswds' +import ExpenseList from "@/app/components/ExpenseList" +import VerifyNav from "@/app/components/VerifyNav" + export default function Page() { const { t } = useTranslation() const router = useRouter() + const jobs = useAppSelector(state => selectJobItems(state)) + const expenseList = [] + + for (const job in jobs) { + expenseList.push() + } const benefits = useAppSelector(state => selectBenefits(state)) const standardDeductionIsBetter = useAppSelector(state => isStandardDeductionBetter(state)) @@ -34,7 +43,7 @@ export default function Page() {

{t('expenses_summary_header')}

{t('expenses_summary_subheader')} - + {expenseList}
diff --git a/app/[locale]/job/[jobId]/payment/[paymentId]/edit/page.test.tsx b/app/[locale]/job/[jobId]/payment/[paymentId]/edit/page.test.tsx new file mode 100644 index 0000000..1e07e61 --- /dev/null +++ b/app/[locale]/job/[jobId]/payment/[paymentId]/edit/page.test.tsx @@ -0,0 +1,70 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import TestWrapper from '@/app/TestWrapper' +import mockRouter from 'next-router-mock' + +import { generateJob, generatePayment } from '@/test/fixtures/generator' +import { generateFormattedDate, today } from '@/test/fixtures/date' + +import Page from './page' +import { EnhancedStore } from '@reduxjs/toolkit/react' +import { makeStore } from '@/lib/store' + +import { addJob } from '@/lib/features/job/jobSlice' +import { addPayment } from '@/lib/features/job/payment/paymentSlice' + +describe('Edit payments', () => { + let store: EnhancedStore + const item1 = generateJob() + const payment1 = generatePayment(item1.id) + + beforeAll(() => { + vi.mock('next/navigation', () => ({ + useRouter: () => mockRouter, + usePathname: () => mockRouter.asPath, + })) + + mockRouter.push('/job/0/payment/0/edit') + store = makeStore() + store.dispatch(addJob(item1)) + store.dispatch(addPayment(payment1)) + render () + }) + + it('should load all existing items to the page', () => { + expect(screen.getByTestId('amount')).toHaveProperty('value', payment1.item.amount.toString()) + // expect(screen.getByTestId('date-picker-external-input')).toHaveProperty('value', payment1.item.date) + expect(screen.getByTestId('payer')).toHaveProperty('value', payment1.item.payer) + }) + + it('should allow for editing of items on the page (check results)', async () => { + const expected = { + amount: '11', + date: '11', + fullDate: '', + payer: 'My client' + } + expected.fullDate = generateFormattedDate(today(), expected.date) + + fireEvent.click(screen.getByTestId('amount'), { + target: { + value: expected.amount + } + }) + fireEvent.click(screen.getByTestId('date-picker-button')) + // const dateButton = screen.getByText(expected.date) + // fireEvent.click(dateButton) + fireEvent.change(screen.getByTestId("payer"), { + target: { + value: expected.payer + } + }) + + await waitFor(() => { + expect(screen.getByTestId('amount')).toHaveProperty('value', expected.amount) + // expect(screen.getByTestId('date-picker-external-input')).toHaveProperty('value', expected.fullDate) + expect(screen.getByTestId('payer')).toHaveProperty('value', expected.payer) + }) + }) + it.skip('when there are existing entries, unchanged fields should not trigger form validation') +}) \ No newline at end of file diff --git a/app/[locale]/job/[jobId]/payment/[paymentId]/edit/page.tsx b/app/[locale]/job/[jobId]/payment/[paymentId]/edit/page.tsx new file mode 100644 index 0000000..8718b2c --- /dev/null +++ b/app/[locale]/job/[jobId]/payment/[paymentId]/edit/page.tsx @@ -0,0 +1,57 @@ +'use client' + +import { useDispatch } from "react-redux" +import { useRouter } from "next/navigation" +import { Grid, GridContainer } from "@trussworks/react-uswds" +import { useTranslation } from "react-i18next" +import { useAppSelector } from "@/lib/hooks" + +import { SetPaymentPayload, selectPaymentItemAt, setPaymentItem } from "@/lib/features/job/payment/paymentSlice" +import { selectJobItemAt } from "@/lib/features/job/jobSlice" + +import FormPayment, { FormPaymentData } from '@/app/[locale]/job/FormPayment' +import VerifyNav from "@/app/components/VerifyNav" + + +export default function EditPayment({ params }: { params: { paymentId: string, jobId: string } }) { + const { t } = useTranslation() + const dispatch = useDispatch() + const router = useRouter() + const payment = useAppSelector(state => selectPaymentItemAt(state, params.paymentId)) + + const job = useAppSelector(state => selectJobItemAt(state, params.jobId)) + const jobDescription = job ? job.description : 'your job' + + function editPaymentClicked({job=params.jobId, amount, date, payer}: FormPaymentData) { + const id = params.paymentId + const payment: SetPaymentPayload = { + id, + item: { + job, + amount, + date, + payer + } + } + + dispatch(setPaymentItem(payment)) + + router.push(`/job/list`) + } + + return ( +
+ +
+ + +
+

{t('add_income_payment_header', {description: jobDescription })}

+ +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/[locale]/job/[jobId]/payment/add/page.test.tsx b/app/[locale]/job/[jobId]/payment/add/page.test.tsx index d84c8b7..a24fd35 100644 --- a/app/[locale]/job/[jobId]/payment/add/page.test.tsx +++ b/app/[locale]/job/[jobId]/payment/add/page.test.tsx @@ -2,44 +2,17 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import TestWrapper from '@/app/TestWrapper' import { vi } from 'vitest' -import { generateJob } from '@/test/fixtures/generator' import mockRouter from 'next-router-mock' +import { generateJob } from '@/test/fixtures/generator' +import { generateFormattedDate, today } from '@/test/fixtures/date' + import Page from './page' import { EnhancedStore } from '@reduxjs/toolkit/react' import { makeStore } from '@/lib/store' import { addJob } from '@/lib/features/job/jobSlice' -/** - * Set date from month day year - * from https://github.com/trussworks/react-uswds/blob/main/src/components/forms/DatePicker/utils.tsx - * - * @param {number} year the year to set - * @param {number} month the month to set (zero-indexed) - * @param {number} date the date to set - * @returns {Date} the set date - */ -export const setDate = (year: number, month: number, date: number): Date => { - const newDate = new Date(0) - newDate.setFullYear(year, month, date) - return newDate -} - -/** - * todays date - * from https://github.com/trussworks/react-uswds/blob/main/src/components/forms/DatePicker/utils.tsx - * - * @returns {Date} todays date - */ -export const today = (): Date => { - const newDate = new Date() - const day = newDate.getDate() - const month = newDate.getMonth() - const year = newDate.getFullYear() - return setDate(year, month, day) -} - describe('Add Payments to Jobs Page', async () => { let store: EnhancedStore const item1 = generateJob() @@ -62,12 +35,8 @@ describe('Add Payments to Jobs Page', async () => { }) it('navigates when fields are filled in', async () => { - const todayDate = today() - const month = todayDate.getMonth()+1 - const formattedMonth = month.toString().length === 1 ? - `0${month}` : month const date = '15' - const expectedDate = `${formattedMonth}/${date}/${todayDate.getFullYear()}`; + const expectedDate = generateFormattedDate(today(), date) const datepicker: HTMLInputElement = screen.getByTestId("date-picker-external-input") fireEvent.change(screen.getByTestId("amount"), { diff --git a/app/[locale]/job/edit/[jobId]/page.tsx b/app/[locale]/job/edit/[jobId]/page.tsx index 4dc9775..480c6b1 100644 --- a/app/[locale]/job/edit/[jobId]/page.tsx +++ b/app/[locale]/job/edit/[jobId]/page.tsx @@ -1,12 +1,15 @@ 'use client' -import VerifyNav from "@/app/components/VerifyNav" -import { JobItem, selectJobItemAt, setJobItem } from "@/lib/features/job/jobSlice" -import { useAppSelector } from "@/lib/hooks" + +import { useDispatch } from "react-redux" +import { useRouter } from "next/navigation" import { Grid, GridContainer } from "@trussworks/react-uswds" import { useTranslation } from "react-i18next" +import { useAppSelector } from "@/lib/hooks" + +import { JobItem, selectJobItemAt, setJobItem } from "@/lib/features/job/jobSlice" + import FormJob, { FormJobData } from "@/app/[locale]/job/FormJob" -import { useDispatch } from "react-redux" -import { useRouter } from "next/navigation" +import VerifyNav from "@/app/components/VerifyNav" export default function EditIncome({ params }: { params: { jobId: string } }) { const { t } = useTranslation() diff --git a/app/[locale]/job/list/page.test.tsx b/app/[locale]/job/list/page.test.tsx index b306ec2..278c645 100644 --- a/app/[locale]/job/list/page.test.tsx +++ b/app/[locale]/job/list/page.test.tsx @@ -28,7 +28,6 @@ describe('List Income in Ledger Page', async () => { it('shows navigation buttons', () => { render () - expect(screen.getByTestId('add_another_button')).toBeDefined() expect(screen.getByTestId('done_button')).toBeDefined() }) diff --git a/app/[locale]/job/list/page.tsx b/app/[locale]/job/list/page.tsx index 24fbd85..15f4c22 100644 --- a/app/[locale]/job/list/page.tsx +++ b/app/[locale]/job/list/page.tsx @@ -4,17 +4,25 @@ import { Button, Grid, GridContainer } from '@trussworks/react-uswds' import { useTranslation } from 'react-i18next' import { useRouter } from "next/navigation" import { useAppSelector } from "@/lib/hooks" + +import { recommendStandardDeduction } from "@/lib/store" +import { selectJobItems } from '@/lib/features/job/jobSlice' import IncomeList from "@/app/components/IncomeList" import VerifyNav from "@/app/components/VerifyNav" -import { recommendStandardDeduction } from "@/lib/store" const DAY_COUNT = 30 export default function Page() { - const { t } = useTranslation() - const router = useRouter() - - const routeToStandardDeductionElection = useAppSelector(state => recommendStandardDeduction(state)) + const { t } = useTranslation() + const router = useRouter() + const jobs = useAppSelector(state => selectJobItems(state)) + const incomeList = [] + + for (const job in jobs) { + incomeList.push() + } + + const routeToStandardDeductionElection = useAppSelector(state => recommendStandardDeduction(state)) function doneClicked() { if (routeToStandardDeductionElection) { @@ -24,21 +32,21 @@ export default function Page() { } } - return ( -
- -
- - -
-

{t('list_income_header', {day_count: DAY_COUNT})}

- {t('list_income_subheader')} - - -
-
-
-
-
- ) + return ( +
+ +
+ + +
+

{t('list_income_header', {day_count: DAY_COUNT})}

+ {t('list_income_subheader')} + {incomeList} + +
+
+
+
+
+ ) } diff --git a/app/[locale]/job/review/JobReviewHeader.test.tsx b/app/[locale]/job/review/JobReviewHeader.test.tsx index 63354cc..b3030ea 100644 --- a/app/[locale]/job/review/JobReviewHeader.test.tsx +++ b/app/[locale]/job/review/JobReviewHeader.test.tsx @@ -1,25 +1,26 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' -import JobReviewHeader from './JobReviewHeader' -import { BenefitsState } from '@/lib/features/benefits/benefitsSlice' import TestWrapper from '@/app/TestWrapper' import { generateBenefits } from '@/test/fixtures/generator' +import JobReviewHeader from './JobReviewHeader' +import { BenefitsState } from '@/lib/features/benefits/benefitsSlice' + describe('Ledger Review Header', async () => { const SNAP_INCOME = 350.00 const MEDICAID_INCOME = 412.00 afterEach(cleanup) - describe('Medicaid and Snap', () => { - let benefits: BenefitsState - beforeEach(() => { - benefits = { - standardDeduction: true, - deductionAmount: 50, - medicaid: true, - snap: true, - } + describe('Medicaid and Snap', () => { + let benefits: BenefitsState + beforeEach(() => { + benefits = generateBenefits({ + standardDeduction: true, + deductionAmount: 50, + medicaid: true, + snap: true, + }) render () }) @@ -49,10 +50,10 @@ describe('Ledger Review Header', async () => { let benefits: BenefitsState beforeEach(() => { benefits = generateBenefits({ - snap: false, + standardDeduction: true, + deductionAmount: 50, medicaid: true, - standardDeduction: false, - deductionAmount: 0 + snap: false, }) render () @@ -84,12 +85,12 @@ describe('Ledger Review Header', async () => { describe('SNAP Only', () => { let benefits: BenefitsState beforeEach(() => { - benefits = { + benefits = generateBenefits({ standardDeduction: true, deductionAmount: 50, medicaid: false, snap: true, - } + }) render () }) diff --git a/app/[locale]/job/review/page.tsx b/app/[locale]/job/review/page.tsx index 55cd95a..a221e7e 100644 --- a/app/[locale]/job/review/page.tsx +++ b/app/[locale]/job/review/page.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { useAppSelector } from "@/lib/hooks" import { useRouter } from "next/navigation" import { selectBenefits } from "@/lib/features/benefits/benefitsSlice" -import { selectTotalPaymentsByAllJobs } from "@/lib/features/job/jobSlice" +import { selectJobItems, selectTotalPaymentsByAllJobs } from "@/lib/features/job/jobSlice" import Link from "next/link" import IncomeList from "@/app/components/IncomeList" import ExpenseList from "@/app/components/ExpenseList" @@ -17,9 +17,18 @@ const DAY_COUNT = 30 export default function Page() { const { t } = useTranslation() const router = useRouter() + const allJobs = useAppSelector(state => selectJobItems(state)) const benefits = useAppSelector(state => selectBenefits(state)) const incomeTotal = useAppSelector(state => selectTotalPaymentsByAllJobs(state)) + const jobItems = [] + const expenseItems = [] + + for (const job in allJobs) { + jobItems.push() + expenseItems.push() + } + function continueButtonClicked() { router.push("/statement/sign") } @@ -39,8 +48,8 @@ export default function Page() {
{t("review_legally_sign")}
- - + {jobItems} + {expenseItems} diff --git a/app/components/ExpenseList.tsx b/app/components/ExpenseList.tsx index d3cfa58..d93c4f2 100644 --- a/app/components/ExpenseList.tsx +++ b/app/components/ExpenseList.tsx @@ -1,51 +1,72 @@ -import { Button, ButtonGroup, Card, CardBody, CardGroup, CardHeader, GridContainer } from "@trussworks/react-uswds"; +'use client' + import { useTranslation } from "react-i18next"; import { useAppSelector } from "@/lib/hooks"; import { useRouter } from "next/navigation"; -import { selectJobItems, selectTotalExpensesByAllJobs } from "@/lib/features/job/jobSlice" +import { Button, ButtonGroup, Card, CardBody, CardGroup, CardHeader, Grid, GridContainer } from "@trussworks/react-uswds"; + +import { selectTotalExpensesByAllJobs } from "@/lib/features/job/jobSlice" +import { selectExpensesByJob } from "@/lib/features/job/expenses/expensesSlice"; + import ExpenseListItem from "./ExpenseListItem"; interface ExpenseListProps { header: string + jobId: string } -export default function ExpenseList({header}: ExpenseListProps) { +export default function ExpenseList({header, jobId}: ExpenseListProps) { const { t } = useTranslation() const router = useRouter() - const jobs = useAppSelector(state => selectJobItems(state)) + const expenses = useAppSelector(state => selectExpensesByJob(state, jobId)) const expenseTotal = useAppSelector(state => selectTotalExpensesByAllJobs(state)) const expenseItemElements = [] - for (const job in jobs) { - expenseItemElements.push() + for (const expense in expenses.byId) { + expenseItemElements.push() } - function getTotal() { - if (expenseTotal > 0) { - return (t('expenses_summary_total', {amount: expenseTotal})) + + function addItemClicked() { + router.push("/job/add") + } + + function addPaymentClicked() { + router.push(`/job/${jobId}/payment/add`) } - return <> + function addExpenseClicked() { + router.push(`/job/{jobId}/expense/add`) } - function addItemClicked() { - router.push("/job/expense/add") + function getTotal() { + return expenseTotal > 0 ? + (t('expenses_summary_total', { amount: expenseTotal })) : + (<>) } return ( - - - {header} - - - {expenseItemElements} - - { getTotal() } + + + + {header} + + + {expenseItemElements} + + { getTotal() } + + + + + - + - - - + +
+ + + ) } \ No newline at end of file diff --git a/app/components/ExpenseListItem.tsx b/app/components/ExpenseListItem.tsx index 046ad58..e8cc3d5 100644 --- a/app/components/ExpenseListItem.tsx +++ b/app/components/ExpenseListItem.tsx @@ -1,8 +1,10 @@ import { useTranslation } from "react-i18next" -import { ExpenseItem, removeExpense, selectExpensesByJob } from "@/lib/features/job/expenses/expensesSlice" -import { JobItem } from "@/lib/features/job/jobSlice" -import { useAppDispatch, useAppSelector } from "@/lib/hooks" +import { useAppDispatch } from "@/lib/hooks" import { useRouter } from "next/navigation" +import { useRef } from "react" + +import { ExpenseItem, removeExpense } from "@/lib/features/job/expenses/expensesSlice" + import { Button, ButtonGroup, @@ -12,41 +14,35 @@ import { ModalHeading, ModalToggleButton } from "@trussworks/react-uswds" -import { useRef } from "react" interface ItemProps { - item: JobItem - index: string + expenseId: string + jobId: string + expense: ExpenseItem } -export default function ExpenseListItem({ item, index }: ItemProps) { + +export default function ExpenseListItem({ expenseId, jobId, expense }: ItemProps) { const ref = useRef(null) const { t } = useTranslation() const dispatch = useAppDispatch() const router = useRouter() - const expenses = useAppSelector(state => selectExpensesByJob(state, index)).map((expense: ExpenseItem) => { - return (
  • {expense.date} ${expense.amount} {expense.name} ({expense.expenseType})
  • ) - }) function onDeleteClicked() { - dispatch(removeExpense(index)) + dispatch(removeExpense(expenseId)) } function editClicked() { - router.push(`/job/expense/edit/${index}`) + router.push(`/job/${jobId}/expense/edit/${expenseId}`) } return ( -
    {item.description}
    -
    {item.business}
    -
    -
      - {expenses} -
    -
    +
    ${expense.amount}
    +
    {expense.date}
    +
    {expense.name}
    - + {t('expenses_summary_list_delete')} {t('expenses_summary_delete_are_you_sure')} diff --git a/app/components/IncomeList.tsx b/app/components/IncomeList.tsx index ba2c24a..93c87c4 100644 --- a/app/components/IncomeList.tsx +++ b/app/components/IncomeList.tsx @@ -1,54 +1,72 @@ 'use client' -import { selectJobItems, selectTotalPaymentsByAllJobs } from "@/lib/features/job/jobSlice" import { useAppSelector } from "@/lib/hooks" import { useRouter } from "next/navigation" -import IncomeListItem from "./IncomeListItem" -import { Button, ButtonGroup, Card, CardBody, CardGroup, CardHeader, GridContainer } from "@trussworks/react-uswds" +import { Button, ButtonGroup, Card, CardBody, CardGroup, CardHeader, Grid, GridContainer } from "@trussworks/react-uswds" import { useTranslation } from "react-i18next" +import { JobItem, selectTotalPaymentsByAllJobs } from "@/lib/features/job/jobSlice" + +import IncomeListItem from "./IncomeListItem" +import { selectPaymentsByJob } from "@/lib/features/job/payment/paymentSlice" + interface Props { dayCount: number - header: string + job: JobItem + jobId: string } -export default function IncomeList({dayCount, header}: Props) { +export default function IncomeList({dayCount, job, jobId}: Props) { const { t } = useTranslation() const router = useRouter() - const jobs = useAppSelector(state => selectJobItems(state)) + const payments = useAppSelector(state => selectPaymentsByJob(state, jobId)) const incomeTotal = useAppSelector(state => selectTotalPaymentsByAllJobs(state)) const incomeItemElements = [] - for (const job in jobs) { - incomeItemElements.push() + for (const payment in payments.byId) { + incomeItemElements.push() } - function getTotal() { - if (incomeTotal > 0) { - return (t('list_income_total', {day_count: dayCount, amount: incomeTotal})) - } + function addItemClicked() { + router.push("/job/add") + } - return <> + function addPaymentClicked() { + router.push(`/job/${jobId}/payment/add`) } - function addItemClicked() { - router.push("/job/add") + function addExpenseClicked() { + router.push(`/job/${jobId}/expense/add`) + } + + function getTotal() { + return incomeTotal > 0 ? + (t('list_income_total', { day_count: dayCount, amount: incomeTotal})) : + (<>) } return ( - - - {header} - - - {incomeItemElements} - - {getTotal()} - - - - - - + + + + {job.description} + + + {incomeItemElements} + + {getTotal()} + + + + + + + + + + + +
    +
    ) } \ No newline at end of file diff --git a/app/components/IncomeListItem.tsx b/app/components/IncomeListItem.tsx index 877c0a5..5240f6a 100644 --- a/app/components/IncomeListItem.tsx +++ b/app/components/IncomeListItem.tsx @@ -1,85 +1,52 @@ import { useTranslation } from "react-i18next" -import { useAppSelector } from "@/lib/hooks" -import { JobItem, removeJob } from "@/lib/features/job/jobSlice" -import { PaymentItem, selectPaymentsByJob } from "@/lib/features/job/payment/paymentSlice" -import { selectExpensesByJob } from "@/lib/features/job/expenses/expensesSlice" import { useAppDispatch } from "@/lib/hooks" import { Grid, ModalToggleButton, Modal, ModalHeading, ModalFooter, ButtonGroup, Button } from "@trussworks/react-uswds" import { useRef } from "react" import { useRouter } from "next/navigation" -import { ExpenseItem } from "@/lib/features/job/expenses/expensesSlice" + +import { PaymentItem, removePayment } from "@/lib/features/job/payment/paymentSlice" interface ItemProps { - item: JobItem - index: string + paymentId: string + jobId: string + payment: PaymentItem } -export default function IncomeListItem({ item, index }: ItemProps) { - const ref = useRef(null) - const { t } = useTranslation() - const dispatch = useAppDispatch() - const router = useRouter() - const payments = useAppSelector(state => selectPaymentsByJob(state, index)).map((payment: PaymentItem) => { - return (
  • {payment.date} ${payment.amount} {t('list_income_by')} {payment.payer}
  • ) - }) - const expenses = useAppSelector(state => selectExpensesByJob(state, index)).map((expense: ExpenseItem) => { - return (
  • {expense.date} ${expense.amount} {expense.name} ({expense.expenseType})
  • ) - }) - - function onDeleteClicked() { - dispatch(removeJob(index)) - } - - function editClicked() { - router.push(`/job/edit/${index}`) - } +export default function IncomeListItem({ paymentId, jobId, payment }: ItemProps) { + const ref = useRef(null) + const { t } = useTranslation() + const dispatch = useAppDispatch() + const router = useRouter() - function addPaymentClicked() { - router.push(`/job/${index}/payment/add`) - } + function onDeleteClicked() { + dispatch(removePayment(paymentId)) + } - function addExpenseClicked() { - router.push(`/job/${index}/expense/add`) - } + function editClicked() { + router.push(`/job/${jobId}/payment/${paymentId}/edit`) + } - return ( - - -
    {item.description}
    -
    {item.business}
    -

    {t('list_income_list_payments')}

    -
    -
      - {payments} -
    -
    -

    {t('list_income_list_expenses')}

    -
    -
      - {expenses} -
    -
    -
    - - {t('list_income_delete_button')} - - {t('list_income_delete_are_you_sure')} - - - {t('list_income_no_delete_item')} - {t('list_income_yes_delete_item')} - - - - - - - - - - + -
    + return ( + + +
    ${payment.amount}
    +
    {payment.date}
    +
    {payment.payer}
    +
    + + {t('list_income_delete_button')} + + {t('list_income_delete_are_you_sure')} + + + {t('list_income_no_delete_item')} + {t('list_income_yes_delete_item')} + + + + +
    -
    - ) + ) } \ No newline at end of file diff --git a/app/i18n/locales/en/translation.json b/app/i18n/locales/en/translation.json index fcd4e88..577154a 100644 --- a/app/i18n/locales/en/translation.json +++ b/app/i18n/locales/en/translation.json @@ -132,7 +132,7 @@ "intro_secure": "Your information is secure", "intro_subheader": "We'll help you document your self-employed or cash based income and send it to your benefits application so you can get benefits faster", "intro_title": "Verify.gov | Self-employment", - "list_income_add_expense_button": "+ Add exenses for this job", + "list_income_add_expense_button": "+ Add expenses for this job", "list_income_add_job_button": "+ Add another job", "list_income_add_payment_button": "+ Add another payment", "list_income_by": "by", @@ -140,9 +140,6 @@ "list_income_delete_button": "Delete", "list_income_done_button": "Done", "list_income_header": "Would you like to add another person, client, or company who paid you in the last {{day_count}} days?", - "list_income_list_header": "Your jobs and payments", - "list_income_list_expenses": "Expenses", - "list_income_list_payments": "Payments", "list_income_no_delete_item": "No, take me back to the list", "list_income_subheader": "Add another person, client, or company below", "list_income_title": "Self-employment income summary", diff --git a/lib/features/job/expenses/expensesSlice.test.ts b/lib/features/job/expenses/expensesSlice.test.ts index 3fefa60..5816e69 100644 --- a/lib/features/job/expenses/expensesSlice.test.ts +++ b/lib/features/job/expenses/expensesSlice.test.ts @@ -81,7 +81,7 @@ describe('ExpenseSlice', () => { const secondJobId = createUuid() store.dispatch(addExpense(generateExpense(secondJobId))) - expect(selectExpensesByJob(store.getState(), secondJobId).length).toBe(1) + expect(selectExpensesByJob(store.getState(), secondJobId).allIds.length).toBe(1) }) diff --git a/lib/features/job/expenses/expensesSlice.ts b/lib/features/job/expenses/expensesSlice.ts index b3075ee..3ca1796 100644 --- a/lib/features/job/expenses/expensesSlice.ts +++ b/lib/features/job/expenses/expensesSlice.ts @@ -53,17 +53,21 @@ export const { addExpense, removeExpense, setExpenseItem } = expenseSlice.action export const selectExpenseItemAt = (state: RootState, id: string) => state.expenses.byId[id] export const selectExpensesByJob = (state: RootState, jobId: string) => { - const selectedExpenses: Array = [] + let selectedExpenses: ExpenseState = initialState for (const expenseId in state.expenses.byId) { - const currentExpense = selectExpenseItemAt(state, expenseId) - if (currentExpense.job === jobId) - selectedExpenses.push(currentExpense) + const currentExpense = state.expenses.byId[expenseId] + if (currentExpense.job === jobId) { + selectedExpenses = expenseSlice.reducer( + selectedExpenses, + addExpense({ + id: expenseId, + item: currentExpense + } as SetExpensePayload)) + } } return selectedExpenses } - - export default expenseSlice.reducer \ No newline at end of file diff --git a/lib/features/job/jobSlice.ts b/lib/features/job/jobSlice.ts index 1fd2b03..193b16f 100644 --- a/lib/features/job/jobSlice.ts +++ b/lib/features/job/jobSlice.ts @@ -1,6 +1,5 @@ import { createSlice, PayloadAction} from '@reduxjs/toolkit' -import { PaymentItem, selectPaymentsByJob } from './payment/paymentSlice' -import { ExpenseItem } from './expenses/expensesSlice' +import { selectPaymentsByJob } from './payment/paymentSlice' import { RootState } from '../../store' import { selectExpensesByJob } from './expenses/expensesSlice' @@ -58,15 +57,31 @@ export const selectJobCount = (state: RootState) => state.jobs.allIds.length * * @param state */ -export const selectTotalPaymentsByJob = (state: RootState, jobId: string) => - selectPaymentsByJob(state, jobId).reduce((total: number, payment: PaymentItem) => total + payment.amount, 0) +export const selectTotalPaymentsByJob = (state: RootState, jobId: string) => { + let total = 0, + payments = selectPaymentsByJob(state, jobId) + + for (const payment in payments.byId) { + total = total + payments.byId[payment].amount + } + + return total +} export const selectTotalPaymentsByAllJobs = (state: RootState) => state.payment.allIds.reduce((total: number, paymentId: string) => total + state.payment.byId[paymentId].amount, 0) -export const selectTotalExpensesByJob = (state: RootState, jobId: string) => - selectExpensesByJob(state, jobId).reduce((total: number, expense: ExpenseItem) => expense.amount + total, 0) +export const selectTotalExpensesByJob = (state: RootState, jobId: string) => { + let total = 0, + expenses = selectExpensesByJob(state, jobId) + + for (const expense in expenses.byId) { + total = total + expenses.byId[expense].amount + } + + return total +} export const selectTotalExpensesByAllJobs = (state: RootState) => state.expenses.allIds.reduce((total: number, expenseId: string) => total + state.expenses.byId[expenseId].amount, 0 ) diff --git a/lib/features/job/payment/paymentSlice.test.ts b/lib/features/job/payment/paymentSlice.test.ts index 77a755a..7615191 100644 --- a/lib/features/job/payment/paymentSlice.test.ts +++ b/lib/features/job/payment/paymentSlice.test.ts @@ -72,7 +72,9 @@ describe('PaymentSlice', () => { store.dispatch(addPayment(generatePayment(secondJobId))) - expect(selectPaymentsByJob(store.getState(), secondJobId).length).toBe(1) + const payments = selectPaymentsByJob(store.getState(), secondJobId) + + expect(payments.allIds.length).toBe(1) }) }) }) \ No newline at end of file diff --git a/lib/features/job/payment/paymentSlice.ts b/lib/features/job/payment/paymentSlice.ts index d3e3ec7..aeb8b2d 100644 --- a/lib/features/job/payment/paymentSlice.ts +++ b/lib/features/job/payment/paymentSlice.ts @@ -49,16 +49,23 @@ export const PaymentSlice = createSlice({ export const { addPayment, removePayment, setPaymentItem } = PaymentSlice.actions export const selectPaymentItemAt = (state: RootState, id: string) => state.payment.byId[id] + export const selectPaymentsByJob = (state: RootState, jobId: string) => { - const selectedPayments: Array = [] + let selectedPayments: PaymentState = initialState for (const paymentId in state.payment.byId) { - const currentPayment = selectPaymentItemAt(state, paymentId) - if (currentPayment.job === jobId) - selectedPayments.push(currentPayment) + const currentPayment = state.payment.byId[paymentId] + if (currentPayment.job === jobId) { + selectedPayments = PaymentSlice.reducer( + selectedPayments, + addPayment({ + id: paymentId, + item: currentPayment + } as SetPaymentPayload)) + } } - return selectedPayments + } export default PaymentSlice.reducer \ No newline at end of file diff --git a/test/fixtures/date.test.ts b/test/fixtures/date.test.ts new file mode 100644 index 0000000..6a1f676 --- /dev/null +++ b/test/fixtures/date.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' + +import { generateFormattedDate, today } from './date' + +describe('today', () => { + it('returns today as a date object', () => { + const todaysDate = new Date() + + expect(today()).toBeInstanceOf(Date) + expect(today().toDateString()).toEqual(todaysDate.toDateString()) + }) +}) + +describe('getFormattedDate', () => { + it('returns a date formatted as MM/DD/YYYY \(with leading zeros\)', () => { + const dateUnderTest = new Date(2024, 5, 1) // June 1, 2024 + + expect(generateFormattedDate(dateUnderTest)).toEqual('06/01/2024') + }) + it('can change the date given the optional parameter', () => { + const dateUnderTest = new Date(2024, 5, 1) // June 1, 2024 + const changedDate = '6' + + expect(generateFormattedDate(dateUnderTest, changedDate)).toEqual('06/06/2024') + }) + it.each([ + ['12/31/2004', 2004, 11, 31], + ['01/01/2025', 2025, 0, 1], + ['02/29/2024', 2024, 1, 29], + ['03/01/2023', 2023, 1, 29] + ])('can handle the edges: %i ', (expected, y, m, d) => { + expect(generateFormattedDate(new Date(y, m, d))).toEqual(expected) + } ) +}) \ No newline at end of file diff --git a/test/fixtures/date.ts b/test/fixtures/date.ts new file mode 100644 index 0000000..71fceee --- /dev/null +++ b/test/fixtures/date.ts @@ -0,0 +1,31 @@ +/** + * todays date as midnight local time + * from https://github.com/trussworks/react-uswds/blob/main/src/components/forms/DatePicker/utils.tsx + * + * @returns {Date} todays date + */ +export const today = (): Date => { + const newDate = new Date() + const day = newDate.getDate() + const month = newDate.getMonth() + const year = newDate.getFullYear() + return new Date(year, month, day) +} + +/** + * generates a date with leading zeros in MM/DD/YYYY format + * note: all error dates will generate as the following day, like + * 9/31/2024 will generate as 10/1/2024 + * + * @param date Date + * @param staticDate string + * @returns string + */ +export const generateFormattedDate = (date: Date, staticDate?: string): string => { + const formattedMonth = addLeadingZero((date.getMonth()+1).toString()) + const day = addLeadingZero(staticDate ? staticDate : date.getDate().toString()) + return `${formattedMonth}/${day}/${date.getFullYear()}`; +} + +const addLeadingZero = (num: string) => num.length === 1 ? +`0${num}` : num as String \ No newline at end of file diff --git a/test/fixtures/generator.ts b/test/fixtures/generator.ts index d239011..912f248 100644 --- a/test/fixtures/generator.ts +++ b/test/fixtures/generator.ts @@ -5,6 +5,8 @@ import { SetExpensePayload, ExpenseItem } from "@/lib/features/job/expenses/expe import { SetJobPayload, JobItem } from "@/lib/features/job/jobSlice" import { SetPaymentPayload, PaymentItem } from "@/lib/features/job/payment/paymentSlice" +import { generateFormattedDate, today } from "./date" + /** * emptyStateObject * Object @@ -49,7 +51,7 @@ export const generateExpense = (jobId: string, payload?: ExpenseItem, expenseId? job: jobId, name: 'Gas', amount: 10, - date: new Date().toString(), + date: generateFormattedDate(new Date('2024-08-22')), expenseType: 'Gas', isMileage: false } as ExpenseItem @@ -91,10 +93,11 @@ export const generateJob = (payload?: JobItem, jobId?: string) => { * @returns SetPaymentPayload */ export const generatePayment = (jobId: string, payload?: PaymentItem, paymentId?: string) => { + const todaysDate = today() const defaultPayload = { job: jobId, amount: 10, - date: '', + date: generateFormattedDate(todaysDate), payer: 'Someone' } as PaymentItem