Skip to content

Commit 7b7f1d5

Browse files
committed
feat: enhance workout copy responses with skipped counts and update notifications
1 parent 2a5dbe5 commit 7b7f1d5

6 files changed

Lines changed: 203 additions & 27 deletions

File tree

boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyWorkoutsResponse.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import java.time.LocalDate
66
data class CopyWorkoutsResponse(
77
val copied: Int,
88
val filteredOut: Int,
9+
val skippedByType: Int,
10+
val skippedAlreadySynced: Int,
911
val startDate: LocalDate,
1012
val endDate: LocalDate,
1113
val externalData: ExternalData
12-
)
14+
)

boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,76 @@ class WorkoutService(
2727

2828
fun copyWorkoutsC2C(request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse {
2929
log.info("Received request for copy calendar to calendar: $request")
30+
3031
val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!!
3132
val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!!
3233

33-
val allWorkoutsToSync = sourceWorkoutRepository.getWorkoutsFromCalendar(request.startDate, request.endDate)
34-
var filteredWorkoutsToSync = allWorkoutsToSync.filter { request.types.contains(it.details.type) }
35-
if (request.skipSynced) {
36-
val plannedWorkouts = targetWorkoutRepository.getWorkoutsFromCalendar(request.startDate, request.endDate)
34+
val allWorkoutsToSync = sourceWorkoutRepository.getWorkoutsFromCalendar(
35+
request.startDate,
36+
request.endDate
37+
)
38+
39+
val workoutsAfterTypeFilter = allWorkoutsToSync.filter {
40+
request.types.contains(it.details.type)
41+
}
42+
43+
val skippedByType = allWorkoutsToSync.size - workoutsAfterTypeFilter.size
44+
45+
val workoutsToSync = if (request.skipSynced) {
46+
val plannedWorkouts = targetWorkoutRepository.getWorkoutsFromCalendar(
47+
request.startDate,
48+
request.endDate
49+
)
3750

38-
filteredWorkoutsToSync = filteredWorkoutsToSync.filter { sourceWorkout ->
39-
plannedWorkouts.none { targetWorkout ->
51+
val partitionedWorkouts = workoutsAfterTypeFilter.partition { sourceWorkout ->
52+
plannedWorkouts.any { targetWorkout ->
4053
hasSameExternalId(
4154
sourceWorkout.details.externalData,
4255
targetWorkout.details.externalData
4356
)
4457
}
4558
}
59+
60+
val alreadySyncedWorkouts = partitionedWorkouts.first
61+
val newWorkouts = partitionedWorkouts.second
62+
63+
log.info(
64+
"Copy calendar to calendar filtering result. total={}, skippedByType={}, skippedAlreadySynced={}, copied={}",
65+
allWorkoutsToSync.size,
66+
skippedByType,
67+
alreadySyncedWorkouts.size,
68+
newWorkouts.size
69+
)
70+
71+
newWorkouts
72+
} else {
73+
log.info(
74+
"Copy calendar to calendar filtering result. total={}, skippedByType={}, skippedAlreadySynced=0, copied={}",
75+
allWorkoutsToSync.size,
76+
skippedByType,
77+
workoutsAfterTypeFilter.size
78+
)
79+
80+
workoutsAfterTypeFilter
4681
}
4782

83+
val skippedAlreadySynced = workoutsAfterTypeFilter.size - workoutsToSync.size
84+
val filteredOut = skippedByType + skippedAlreadySynced
85+
4886
val response = CopyWorkoutsResponse(
49-
filteredWorkoutsToSync.size,
50-
allWorkoutsToSync.size - filteredWorkoutsToSync.size,
51-
request.startDate,
52-
request.endDate,
53-
ExternalData.empty() // TODO figure smth better
87+
copied = workoutsToSync.size,
88+
filteredOut = filteredOut,
89+
skippedByType = skippedByType,
90+
skippedAlreadySynced = skippedAlreadySynced,
91+
startDate = request.startDate,
92+
endDate = request.endDate,
93+
externalData = ExternalData.empty()
5494
)
55-
targetWorkoutRepository.saveWorkoutsToCalendar(filteredWorkoutsToSync)
95+
96+
targetWorkoutRepository.saveWorkoutsToCalendar(workoutsToSync)
97+
5698
log.info("Saved workouts to calendar successfully: $response")
99+
57100
return response
58101
}
59102

@@ -68,12 +111,16 @@ class WorkoutService(
68111

69112
val newPlan = targetPlanRepository.createLibraryContainer(request.name, request.isPlan, request.startDate)
70113
targetWorkoutRepository.saveWorkoutsToLibrary(newPlan, filteredWorkouts)
114+
val skippedByType = allWorkouts.size - filteredWorkouts.size
115+
71116
return CopyWorkoutsResponse(
72-
filteredWorkouts.size,
73-
allWorkouts.size - filteredWorkouts.size,
74-
request.startDate,
75-
request.endDate,
76-
newPlan.externalData
117+
copied = filteredWorkouts.size,
118+
filteredOut = skippedByType,
119+
skippedByType = skippedByType,
120+
skippedAlreadySynced = 0,
121+
startDate = request.startDate,
122+
endDate = request.endDate,
123+
externalData = newPlan.externalData
77124
)
78125
}
79126

@@ -84,7 +131,15 @@ class WorkoutService(
84131

85132
val workout = sourceWorkoutRepository.getWorkoutFromLibrary(request.workoutExternalData)
86133
targetWorkoutRepository.saveWorkoutsToLibrary(request.targetLibraryContainer, listOf(workout))
87-
return CopyWorkoutsResponse(1, 0, LocalDate.now(), LocalDate.now(), request.targetLibraryContainer.externalData)
134+
return CopyWorkoutsResponse(
135+
copied = 1,
136+
filteredOut = 0,
137+
skippedByType = 0,
138+
skippedAlreadySynced = 0,
139+
startDate = LocalDate.now(),
140+
endDate = LocalDate.now(),
141+
externalData = request.targetLibraryContainer.externalData
142+
)
88143
}
89144

90145
fun findWorkoutsByName(platform: Platform, name: String): List<WorkoutDetails> {

ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class CopyCalendarToCalendarComponent implements OnInit {
102102
switchMap(() => this.loadScheduleRequests()),
103103
finalize(() => this.inProgress = false)
104104
).subscribe(() => {
105-
this.notificationService.success(`Scheduled sync job`)
105+
this.notificationService.scheduledSyncCreated()
106106
})
107107
}
108108

@@ -123,8 +123,11 @@ export class CopyCalendarToCalendarComponent implements OnInit {
123123
this.workoutClient.copyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe(
124124
finalize(() => this.inProgress = false)
125125
).subscribe((response) => {
126-
this.notificationService.success(
127-
`Planned: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`)
126+
this.notificationService.copyCalendarToCalendarCompleted(
127+
response,
128+
Platform.getTitle(direction.sourcePlatform),
129+
Platform.getTitle(direction.targetPlatform)
130+
)
128131
})
129132
}
130133

@@ -156,7 +159,7 @@ export class CopyCalendarToCalendarComponent implements OnInit {
156159
switchMap(() => this.loadScheduleRequests()),
157160
finalize(() => this.inProgress = false)
158161
).subscribe(() => {
159-
this.notificationService.success(`Deleted job`)
162+
this.notificationService.scheduledSyncDeleted()
160163
})
161164
}
162165
}

ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@ export class TrCopyCalendarToLibraryComponent implements OnInit {
8282
this.workoutClient.copyCalendarToLibrary(name, startDate, endDate, trainingTypes, this.direction, isPlan).pipe(
8383
finalize(() => this.inProgress = false)
8484
).subscribe((response) => {
85-
this.notificationService.success(
86-
`Copied: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`)
85+
this.notificationService.copyCalendarToLibraryCompleted(response, name)
8786
})
8887
}
8988
}

ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ export class TpCopyCalendarToLibraryComponent implements OnInit {
8686
this.workoutClient.copyCalendarToLibrary(name, startDate, endDate, trainingTypes, this.direction, isPlan).pipe(
8787
finalize(() => this.inProgress = false)
8888
).subscribe((response) => {
89-
this.notificationService.success(
90-
`Copied: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`)
89+
this.notificationService.copyCalendarToLibraryCompleted(response, name)
9190
})
9291
}
9392
}

ui/src/infrastructure/notification.service.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,102 @@ export class NotificationService {
2929
this.open(message, 'error');
3030
}
3131

32+
copyCalendarToCalendarCompleted(
33+
response: any,
34+
sourcePlatformTitle?: string,
35+
targetPlatformTitle?: string
36+
): void {
37+
const copied = response.copied ?? 0;
38+
const skippedByType = response.skippedByType ?? 0;
39+
const skippedAlreadySynced = response.skippedAlreadySynced ?? 0;
40+
41+
const direction = sourcePlatformTitle && targetPlatformTitle
42+
? ` from ${sourcePlatformTitle} to ${targetPlatformTitle}`
43+
: '';
44+
45+
const lines: string[] = [];
46+
47+
if (copied > 0) {
48+
lines.push(`${this.formatCount(copied, 'workout')} synced${direction}.`);
49+
} else if (skippedAlreadySynced > 0 || skippedByType > 0) {
50+
lines.push('No workouts copied.');
51+
} else {
52+
lines.push('No workouts found for the selected period.');
53+
}
54+
55+
if (skippedAlreadySynced > 0) {
56+
lines.push(`${this.formatCount(skippedAlreadySynced, 'already synced workout')} skipped.`);
57+
}
58+
59+
if (skippedByType > 0) {
60+
lines.push(`${this.formatCount(skippedByType, 'workout')} skipped by type filter.`);
61+
}
62+
63+
this.success(this.joinLines([
64+
...lines,
65+
this.formatPeriod(response.startDate, response.endDate)
66+
]));
67+
}
68+
69+
copyCalendarToLibraryCompleted(response: any, libraryName?: string): void {
70+
const copied = response.copied ?? 0;
71+
const skippedByType = response.skippedByType ?? 0;
72+
73+
const destination = libraryName
74+
? ` to "${libraryName}"`
75+
: '';
76+
77+
const lines: string[] = [];
78+
79+
if (copied > 0) {
80+
lines.push(`${this.formatCount(copied, 'workout')} copied${destination}.`);
81+
} else if (skippedByType > 0) {
82+
lines.push('No workouts copied.');
83+
} else {
84+
lines.push('No workouts found for the selected period.');
85+
}
86+
87+
if (skippedByType > 0) {
88+
lines.push(`${this.formatCount(skippedByType, 'workout')} skipped by type filter.`);
89+
}
90+
91+
this.success(this.joinLines([
92+
...lines,
93+
this.formatPeriod(response.startDate, response.endDate)
94+
]));
95+
}
96+
97+
singleWorkoutCopied(workoutName?: string, destinationName?: string): void {
98+
const workout = workoutName
99+
? `"${workoutName}"`
100+
: 'Workout';
101+
102+
const destination = destinationName
103+
? ` to "${destinationName}"`
104+
: '';
105+
106+
this.success(`${workout} copied successfully${destination}.`);
107+
}
108+
109+
libraryContainerCopied(planName: string, workouts: number): void {
110+
this.success(this.joinLines([
111+
`"${planName}" copied successfully.`,
112+
`${this.formatCount(workouts, 'workout')} included.`
113+
]));
114+
}
115+
116+
configurationSaved(): void {
117+
this.success('Configuration saved successfully.');
118+
}
119+
120+
scheduledSyncCreated(): void {
121+
this.success('Scheduled sync created. It will run every 20 minutes for today.');
122+
}
123+
124+
scheduledSyncDeleted(): void {
125+
this.success('Scheduled sync deleted.');
126+
}
127+
32128
private open(message: string, type: 'success' | 'error'): void {
33129
const isMobile = this.breakpointObserver.isMatched('(max-width: 768px)');
34130

@@ -52,4 +148,26 @@ export class NotificationService {
52148
this.snackBar.dismiss();
53149
this.snackBar.open(message, 'Close', config);
54150
}
151+
152+
private formatCount(count: number, singular: string, plural?: string): string {
153+
return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
154+
}
155+
156+
private formatPeriod(startDate?: string, endDate?: string): string | undefined {
157+
if (!startDate || !endDate) {
158+
return undefined;
159+
}
160+
161+
if (startDate === endDate) {
162+
return `Date: ${startDate}`;
163+
}
164+
165+
return `Period: ${startDate} to ${endDate}`;
166+
}
167+
168+
private joinLines(lines: Array<string | undefined | null>): string {
169+
return lines
170+
.filter((line): line is string => !!line)
171+
.join('\n');
172+
}
55173
}

0 commit comments

Comments
 (0)