diff --git a/src/apps/work/src/lib/schemas/engagement-editor.schema.spec.ts b/src/apps/work/src/lib/schemas/engagement-editor.schema.spec.ts index 1c9de99a2..da3ec4949 100644 --- a/src/apps/work/src/lib/schemas/engagement-editor.schema.spec.ts +++ b/src/apps/work/src/lib/schemas/engagement-editor.schema.spec.ts @@ -55,6 +55,7 @@ function createAssignmentDetails( durationMonths: '3', memberHandle, ratePerHour: '75', + standardHoursPerDay: '8', standardHoursPerWeek: '40', startDate: '2026-04-15T00:00:00.000Z', } @@ -78,28 +79,28 @@ describe('engagementEditorSchema', () => { ) }) - it('rejects private engagements when not all required member slots are assigned', async () => { - const validationMessages = await getValidationMessages({ + it('accepts private engagements without assigned members', async () => { + await expect(engagementEditorSchema.validate({ ...createValidFormValues(), - assignedMemberHandles: ['testaws1'], - assignmentDetails: [createAssignmentDetails('testaws1')], + assignedMemberHandles: [], + assignmentDetails: [], isPrivate: true, requiredMemberCount: 2, + }, { + abortEarly: false, + })).resolves.toMatchObject({ + assignedMemberHandles: [], + isPrivate: true, }) - - expect(validationMessages) - .toContain( - 'All 2 member assignments are required for private engagements', - ) }) - it('accepts private engagements with complete assignment details for each required member', async () => { + it('accepts private engagements with complete assignment details for assigned members', async () => { await expect(engagementEditorSchema.validate({ ...createValidFormValues(), assignedMemberHandles: ['testaws1'], assignmentDetails: [createAssignmentDetails('testaws1')], isPrivate: true, - requiredMemberCount: 1, + requiredMemberCount: 2, }, { abortEarly: false, })).resolves.toMatchObject({ diff --git a/src/apps/work/src/lib/schemas/engagement-editor.schema.ts b/src/apps/work/src/lib/schemas/engagement-editor.schema.ts index 27c861fae..f6bc2a79d 100644 --- a/src/apps/work/src/lib/schemas/engagement-editor.schema.ts +++ b/src/apps/work/src/lib/schemas/engagement-editor.schema.ts @@ -135,46 +135,8 @@ export const engagementEditorSchema: yup.ObjectSchema schema.optional(), - then: schema => schema.test( - 'private-assignment-count', - 'Assign to Member is required', - function validateAssignedMembers(value: unknown[] | undefined) { - const requiredMemberCount = toPositiveInteger(this.parent.requiredMemberCount) - const normalizedHandles = Array.isArray(value) - ? value.map(normalizeHandle) - : [] - - if (requiredMemberCount === undefined) { - if (normalizedHandles.some(Boolean)) { - return true - } - - return this.createError({ - message: 'Assign to Member is required', - }) - } - - const requiredHandles = normalizedHandles.slice(0, requiredMemberCount) - - if ( - requiredHandles.length === requiredMemberCount - && requiredHandles.every(Boolean) - ) { - return true - } - - return this.createError({ - message: requiredMemberCount === 1 - ? 'Assign to Member is required' - : `All ${requiredMemberCount} member assignments are required for private engagements`, - }) - }, - ), - }), + .defined()) + .optional(), assignmentDetails: yup .array() .optional() @@ -185,14 +147,24 @@ export const engagementEditorSchema: yup.ObjectSchema = visibleHandles + .map((memberHandle: string, index: number) => ({ + index, + memberHandle, + })) + .filter(entry => Boolean(entry.memberHandle)) + + if (assignedHandleEntries.length < 1) { return true } @@ -200,8 +172,8 @@ export const engagementEditorSchema: yup.ObjectSchema ( - hasCompleteAssignmentDetails(assignmentDetails[index], memberHandle) + const hasCompleteDetails = assignedHandleEntries.every(entry => ( + hasCompleteAssignmentDetails(assignmentDetails[entry.index], entry.memberHandle) )) if (hasCompleteDetails) { @@ -209,9 +181,11 @@ export const engagementEditorSchema: yup.ObjectSchema { })) }) + it('saves a private engagement with only terminal assignments without member assignment payload', async () => { + const user = userEvent.setup() + const completedAssignment = { + agreementRate: '800', + durationMonths: 3, + endDate: '2026-05-31T00:00:00.000Z', + engagementId: 'engagement-completed', + id: 'assignment-completed', + memberHandle: 'completed_member', + memberId: '111', + ratePerHour: '20', + standardHoursPerWeek: 40, + startDate: '2026-05-01T00:00:00.000Z', + status: 'COMPLETED', + termsAccepted: true, + } + + mockedUpdateEngagement.mockResolvedValue({ + anticipatedStart: 'Immediate', + assignedMemberHandles: [], + assignments: [completedAssignment], + compensationRange: '', + countries: ['US'], + createdAt: '', + description: 'Completed engagement description', + durationWeeks: 4, + id: 'engagement-completed', + isPrivate: true, + projectId: '123', + requiredMemberCount: 2, + role: 'SOFTWARE_DEVELOPER', + skills: [ + { + id: 'skill-1', + name: 'React', + }, + ], + status: 'Closed', + timezones: ['America/New_York'], + title: 'Completed private engagement', + updatedAt: '', + workload: 'FULL_TIME', + } as any) + + render( + + + , + ) + + await user.click(screen.getByRole('button', { name: 'Save Engagement' })) + + await waitFor(() => { + expect(mockedUpdateEngagement) + .toHaveBeenCalled() + }) + + const payload = mockedUpdateEngagement.mock.calls[0][1] as { + assignedMemberHandles?: string[] + assignmentDetails?: Array<{ memberHandle: string }> + requiredMemberCount?: number + status?: string + } + + expect(payload.status) + .toBe('Closed') + expect(payload.requiredMemberCount) + .toBe(2) + expect(payload) + .not + .toHaveProperty('assignedMemberHandles') + expect(payload) + .not + .toHaveProperty('assignmentDetails') + }) + it('redirects to the saved parent project engagements list after creating an engagement', async () => { const user = userEvent.setup() diff --git a/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.spec.tsx b/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.spec.tsx index b2239fa33..d83f13c84 100644 --- a/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.spec.tsx +++ b/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.spec.tsx @@ -76,8 +76,9 @@ jest.mock('../../../../lib/components/form', () => { jest.mock('../../../../lib/utils', () => ({ formatAssignmentCurrency: (value?: string): string => (value ? `$${value}` : ''), - getAssignmentStandardHoursPerWeek: (detail: { standardHoursPerWeek?: string }): string => ( - detail.standardHoursPerWeek || '' + getAssignmentPaymentCycle: (): string => 'Weekly', + getAssignmentStandardHoursPerDay: (detail: { standardHoursPerDay?: string }): string => ( + detail.standardHoursPerDay || '' ), })) @@ -110,6 +111,7 @@ const defaultAssignmentDetails = { memberHandle: 'assigned_member', otherRemarks: 'active notes', ratePerHour: '20', + standardHoursPerDay: '8', standardHoursPerWeek: '40', startDate: '2026-05-01T00:00:00.000Z', } diff --git a/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.tsx b/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.tsx index c2a357e4a..1778ccc08 100644 --- a/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.tsx +++ b/src/apps/work/src/pages/engagements/EngagementEditorPage/components/EngagementPrivateSection.tsx @@ -260,7 +260,6 @@ export const EngagementPrivateSection: FC = ( setActiveAssignmentIndex(index) }} placeholder='Search user handle' - required valueField='handle' /> )}