Skip to content

Commit

Permalink
Merge pull request #195 from autentia/origin/feature/new_subcontracte…
Browse files Browse the repository at this point in the history
…d_activities_filters

Origin/feature/new subcontracted activities filters
  • Loading branch information
fjmpaez authored May 28, 2024
2 parents 087269e + 9958d33 commit 4b01cc6
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { SubcontractedActivityRepository } from '../domain/subcontracted-activit
import { SubcontractedActivitiesWithRoleInformation } from '../domain/services/subcontracted-activities-with-role-information'
import { GetSubcontractedActivitiesQry } from './get-subcontracted-activities-qry'
import { UserMother } from '../../../../../test-utils/mothers/user-mother'
import { GetSubcontractedActivitiesQueryParams } from '../domain/get-subcontracted-activities-query-params'

describe('GetSubcontractedActivitiesQry', () => {
it('should return subcontracted activities sorted by the given interval', async () => {
const { getSubcontractedActivitiesQry, interval, subcontractedActivities } = setup()
const { getSubcontractedActivitiesQry, params, subcontractedActivities } = setup()

const result = await getSubcontractedActivitiesQry.internalExecute(interval)
const result = await getSubcontractedActivitiesQry.internalExecute(params)

expect(result).toEqual(subcontractedActivities)
})
Expand All @@ -24,9 +25,10 @@ function setup() {
const searchProjectRolesQry = mock<SearchProjectRolesQry>()
const getUserLoggedQry = mock<GetUserLoggedQry>()

const interval = {
start: new Date('2024-05'),
end: new Date('2024-07')
const params: GetSubcontractedActivitiesQueryParams = {
startDate: '2024-05-01',
endDate: '2024-07-30',
organizationId: 1
}

const user = UserMother.userWithoutRoles()
Expand All @@ -36,9 +38,7 @@ function setup() {
SubcontractedActivityMother.minutesActivityWithProjectRoleIdA(),
SubcontractedActivityMother.minutesBillableActivityWithProjectRoleId()
]
subcontractedActivityRepository.getAll
.calledWith(interval, 1)
.mockResolvedValue(activitiesResponse)
subcontractedActivityRepository.getAll.calledWith(params).mockResolvedValue(activitiesResponse)

const projectRolesInformation = SearchMother.roles()
searchProjectRolesQry.execute.mockResolvedValue(projectRolesInformation)
Expand All @@ -52,12 +52,11 @@ function setup() {
getSubcontractedActivitiesQry: new GetSubcontractedActivitiesQry(
subcontractedActivityRepository,
searchProjectRolesQry,
new SubcontractedActivitiesWithRoleInformation(),
getUserLoggedQry
new SubcontractedActivitiesWithRoleInformation()
),
subcontractedActivityRepository,
searchProjectRolesQry,
interval,
params,
subcontractedActivities
}
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
import { Query, UseCaseKey } from '@archimedes/arch'
import { GetUserLoggedQry } from '../../../../shared/user/application/get-user-logged-qry'
import { SUBCONTRACTED_ACTIVITY_REPOSITORY } from '../../../../../shared/di/container-tokens'
import { DateInterval } from '../../../../../shared/types/date-interval'
import { chrono } from '../../../../../shared/utils/chrono'
import { inject, singleton } from 'tsyringe'
import { SearchProjectRolesQry } from '../../search/application/search-project-roles-qry'
import { SubcontractedActivity } from '../domain/subcontracted-activity'
import type { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'
import { SubcontractedActivitiesWithRoleInformation } from '../domain/services/subcontracted-activities-with-role-information'
import { GetSubcontractedActivitiesQueryParams } from '../domain/get-subcontracted-activities-query-params'

@UseCaseKey('GetSubcontractedActivitiesQry')
@singleton()
export class GetSubcontractedActivitiesQry extends Query<SubcontractedActivity[], DateInterval> {
export class GetSubcontractedActivitiesQry extends Query<
SubcontractedActivity[],
GetSubcontractedActivitiesQueryParams
> {
constructor(
@inject(SUBCONTRACTED_ACTIVITY_REPOSITORY)
private readonly subcontractedActivityRepository: SubcontractedActivityRepository,
private readonly searchProjectRolesQry: SearchProjectRolesQry,
private readonly subcontractedActivitiesWithRoleInformation: SubcontractedActivitiesWithRoleInformation,
private readonly getUserLoggedQry: GetUserLoggedQry
private readonly subcontractedActivitiesWithRoleInformation: SubcontractedActivitiesWithRoleInformation
) {
super()
}

async internalExecute(dateInterval: DateInterval): Promise<SubcontractedActivity[]> {
const { id } = await this.getUserLoggedQry.execute()
const activitiesResponse = await this.subcontractedActivityRepository.getAll(dateInterval, id)
async internalExecute(
filters: GetSubcontractedActivitiesQueryParams
): Promise<SubcontractedActivity[]> {
const activitiesResponse = await this.subcontractedActivityRepository.getAll(filters)
const activitiesSorted = activitiesResponse.slice()
activitiesSorted.sort((a, b) => (chrono(new Date(a.month)).isAfter(new Date(b.month)) ? 1 : -1))

const projectRoleIds = activitiesSorted.map((a) => a.projectRoleId)
const uniqueProjectRoleIds = Array.from(new Set(projectRoleIds))
const { start } = dateInterval

const projectRolesInformation = await this.searchProjectRolesQry.execute({
ids: uniqueProjectRoleIds,
year: start.getFullYear()
year: chrono(filters.startDate).getDate().getFullYear()
})

return this.subcontractedActivitiesWithRoleInformation.addRoleInformationToActivities(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Id } from '../../../../../shared/types/id'

export interface GetSubcontractedActivitiesQueryParams {
userId?: Id
startDate: string
endDate: string
organizationId?: number
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DateInterval } from '../../../../../shared/types/date-interval'
import { Id } from '../../../../../shared/types/id'
import { NewSubcontractedActivity } from './new-subcontracted-activity'
import { UpdateSubcontractedActivity } from './update-subcontracted-activity'
Expand All @@ -7,8 +6,7 @@ import { SubcontractedActivityWithProjectRoleId } from './subcontracted-activity

export interface SubcontractedActivityRepository {
getAll(
{ start, end }: DateInterval,
userId: Id
queryParams: GetSubcontractedActivitiesQueryParams
): Promise<SubcontractedActivityWithProjectRoleId[]>

getActivitiesBasedOnFilters(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { mock } from 'jest-mock-extended'
import { HttpClient } from '../../../../../shared/http/http-client'
import { DateInterval } from '../../../../../shared/types/date-interval'
import { chrono } from '../../../../../shared/utils/chrono'
import { SubcontractedActivityMother } from '../../../../../test-utils/mothers/subcontracted-activity-mother'
import { SubcontractedActivityWithProjectRoleIdMapper } from './subcontracted-activity-with-project-role-id-mapper'
import { HttpSubcontractedActivityRepository } from './http-subcontracted-activity-repository'
import { NewSubcontractedActivityDto } from './new-subcontracted-activity-dto'
import { SubcontractedActivityWithProjectRoleId } from '../domain/subcontracted-activity-with-project-role-id'
import { GetSubcontractedActivitiesQueryParams } from '../domain/get-subcontracted-activities-query-params'

describe('HttpSubcontractedActivityRepository', () => {
it('should call http client for all activities', async () => {
const { httpClient, httpSubcontractedActivityRepository } = setup()
const interval: DateInterval = {
start: new Date('2023-03-23T00:00:00.000Z'),
end: new Date('2023-03-30T00:00:00.000Z')
const params: GetSubcontractedActivitiesQueryParams = {
startDate: '2024-05-01',
endDate: '2024-07-30',
organizationId: 1
}
const userId = 1
const response = SubcontractedActivityMother.subcontractedActivitiesSerialized().map((x) =>
SubcontractedActivityWithProjectRoleIdMapper.toDomain(x)
)
httpClient.get.mockResolvedValue(
SubcontractedActivityMother.subcontractedActivitiesSerialized()
)

const result = await httpSubcontractedActivityRepository.getAll(interval, userId)
const result = await httpSubcontractedActivityRepository.getAll(params)
const resultInMinutes = result.map((element) => {
const r: SubcontractedActivityWithProjectRoleId = {
...element,
Expand All @@ -32,11 +31,7 @@ describe('HttpSubcontractedActivityRepository', () => {
return r
})
expect(httpClient.get).toHaveBeenCalledWith('/api/subcontracted-activity', {
params: {
startDate: chrono(interval.start).format(chrono.DATE_FORMAT),
endDate: chrono(interval.end).format(chrono.DATE_FORMAT),
userId
}
params
})
expect(resultInMinutes).toEqual(response)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { HttpClient } from '../../../../../shared/http/http-client'
import { DateInterval } from '../../../../../shared/types/date-interval'
import { Id } from '../../../../../shared/types/id'
import { chrono, getLastDayOfMonth } from '../../../../../shared/utils/chrono'
import { singleton } from 'tsyringe'
import { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'
import { SubcontractedActivityWithProjectRoleId } from '../domain/subcontracted-activity-with-project-role-id'
Expand All @@ -22,17 +20,13 @@ export class HttpSubcontractedActivityRepository implements SubcontractedActivit
constructor(private readonly httpClient: HttpClient) {}

async getAll(
{ start, end }: DateInterval,
userId: number
queryParams: GetSubcontractedActivitiesQueryParams
): Promise<SubcontractedActivityWithProjectRoleId[]> {
end = getLastDayOfMonth(end)
const data = await this.httpClient.get<SubcontractedActivityWithProjectRoleIdDto[]>(
HttpSubcontractedActivityRepository.activityPath,
{
params: {
startDate: chrono(start).format(chrono.DATE_FORMAT),
endDate: chrono(end).format(chrono.DATE_FORMAT),
userId
...queryParams
}
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { object, string } from 'yup'
import { ObjectSchema, object, string } from 'yup'
import { i18n } from '../../../../../../../../../shared/i18n/i18n'
import { chrono } from '../../../../../../../../../shared/utils/chrono'
import { Organization } from '../../../../../../organization/domain/organization'

export interface SubcontractedActivityFilterFormSchema {
startDate: string
endDate: string
organization: Organization | undefined
}

export const SubcontractedActivityFilterFormValidationSchema = object({
Expand All @@ -13,7 +15,7 @@ export const SubcontractedActivityFilterFormValidationSchema = object({
.required(i18n.t('form_errors.field_required'))
.test('is-greater', i18n.t('form_errors.end_date_greater'), function () {
const { startDate, endDate } = this.parent

return chrono(endDate).isSame(startDate, 'day') || chrono(endDate).isAfter(startDate)
})
}),
organization: object().optional() as ObjectSchema<Organization | undefined>
}).defined()
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { SubcontractedActivityFilterForm } from './subcontracted-activity-filter-form'
import { chrono } from '../../../../../../../../../shared/utils/chrono'
import {
render,
screen,
Expand All @@ -8,14 +7,15 @@ import {
fireEvent,
userEvent
} from '../../../../../../../../../test-utils/render'
import { GetSubcontractedActivitiesQueryParams } from '../../../../../domain/get-subcontracted-activities-query-params'

describe('SubcontractedActivityFilterForm', () => {
it('should set default form values', () => {
setup()
const startDate = screen.getByLabelText('subcontracted_activity_form.start_date')
const endDate = screen.getByLabelText('subcontracted_activity_form.end_date')
expect(startDate).toHaveValue('2024-02')
expect(endDate).toHaveValue('2024-04')
expect(startDate).toHaveValue('2024-03')
expect(endDate).toHaveValue('2024-07')
})

it('should call onFiltersChange when there is a change', async () => {
Expand All @@ -30,10 +30,11 @@ describe('SubcontractedActivityFilterForm', () => {
expect(onFiltersChangeSpy).toHaveBeenCalledTimes(1)
})

expect(onFiltersChangeSpy).toHaveBeenCalledWith(
chrono('2024-04-01').getDate(),
chrono('2024-04-01').getDate()
)
expect(onFiltersChangeSpy).toHaveBeenCalledWith({
startDate: '2024-04-01',
endDate: '2024-07-31',
organizationId: undefined
})
})

it('should show error when startDate is greater than endDate', async () => {
Expand Down Expand Up @@ -86,9 +87,10 @@ describe('SubcontractedActivityFilterForm', () => {
})

function setup() {
const filters = {
start: new Date('2024-02-01'),
end: new Date('2024-04-01')
const filters: GetSubcontractedActivitiesQueryParams = {
startDate: '2024-03-01',
endDate: '2024-07-31',
organizationId: 1
}

const onFiltersChangeSpy = jest.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
} from './subcontracted-activity-filter-form.schema'
import { chrono } from '../../../../../../../../../shared/utils/chrono'
import { MonthField } from '../../../../../../../../../shared/components/form-fields/month-field'
import { OrganizationsCombo } from '../../../activity-form/components/combos/organizations-combo'
import { GetSubcontractedActivitiesQueryParams } from '../../../../../domain/get-subcontracted-activities-query-params'

interface Props {
filters: { start: Date; end: Date }
onFiltersChange: (startDate: Date, endDate: Date) => Promise<void>
filters: GetSubcontractedActivitiesQueryParams
onFiltersChange: (params: GetSubcontractedActivitiesQueryParams) => Promise<void>
}

export const SubcontractedActivityFilterForm: FC<Props> = (props) => {
Expand All @@ -22,37 +24,56 @@ export const SubcontractedActivityFilterForm: FC<Props> = (props) => {
const {
register,
handleSubmit,
control,
trigger,
watch,
formState: { errors }
} = useForm<SubcontractedActivityFilterFormSchema>({
defaultValues: {
startDate: chrono(filters.start).format('yyyy-MM'),
endDate: chrono(filters.end).format('yyyy-MM')
startDate: chrono(filters.startDate).format('yyyy-MM'),
endDate: chrono(filters.endDate).format('yyyy-MM'),
organization: undefined
},
resolver: yupResolver(SubcontractedActivityFilterFormValidationSchema),
mode: 'onChange'
})

const onSubmit = useCallback(
(data: SubcontractedActivityFilterFormSchema) => {
onFiltersChange(chrono(data.startDate).getDate(), chrono(data.endDate).getDate())
const startDate = chrono(data.startDate).startOf('month').format('yyyy-MM-dd')
const endDate = chrono(data.endDate).endOf('month').format('yyyy-MM-dd')
const organizationId = data.organization?.id

onFiltersChange({
startDate,
endDate,
organizationId
})
},
[onFiltersChange]
)

useEffect(() => {
const subscription = watch(async () => {
const subscription = watch(async (newValues) => {
const isValid = await trigger()
if (isValid) {
handleSubmit(onSubmit)()
// TODO: review types here
//@ts-ignore
handleSubmit(() => onSubmit(newValues))()
}
})
return () => subscription?.unsubscribe()
}, [watch, handleSubmit, filters, trigger, onSubmit])
}, [handleSubmit])

Check warning on line 66 in src/features/binnacle/features/activity/ui/components/subcontracted-activities-list/components/activity-filter/subcontracted-activity-filter-form.tsx

View workflow job for this annotation

GitHub Actions / Test

React Hook useEffect has missing dependencies: 'onSubmit', 'trigger', and 'watch'. Either include them or remove the dependency array

return (
<Stack as="form" direction={['column', 'row']} spacing={4} marginBottom={5} marginTop={4}>
<Box>
<OrganizationsCombo
control={control}
isReadOnly={false}
organizationFilters={{ imputable: true }}
/>
</Box>
<Box gridArea="start">
<MonthField
error={errors.startDate?.message}
Expand Down
Loading

0 comments on commit 4b01cc6

Please sign in to comment.