Skip to content

Commit 5a6f1d6

Browse files
committed
PM-1505 - clone scorecard
1 parent 9562956 commit 5a6f1d6

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed

src/api/scorecard/scorecard.controller.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,27 @@ export class ScorecardController {
204204
});
205205
return result;
206206
}
207+
208+
@Post('/:id/clone')
209+
@Roles(UserRole.Admin)
210+
@Scopes(Scope.CreateScorecard)
211+
@ApiOperation({
212+
summary: 'Clone a scorecard',
213+
description: 'Roles: Admin | Scopes: create:scorecard',
214+
})
215+
@ApiParam({
216+
name: 'id',
217+
description: 'The ID of the scorecard to clone',
218+
example: 'abc123',
219+
})
220+
@ApiResponse({
221+
status: 201,
222+
description: 'Scorecard cloned successfully.',
223+
type: ScorecardResponseDto,
224+
})
225+
@ApiResponse({ status: 403, description: 'Forbidden.' })
226+
@ApiResponse({ status: 404, description: 'Scorecard not found.' })
227+
async cloneScorecard(@Param('id') id: string): Promise<ScorecardResponseDto> {
228+
return this.scorecardService.cloneScorecard(id);
229+
}
207230
}

src/api/scorecard/scorecard.service.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,98 @@ export class ScoreCardService {
186186
scoreCards: data as ScorecardResponseDto[],
187187
};
188188
}
189+
190+
async cloneScorecard(
191+
id: string
192+
): Promise<ScorecardResponseDto> {
193+
const original = await this.prisma.scorecard
194+
.findUnique({
195+
where: { id },
196+
include: {
197+
scorecardGroups: {
198+
include: {
199+
sections: {
200+
include: {
201+
questions: true,
202+
},
203+
},
204+
},
205+
},
206+
},
207+
})
208+
.catch((error) => {
209+
if (error.code !== 'P2025') {
210+
throw new NotFoundException({ message: `Scorecard not found.` });
211+
}
212+
throw new InternalServerErrorException({
213+
message: `Error: ${error.code}`,
214+
});
215+
});
216+
217+
if (!original) {
218+
throw new NotFoundException({ message: `Scorecard not found.` });
219+
}
220+
221+
// Remove id fields from nested objects for cloning
222+
const cloneGroups = original.scorecardGroups.map((group: any) => ({
223+
...group,
224+
id: undefined,
225+
scorecardId: undefined,
226+
sections: group.sections.map((section: any) => ({
227+
...section,
228+
id: undefined,
229+
scorecardGroupId: undefined,
230+
questions: section.questions.map((question: any) => ({
231+
...question,
232+
id: undefined,
233+
sectionId: undefined,
234+
scorecardSectionId: undefined,
235+
})),
236+
})),
237+
}));
238+
239+
const clonedScorecard = await this.prisma.scorecard.create({
240+
data: {
241+
...original,
242+
id: undefined,
243+
name: `${original.name} (Clone)`,
244+
createdAt: undefined,
245+
updatedAt: undefined,
246+
scorecardGroups: {
247+
create: cloneGroups.map((group: any) => ({
248+
...group,
249+
createdAt: undefined,
250+
updatedAt: undefined,
251+
sections: {
252+
create: group.sections.map((section: any) => ({
253+
...section,
254+
createdAt: undefined,
255+
updatedAt: undefined,
256+
questions: {
257+
create: section.questions.map((question: any) => ({
258+
...question,
259+
createdAt: undefined,
260+
updatedAt: undefined,
261+
})),
262+
},
263+
})),
264+
},
265+
})),
266+
},
267+
},
268+
include: {
269+
scorecardGroups: {
270+
include: {
271+
sections: {
272+
include: {
273+
questions: true,
274+
},
275+
},
276+
},
277+
},
278+
},
279+
});
280+
281+
return clonedScorecard as ScorecardResponseDto;
282+
}
189283
}

0 commit comments

Comments
 (0)