Skip to content

Commit

Permalink
Merge pull request #192 from autentia/origin/feature/subcontracted_ac…
Browse files Browse the repository at this point in the history
…tivities

Origin/feature/subcontracted activities
  • Loading branch information
fjmpaez authored May 14, 2024
2 parents b72a43d + b0dd1e1 commit e59bc32
Show file tree
Hide file tree
Showing 77 changed files with 3,066 additions and 31 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"build:prod": "npm run build && npm run zip-prod",
"zip-int": "cd build; zip -r ../binnacle_front_${npm_package_version}_int.zip * .htaccess",
"zip-prod": "cd build; zip -r ../binnacle_front_$npm_package_version.zip * .htaccess",
"prepare": "husky install"
"prepare": "husky install",
"jest": "jest",
"cypress": "cypress"
},
"dependencies": {
"@archimedes/arch": "2.2.1",
Expand Down
10 changes: 10 additions & 0 deletions src/app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { RequireActivityApproval } from './shared/router/require-activity-approv
import { LazyActivitiesPage } from './features/binnacle/features/activity/ui/activities-page.lazy'
import { useIsMobile } from './shared/hooks/use-is-mobile'
import { LazyAvailabilityPage } from './features/binnacle/features/availability/ui/availability-page.lazy'
import { LazySubcontractedActivitiesPage } from './features/binnacle/features/activity/ui/subcontracted-activies-page.lazy'
import { RequireSubcontractedActivityManager } from './shared/router/require-subcontracted-manager'

export const AppRoutes: FC = () => {
const isMobile = useIsMobile()
Expand Down Expand Up @@ -77,6 +79,14 @@ export const AppRoutes: FC = () => {
}
/>
</Route>
<Route
path={rawPaths.subcontractedActivities}
element={
<RequireSubcontractedActivityManager>
<LazySubcontractedActivitiesPage />
</RequireSubcontractedActivityManager>
}
/>
<Route
path={rawPaths.pendingActivities}
element={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { mock } from 'jest-mock-extended'
import { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'
import { NewSubcontractedActivity } from '../domain/new-subcontracted-activity'
import { CreateSubcontractedActivityCmd } from './create-subcontracted-activity-cmd'

describe('CreateSubcontractedActivityCmd', () => {
it('should create a new subcontracted activity', async () => {
const {
createSubcontractedActivityCmd,
subcontractedActivityRepository,
newSubcontractedActivity
} = setup()

await createSubcontractedActivityCmd.internalExecute(newSubcontractedActivity)

expect(subcontractedActivityRepository.create).toBeCalledWith(newSubcontractedActivity)
})
})

function setup() {
const subcontractedActivityRepository = mock<SubcontractedActivityRepository>()

const newSubcontractedActivity: NewSubcontractedActivity = {
description: 'any-description',
projectRoleId: 1,
duration: 333,
month: '2024-05'
}

return {
createSubcontractedActivityCmd: new CreateSubcontractedActivityCmd(
subcontractedActivityRepository
),
subcontractedActivityRepository,
newSubcontractedActivity
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Command, UseCaseKey } from '@archimedes/arch'
import { SUBCONTRACTED_ACTIVITY_REPOSITORY } from '../../../../../shared/di/container-tokens'
import { inject, singleton } from 'tsyringe'
import { NewSubcontractedActivity } from '../domain/new-subcontracted-activity'
import type { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'

@UseCaseKey('CreateSubcontractedActivityCmd')
@singleton()
export class CreateSubcontractedActivityCmd extends Command<NewSubcontractedActivity> {
constructor(
@inject(SUBCONTRACTED_ACTIVITY_REPOSITORY)
private subcontractedActivityRepository: SubcontractedActivityRepository
) {
super()
}
async internalExecute(newActivity: NewSubcontractedActivity): Promise<void> {
await this.subcontractedActivityRepository.create(newActivity)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { mock } from 'jest-mock-extended'
import { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'
import { DeleteSubcontractedActivityCmd } from './delete-subcontracted-activity-cmd'

describe('DeleteSubcontractedActivityCmd', () => {
it('should delete a subcontracted activity by id', async () => {
const { deleteSubcontractedActivityCmd, subcontractedActivityRepository } = setup()
const id = 1

await deleteSubcontractedActivityCmd.internalExecute(id)

expect(subcontractedActivityRepository.delete).toBeCalledWith(id)
})
})

function setup() {
const subcontractedActivityRepository = mock<SubcontractedActivityRepository>()

return {
deleteSubcontractedActivityCmd: new DeleteSubcontractedActivityCmd(
subcontractedActivityRepository
),
subcontractedActivityRepository
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Command, UseCaseKey } from '@archimedes/arch'
import { SUBCONTRACTED_ACTIVITY_REPOSITORY } from '../../../../../shared/di/container-tokens'
import { Id } from '../../../../../shared/types/id'
import { inject, singleton } from 'tsyringe'
import type { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'

@UseCaseKey('DeleteSubcontractedActivityCmd')
@singleton()
export class DeleteSubcontractedActivityCmd extends Command<Id> {
constructor(
@inject(SUBCONTRACTED_ACTIVITY_REPOSITORY)
private subcontractedActivityRepository: SubcontractedActivityRepository
) {
super()
}

async internalExecute(id: Id): Promise<void> {
await this.subcontractedActivityRepository.delete(id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { GetUserLoggedQry } from '../../../../shared/user/application/get-user-logged-qry'
import { mock } from 'jest-mock-extended'
import { chrono } from '../../../../../shared/utils/chrono'
import { SubcontractedActivityMother } from '../../../../../test-utils/mothers/subcontracted-activity-mother'
import { SearchMother } from '../../../../../test-utils/mothers/search-mother'
import { SearchProjectRolesQry } from '../../search/application/search-project-roles-qry'
import { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'
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'

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

const result = await getSubcontractedActivitiesQry.internalExecute(interval)

expect(result).toEqual(subcontractedActivities)
})
})

function setup() {
const subcontractedActivityRepository = mock<SubcontractedActivityRepository>()
const searchProjectRolesQry = mock<SearchProjectRolesQry>()
const getUserLoggedQry = mock<GetUserLoggedQry>()

const interval = {
start: new Date('2024-05'),
end: new Date('2024-07')
}

const user = UserMother.userWithoutRoles()
getUserLoggedQry.execute.mockResolvedValue(user)

const activitiesResponse = [
SubcontractedActivityMother.minutesActivityWithProjectRoleIdA(),
SubcontractedActivityMother.minutesBillableActivityWithProjectRoleId()
]
subcontractedActivityRepository.getAll
.calledWith(interval, 1)
.mockResolvedValue(activitiesResponse)

const projectRolesInformation = SearchMother.roles()
searchProjectRolesQry.execute.mockResolvedValue(projectRolesInformation)

const subcontractedActivities = SubcontractedActivityMother.subcontractedActivities()
subcontractedActivities.sort((a, b) =>
chrono(new Date(a.month)).isAfter(new Date(b.month)) ? 1 : -1
)

return {
getSubcontractedActivitiesQry: new GetSubcontractedActivitiesQry(
subcontractedActivityRepository,
searchProjectRolesQry,
new SubcontractedActivitiesWithRoleInformation(),
getUserLoggedQry
),
subcontractedActivityRepository,
searchProjectRolesQry,
interval,
subcontractedActivities
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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'

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

async internalExecute(dateInterval: DateInterval): Promise<SubcontractedActivity[]> {
const { id } = await this.getUserLoggedQry.execute()
const activitiesResponse = await this.subcontractedActivityRepository.getAll(dateInterval, id)
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()
})

return this.subcontractedActivitiesWithRoleInformation.addRoleInformationToActivities(
activitiesSorted,
projectRolesInformation
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { mock } from 'jest-mock-extended'
import { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'
import { UpdateSubcontractedActivity } from '../domain/update-subcontracted-activity'
import { UpdateSubcontractedActivityCmd } from './update-subcontracted-activity-cmd'

describe('UpdateSubcontractedActivityCmd', () => {
it('should update the subcontracted activity correctly', async () => {
const {
updateSubcontractedActivityCmd,
subcontractedActivityRepository,
updateSubcontractedActivity
} = setup()

await updateSubcontractedActivityCmd.internalExecute(updateSubcontractedActivity)

expect(subcontractedActivityRepository.update).toBeCalledWith(updateSubcontractedActivity)
})
})

function setup() {
const subcontractedActivityRepository = mock<SubcontractedActivityRepository>()

const updateSubcontractedActivity: UpdateSubcontractedActivity = {
id: 1,
description: 'Minutes activity',
projectRoleId: 1,
duration: 555,
month: '2024-05'
}

return {
updateSubcontractedActivityCmd: new UpdateSubcontractedActivityCmd(
subcontractedActivityRepository
),
subcontractedActivityRepository,
updateSubcontractedActivity
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Command, UseCaseKey } from '@archimedes/arch'
import { SUBCONTRACTED_ACTIVITY_REPOSITORY } from '../../../../../shared/di/container-tokens'
import { inject, singleton } from 'tsyringe'
import { UpdateSubcontractedActivity } from '../domain/update-subcontracted-activity'
import type { SubcontractedActivityRepository } from '../domain/subcontracted-activity-repository'

@UseCaseKey('UpdateSubcontractedActivityCmd')
@singleton()
export class UpdateSubcontractedActivityCmd extends Command<UpdateSubcontractedActivity> {
constructor(
@inject(SUBCONTRACTED_ACTIVITY_REPOSITORY)
private subcontractedActivityRepository: SubcontractedActivityRepository
) {
super()
}

async internalExecute(activity: UpdateSubcontractedActivity): Promise<void> {
await this.subcontractedActivityRepository.update(activity)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Id } from '../../../../../shared/types/id'

export interface GetSubcontractedActivitiesQueryParams {
userId?: Id
startDate: string
endDate: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SubcontractedActivityWithProjectRoleId } from './subcontracted-activity-with-project-role-id'

export type NewSubcontractedActivity = Pick<
SubcontractedActivityWithProjectRoleId,
'description' | 'projectRoleId' | 'duration' | 'month'
>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { SearchProjectRolesResult } from '../../../search/domain/search-project-roles-result'
import { singleton } from 'tsyringe'
import { SubcontractedActivityWithProjectRoleId } from '../subcontracted-activity-with-project-role-id'
import { SubcontractedActivity } from '../subcontracted-activity'

@singleton()
export class SubcontractedActivitiesWithRoleInformation {
addRoleInformationToActivities(
subcontractedActivitiesWithProjectRoleId: SubcontractedActivityWithProjectRoleId[],
searchProjectRolesResult: SearchProjectRolesResult
): SubcontractedActivity[] {
return subcontractedActivitiesWithProjectRoleId
.map((subcontractedActivityProjectRoleId) => {
const { projectRoleId, ...subcontractedActivityDetails } =
subcontractedActivityProjectRoleId

const projectRole = searchProjectRolesResult.projectRoles.find(
(p) => p.id === projectRoleId
)

const project = searchProjectRolesResult.projects.find(
(p) => p.id === projectRole?.projectId
)

const organization = searchProjectRolesResult.organizations.find(
(o) => o.id === project?.organizationId
)

if (!organization) return

return {
...subcontractedActivityDetails,
organization,
project,
projectRole
} as SubcontractedActivity
})
.filter((x) => x !== undefined) as SubcontractedActivity[]
}
}
Loading

0 comments on commit e59bc32

Please sign in to comment.