Skip to content

Commit cc4cd5f

Browse files
authored
Merge pull request #1712 from topcoder-platform/PM-4824
PM-4824: honor actual predecessor end dates in schedule editor
2 parents ede24b8 + d2f6f17 commit cc4cd5f

4 files changed

Lines changed: 81 additions & 2 deletions

File tree

src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ The form uses `challengeBasicInfoSchema` from `src/apps/work/src/lib/schemas/cha
5050
- `ChallengeNameField`: text input.
5151
- `ChallengeTrackField`: track selector from `useFetchChallengeTracks`.
5252
- `ChallengeTypeField`: active type selector from `useFetchChallengeTypes`, excluding `Topgear Task` because that flow is not launchable from the work app editor. When the selected track is Design or QA, it also hides `Marathon Match` to match the legacy work-manager create flow and clears any now-invalid preselection.
53-
- `ChallengeScheduleSection`: schedule editor for challenge start and phase dates. It keeps the detected timezone above the controls, renders the `Start Date` label with the `Scheduled` and `Immediately` start-mode radios aligned to the end of that header row above the input with a green selected state, persists the selected start mode in challenge metadata so saved `/edit` and `/view` routes reopen with the correct radio state, recalculates root phase dates when the challenge start changes, and keeps completed phases' end-date and duration controls locked to match legacy work-manager behavior. `Task` challenges hide this editable section across create, edit, and read-only view routes to match legacy work-manager behavior.
53+
- `ChallengeScheduleSection`: schedule editor for challenge start and phase dates. It keeps the detected timezone above the controls, renders the `Start Date` label with the `Scheduled` and `Immediately` start-mode radios aligned to the end of that header row above the input with a green selected state, persists the selected start mode in challenge metadata so saved `/edit` and `/view` routes reopen with the correct radio state, recalculates root phase dates when the challenge start changes, honors a completed predecessor phase's actual end date when deriving successor schedule rows, and keeps completed phases' end-date and duration controls locked to match legacy work-manager behavior. `Task` challenges hide this editable section across create, edit, and read-only view routes to match legacy work-manager behavior.
5454
- `DesignWorkTypeField`: shown for Design + Challenge, with the legacy work-type options (`Application Front-End Design`, `Print/Presentation`, `Web Design`, `Widget or Mobile Screen Design`, `Wireframes`). The selected value is stored in challenge tags.
5555
- `FunChallengeField`: shown for `Marathon Match` type and remains editable after creation so the form can switch between fun-challenge and standard marathon-match fields.
5656
- `ReviewersField`: hidden for `Task` and `Marathon Match` challenges because manual reviewer assignment is handled elsewhere. On the human-review tab, each manual reviewer card keeps the legacy review-type dropdown, backfills missing legacy review-type values from the matching default reviewer or iterative-review phase fallback, and each manual reviewer phase selector hides registration/submission phases and any phase already assigned on another manual reviewer card while preserving the card's current selection.

src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeScheduleSection/ChallengeScheduleSection.component.spec.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,50 @@ describe('ChallengeScheduleSection component', () => {
514514
isEndDateEditable: true,
515515
}))
516516
})
517+
518+
it('uses a completed predecessor actual end date for the submission row start time', () => {
519+
render(
520+
<TestHarness
521+
phases={[
522+
{
523+
actualEndDate: '2026-04-09T13:14:00.000Z',
524+
duration: 2880,
525+
id: 'phase-1',
526+
isOpen: false,
527+
name: 'Checkpoint Review',
528+
phaseId: 'checkpoint-review-phase',
529+
scheduledEndDate: '2026-04-11T13:02:00.000Z',
530+
scheduledStartDate: '2026-04-09T13:02:00.000Z',
531+
},
532+
{
533+
duration: 19,
534+
id: 'phase-2',
535+
isOpen: true,
536+
name: 'Submission',
537+
phaseId: 'submission-phase',
538+
predecessor: 'checkpoint-review-phase',
539+
scheduledEndDate: '2026-04-09T13:33:00.000Z',
540+
scheduledStartDate: '2026-04-09T13:14:00.000Z',
541+
},
542+
]}
543+
startDate='2026-04-09T12:36:00.000Z'
544+
/>,
545+
)
546+
547+
const renderedPhaseRows = mockPhaseEditorRow.mock.calls
548+
.map(([props]) => props as {
549+
phase?: {
550+
name?: string
551+
}
552+
startDate?: string
553+
})
554+
const submissionRow = [...renderedPhaseRows]
555+
.reverse()
556+
.find(props => props.phase?.name === 'Submission')
557+
558+
expect(submissionRow)
559+
.toEqual(expect.objectContaining({
560+
startDate: '2026-04-09T13:14:00.000Z',
561+
}))
562+
})
517563
})

src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeScheduleSection/ChallengeScheduleSection.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,35 @@ describe('ChallengeScheduleSection helpers', () => {
9999
expect(result.phases[2]?.scheduledStartDate)
100100
.toBe(updatedStartDate)
101101
})
102+
103+
it('aligns successor phases to a predecessor actual end date when the predecessor closes early', () => {
104+
const checkpointReviewActualEnd = '2026-04-09T13:14:00.000Z'
105+
const phases: ChallengePhase[] = [
106+
buildPhase({
107+
actualEndDate: checkpointReviewActualEnd,
108+
duration: 48 * 60,
109+
name: 'Checkpoint Review',
110+
phaseId: 'checkpoint-review',
111+
scheduledEndDate: '2026-04-11T13:02:00.000Z',
112+
scheduledStartDate: '2026-04-09T13:02:00.000Z',
113+
}),
114+
buildPhase({
115+
duration: 19,
116+
name: 'Submission',
117+
phaseId: 'submission',
118+
predecessor: 'checkpoint-review',
119+
scheduledEndDate: '2026-04-09T13:33:00.000Z',
120+
scheduledStartDate: '2026-04-09T13:14:00.000Z',
121+
}),
122+
]
123+
124+
const result = recalculatePhases(phases, '2026-04-09T12:36:00.000Z')
125+
126+
expect(result.phases[1]?.scheduledStartDate)
127+
.toBe(checkpointReviewActualEnd)
128+
expect(result.phases[1]?.scheduledEndDate)
129+
.toBe('2026-04-09T13:33:00.000Z')
130+
})
102131
})
103132

104133
describe('buildSchedulePhaseRows', () => {

src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeScheduleSection/ChallengeScheduleSection.utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export function normalizePhaseName(value: unknown): string {
6464
.toLowerCase()
6565
}
6666

67+
function getPredecessorEndDate(phase?: ChallengePhase): Date | undefined {
68+
return toDate(phase?.actualEndDate || phase?.scheduledEndDate)
69+
}
70+
6771
function isIterativeReviewPhase(phaseName: unknown): boolean {
6872
return normalizePhaseName(phaseName) === 'iterative review'
6973
}
@@ -203,7 +207,7 @@ export function recalculatePhases(
203207
if (phase.predecessor) {
204208
const predecessorPhase = calculatedByPhaseId.get(phase.predecessor)
205209
const predecessorStartDate = toDate(predecessorPhase?.scheduledStartDate)
206-
const predecessorEndDate = toDate(predecessorPhase?.scheduledEndDate)
210+
const predecessorEndDate = getPredecessorEndDate(predecessorPhase)
207211

208212
if (predecessorEndDate) {
209213
phaseStartDate = isIterativeReviewPhase(phase.name)

0 commit comments

Comments
 (0)