feat(komoot): refine activity list for import status and mapped types
Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
parent
aef23720d6
commit
6d89426584
5 changed files with 50 additions and 19 deletions
|
|
@ -37,9 +37,13 @@ public class KomootImportController {
|
|||
@Valid @RequestBody KomootImportRequest request,
|
||||
Authentication authentication
|
||||
) {
|
||||
UUID fitPubUserId = userRepository.findByUsername(authentication.getName())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Authenticated user not found"))
|
||||
.getId();
|
||||
|
||||
log.info("User {} requested Komoot activity preview for Komoot ID {}",
|
||||
authentication.getName(), request.userId());
|
||||
KomootActivitiesResponse response = komootImportService.fetchCompletedActivities(request);
|
||||
KomootActivitiesResponse response = komootImportService.fetchCompletedActivities(request, fitPubUserId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ public record KomootActivitySummaryDTO(
|
|||
long id,
|
||||
String name,
|
||||
String sport,
|
||||
String mappedActivityType,
|
||||
String status,
|
||||
String type,
|
||||
OffsetDateTime date,
|
||||
Double distanceMeters,
|
||||
Integer durationSeconds,
|
||||
Integer timeInMotionSeconds,
|
||||
Double elevationUp
|
||||
Double elevationUp,
|
||||
boolean imported
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,8 +61,10 @@ public class KomootImportService {
|
|||
@Value("${fitpub.komoot.base-url:https://www.komoot.com}")
|
||||
private String komootBaseUrl;
|
||||
|
||||
public KomootActivitiesResponse fetchCompletedActivities(KomootImportRequest request) {
|
||||
public KomootActivitiesResponse fetchCompletedActivities(KomootImportRequest request, UUID fitPubUserId) {
|
||||
List<KomootActivitySummaryDTO> activities = new ArrayList<>();
|
||||
Set<Long> importedKomootActivityIds = new HashSet<>(
|
||||
activityRepository.findImportedKomootActivityIdsByUserId(fitPubUserId));
|
||||
|
||||
URI nextUri = buildInitialUri(request);
|
||||
HttpEntity<Void> httpEntity = new HttpEntity<>(buildHeaders(request.email(), request.password()));
|
||||
|
|
@ -76,7 +78,7 @@ public class KomootImportService {
|
|||
if (root == null) {
|
||||
throw new IllegalStateException("Komoot returned an empty response body.");
|
||||
}
|
||||
extractActivities(root, activities);
|
||||
extractActivities(root, activities, importedKomootActivityIds);
|
||||
nextUri = extractNextUri(root);
|
||||
}
|
||||
} catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) {
|
||||
|
|
@ -223,24 +225,27 @@ public class KomootImportService {
|
|||
return "Basic " + encoded;
|
||||
}
|
||||
|
||||
private void extractActivities(JsonNode root, List<KomootActivitySummaryDTO> activities) {
|
||||
private void extractActivities(JsonNode root, List<KomootActivitySummaryDTO> activities, Set<Long> importedKomootActivityIds) {
|
||||
JsonNode tours = root.path("_embedded").path("tours");
|
||||
if (!tours.isArray()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (JsonNode tour : tours) {
|
||||
long activityId = tour.path("id").asLong();
|
||||
activities.add(new KomootActivitySummaryDTO(
|
||||
tour.path("id").asLong(),
|
||||
activityId,
|
||||
nullableText(tour, "name"),
|
||||
nullableText(tour, "sport"),
|
||||
mapKomootSportToActivityType(nullableText(tour, "sport")).name(),
|
||||
nullableText(tour, "status"),
|
||||
nullableText(tour, "type"),
|
||||
parseDate(tour.path("date").asText(null)),
|
||||
nullableDouble(tour, "distance"),
|
||||
nullableInteger(tour, "duration"),
|
||||
nullableInteger(tour, "time_in_motion"),
|
||||
nullableDouble(tour, "elevation_up")
|
||||
nullableDouble(tour, "elevation_up"),
|
||||
importedKomootActivityIds.contains(activityId)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -312,7 +317,7 @@ public class KomootImportService {
|
|||
Set<Long> importedKomootActivityIds = new HashSet<>(
|
||||
activityRepository.findImportedKomootActivityIdsByUserId(fitPubUserId));
|
||||
|
||||
List<KomootActivitySummaryDTO> activities = new ArrayList<>(fetchCompletedActivities(request).activities());
|
||||
List<KomootActivitySummaryDTO> activities = new ArrayList<>(fetchCompletedActivities(request, fitPubUserId).activities());
|
||||
activities.sort(Comparator.comparing(
|
||||
KomootActivitySummaryDTO::date,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="loadActivitiesBtn">
|
||||
<span id="loadActivitiesText">
|
||||
<i class="bi bi-arrow-repeat"></i> Load Completed Activities
|
||||
<i class="bi bi-arrow-repeat"></i> Load Komoot Activities
|
||||
</span>
|
||||
<span id="loadActivitiesSpinner" class="d-none">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-list-check"></i>
|
||||
Completed Activities
|
||||
Komoot Activities
|
||||
</h4>
|
||||
<span class="badge text-bg-secondary" id="resultCount">0</span>
|
||||
</div>
|
||||
|
|
@ -101,10 +101,11 @@
|
|||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Date</th>
|
||||
<th>Sport</th>
|
||||
<th>Type</th>
|
||||
<th>Distance</th>
|
||||
<th>Duration</th>
|
||||
<th>Elevation</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="resultsBody"></tbody>
|
||||
|
|
@ -210,26 +211,33 @@
|
|||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function formatActivityTypeBadge(activityType) {
|
||||
const normalizedType = String(activityType).toLowerCase().replaceAll('_', '-');
|
||||
return `<span class="activity-type-badge activity-type-${escapeHtml(normalizedType)}">${escapeHtml(activityType)}</span>`;
|
||||
}
|
||||
|
||||
function renderActivities(activities) {
|
||||
resultCount.textContent = activities.length;
|
||||
|
||||
if (activities.length === 0) {
|
||||
resultsBody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4">No completed activities found.</td></tr>';
|
||||
resultsBody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4">No completed activities found.</td></tr>';
|
||||
resultsSection.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
resultsBody.innerHTML = activities.map(activity => `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold">${escapeHtml(activity.name || 'Untitled activity')}</div>
|
||||
<div class="text-muted small">${escapeHtml(activity.type || '-')}</div>
|
||||
</td>
|
||||
<td><div class="fw-semibold">${escapeHtml(activity.name || 'Untitled activity')}</div></td>
|
||||
<td>${formatDate(activity.date)}</td>
|
||||
<td>${escapeHtml(activity.sport || '-')}</td>
|
||||
<td>${formatActivityTypeBadge(activity.mappedActivityType)}</td>
|
||||
<td>${formatDistance(activity.distanceMeters)}</td>
|
||||
<td>${formatDuration(activity.timeInMotionSeconds || activity.durationSeconds)}</td>
|
||||
<td>${formatElevation(activity.elevationUp)}</td>
|
||||
<td class="text-center">
|
||||
${activity.imported
|
||||
? '<i class="bi bi-check-circle-fill text-success" title="Already imported" aria-label="Already imported"></i>'
|
||||
: '<i class="bi bi-plus-circle text-muted" title="New activity" aria-label="New activity"></i>'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ class KomootImportServiceTest {
|
|||
void shouldFetchAndMergePagedCompletedActivities() {
|
||||
String authHeader = "Basic " + Base64.getEncoder()
|
||||
.encodeToString("user@example.com:secret".getBytes(StandardCharsets.UTF_8));
|
||||
UUID userId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
|
||||
when(activityRepository.findImportedKomootActivityIdsByUserId(userId)).thenReturn(List.of(1002L));
|
||||
|
||||
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&sort_field=date&sort_direction=desc&limit=100&status=private&name=&hl=en&page=0"))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
|
|
@ -123,13 +126,16 @@ class KomootImportServiceTest {
|
|||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
KomootActivitiesResponse response = service.fetchCompletedActivities(
|
||||
new KomootImportRequest("user@example.com", "secret", "123456", null, null));
|
||||
new KomootImportRequest("user@example.com", "secret", "123456", null, null),
|
||||
userId);
|
||||
|
||||
assertThat(response.totalCount()).isEqualTo(2);
|
||||
assertThat(response.activities()).hasSize(2);
|
||||
assertThat(response.activities().get(0).id()).isEqualTo(1001L);
|
||||
assertThat(response.activities().get(0).imported()).isFalse();
|
||||
assertThat(response.activities().get(0).timeInMotionSeconds()).isEqualTo(7800);
|
||||
assertThat(response.activities().get(1).name()).isEqualTo("Lunch Walk");
|
||||
assertThat(response.activities().get(1).imported()).isTrue();
|
||||
|
||||
server.verify();
|
||||
}
|
||||
|
|
@ -139,6 +145,9 @@ class KomootImportServiceTest {
|
|||
void shouldFilterCompletedActivitiesByInclusiveDateRange() {
|
||||
String authHeader = "Basic " + Base64.getEncoder()
|
||||
.encodeToString("user@example.com:secret".getBytes(StandardCharsets.UTF_8));
|
||||
UUID userId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
|
||||
when(activityRepository.findImportedKomootActivityIdsByUserId(userId)).thenReturn(List.of(1003L));
|
||||
|
||||
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&sort_field=date&sort_direction=desc&limit=100&start_date=2026-04-25T22:00:00.000Z&end_date=2026-04-27T21:59:59.999Z"))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
|
|
@ -176,10 +185,13 @@ class KomootImportServiceTest {
|
|||
"123456",
|
||||
LocalDate.of(2026, 4, 26),
|
||||
LocalDate.of(2026, 4, 27)
|
||||
));
|
||||
),
|
||||
userId);
|
||||
|
||||
assertThat(response.totalCount()).isEqualTo(2);
|
||||
assertThat(response.activities()).extracting("id").containsExactly(1002L, 1003L);
|
||||
assertThat(response.activities().get(0).imported()).isFalse();
|
||||
assertThat(response.activities().get(1).imported()).isTrue();
|
||||
|
||||
server.verify();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue